Skip to content
43 changes: 43 additions & 0 deletions src/Controls/src/Core/Routing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,49 @@ public static void SetRoute(Element obj, string value)
obj.SetValue(RouteProperty, value);
}

internal static void ValidateForDuplicates(Element element, string route)
{
// If setting the same route to the same element, no need to validate
var currentRoute = GetRoute(element);
if (currentRoute == route)
{
return;
}

// Only validate user-defined routes
if (string.IsNullOrEmpty(route) || !IsUserDefined(route))
{
return;
}

// Check for duplicate routes among siblings (elements with the same parent)
var parent = element.Parent;
if (parent == null)
{
return;
}

foreach (var child in parent.LogicalChildrenInternal)
{
if (child == element)
continue;

var siblingRoute = GetRoute(child);
if (siblingRoute == route)
{
throw new ArgumentException(
$"Duplicated Route: \"{route}\" is already registered to another element of type {child.GetType().Name}. " +
$"Routes must be unique among siblings to avoid navigation conflicts.",
nameof(route));
}
}
}

internal static void RemoveElementRoute(Element element)
{
// No longer needed with sibling-based validation, but keep for API compatibility
}

static void ValidateRoute(string route, RouteFactory routeFactory)
{
if (string.IsNullOrWhiteSpace(route))
Expand Down
6 changes: 5 additions & 1 deletion src/Controls/src/Core/Shell/BaseShellItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,11 @@ public bool IsEnabled
public string Route
{
get { return Routing.GetRoute(this); }
set { Routing.SetRoute(this, value); }
set
{
Routing.ValidateForDuplicates(this, value);
Routing.SetRoute(this, value);
}
}

/// <include file="../../../docs/Microsoft.Maui.Controls/BaseShellItem.xml" path="//Member[@MemberName='Title']/Docs/*" />
Expand Down
118 changes: 118 additions & 0 deletions src/Controls/tests/Core.UnitTests/ShellTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1674,5 +1674,123 @@ public void ShellContentTitleShouldNotBeAppliedMultipleTimesWithStringFormat()

Assert.Equal("Title: Hello, World!", shellContent.Title);
}

[Fact]
public void DuplicateSiblingRoutesShouldThrowArgumentException()
{
var shell = new Shell();
var sameRoute = "DuplicateRoute";

var flyoutItem = new FlyoutItem();
var shellSection = new ShellSection();
var shellContent1 = new ShellContent { Title = "Page1", Content = new ContentPage() };
var shellContent2 = new ShellContent { Title = "Page2", Content = new ContentPage() };
shellSection.Items.Add(shellContent1);
shellSection.Items.Add(shellContent2);
flyoutItem.Items.Add(shellSection);
shell.Items.Add(flyoutItem);

shellContent1.Route = sameRoute;
var exception = Assert.Throws<ArgumentException>(() =>
{
shellContent2.Route = sameRoute;
});

Assert.Equal($"Duplicated Route: \"{sameRoute}\" is already registered to another element of type ShellContent. Routes must be unique among siblings to avoid navigation conflicts. (Parameter 'route')", exception.Message);
}

[Fact]
public void SameRouteInDifferentParentsIsAllowed()
{
var shell = new Shell();
var sameRoute = "SharedRoute";

// Create two different ShellSections, each with their own ShellContent
var flyoutItem = new FlyoutItem();
var shellSection1 = new ShellSection();
var shellSection2 = new ShellSection();
var shellContent1 = new ShellContent { Title = "Page1", Content = new ContentPage() };
var shellContent2 = new ShellContent { Title = "Page2", Content = new ContentPage() };

shellSection1.Items.Add(shellContent1);
shellSection2.Items.Add(shellContent2);
flyoutItem.Items.Add(shellSection1);
flyoutItem.Items.Add(shellSection2);
shell.Items.Add(flyoutItem);

// Both should be able to have the same route since they're in different parents
shellContent1.Route = sameRoute;
shellContent2.Route = sameRoute; // Should not throw - different parents

Assert.Equal(sameRoute, shellContent1.Route);
Assert.Equal(sameRoute, shellContent2.Route);
}

[Fact]
public void ChangingRouteAllowsReuseAmongSiblings()
{
var shell = new Shell();
var route = "TestRoute";

var flyoutItem = new FlyoutItem();
var shellSection = new ShellSection();
var shellContent1 = new ShellContent { Title = "Page1", Content = new ContentPage() };
var shellContent2 = new ShellContent { Title = "Page2", Content = new ContentPage() };
shellSection.Items.Add(shellContent1);
shellSection.Items.Add(shellContent2);
flyoutItem.Items.Add(shellSection);
shell.Items.Add(flyoutItem);

// Set initial route
shellContent1.Route = route;

// Change the route to something else
shellContent1.Route = "NewRoute";

// Now the original route should be available for the sibling
shellContent2.Route = route; // Should not throw
Assert.Equal(route, shellContent2.Route);
}

[Fact]
public void RemovingElementClearsRoute()
{
var shell = new Shell();
var route = "RemovableRoute";

var flyoutItem = new FlyoutItem();
var shellSection = new ShellSection();
var shellContent1 = new ShellContent { Title = "Page1", Content = new ContentPage() };
var shellContent2 = new ShellContent { Title = "Page2", Content = new ContentPage() };
shellSection.Items.Add(shellContent1);
shellSection.Items.Add(shellContent2);
flyoutItem.Items.Add(shellSection);
shell.Items.Add(flyoutItem);

shellContent1.Route = route;

// Remove the element from ShellSection.Items
shellSection.Items.Remove(shellContent1);

// Now the route should be available for another element
shellContent2.Route = route; // Should not throw
Assert.Equal(route, shellContent2.Route);
}

[Fact]
public void ReassigningSameRouteToSameElementDoesNotThrow()
{
var shell = new Shell();
var route = "SameRoute";

var flyoutItem = new FlyoutItem();
var shellContent = new ShellContent { Title = "Page" };
flyoutItem.Items.Add(shellContent);
shell.Items.Add(flyoutItem);

shellContent.Route = route;
shellContent.Route = route; // Should not throw - same element, same route
Assert.Equal(route, shellContent.Route);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ protected override void Init()

CurrentItem = Items.Last();

AddTopTab(TopTab);
AddBottomTab("Bottom tab");
}

Expand Down
Loading