diff --git a/src/Controls/src/Core/Routing.cs b/src/Controls/src/Core/Routing.cs index 8a5104be80b6..f8c3cd690f18 100644 --- a/src/Controls/src/Core/Routing.cs +++ b/src/Controls/src/Core/Routing.cs @@ -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)) diff --git a/src/Controls/src/Core/Shell/BaseShellItem.cs b/src/Controls/src/Core/Shell/BaseShellItem.cs index 74f003f0dba3..1d8ace61d754 100644 --- a/src/Controls/src/Core/Shell/BaseShellItem.cs +++ b/src/Controls/src/Core/Shell/BaseShellItem.cs @@ -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); + } } /// diff --git a/src/Controls/tests/Core.UnitTests/ShellTests.cs b/src/Controls/tests/Core.UnitTests/ShellTests.cs index e6713c88d94e..a54c61bd9c89 100644 --- a/src/Controls/tests/Core.UnitTests/ShellTests.cs +++ b/src/Controls/tests/Core.UnitTests/ShellTests.cs @@ -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(() => + { + 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); + } } } diff --git a/src/Controls/tests/TestCases.HostApp/Issues/XFIssue/Issue6878.cs b/src/Controls/tests/TestCases.HostApp/Issues/XFIssue/Issue6878.cs index 2cd660260a5a..6f3feec88c6a 100644 --- a/src/Controls/tests/TestCases.HostApp/Issues/XFIssue/Issue6878.cs +++ b/src/Controls/tests/TestCases.HostApp/Issues/XFIssue/Issue6878.cs @@ -28,7 +28,6 @@ protected override void Init() CurrentItem = Items.Last(); - AddTopTab(TopTab); AddBottomTab("Bottom tab"); }