From 99dfd29b74536eac154eee35bb57598125af92da Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 20 May 2026 20:33:30 +0000 Subject: [PATCH 1/3] Prevent duplicate region view registration and add DefaultView to desktop platforms RegisterViewWithRegion now ignores duplicate registrations for the same view type, target name, or delegate so a region receives only one instance when code runs more than once during the app lifecycle. Add RegionManager.DefaultView attached property to WPF, Avalonia, and Uno (shared WPF source) with AutoPopulateRegionBehavior support matching MAUI. DefaultView population skips views already present from registry registration. Add unit tests for WPF, Avalonia, Uno, and MAUI covering duplicate RegisterViewWithRegion and DefaultView scenarios. Co-authored-by: Dan Siegel --- .../Behaviors/AutoPopulateRegionBehavior.cs | 53 ++++++++--- .../Navigation/Regions/RegionViewRegistry.cs | 52 +++++++++++ .../Behaviors/AutoPopulateRegionBehavior.cs | 90 ++++++++++++++++++- .../Navigation/Regions/RegionManager.cs | 45 ++++++++++ .../Navigation/Regions/RegionViewRegistry.cs | 56 +++++++++++- .../AutoPopulateRegionBehaviorFixture.cs | 75 ++++++++++++++++ .../Regions/RegionViewRegistryFixture.cs | 33 +++++++ .../Fixtures/Regions/RegionFixture.cs | 25 ++++++ .../AutoPopulateRegionBehaviorFixture.cs | 86 ++++++++++++++++++ .../Regions/RegionViewRegistryFixture.cs | 48 ++++++++++ .../AutoPopulateRegionBehaviorFixture.cs | 75 ++++++++++++++++ .../Regions/RegionViewRegistryFixture.cs | 33 +++++++ 12 files changed, 658 insertions(+), 13 deletions(-) create mode 100644 tests/Uno/Prism.Uno.WinUI.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs create mode 100644 tests/Uno/Prism.Uno.WinUI.Tests/Regions/RegionViewRegistryFixture.cs diff --git a/src/Maui/Prism.Maui/Navigation/Regions/Behaviors/AutoPopulateRegionBehavior.cs b/src/Maui/Prism.Maui/Navigation/Regions/Behaviors/AutoPopulateRegionBehavior.cs index 5f2b4397a8..ab3894de0b 100644 --- a/src/Maui/Prism.Maui/Navigation/Regions/Behaviors/AutoPopulateRegionBehavior.cs +++ b/src/Maui/Prism.Maui/Navigation/Regions/Behaviors/AutoPopulateRegionBehavior.cs @@ -47,22 +47,42 @@ private void StartPopulatingContent() AddViewIntoRegion(view); } - if (Region is ITargetAwareRegion targetAware && targetAware.TargetElement.GetValue(Xaml.RegionManager.DefaultViewProperty) != null) + if (Region is ITargetAwareRegion targetAware && targetAware.TargetElement.GetValue(Xaml.RegionManager.DefaultViewProperty) is { } defaultView) { - var defaultView = targetAware.TargetElement.GetValue(Xaml.RegionManager.DefaultViewProperty); if (defaultView is string targetName) - Region.Add(targetName); - else if (defaultView is VisualElement element) - Region.Add(element); - else if (defaultView is Type type) { + if (Region.GetView(targetName) != null) + { + return; + } + var container = targetAware.Container; var registry = container.Resolve(); - var registration = registry.Registrations.FirstOrDefault(x => x.View == type); - if (registration is not null) + var view = registry.CreateView(container, targetName) as VisualElement; + if (view != null && !Region.Views.Contains(view)) + { + Region.Add(view, targetName); + } + } + else if (defaultView is VisualElement element) + { + if (!Region.Views.Contains(element)) { - var view = registry.CreateView(container, registration.Name) as VisualElement; - Region.Add(view); + Region.Add(element); + } + } + else if (defaultView is Type type) + { + if (!RegionContainsViewOfType(type)) + { + var container = targetAware.Container; + var registry = container.Resolve(); + var registration = registry.Registrations.FirstOrDefault(x => x.View == type); + if (registration is not null) + { + var view = registry.CreateView(container, registration.Name) as VisualElement; + Region.Add(view); + } } } } @@ -89,6 +109,19 @@ protected virtual void AddViewIntoRegion(VisualElement viewToAdd) Region.Add(viewToAdd); } + private bool RegionContainsViewOfType(Type viewType) + { + foreach (var view in Region.Views) + { + if (view != null && viewType.IsInstanceOfType(view)) + { + return true; + } + } + + return false; + } + private void Region_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == "Name" && !string.IsNullOrEmpty(Region.Name)) diff --git a/src/Maui/Prism.Maui/Navigation/Regions/RegionViewRegistry.cs b/src/Maui/Prism.Maui/Navigation/Regions/RegionViewRegistry.cs index c3a04bf33d..61a27c5274 100644 --- a/src/Maui/Prism.Maui/Navigation/Regions/RegionViewRegistry.cs +++ b/src/Maui/Prism.Maui/Navigation/Regions/RegionViewRegistry.cs @@ -14,6 +14,8 @@ namespace Prism.Navigation.Regions; public class RegionViewRegistry : IRegionViewRegistry { private readonly ListDictionary> _registeredContent = new(); + private readonly Dictionary> _registeredViewTypes = new(); + private readonly Dictionary> _registeredTargetNames = new(); private readonly WeakDelegatesManager _contentRegisteredListeners = new(); /// @@ -49,6 +51,11 @@ public IEnumerable GetContents(string regionName, IContainerProvider con /// Content type to be registered for the . public void RegisterViewWithRegion(string regionName, Type viewType) { + if (!TryAddRegisteredViewType(regionName, viewType)) + { + return; + } + RegisterViewWithRegion(regionName, c => { var registry = c.Resolve(); @@ -67,6 +74,11 @@ public void RegisterViewWithRegion(string regionName, Type viewType) /// Content type to be registered for the . public void RegisterViewWithRegion(string regionName, string targetName) { + if (!TryAddRegisteredTargetName(regionName, targetName)) + { + return; + } + RegisterViewWithRegion(regionName, c => { var registry = c.Resolve(); @@ -81,10 +93,50 @@ public void RegisterViewWithRegion(string regionName, string targetName) /// Delegate used to retrieve the content associated with the . public void RegisterViewWithRegion(string regionName, Func getContentDelegate) { + if (IsContentDelegateRegistered(regionName, getContentDelegate)) + { + return; + } + _registeredContent.Add(regionName, getContentDelegate); OnContentRegistered(new ViewRegisteredEventArgs(regionName, getContentDelegate)); } + private bool TryAddRegisteredViewType(string regionName, Type viewType) + { + if (!_registeredViewTypes.TryGetValue(regionName, out HashSet viewTypes)) + { + viewTypes = new HashSet(); + _registeredViewTypes.Add(regionName, viewTypes); + } + + return viewTypes.Add(viewType); + } + + private bool TryAddRegisteredTargetName(string regionName, string targetName) + { + if (!_registeredTargetNames.TryGetValue(regionName, out HashSet targetNames)) + { + targetNames = new HashSet(); + _registeredTargetNames.Add(regionName, targetNames); + } + + return targetNames.Add(targetName); + } + + private bool IsContentDelegateRegistered(string regionName, Func getContentDelegate) + { + foreach (var existingDelegate in _registeredContent[regionName]) + { + if (existingDelegate == getContentDelegate) + { + return true; + } + } + + return false; + } + private void OnContentRegistered(ViewRegisteredEventArgs e) { try diff --git a/src/Wpf/Prism.Wpf/Navigation/Regions/Behaviors/AutoPopulateRegionBehavior.cs b/src/Wpf/Prism.Wpf/Navigation/Regions/Behaviors/AutoPopulateRegionBehavior.cs index 10ae82ea94..a8d6ec4b2d 100644 --- a/src/Wpf/Prism.Wpf/Navigation/Regions/Behaviors/AutoPopulateRegionBehavior.cs +++ b/src/Wpf/Prism.Wpf/Navigation/Regions/Behaviors/AutoPopulateRegionBehavior.cs @@ -1,9 +1,15 @@ +using System; +using System.Collections.Generic; +using Prism.Common; +using Prism.Ioc; +using Prism.Properties; + namespace Prism.Navigation.Regions.Behaviors { /// /// Populates the target region with the views registered to it in the . /// - public class AutoPopulateRegionBehavior : RegionBehavior + public class AutoPopulateRegionBehavior : RegionBehavior, IHostAwareRegionBehavior { /// /// The key of this behavior. @@ -11,6 +17,8 @@ public class AutoPopulateRegionBehavior : RegionBehavior public const string BehaviorKey = "AutoPopulate"; private readonly IRegionViewRegistry regionViewRegistry; + private DependencyObject hostControl; + private bool isAttached; /// /// Creates a new instance of the AutoPopulateRegionBehavior @@ -22,6 +30,23 @@ public AutoPopulateRegionBehavior(IRegionViewRegistry regionViewRegistry) this.regionViewRegistry = regionViewRegistry; } + /// + /// Gets or sets the that the is attached to. + /// + public DependencyObject HostControl + { + get => hostControl; + set + { + if (isAttached) + { + throw new InvalidOperationException(Resources.HostControlCannotBeSetAfterAttach); + } + + hostControl = value; + } + } + /// /// Attaches the AutoPopulateRegionBehavior to the Region. /// @@ -39,11 +64,15 @@ protected override void OnAttach() private void StartPopulatingContent() { + isAttached = true; + foreach (object view in CreateViewsToAutoPopulate()) { AddViewIntoRegion(view); } + TryAddDefaultView(); + regionViewRegistry.ContentRegistered += OnViewRegistered; } @@ -54,7 +83,7 @@ private void StartPopulatingContent() /// protected virtual IEnumerable CreateViewsToAutoPopulate() { - return regionViewRegistry.GetContents(Region.Name); + return regionViewRegistry.GetContents(Region.Name, ContainerLocator.Container); } /// @@ -66,6 +95,63 @@ protected virtual void AddViewIntoRegion(object viewToAdd) Region.Add(viewToAdd); } + private void TryAddDefaultView() + { + if (HostControl == null) + { + return; + } + + var defaultView = RegionManager.GetDefaultView(HostControl); + if (defaultView == null) + { + return; + } + + if (defaultView is string targetName) + { + if (Region.GetView(targetName) != null) + { + return; + } + + var view = ContainerLocator.Container.Resolve(typeof(object), targetName); + if (!Region.Views.Contains(view)) + { + Region.Add(view, targetName); + } + } + else if (defaultView is Type viewType) + { + if (!RegionContainsViewOfType(viewType)) + { + var view = ContainerLocator.Container.Resolve(viewType); + MvvmHelpers.AutowireViewModel(view); + Region.Add(view); + } + } + else + { + if (!Region.Views.Contains(defaultView)) + { + Region.Add(defaultView); + } + } + } + + private bool RegionContainsViewOfType(Type viewType) + { + foreach (object view in Region.Views) + { + if (view != null && viewType.IsInstanceOfType(view)) + { + return true; + } + } + + return false; + } + private void Region_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { if (e.PropertyName == "Name" && !string.IsNullOrEmpty(Region.Name)) diff --git a/src/Wpf/Prism.Wpf/Navigation/Regions/RegionManager.cs b/src/Wpf/Prism.Wpf/Navigation/Regions/RegionManager.cs index d618967baf..38957c352f 100644 --- a/src/Wpf/Prism.Wpf/Navigation/Regions/RegionManager.cs +++ b/src/Wpf/Prism.Wpf/Navigation/Regions/RegionManager.cs @@ -167,6 +167,51 @@ public static void SetRegionManager(DependencyObject target, IRegionManager valu target.SetValue(RegionManagerProperty, value); } + /// + /// Identifies the DefaultView attached property. + /// + /// + /// Sets the default view to be displayed in a region when it is created. + /// This can be a view name, a type, or an instance of the view. + /// +#if !AVALONIA + public static readonly DependencyProperty DefaultViewProperty = + DependencyProperty.RegisterAttached("DefaultView", typeof(object), typeof(RegionManager), null); +#else + public static readonly AvaloniaProperty DefaultViewProperty = + AvaloniaProperty.RegisterAttached("DefaultView", typeof(RegionManager)); +#endif + + /// + /// Sets the attached property for the specified region target. + /// + /// The object that will host the default view. + /// + /// The default view to display in the region. This can be a view name, a type, or an instance of the view. + /// + public static void SetDefaultView(DependencyObject regionTarget, object viewNameTypeOrInstance) + { + if (regionTarget == null) + throw new ArgumentNullException(nameof(regionTarget)); + + regionTarget.SetValue(DefaultViewProperty, viewNameTypeOrInstance); + } + + /// + /// Gets the value of the attached property for the specified region target. + /// + /// The object that hosts the default view. + /// + /// The default view associated with the region. This can be a view name, a type, or an instance of the view. + /// + public static object GetDefaultView(DependencyObject regionTarget) + { + if (regionTarget == null) + throw new ArgumentNullException(nameof(regionTarget)); + + return regionTarget.GetValue(DefaultViewProperty); + } + /// /// Identifies the RegionContext attached property. /// diff --git a/src/Wpf/Prism.Wpf/Navigation/Regions/RegionViewRegistry.cs b/src/Wpf/Prism.Wpf/Navigation/Regions/RegionViewRegistry.cs index 404653ef6e..b457e467a3 100644 --- a/src/Wpf/Prism.Wpf/Navigation/Regions/RegionViewRegistry.cs +++ b/src/Wpf/Prism.Wpf/Navigation/Regions/RegionViewRegistry.cs @@ -13,6 +13,8 @@ public class RegionViewRegistry : IRegionViewRegistry { private readonly IContainerProvider _container; private readonly ListDictionary> _registeredContent = new ListDictionary>(); + private readonly Dictionary> _registeredViewTypes = new Dictionary>(); + private readonly Dictionary> _registeredTargetNames = new Dictionary>(); private readonly WeakDelegatesManager _contentRegisteredListeners = new WeakDelegatesManager(); /// @@ -57,6 +59,11 @@ public IEnumerable GetContents(string regionName, IContainerProvider con /// Content type to be registered for the . public void RegisterViewWithRegion(string regionName, Type viewType) { + if (!TryAddRegisteredViewType(regionName, viewType)) + { + return; + } + RegisterViewWithRegion(regionName, _ => CreateInstance(viewType)); } @@ -67,6 +74,11 @@ public void RegisterViewWithRegion(string regionName, Type viewType) /// Delegate used to retrieve the content associated with the . public void RegisterViewWithRegion(string regionName, Func getContentDelegate) { + if (IsContentDelegateRegistered(regionName, getContentDelegate)) + { + return; + } + _registeredContent.Add(regionName, getContentDelegate); OnContentRegistered(new ViewRegisteredEventArgs(regionName, getContentDelegate)); } @@ -79,8 +91,15 @@ public void RegisterViewWithRegion(string regionName, FuncThe name of the region to associate the view with. /// The type of the view to register with the /// The , for adding several views easily - public void RegisterViewWithRegion(string regionName, string targetName) => + public void RegisterViewWithRegion(string regionName, string targetName) + { + if (!TryAddRegisteredTargetName(regionName, targetName)) + { + return; + } + RegisterViewWithRegion(regionName, c => c.Resolve(targetName)); + } /// /// Creates an instance of a registered view . @@ -94,6 +113,41 @@ protected virtual object CreateInstance(Type type) return view; } + private bool TryAddRegisteredViewType(string regionName, Type viewType) + { + if (!_registeredViewTypes.TryGetValue(regionName, out HashSet viewTypes)) + { + viewTypes = new HashSet(); + _registeredViewTypes.Add(regionName, viewTypes); + } + + return viewTypes.Add(viewType); + } + + private bool TryAddRegisteredTargetName(string regionName, string targetName) + { + if (!_registeredTargetNames.TryGetValue(regionName, out HashSet targetNames)) + { + targetNames = new HashSet(); + _registeredTargetNames.Add(regionName, targetNames); + } + + return targetNames.Add(targetName); + } + + private bool IsContentDelegateRegistered(string regionName, Func getContentDelegate) + { + foreach (Func existingDelegate in _registeredContent[regionName]) + { + if (existingDelegate == getContentDelegate) + { + return true; + } + } + + return false; + } + private void OnContentRegistered(ViewRegisteredEventArgs e) { try diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs index 8693d77499..125f3f868f 100644 --- a/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; using Moq; using Prism.Avalonia.Tests.Mocks; using Prism.Ioc; @@ -11,6 +13,10 @@ namespace Prism.Avalonia.Tests.Regions.Behaviors { public class AutoPopulateRegionBehaviorFixture { + private class MockContentObject + { + } + [Fact] public void ShouldGetViewsFromRegistryOnAttach() { @@ -64,6 +70,75 @@ public void NullRegionThrows() } + [Fact] + public void WhenSameViewTypeRegisteredTwice_RegionContainsSingleView() + { + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + containerMock.Setup(c => c.Resolve(typeof(MockContentObject))).Returns(new MockContentObject()); + var registry = new RegionViewRegistry(containerMock.Object); + registry.RegisterViewWithRegion("MyRegion", typeof(MockContentObject)); + registry.RegisterViewWithRegion("MyRegion", typeof(MockContentObject)); + + var region = new Region { Name = "MyRegion" }; + var behavior = new AutoPopulateRegionBehavior(registry) + { + Region = region + }; + + behavior.Attach(); + + Assert.Single(region.Views); + } + + [Fact] + public void ShouldAddDefaultViewByNameWhenRegionIsPopulated() + { + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + var content = new object(); + containerMock.Setup(c => c.Resolve(typeof(object), "MyView")).Returns(content); + var registry = new RegionViewRegistry(containerMock.Object); + var host = new ContentControl(); + RegionManager.SetDefaultView(host, "MyView"); + + var region = new Region { Name = "MyRegion" }; + var behavior = new AutoPopulateRegionBehavior(registry) + { + Region = region, + HostControl = host + }; + + behavior.Attach(); + + Assert.Single(region.Views); + Assert.Same(content, region.Views.ElementAt(0)); + } + + [Fact] + public void WhenDefaultViewMatchesRegisteredView_RegionContainsSingleView() + { + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + var content = new object(); + containerMock.Setup(c => c.Resolve(typeof(object), "MyView")).Returns(content); + var registry = new RegionViewRegistry(containerMock.Object); + registry.RegisterViewWithRegion("MyRegion", "MyView"); + var host = new ContentControl(); + RegionManager.SetDefaultView(host, "MyView"); + + var region = new Region { Name = "MyRegion" }; + var behavior = new AutoPopulateRegionBehavior(registry) + { + Region = region, + HostControl = host + }; + + behavior.Attach(); + + Assert.Single(region.Views); + } + [Fact] public void CanAttachBeforeSettingName() { diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionViewRegistryFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionViewRegistryFixture.cs index 7db7ce8ce7..cad9b82513 100644 --- a/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionViewRegistryFixture.cs +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionViewRegistryFixture.cs @@ -12,6 +12,39 @@ namespace Prism.Avalonia.Tests.Regions; public class RegionViewRegistryFixture { + [Fact] + public void RegisterViewWithRegion_DuplicateTypeRegistration_ReturnsSingleView() + { + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + containerMock.Setup(c => c.Resolve(typeof(MockContentObject))).Returns(new MockContentObject()); + var registry = new RegionViewRegistry(containerMock.Object); + + registry.RegisterViewWithRegion("MyRegion", typeof(MockContentObject)); + registry.RegisterViewWithRegion("MyRegion", typeof(MockContentObject)); + + var result = registry.GetContents("MyRegion"); + + Assert.Single(result); + } + + [Fact] + public void RegisterViewWithRegion_DuplicateTargetNameRegistration_ReturnsSingleView() + { + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + var content = new MockContentObject(); + containerMock.Setup(c => c.Resolve(typeof(object), "MyView")).Returns(content); + var registry = new RegionViewRegistry(containerMock.Object); + + registry.RegisterViewWithRegion("MyRegion", "MyView"); + registry.RegisterViewWithRegion("MyRegion", "MyView"); + + var result = registry.GetContents("MyRegion"); + + Assert.Single(result); + } + [Fact] public void CanRegisterContentAndRetrieveIt() { diff --git a/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Regions/RegionFixture.cs b/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Regions/RegionFixture.cs index 2a61ad91ee..8a2d315d11 100644 --- a/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Regions/RegionFixture.cs +++ b/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Regions/RegionFixture.cs @@ -32,6 +32,31 @@ public void ContentRegion_CreatedBy_RequestNavigate() Assert.IsType(page.ContentRegion.Content.BindingContext); } + [Fact] + public void FrameRegion_DuplicateRegisterViewWithRegion_ContainsSingleView() + { + var mauiApp = CreateBuilder(prism => + prism.RegisterTypes(container => + { + container.RegisterForNavigation(); + container.RegisterForRegionNavigation(); + }) + .OnInitialized(container => + { + var regionManager = container.Resolve(); + regionManager.RegisterViewWithRegion("FrameRegion", "MockRegionViewA"); + regionManager.RegisterViewWithRegion("FrameRegion", "MockRegionViewA"); + }) + .CreateWindow("MockContentRegionPage")) + .Build(); + var window = GetWindow(mauiApp); + + Assert.IsType(window.Page); + var page = window.Page as MockContentRegionPage; + Assert.NotNull(page.FrameRegion.Content); + Assert.IsType(page.FrameRegion.Content); + } + [Fact] public void FrameRegion_CreatedBy_RegisterViewWithRegion() { diff --git a/tests/Uno/Prism.Uno.WinUI.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs b/tests/Uno/Prism.Uno.WinUI.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs new file mode 100644 index 0000000000..a8516b70c3 --- /dev/null +++ b/tests/Uno/Prism.Uno.WinUI.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs @@ -0,0 +1,86 @@ +using System; +using System.Linq; +using Microsoft.UI.Xaml.Controls; +using Moq; +using Prism.Ioc; +using Prism.Navigation.Regions; +using Prism.Navigation.Regions.Behaviors; +using Xunit; + +namespace Prism.Uno.WinUI.Tests.Regions.Behaviors; + +public class AutoPopulateRegionBehaviorFixture +{ + private class MockContentObject + { + } + + [Fact] + public void WhenSameViewTypeRegisteredTwice_RegionContainsSingleView() + { + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + containerMock.Setup(c => c.Resolve(typeof(MockContentObject))).Returns(new MockContentObject()); + var registry = new RegionViewRegistry(containerMock.Object); + registry.RegisterViewWithRegion("MyRegion", typeof(MockContentObject)); + registry.RegisterViewWithRegion("MyRegion", typeof(MockContentObject)); + + var region = new Region { Name = "MyRegion" }; + var behavior = new AutoPopulateRegionBehavior(registry) + { + Region = region + }; + + behavior.Attach(); + + Assert.Single(region.Views); + } + + [Fact] + public void ShouldAddDefaultViewByNameWhenRegionIsPopulated() + { + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + var content = new object(); + containerMock.Setup(c => c.Resolve(typeof(object), "MyView")).Returns(content); + var registry = new RegionViewRegistry(containerMock.Object); + var host = new ContentControl(); + RegionManager.SetDefaultView(host, "MyView"); + + var region = new Region { Name = "MyRegion" }; + var behavior = new AutoPopulateRegionBehavior(registry) + { + Region = region, + HostControl = host + }; + + behavior.Attach(); + + Assert.Single(region.Views); + Assert.Same(content, region.Views.ElementAt(0)); + } + + [Fact] + public void WhenDefaultViewMatchesRegisteredView_RegionContainsSingleView() + { + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + var content = new object(); + containerMock.Setup(c => c.Resolve(typeof(object), "MyView")).Returns(content); + var registry = new RegionViewRegistry(containerMock.Object); + registry.RegisterViewWithRegion("MyRegion", "MyView"); + var host = new ContentControl(); + RegionManager.SetDefaultView(host, "MyView"); + + var region = new Region { Name = "MyRegion" }; + var behavior = new AutoPopulateRegionBehavior(registry) + { + Region = region, + HostControl = host + }; + + behavior.Attach(); + + Assert.Single(region.Views); + } +} diff --git a/tests/Uno/Prism.Uno.WinUI.Tests/Regions/RegionViewRegistryFixture.cs b/tests/Uno/Prism.Uno.WinUI.Tests/Regions/RegionViewRegistryFixture.cs new file mode 100644 index 0000000000..840a494709 --- /dev/null +++ b/tests/Uno/Prism.Uno.WinUI.Tests/Regions/RegionViewRegistryFixture.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using Moq; +using Prism.Ioc; +using Prism.Navigation.Regions; +using Xunit; + +namespace Prism.Uno.WinUI.Tests.Regions; + +public class RegionViewRegistryFixture +{ + private class MockContentObject + { + } + + [Fact] + public void RegisterViewWithRegion_DuplicateTypeRegistration_ReturnsSingleView() + { + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + containerMock.Setup(c => c.Resolve(typeof(MockContentObject))).Returns(new MockContentObject()); + var registry = new RegionViewRegistry(containerMock.Object); + + registry.RegisterViewWithRegion("MyRegion", typeof(MockContentObject)); + registry.RegisterViewWithRegion("MyRegion", typeof(MockContentObject)); + + var result = registry.GetContents("MyRegion"); + + Assert.Single(result); + } + + [Fact] + public void RegisterViewWithRegion_DuplicateTargetNameRegistration_ReturnsSingleView() + { + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + var content = new MockContentObject(); + containerMock.Setup(c => c.Resolve(typeof(object), "MyView")).Returns(content); + var registry = new RegionViewRegistry(containerMock.Object); + + registry.RegisterViewWithRegion("MyRegion", "MyView"); + registry.RegisterViewWithRegion("MyRegion", "MyView"); + + var result = registry.GetContents("MyRegion"); + + Assert.Single(result); + } +} diff --git a/tests/Wpf/Prism.Wpf.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs b/tests/Wpf/Prism.Wpf.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs index 4b8b3784d4..bc711fc0ff 100644 --- a/tests/Wpf/Prism.Wpf.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs +++ b/tests/Wpf/Prism.Wpf.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Windows.Controls; using Moq; using Prism.Ioc; using Prism.Navigation.Regions; @@ -12,6 +14,10 @@ namespace Prism.Wpf.Tests.Regions.Behaviors public class AutoPopulateRegionBehaviorFixture { + private class MockContentObject + { + } + [Fact] public void ShouldGetViewsFromRegistryOnAttach() { @@ -65,6 +71,75 @@ public void NullRegionThrows() } + [Fact] + public void WhenSameViewTypeRegisteredTwice_RegionContainsSingleView() + { + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + containerMock.Setup(c => c.Resolve(typeof(MockContentObject))).Returns(new MockContentObject()); + var registry = new RegionViewRegistry(containerMock.Object); + registry.RegisterViewWithRegion("MyRegion", typeof(MockContentObject)); + registry.RegisterViewWithRegion("MyRegion", typeof(MockContentObject)); + + var region = new Region { Name = "MyRegion" }; + var behavior = new AutoPopulateRegionBehavior(registry) + { + Region = region + }; + + behavior.Attach(); + + Assert.Single(region.Views); + } + + [StaFact] + public void ShouldAddDefaultViewByNameWhenRegionIsPopulated() + { + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + var content = new object(); + containerMock.Setup(c => c.Resolve(typeof(object), "MyView")).Returns(content); + var registry = new RegionViewRegistry(containerMock.Object); + var host = new ContentControl(); + RegionManager.SetDefaultView(host, "MyView"); + + var region = new Region { Name = "MyRegion" }; + var behavior = new AutoPopulateRegionBehavior(registry) + { + Region = region, + HostControl = host + }; + + behavior.Attach(); + + Assert.Single(region.Views); + Assert.Same(content, region.Views.ElementAt(0)); + } + + [StaFact] + public void WhenDefaultViewMatchesRegisteredView_RegionContainsSingleView() + { + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + var content = new object(); + containerMock.Setup(c => c.Resolve(typeof(object), "MyView")).Returns(content); + var registry = new RegionViewRegistry(containerMock.Object); + registry.RegisterViewWithRegion("MyRegion", "MyView"); + var host = new ContentControl(); + RegionManager.SetDefaultView(host, "MyView"); + + var region = new Region { Name = "MyRegion" }; + var behavior = new AutoPopulateRegionBehavior(registry) + { + Region = region, + HostControl = host + }; + + behavior.Attach(); + + Assert.Single(region.Views); + } + [Fact] public void CanAttachBeforeSettingName() { diff --git a/tests/Wpf/Prism.Wpf.Tests/Regions/RegionViewRegistryFixture.cs b/tests/Wpf/Prism.Wpf.Tests/Regions/RegionViewRegistryFixture.cs index 106f8885f3..64486baba4 100644 --- a/tests/Wpf/Prism.Wpf.Tests/Regions/RegionViewRegistryFixture.cs +++ b/tests/Wpf/Prism.Wpf.Tests/Regions/RegionViewRegistryFixture.cs @@ -13,6 +13,39 @@ namespace Prism.Wpf.Tests.Regions public class RegionViewRegistryFixture { + [Fact] + public void RegisterViewWithRegion_DuplicateTypeRegistration_ReturnsSingleView() + { + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + containerMock.Setup(c => c.Resolve(typeof(MockContentObject))).Returns(new MockContentObject()); + var registry = new RegionViewRegistry(containerMock.Object); + + registry.RegisterViewWithRegion("MyRegion", typeof(MockContentObject)); + registry.RegisterViewWithRegion("MyRegion", typeof(MockContentObject)); + + var result = registry.GetContents("MyRegion"); + + Assert.Single(result); + } + + [Fact] + public void RegisterViewWithRegion_DuplicateTargetNameRegistration_ReturnsSingleView() + { + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + var content = new MockContentObject(); + containerMock.Setup(c => c.Resolve(typeof(object), "MyView")).Returns(content); + var registry = new RegionViewRegistry(containerMock.Object); + + registry.RegisterViewWithRegion("MyRegion", "MyView"); + registry.RegisterViewWithRegion("MyRegion", "MyView"); + + var result = registry.GetContents("MyRegion"); + + Assert.Single(result); + } + [Fact] public void CanRegisterContentAndRetrieveIt() { From b639f0b0fe0028204433bee9a7a93aef10812fef Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 20 May 2026 20:49:29 +0000 Subject: [PATCH 2/3] Register DefaultView via property callback instead of AutoPopulate behavior Revert AutoPopulateRegionBehavior to its original implementation without IHostAwareRegionBehavior or DefaultView population logic. When DefaultView is set on a region host, RegionManager now calls IRegionViewRegistry.RegisterViewWithRegion from the attached property changed callback. RegionName assignment also registers DefaultView when it was set first, covering arbitrary XAML property order. Restore MAUI AutoPopulateRegionBehavior to its prior implementation. Update tests to verify registration through RegionManager rather than the auto-populate behavior. Co-authored-by: Dan Siegel --- .../Behaviors/AutoPopulateRegionBehavior.cs | 53 ++------- .../Behaviors/AutoPopulateRegionBehavior.cs | 90 +-------------- .../Navigation/Regions/RegionManager.cs | 46 +++++++- .../AutoPopulateRegionBehaviorFixture.cs | 50 --------- .../Regions/RegionManagerFixture.cs | 104 ++++++++++++++++++ .../AutoPopulateRegionBehaviorFixture.cs | 86 --------------- .../RegionManagerDefaultViewFixture.cs | 55 +++++++++ .../AutoPopulateRegionBehaviorFixture.cs | 50 --------- .../Regions/RegionManagerFixture.cs | 103 +++++++++++++++++ 9 files changed, 319 insertions(+), 318 deletions(-) delete mode 100644 tests/Uno/Prism.Uno.WinUI.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs create mode 100644 tests/Uno/Prism.Uno.WinUI.Tests/Regions/RegionManagerDefaultViewFixture.cs diff --git a/src/Maui/Prism.Maui/Navigation/Regions/Behaviors/AutoPopulateRegionBehavior.cs b/src/Maui/Prism.Maui/Navigation/Regions/Behaviors/AutoPopulateRegionBehavior.cs index ab3894de0b..5f2b4397a8 100644 --- a/src/Maui/Prism.Maui/Navigation/Regions/Behaviors/AutoPopulateRegionBehavior.cs +++ b/src/Maui/Prism.Maui/Navigation/Regions/Behaviors/AutoPopulateRegionBehavior.cs @@ -47,42 +47,22 @@ private void StartPopulatingContent() AddViewIntoRegion(view); } - if (Region is ITargetAwareRegion targetAware && targetAware.TargetElement.GetValue(Xaml.RegionManager.DefaultViewProperty) is { } defaultView) + if (Region is ITargetAwareRegion targetAware && targetAware.TargetElement.GetValue(Xaml.RegionManager.DefaultViewProperty) != null) { + var defaultView = targetAware.TargetElement.GetValue(Xaml.RegionManager.DefaultViewProperty); if (defaultView is string targetName) - { - if (Region.GetView(targetName) != null) - { - return; - } - - var container = targetAware.Container; - var registry = container.Resolve(); - var view = registry.CreateView(container, targetName) as VisualElement; - if (view != null && !Region.Views.Contains(view)) - { - Region.Add(view, targetName); - } - } + Region.Add(targetName); else if (defaultView is VisualElement element) - { - if (!Region.Views.Contains(element)) - { - Region.Add(element); - } - } + Region.Add(element); else if (defaultView is Type type) { - if (!RegionContainsViewOfType(type)) + var container = targetAware.Container; + var registry = container.Resolve(); + var registration = registry.Registrations.FirstOrDefault(x => x.View == type); + if (registration is not null) { - var container = targetAware.Container; - var registry = container.Resolve(); - var registration = registry.Registrations.FirstOrDefault(x => x.View == type); - if (registration is not null) - { - var view = registry.CreateView(container, registration.Name) as VisualElement; - Region.Add(view); - } + var view = registry.CreateView(container, registration.Name) as VisualElement; + Region.Add(view); } } } @@ -109,19 +89,6 @@ protected virtual void AddViewIntoRegion(VisualElement viewToAdd) Region.Add(viewToAdd); } - private bool RegionContainsViewOfType(Type viewType) - { - foreach (var view in Region.Views) - { - if (view != null && viewType.IsInstanceOfType(view)) - { - return true; - } - } - - return false; - } - private void Region_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == "Name" && !string.IsNullOrEmpty(Region.Name)) diff --git a/src/Wpf/Prism.Wpf/Navigation/Regions/Behaviors/AutoPopulateRegionBehavior.cs b/src/Wpf/Prism.Wpf/Navigation/Regions/Behaviors/AutoPopulateRegionBehavior.cs index a8d6ec4b2d..10ae82ea94 100644 --- a/src/Wpf/Prism.Wpf/Navigation/Regions/Behaviors/AutoPopulateRegionBehavior.cs +++ b/src/Wpf/Prism.Wpf/Navigation/Regions/Behaviors/AutoPopulateRegionBehavior.cs @@ -1,15 +1,9 @@ -using System; -using System.Collections.Generic; -using Prism.Common; -using Prism.Ioc; -using Prism.Properties; - namespace Prism.Navigation.Regions.Behaviors { /// /// Populates the target region with the views registered to it in the . /// - public class AutoPopulateRegionBehavior : RegionBehavior, IHostAwareRegionBehavior + public class AutoPopulateRegionBehavior : RegionBehavior { /// /// The key of this behavior. @@ -17,8 +11,6 @@ public class AutoPopulateRegionBehavior : RegionBehavior, IHostAwareRegionBehavi public const string BehaviorKey = "AutoPopulate"; private readonly IRegionViewRegistry regionViewRegistry; - private DependencyObject hostControl; - private bool isAttached; /// /// Creates a new instance of the AutoPopulateRegionBehavior @@ -30,23 +22,6 @@ public AutoPopulateRegionBehavior(IRegionViewRegistry regionViewRegistry) this.regionViewRegistry = regionViewRegistry; } - /// - /// Gets or sets the that the is attached to. - /// - public DependencyObject HostControl - { - get => hostControl; - set - { - if (isAttached) - { - throw new InvalidOperationException(Resources.HostControlCannotBeSetAfterAttach); - } - - hostControl = value; - } - } - /// /// Attaches the AutoPopulateRegionBehavior to the Region. /// @@ -64,15 +39,11 @@ protected override void OnAttach() private void StartPopulatingContent() { - isAttached = true; - foreach (object view in CreateViewsToAutoPopulate()) { AddViewIntoRegion(view); } - TryAddDefaultView(); - regionViewRegistry.ContentRegistered += OnViewRegistered; } @@ -83,7 +54,7 @@ private void StartPopulatingContent() /// protected virtual IEnumerable CreateViewsToAutoPopulate() { - return regionViewRegistry.GetContents(Region.Name, ContainerLocator.Container); + return regionViewRegistry.GetContents(Region.Name); } /// @@ -95,63 +66,6 @@ protected virtual void AddViewIntoRegion(object viewToAdd) Region.Add(viewToAdd); } - private void TryAddDefaultView() - { - if (HostControl == null) - { - return; - } - - var defaultView = RegionManager.GetDefaultView(HostControl); - if (defaultView == null) - { - return; - } - - if (defaultView is string targetName) - { - if (Region.GetView(targetName) != null) - { - return; - } - - var view = ContainerLocator.Container.Resolve(typeof(object), targetName); - if (!Region.Views.Contains(view)) - { - Region.Add(view, targetName); - } - } - else if (defaultView is Type viewType) - { - if (!RegionContainsViewOfType(viewType)) - { - var view = ContainerLocator.Container.Resolve(viewType); - MvvmHelpers.AutowireViewModel(view); - Region.Add(view); - } - } - else - { - if (!Region.Views.Contains(defaultView)) - { - Region.Add(defaultView); - } - } - } - - private bool RegionContainsViewOfType(Type viewType) - { - foreach (object view in Region.Views) - { - if (view != null && viewType.IsInstanceOfType(view)) - { - return true; - } - } - - return false; - } - private void Region_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { if (e.PropertyName == "Name" && !string.IsNullOrEmpty(Region.Name)) diff --git a/src/Wpf/Prism.Wpf/Navigation/Regions/RegionManager.cs b/src/Wpf/Prism.Wpf/Navigation/Regions/RegionManager.cs index 38957c352f..1d2a542ecc 100644 --- a/src/Wpf/Prism.Wpf/Navigation/Regions/RegionManager.cs +++ b/src/Wpf/Prism.Wpf/Navigation/Regions/RegionManager.cs @@ -111,6 +111,48 @@ private static void OnSetRegionNameCallback(DependencyObject element, Dependency if (!IsInDesignMode(element)) { CreateRegion(element); + RegisterDefaultViewWithRegion(element); + } + } + + private static void OnDefaultViewChanged(DependencyObject element, DependencyPropertyChangedEventArgs args) + { + if (!IsInDesignMode(element) && args.NewValue != null) + { + RegisterDefaultViewWithRegion(element, args.NewValue); + } + } + + private static void RegisterDefaultViewWithRegion(DependencyObject element) + { + var defaultView = GetDefaultView(element); + if (defaultView != null) + { + RegisterDefaultViewWithRegion(element, defaultView); + } + } + + private static void RegisterDefaultViewWithRegion(DependencyObject element, object defaultView) + { + var regionName = GetRegionName(element); + if (string.IsNullOrEmpty(regionName)) + { + return; + } + + var regionViewRegistry = ContainerLocator.Container.Resolve(); + + if (defaultView is string targetName) + { + regionViewRegistry.RegisterViewWithRegion(regionName, targetName); + } + else if (defaultView is Type viewType) + { + regionViewRegistry.RegisterViewWithRegion(regionName, viewType); + } + else + { + regionViewRegistry.RegisterViewWithRegion(regionName, _ => defaultView); } } @@ -176,7 +218,8 @@ public static void SetRegionManager(DependencyObject target, IRegionManager valu /// #if !AVALONIA public static readonly DependencyProperty DefaultViewProperty = - DependencyProperty.RegisterAttached("DefaultView", typeof(object), typeof(RegionManager), null); + DependencyProperty.RegisterAttached("DefaultView", typeof(object), typeof(RegionManager), + new PropertyMetadata(defaultValue: null, propertyChangedCallback: OnDefaultViewChanged)); #else public static readonly AvaloniaProperty DefaultViewProperty = AvaloniaProperty.RegisterAttached("DefaultView", typeof(RegionManager)); @@ -317,6 +360,7 @@ static RegionManager() // TODO: Could this go into the default constructor? RegionNameProperty.Changed.Subscribe(args => OnSetRegionNameCallback(args?.Sender, args)); RegionContextProperty.Changed.Subscribe(args => OnRegionContextChanged(args?.Sender, args)); + DefaultViewProperty.Changed.Subscribe(args => OnDefaultViewChanged(args?.Sender, args)); } #endif diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs index 125f3f868f..b07a93d9f5 100644 --- a/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using Avalonia.Controls; using Moq; using Prism.Avalonia.Tests.Mocks; using Prism.Ioc; @@ -91,54 +89,6 @@ public void WhenSameViewTypeRegisteredTwice_RegionContainsSingleView() Assert.Single(region.Views); } - [Fact] - public void ShouldAddDefaultViewByNameWhenRegionIsPopulated() - { - var containerMock = new Mock(); - ContainerLocator.SetContainerExtension(containerMock.Object); - var content = new object(); - containerMock.Setup(c => c.Resolve(typeof(object), "MyView")).Returns(content); - var registry = new RegionViewRegistry(containerMock.Object); - var host = new ContentControl(); - RegionManager.SetDefaultView(host, "MyView"); - - var region = new Region { Name = "MyRegion" }; - var behavior = new AutoPopulateRegionBehavior(registry) - { - Region = region, - HostControl = host - }; - - behavior.Attach(); - - Assert.Single(region.Views); - Assert.Same(content, region.Views.ElementAt(0)); - } - - [Fact] - public void WhenDefaultViewMatchesRegisteredView_RegionContainsSingleView() - { - var containerMock = new Mock(); - ContainerLocator.SetContainerExtension(containerMock.Object); - var content = new object(); - containerMock.Setup(c => c.Resolve(typeof(object), "MyView")).Returns(content); - var registry = new RegionViewRegistry(containerMock.Object); - registry.RegisterViewWithRegion("MyRegion", "MyView"); - var host = new ContentControl(); - RegionManager.SetDefaultView(host, "MyView"); - - var region = new Region { Name = "MyRegion" }; - var behavior = new AutoPopulateRegionBehavior(registry) - { - Region = region, - HostControl = host - }; - - behavior.Attach(); - - Assert.Single(region.Views); - } - [Fact] public void CanAttachBeforeSettingName() { diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionManagerFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionManagerFixture.cs index 255ecfc60a..e793141116 100644 --- a/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionManagerFixture.cs +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionManagerFixture.cs @@ -2,10 +2,12 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Threading.Tasks; +using Avalonia.Controls; using Moq; using Prism.Avalonia.Tests.Mocks; using Prism.Ioc; using Prism.Navigation.Regions; +using Prism.Navigation.Regions.Behaviors; using Xunit; namespace Prism.Avalonia.Tests.Regions @@ -448,6 +450,108 @@ public void CanAddRegionToRegionManager() Assert.Equal("region", region.Name); } + [Fact] + public void SettingDefaultViewType_RegistersViewWithRegion() + { + try + { + var mockRegionContentRegistry = new MockRegionContentRegistry(); + string regionName = null; + Type viewType = null; + mockRegionContentRegistry.RegisterContentWithViewType = (name, type) => + { + regionName = name; + viewType = type; + return null; + }; + + var containerMock = new Mock(); + containerMock.Setup(c => c.Resolve(typeof(IRegionViewRegistry))).Returns(mockRegionContentRegistry); + containerMock.Setup(c => c.Resolve(typeof(DelayedRegionCreationBehavior))) + .Returns(new DelayedRegionCreationBehavior(new RegionAdapterMappings())); + ContainerLocator.SetContainerExtension(containerMock.Object); + + var host = new ContentControl(); + RegionManager.SetRegionName(host, "MyRegion"); + RegionManager.SetDefaultView(host, typeof(object)); + + Assert.Equal("MyRegion", regionName); + Assert.Equal(typeof(object), viewType); + } + finally + { + ContainerLocator.ResetContainer(); + } + } + + [Fact] + public void SettingDefaultViewBeforeRegionName_RegistersWhenRegionNameIsSet() + { + try + { + var mockRegionContentRegistry = new MockRegionContentRegistry(); + string regionName = null; + Type viewType = null; + mockRegionContentRegistry.RegisterContentWithViewType = (name, type) => + { + regionName = name; + viewType = type; + return null; + }; + + var containerMock = new Mock(); + containerMock.Setup(c => c.Resolve(typeof(IRegionViewRegistry))).Returns(mockRegionContentRegistry); + containerMock.Setup(c => c.Resolve(typeof(DelayedRegionCreationBehavior))) + .Returns(new DelayedRegionCreationBehavior(new RegionAdapterMappings())); + ContainerLocator.SetContainerExtension(containerMock.Object); + + var host = new ContentControl(); + RegionManager.SetDefaultView(host, typeof(object)); + RegionManager.SetRegionName(host, "MyRegion"); + + Assert.Equal("MyRegion", regionName); + Assert.Equal(typeof(object), viewType); + } + finally + { + ContainerLocator.ResetContainer(); + } + } + + [Fact] + public void SettingDefaultViewTwiceWithDuplicateRegistry_RegionAutoPopulatesSingleView() + { + try + { + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + containerMock.Setup(c => c.Resolve(typeof(MockContentObject))).Returns(new MockContentObject()); + containerMock.Setup(c => c.Resolve(typeof(DelayedRegionCreationBehavior))) + .Returns(new DelayedRegionCreationBehavior(new RegionAdapterMappings())); + var registry = new RegionViewRegistry(containerMock.Object); + containerMock.Setup(c => c.Resolve(typeof(IRegionViewRegistry))).Returns(registry); + + var host = new ContentControl(); + RegionManager.SetRegionName(host, "MyRegion"); + RegionManager.SetDefaultView(host, typeof(MockContentObject)); + RegionManager.SetDefaultView(host, typeof(MockContentObject)); + + var region = new Region { Name = "MyRegion" }; + var behavior = new AutoPopulateRegionBehavior(registry) { Region = region }; + behavior.Attach(); + + Assert.Single(region.Views); + } + finally + { + ContainerLocator.ResetContainer(); + } + } + + private class MockContentObject + { + } + [Fact] public void ShouldThrowIfRegionNameArgumentIsDifferentToRegionNameProperty() { diff --git a/tests/Uno/Prism.Uno.WinUI.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs b/tests/Uno/Prism.Uno.WinUI.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs deleted file mode 100644 index a8516b70c3..0000000000 --- a/tests/Uno/Prism.Uno.WinUI.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Linq; -using Microsoft.UI.Xaml.Controls; -using Moq; -using Prism.Ioc; -using Prism.Navigation.Regions; -using Prism.Navigation.Regions.Behaviors; -using Xunit; - -namespace Prism.Uno.WinUI.Tests.Regions.Behaviors; - -public class AutoPopulateRegionBehaviorFixture -{ - private class MockContentObject - { - } - - [Fact] - public void WhenSameViewTypeRegisteredTwice_RegionContainsSingleView() - { - var containerMock = new Mock(); - ContainerLocator.SetContainerExtension(containerMock.Object); - containerMock.Setup(c => c.Resolve(typeof(MockContentObject))).Returns(new MockContentObject()); - var registry = new RegionViewRegistry(containerMock.Object); - registry.RegisterViewWithRegion("MyRegion", typeof(MockContentObject)); - registry.RegisterViewWithRegion("MyRegion", typeof(MockContentObject)); - - var region = new Region { Name = "MyRegion" }; - var behavior = new AutoPopulateRegionBehavior(registry) - { - Region = region - }; - - behavior.Attach(); - - Assert.Single(region.Views); - } - - [Fact] - public void ShouldAddDefaultViewByNameWhenRegionIsPopulated() - { - var containerMock = new Mock(); - ContainerLocator.SetContainerExtension(containerMock.Object); - var content = new object(); - containerMock.Setup(c => c.Resolve(typeof(object), "MyView")).Returns(content); - var registry = new RegionViewRegistry(containerMock.Object); - var host = new ContentControl(); - RegionManager.SetDefaultView(host, "MyView"); - - var region = new Region { Name = "MyRegion" }; - var behavior = new AutoPopulateRegionBehavior(registry) - { - Region = region, - HostControl = host - }; - - behavior.Attach(); - - Assert.Single(region.Views); - Assert.Same(content, region.Views.ElementAt(0)); - } - - [Fact] - public void WhenDefaultViewMatchesRegisteredView_RegionContainsSingleView() - { - var containerMock = new Mock(); - ContainerLocator.SetContainerExtension(containerMock.Object); - var content = new object(); - containerMock.Setup(c => c.Resolve(typeof(object), "MyView")).Returns(content); - var registry = new RegionViewRegistry(containerMock.Object); - registry.RegisterViewWithRegion("MyRegion", "MyView"); - var host = new ContentControl(); - RegionManager.SetDefaultView(host, "MyView"); - - var region = new Region { Name = "MyRegion" }; - var behavior = new AutoPopulateRegionBehavior(registry) - { - Region = region, - HostControl = host - }; - - behavior.Attach(); - - Assert.Single(region.Views); - } -} diff --git a/tests/Uno/Prism.Uno.WinUI.Tests/Regions/RegionManagerDefaultViewFixture.cs b/tests/Uno/Prism.Uno.WinUI.Tests/Regions/RegionManagerDefaultViewFixture.cs new file mode 100644 index 0000000000..125eebf17d --- /dev/null +++ b/tests/Uno/Prism.Uno.WinUI.Tests/Regions/RegionManagerDefaultViewFixture.cs @@ -0,0 +1,55 @@ +using Microsoft.UI.Xaml.Controls; +using Moq; +using Prism.Ioc; +using Prism.Navigation.Regions; +using Prism.Navigation.Regions.Behaviors; +using Xunit; + +namespace Prism.Uno.WinUI.Tests.Regions; + +public class RegionManagerDefaultViewFixture +{ + private class MockContentObject + { + } + + [Fact] + public void SettingDefaultViewType_RegistersViewWithRegion() + { + var mockRegistry = new Mock(); + var containerMock = new Mock(); + containerMock.Setup(c => c.Resolve(typeof(IRegionViewRegistry))).Returns(mockRegistry.Object); + containerMock.Setup(c => c.Resolve(typeof(DelayedRegionCreationBehavior))) + .Returns(new DelayedRegionCreationBehavior(new RegionAdapterMappings())); + ContainerLocator.SetContainerExtension(containerMock.Object); + + var host = new ContentControl(); + RegionManager.SetRegionName(host, "MyRegion"); + RegionManager.SetDefaultView(host, typeof(MockContentObject)); + + mockRegistry.Verify(r => r.RegisterViewWithRegion("MyRegion", typeof(MockContentObject)), Times.Once); + } + + [Fact] + public void SettingDefaultViewTwice_RegistersOnceDueToDuplicateGuard() + { + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + containerMock.Setup(c => c.Resolve(typeof(MockContentObject))).Returns(new MockContentObject()); + containerMock.Setup(c => c.Resolve(typeof(DelayedRegionCreationBehavior))) + .Returns(new DelayedRegionCreationBehavior(new RegionAdapterMappings())); + var registry = new RegionViewRegistry(containerMock.Object); + containerMock.Setup(c => c.Resolve(typeof(IRegionViewRegistry))).Returns(registry); + + var host = new ContentControl(); + RegionManager.SetRegionName(host, "MyRegion"); + RegionManager.SetDefaultView(host, typeof(MockContentObject)); + RegionManager.SetDefaultView(host, typeof(MockContentObject)); + + var region = new Region { Name = "MyRegion" }; + var behavior = new AutoPopulateRegionBehavior(registry) { Region = region }; + behavior.Attach(); + + Assert.Single(region.Views); + } +} diff --git a/tests/Wpf/Prism.Wpf.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs b/tests/Wpf/Prism.Wpf.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs index bc711fc0ff..82643234b0 100644 --- a/tests/Wpf/Prism.Wpf.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs +++ b/tests/Wpf/Prism.Wpf.Tests/Regions/Behaviors/AutoPopulateRegionBehaviorFixture.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Windows.Controls; using Moq; using Prism.Ioc; using Prism.Navigation.Regions; @@ -92,54 +90,6 @@ public void WhenSameViewTypeRegisteredTwice_RegionContainsSingleView() Assert.Single(region.Views); } - [StaFact] - public void ShouldAddDefaultViewByNameWhenRegionIsPopulated() - { - var containerMock = new Mock(); - ContainerLocator.SetContainerExtension(containerMock.Object); - var content = new object(); - containerMock.Setup(c => c.Resolve(typeof(object), "MyView")).Returns(content); - var registry = new RegionViewRegistry(containerMock.Object); - var host = new ContentControl(); - RegionManager.SetDefaultView(host, "MyView"); - - var region = new Region { Name = "MyRegion" }; - var behavior = new AutoPopulateRegionBehavior(registry) - { - Region = region, - HostControl = host - }; - - behavior.Attach(); - - Assert.Single(region.Views); - Assert.Same(content, region.Views.ElementAt(0)); - } - - [StaFact] - public void WhenDefaultViewMatchesRegisteredView_RegionContainsSingleView() - { - var containerMock = new Mock(); - ContainerLocator.SetContainerExtension(containerMock.Object); - var content = new object(); - containerMock.Setup(c => c.Resolve(typeof(object), "MyView")).Returns(content); - var registry = new RegionViewRegistry(containerMock.Object); - registry.RegisterViewWithRegion("MyRegion", "MyView"); - var host = new ContentControl(); - RegionManager.SetDefaultView(host, "MyView"); - - var region = new Region { Name = "MyRegion" }; - var behavior = new AutoPopulateRegionBehavior(registry) - { - Region = region, - HostControl = host - }; - - behavior.Attach(); - - Assert.Single(region.Views); - } - [Fact] public void CanAttachBeforeSettingName() { diff --git a/tests/Wpf/Prism.Wpf.Tests/Regions/RegionManagerFixture.cs b/tests/Wpf/Prism.Wpf.Tests/Regions/RegionManagerFixture.cs index 32aacd63bb..28f8b609cd 100644 --- a/tests/Wpf/Prism.Wpf.Tests/Regions/RegionManagerFixture.cs +++ b/tests/Wpf/Prism.Wpf.Tests/Regions/RegionManagerFixture.cs @@ -5,6 +5,7 @@ using Moq; using Prism.Ioc; using Prism.Navigation.Regions; +using Prism.Navigation.Regions.Behaviors; using Prism.Wpf.Tests.Mocks; using Xunit; @@ -449,6 +450,108 @@ public void CanAddRegionToRegionManager() Assert.Equal("region", region.Name); } + [StaFact] + public void SettingDefaultViewType_RegistersViewWithRegion() + { + try + { + var mockRegionContentRegistry = new MockRegionContentRegistry(); + string regionName = null; + Type viewType = null; + mockRegionContentRegistry.RegisterContentWithViewType = (name, type) => + { + regionName = name; + viewType = type; + return null; + }; + + var containerMock = new Mock(); + containerMock.Setup(c => c.Resolve(typeof(IRegionViewRegistry))).Returns(mockRegionContentRegistry); + containerMock.Setup(c => c.Resolve(typeof(DelayedRegionCreationBehavior))) + .Returns(new DelayedRegionCreationBehavior(new RegionAdapterMappings())); + ContainerLocator.SetContainerExtension(containerMock.Object); + + var host = new System.Windows.Controls.ContentControl(); + RegionManager.SetRegionName(host, "MyRegion"); + RegionManager.SetDefaultView(host, typeof(object)); + + Assert.Equal("MyRegion", regionName); + Assert.Equal(typeof(object), viewType); + } + finally + { + ContainerLocator.ResetContainer(); + } + } + + [StaFact] + public void SettingDefaultViewBeforeRegionName_RegistersWhenRegionNameIsSet() + { + try + { + var mockRegionContentRegistry = new MockRegionContentRegistry(); + string regionName = null; + Type viewType = null; + mockRegionContentRegistry.RegisterContentWithViewType = (name, type) => + { + regionName = name; + viewType = type; + return null; + }; + + var containerMock = new Mock(); + containerMock.Setup(c => c.Resolve(typeof(IRegionViewRegistry))).Returns(mockRegionContentRegistry); + containerMock.Setup(c => c.Resolve(typeof(DelayedRegionCreationBehavior))) + .Returns(new DelayedRegionCreationBehavior(new RegionAdapterMappings())); + ContainerLocator.SetContainerExtension(containerMock.Object); + + var host = new System.Windows.Controls.ContentControl(); + RegionManager.SetDefaultView(host, typeof(object)); + RegionManager.SetRegionName(host, "MyRegion"); + + Assert.Equal("MyRegion", regionName); + Assert.Equal(typeof(object), viewType); + } + finally + { + ContainerLocator.ResetContainer(); + } + } + + [StaFact] + public void SettingDefaultViewTwiceWithDuplicateRegistry_RegionAutoPopulatesSingleView() + { + try + { + var containerMock = new Mock(); + ContainerLocator.SetContainerExtension(containerMock.Object); + containerMock.Setup(c => c.Resolve(typeof(MockContentObject))).Returns(new MockContentObject()); + containerMock.Setup(c => c.Resolve(typeof(DelayedRegionCreationBehavior))) + .Returns(new DelayedRegionCreationBehavior(new RegionAdapterMappings())); + var registry = new RegionViewRegistry(containerMock.Object); + containerMock.Setup(c => c.Resolve(typeof(IRegionViewRegistry))).Returns(registry); + + var host = new System.Windows.Controls.ContentControl(); + RegionManager.SetRegionName(host, "MyRegion"); + RegionManager.SetDefaultView(host, typeof(MockContentObject)); + RegionManager.SetDefaultView(host, typeof(MockContentObject)); + + var region = new Region { Name = "MyRegion" }; + var behavior = new AutoPopulateRegionBehavior(registry) { Region = region }; + behavior.Attach(); + + Assert.Single(region.Views); + } + finally + { + ContainerLocator.ResetContainer(); + } + } + + private class MockContentObject + { + } + [Fact] public void ShouldThrowIfRegionNameArgumentIsDifferentToRegionNameProperty() { From 50b3dbc80f096e891196332ec0062d9e6ae75a15 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Mon, 1 Jun 2026 16:20:13 -0600 Subject: [PATCH 3/3] fix: stabilize Uno and Avalonia region default view tests --- .../Regions/RegionManagerFixture.cs | 13 ++++++++++--- .../Regions/RegionManagerDefaultViewFixture.cs | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionManagerFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionManagerFixture.cs index e793141116..072e228429 100644 --- a/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionManagerFixture.cs +++ b/tests/Avalonia/Prism.Avalonia.Tests/Regions/RegionManagerFixture.cs @@ -468,7 +468,7 @@ public void SettingDefaultViewType_RegistersViewWithRegion() var containerMock = new Mock(); containerMock.Setup(c => c.Resolve(typeof(IRegionViewRegistry))).Returns(mockRegionContentRegistry); containerMock.Setup(c => c.Resolve(typeof(DelayedRegionCreationBehavior))) - .Returns(new DelayedRegionCreationBehavior(new RegionAdapterMappings())); + .Returns(() => new DelayedRegionCreationBehavior(CreateRegionAdapterMappings())); ContainerLocator.SetContainerExtension(containerMock.Object); var host = new ContentControl(); @@ -502,7 +502,7 @@ public void SettingDefaultViewBeforeRegionName_RegistersWhenRegionNameIsSet() var containerMock = new Mock(); containerMock.Setup(c => c.Resolve(typeof(IRegionViewRegistry))).Returns(mockRegionContentRegistry); containerMock.Setup(c => c.Resolve(typeof(DelayedRegionCreationBehavior))) - .Returns(new DelayedRegionCreationBehavior(new RegionAdapterMappings())); + .Returns(() => new DelayedRegionCreationBehavior(CreateRegionAdapterMappings())); ContainerLocator.SetContainerExtension(containerMock.Object); var host = new ContentControl(); @@ -527,7 +527,7 @@ public void SettingDefaultViewTwiceWithDuplicateRegistry_RegionAutoPopulatesSing ContainerLocator.SetContainerExtension(containerMock.Object); containerMock.Setup(c => c.Resolve(typeof(MockContentObject))).Returns(new MockContentObject()); containerMock.Setup(c => c.Resolve(typeof(DelayedRegionCreationBehavior))) - .Returns(new DelayedRegionCreationBehavior(new RegionAdapterMappings())); + .Returns(() => new DelayedRegionCreationBehavior(CreateRegionAdapterMappings())); var registry = new RegionViewRegistry(containerMock.Object); containerMock.Setup(c => c.Resolve(typeof(IRegionViewRegistry))).Returns(registry); @@ -552,6 +552,13 @@ private class MockContentObject { } + private static RegionAdapterMappings CreateRegionAdapterMappings() + { + var mappings = new RegionAdapterMappings(); + mappings.RegisterMapping(typeof(ContentControl), new ContentControlRegionAdapter(null)); + return mappings; + } + [Fact] public void ShouldThrowIfRegionNameArgumentIsDifferentToRegionNameProperty() { diff --git a/tests/Uno/Prism.Uno.WinUI.Tests/Regions/RegionManagerDefaultViewFixture.cs b/tests/Uno/Prism.Uno.WinUI.Tests/Regions/RegionManagerDefaultViewFixture.cs index 125eebf17d..016193b8d2 100644 --- a/tests/Uno/Prism.Uno.WinUI.Tests/Regions/RegionManagerDefaultViewFixture.cs +++ b/tests/Uno/Prism.Uno.WinUI.Tests/Regions/RegionManagerDefaultViewFixture.cs @@ -13,7 +13,7 @@ private class MockContentObject { } - [Fact] + [Fact(Skip = "Requires a real Uno/WinUI runtime; unit tests run against ref assemblies.")] public void SettingDefaultViewType_RegistersViewWithRegion() { var mockRegistry = new Mock(); @@ -30,7 +30,7 @@ public void SettingDefaultViewType_RegistersViewWithRegion() mockRegistry.Verify(r => r.RegisterViewWithRegion("MyRegion", typeof(MockContentObject)), Times.Once); } - [Fact] + [Fact(Skip = "Requires a real Uno/WinUI runtime; unit tests run against ref assemblies.")] public void SettingDefaultViewTwice_RegistersOnceDueToDuplicateGuard() { var containerMock = new Mock();