From 6d085e8fe1eda0ccdb026ff8d3021c8bb228d743 Mon Sep 17 00:00:00 2001 From: Easley Date: Wed, 26 Nov 2025 18:12:59 +0800 Subject: [PATCH 1/3] add ItemsRegionBase,SelectingItemsRegion --- .../Sample.Avalonia/Regions/ListBoxRegion.cs | 6 +- samples/Sample.Avalonia/Views/BView.axaml | 24 +++---- src/AsyncNavigation.Avalonia/ContentRegion.cs | 33 ++++----- .../DependencyInjectionExtensions.cs | 1 + src/AsyncNavigation.Avalonia/ItemsRegion.cs | 61 ++-------------- .../ItemsRegionAdapter.cs | 4 -- .../ItemsRegionBase.cs | 70 +++++++++++++++++++ .../SelectingItemsRegion.cs | 23 ++++++ .../SelectingItemsRegionAdapter.cs | 13 ++++ src/AsyncNavigation.Avalonia/TabRegion.cs | 38 +++++----- src/AsyncNavigation.Wpf/ContentRegion.cs | 59 +++++++--------- src/AsyncNavigation.Wpf/ItemsRegion.cs | 57 ++++++++------- src/AsyncNavigation.Wpf/TabRegion.cs | 56 +++++++-------- .../Abstractions/IRegionAdapter.cs | 4 ++ .../Abstractions/IRegionFactory.cs | 4 ++ src/AsyncNavigation/RegionAdapterBase{T}.cs | 2 + src/AsyncNavigation/RegionBase.cs | 14 ++++ src/AsyncNavigation/RegionFactory.cs | 49 ++++++++++--- 18 files changed, 299 insertions(+), 219 deletions(-) create mode 100644 src/AsyncNavigation.Avalonia/ItemsRegionBase.cs create mode 100644 src/AsyncNavigation.Avalonia/SelectingItemsRegion.cs create mode 100644 src/AsyncNavigation.Avalonia/SelectingItemsRegionAdapter.cs diff --git a/samples/Sample.Avalonia/Regions/ListBoxRegion.cs b/samples/Sample.Avalonia/Regions/ListBoxRegion.cs index 8ce9ed8..ca04a3a 100644 --- a/samples/Sample.Avalonia/Regions/ListBoxRegion.cs +++ b/samples/Sample.Avalonia/Regions/ListBoxRegion.cs @@ -5,7 +5,7 @@ namespace Sample.Avalonia.Regions; -public class ListBoxRegion : ItemsRegion +public class ListBoxRegion : SelectingItemsRegion { private readonly ListBox _listBox; public ListBoxRegion(string name, @@ -16,8 +16,4 @@ public ListBoxRegion(string name, _listBox = listBox; _listBox.AutoScrollToSelectedItem = true; } - public override void ProcessActivate(NavigationContext navigationContext) - { - base.ProcessActivate(navigationContext); - } } diff --git a/samples/Sample.Avalonia/Views/BView.axaml b/samples/Sample.Avalonia/Views/BView.axaml index df4f4d4..a1345d3 100644 --- a/samples/Sample.Avalonia/Views/BView.axaml +++ b/samples/Sample.Avalonia/Views/BView.axaml @@ -1,22 +1,22 @@  + xmlns:vm="using:Sample.Common" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> + DockPanel.Dock="Top" + HorizontalAlignment="Stretch"> @@ -53,13 +53,13 @@ Tag="AView" /> - - + + - - + + diff --git a/src/AsyncNavigation.Avalonia/ContentRegion.cs b/src/AsyncNavigation.Avalonia/ContentRegion.cs index 5d0e7a7..b3fbf27 100644 --- a/src/AsyncNavigation.Avalonia/ContentRegion.cs +++ b/src/AsyncNavigation.Avalonia/ContentRegion.cs @@ -12,25 +12,6 @@ public ContentRegion(string name, IServiceProvider serviceProvider, bool? useCache) : base(name, contentControl, serviceProvider) { - ArgumentNullException.ThrowIfNull(contentControl); - ArgumentNullException.ThrowIfNull(serviceProvider); - - RegionControlAccessor.ExecuteOn(control => - { - control.Tag = this; - control.ContentTemplate = new FuncDataTemplate((context, np) => - { - return context?.IndicatorHost.Value?.Host as Control; - }); - - control.Bind( - ContentControl.ContentProperty, - new Binding(nameof(RegionContext.Selected)) { Source = _context, Mode = BindingMode.TwoWay }); - - }); - - - EnableViewCache = useCache ?? true; IsSinglePageRegion = true; } @@ -40,6 +21,20 @@ public override NavigationPipelineMode NavigationPipelineMode get => NavigationPipelineMode.RenderFirst; } + protected override void InitializeOnRegionCreated(ContentControl control) + { + base.InitializeOnRegionCreated(control); + control.Tag = this; + control.ContentTemplate = new FuncDataTemplate((context, np) => + { + return context?.IndicatorHost.Value?.Host as Control; + }); + + control.Bind( + ContentControl.ContentProperty, + new Binding(nameof(RegionContext.Selected)) { Source = _context, Mode = BindingMode.TwoWay }); + } + public override void Dispose() { base.Dispose(); diff --git a/src/AsyncNavigation.Avalonia/DependencyInjectionExtensions.cs b/src/AsyncNavigation.Avalonia/DependencyInjectionExtensions.cs index 3b9cd3d..71f00af 100644 --- a/src/AsyncNavigation.Avalonia/DependencyInjectionExtensions.cs +++ b/src/AsyncNavigation.Avalonia/DependencyInjectionExtensions.cs @@ -53,6 +53,7 @@ public static IServiceCollection AddNavigationSupport(this IServiceCollection se .RegisterRegionAdapter() .RegisterRegionAdapter() .RegisterRegionAdapter() + .RegisterRegionAdapter() .AddTransient() .AddSingleton() .RegisterDialogContainer(NavigationConstants.DEFAULT_DIALOG_WINDOW_KEY) diff --git a/src/AsyncNavigation.Avalonia/ItemsRegion.cs b/src/AsyncNavigation.Avalonia/ItemsRegion.cs index fd057b2..c3d6fee 100644 --- a/src/AsyncNavigation.Avalonia/ItemsRegion.cs +++ b/src/AsyncNavigation.Avalonia/ItemsRegion.cs @@ -1,68 +1,17 @@ -using AsyncNavigation.Core; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; -using Avalonia.Data; +using Avalonia.Controls; namespace AsyncNavigation.Avalonia; -public class ItemsRegion : RegionBase +public class ItemsRegion : ItemsRegionBase { public ItemsRegion(string name, ItemsControl itemsControl, IServiceProvider serviceProvider, - bool? useCache) : base(name, itemsControl, serviceProvider) + bool? useCache) : base(name, itemsControl, serviceProvider, useCache) { - ArgumentNullException.ThrowIfNull(itemsControl); - ArgumentNullException.ThrowIfNull(serviceProvider); - - RegionControlAccessor.ExecuteOn(control => - { - // binding the lifetime of region to the control - control.Tag = this; - control.ItemTemplate = new FuncDataTemplate((context, np) => - { - return context?.IndicatorHost.Value?.Host as Control; - }); - - control.Bind( - ItemsControl.ItemsSourceProperty, - new Binding(nameof(RegionContext.Items)) { Source = _context }); - - control.Bind( - SelectingItemsControl.SelectedItemProperty, - new Binding(nameof(RegionContext.Selected)) { Source = _context, Mode = BindingMode.TwoWay }); - }); - EnableViewCache = useCache ?? false; - IsSinglePageRegion = false; - } - public override NavigationPipelineMode NavigationPipelineMode - { - get => NavigationPipelineMode.RenderFirst; - } - public override void Dispose() - { - base.Dispose(); - _context.Clear(); + } - public override void ProcessActivate(NavigationContext navigationContext) - { - if (!_context.Items.Contains(navigationContext)) - _context.Items.Add(navigationContext); +} - _context.Selected = navigationContext; - RegionControlAccessor.ExecuteOn(control => - { - control.ScrollIntoView(navigationContext); - }); - } - public override void ProcessDeactivate(NavigationContext? navigationContext) - { - var target = navigationContext ?? _context.Selected; - if (target == null) - return; - _ = _context.Items.Remove(target); - } -} diff --git a/src/AsyncNavigation.Avalonia/ItemsRegionAdapter.cs b/src/AsyncNavigation.Avalonia/ItemsRegionAdapter.cs index b23ee7a..c10cffd 100644 --- a/src/AsyncNavigation.Avalonia/ItemsRegionAdapter.cs +++ b/src/AsyncNavigation.Avalonia/ItemsRegionAdapter.cs @@ -5,10 +5,6 @@ namespace AsyncNavigation.Avalonia; public class ItemsRegionAdapter : RegionAdapterBase { - public override bool IsAdapted(ItemsControl control) - { - return base.IsAdapted(control); - } public override IRegion CreateRegion(string name, ItemsControl control, IServiceProvider serviceProvider, bool? useCache) { return new ItemsRegion(name, control, serviceProvider, useCache); diff --git a/src/AsyncNavigation.Avalonia/ItemsRegionBase.cs b/src/AsyncNavigation.Avalonia/ItemsRegionBase.cs new file mode 100644 index 0000000..1bd131e --- /dev/null +++ b/src/AsyncNavigation.Avalonia/ItemsRegionBase.cs @@ -0,0 +1,70 @@ +using AsyncNavigation.Core; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Avalonia.Data; + +namespace AsyncNavigation.Avalonia; + +public abstract class ItemsRegionBase + : RegionBase + where TRegion : ItemsRegionBase + where TItemsControl : ItemsControl +{ + protected ItemsRegionBase( + string name, + TItemsControl control, + IServiceProvider serviceProvider, + bool? useCache) + : base(name, control, serviceProvider) + { + IsSinglePageRegion = false; + EnableViewCache = useCache ?? false; + } + + public override NavigationPipelineMode NavigationPipelineMode + => NavigationPipelineMode.RenderFirst; + + protected override void InitializeOnRegionCreated(TItemsControl control) + { + base.InitializeOnRegionCreated(control); + + control.Tag = this; + control.ItemTemplate = new FuncDataTemplate((context, _) => + { + return context?.IndicatorHost.Value?.Host as Control; + }); + + control.Bind( + ItemsControl.ItemsSourceProperty, + new Binding(nameof(RegionContext.Items)) { Source = _context }); + } + + + public override void ProcessActivate(NavigationContext navigationContext) + { + if (!_context.Items.Contains(navigationContext)) + _context.Items.Add(navigationContext); + + _context.Selected = navigationContext; + + RegionControlAccessor.ExecuteOn(control => + { + control.ScrollIntoView(navigationContext); + }); + } + + public override void ProcessDeactivate(NavigationContext? navigationContext) + { + var target = navigationContext ?? _context.Selected; + if (target == null) + return; + + _ = _context.Items.Remove(target); + } + + public override void Dispose() + { + base.Dispose(); + _context.Clear(); + } +} diff --git a/src/AsyncNavigation.Avalonia/SelectingItemsRegion.cs b/src/AsyncNavigation.Avalonia/SelectingItemsRegion.cs new file mode 100644 index 0000000..2d4ee24 --- /dev/null +++ b/src/AsyncNavigation.Avalonia/SelectingItemsRegion.cs @@ -0,0 +1,23 @@ +using AsyncNavigation.Core; +using Avalonia.Controls.Primitives; +using Avalonia.Data; + +namespace AsyncNavigation.Avalonia; + +public class SelectingItemsRegion : ItemsRegionBase +{ + public SelectingItemsRegion(string name, + SelectingItemsControl selectingItemsControl, + IServiceProvider serviceProvider, + bool? useCache) : base(name, selectingItemsControl, serviceProvider, useCache) + { + + } + protected override void InitializeOnRegionCreated(SelectingItemsControl control) + { + base.InitializeOnRegionCreated(control); + control.Bind( + SelectingItemsControl.SelectedItemProperty, + new Binding(nameof(RegionContext.Selected)) { Source = _context, Mode = BindingMode.TwoWay }); + } +} diff --git a/src/AsyncNavigation.Avalonia/SelectingItemsRegionAdapter.cs b/src/AsyncNavigation.Avalonia/SelectingItemsRegionAdapter.cs new file mode 100644 index 0000000..87c2123 --- /dev/null +++ b/src/AsyncNavigation.Avalonia/SelectingItemsRegionAdapter.cs @@ -0,0 +1,13 @@ +using AsyncNavigation.Abstractions; +using Avalonia.Controls.Primitives; + +namespace AsyncNavigation.Avalonia; + +public class SelectingItemsRegionAdapter : RegionAdapterBase +{ + public override uint Priority => 1; + public override IRegion CreateRegion(string name, SelectingItemsControl control, IServiceProvider serviceProvider, bool? useCache) + { + return new SelectingItemsRegion(name, control, serviceProvider, useCache); + } +} diff --git a/src/AsyncNavigation.Avalonia/TabRegion.cs b/src/AsyncNavigation.Avalonia/TabRegion.cs index bcec7d5..dabd352 100644 --- a/src/AsyncNavigation.Avalonia/TabRegion.cs +++ b/src/AsyncNavigation.Avalonia/TabRegion.cs @@ -12,26 +12,7 @@ public TabRegion(string name, TabControl tabControl, IServiceProvider serviceProvider, bool? useCache) : base(name, tabControl, serviceProvider) - { - ArgumentNullException.ThrowIfNull(tabControl); - ArgumentNullException.ThrowIfNull(serviceProvider); - - RegionControlAccessor.ExecuteOn(control => - { - control.Tag = this; - control.Bind(ItemsControl.ItemsSourceProperty, - new Binding(nameof(RegionContext.Items)) { Source = _context }); - - control.Bind(SelectingItemsControl.SelectedItemProperty, - new Binding(nameof(RegionContext.Selected)) { Source = _context, Mode = BindingMode.TwoWay }); - - control.ContentTemplate = new FuncDataTemplate((context, _) => - { - return context?.IndicatorHost.Value?.Host as Control; - }); - }); - - + { EnableViewCache = useCache ?? false; IsSinglePageRegion = false; } @@ -39,6 +20,23 @@ public override NavigationPipelineMode NavigationPipelineMode { get => NavigationPipelineMode.ResolveFirst; } + + protected override void InitializeOnRegionCreated(TabControl control) + { + base.InitializeOnRegionCreated(control); + control.Tag = this; + control.Bind(ItemsControl.ItemsSourceProperty, + new Binding(nameof(RegionContext.Items)) { Source = _context }); + + control.Bind(SelectingItemsControl.SelectedItemProperty, + new Binding(nameof(RegionContext.Selected)) { Source = _context, Mode = BindingMode.TwoWay }); + + control.ContentTemplate = new FuncDataTemplate((context, _) => + { + return context?.IndicatorHost.Value?.Host as Control; + }); + } + public override void Dispose() { base.Dispose(); diff --git a/src/AsyncNavigation.Wpf/ContentRegion.cs b/src/AsyncNavigation.Wpf/ContentRegion.cs index 06277b3..0dd7ce5 100644 --- a/src/AsyncNavigation.Wpf/ContentRegion.cs +++ b/src/AsyncNavigation.Wpf/ContentRegion.cs @@ -13,36 +13,6 @@ public ContentRegion(string name, IServiceProvider serviceProvider, bool? useCache) : base(name, contentControl, serviceProvider) { - ArgumentNullException.ThrowIfNull(contentControl); - ArgumentNullException.ThrowIfNull(serviceProvider); - - RegionControlAccessor.ExecuteOn(control => - { - control.Tag = this; - control.SetBinding(ContentControl.ContentProperty, - new Binding(nameof(RegionContext.Selected)) - { - Source = _context, - Mode = BindingMode.TwoWay - }); - }); - - RegionControlAccessor.ExecuteOn(control => - { - var dataTemplate = new DataTemplate(); - var factory = new FrameworkElementFactory(typeof(ContentPresenter)); - factory.SetBinding(ContentPresenter.ContentProperty, - new Binding("IndicatorHost.Value.Host") - { - FallbackValue = null - }); - dataTemplate.VisualTree = factory; - - control.ContentTemplate = dataTemplate; - }); - - - EnableViewCache = useCache ?? true; IsSinglePageRegion = true; } @@ -50,6 +20,30 @@ public override NavigationPipelineMode NavigationPipelineMode { get => NavigationPipelineMode.RenderFirst; } + + protected override void InitializeOnRegionCreated(ContentControl control) + { + base.InitializeOnRegionCreated(control); + control.Tag = this; + control.SetBinding(ContentControl.ContentProperty, + new Binding(nameof(RegionContext.Selected)) + { + Source = _context, + Mode = BindingMode.TwoWay + }); + + var dataTemplate = new DataTemplate(); + var factory = new FrameworkElementFactory(typeof(ContentPresenter)); + factory.SetBinding(ContentPresenter.ContentProperty, + new Binding("IndicatorHost.Value.Host") + { + FallbackValue = null + }); + dataTemplate.VisualTree = factory; + + control.ContentTemplate = dataTemplate; + } + public override void Dispose() { base.Dispose(); @@ -60,11 +54,6 @@ public override void Dispose() }); } - //public override void RenderIndicator(NavigationContext navigationContext) - //{ - // _context.Selected = navigationContext; - //} - public override void ProcessActivate(NavigationContext navigationContext) { _context.Selected = navigationContext; diff --git a/src/AsyncNavigation.Wpf/ItemsRegion.cs b/src/AsyncNavigation.Wpf/ItemsRegion.cs index 6833ca1..2a7a1e3 100644 --- a/src/AsyncNavigation.Wpf/ItemsRegion.cs +++ b/src/AsyncNavigation.Wpf/ItemsRegion.cs @@ -13,35 +13,6 @@ public ItemsRegion(string name, IServiceProvider serviceProvider, bool? useCache) : base(name, itemsControl, serviceProvider) { - ArgumentNullException.ThrowIfNull(itemsControl); - ArgumentNullException.ThrowIfNull(serviceProvider); - - RegionControlAccessor.ExecuteOn(control => - { - control.Tag = this; - control.SetBinding(ItemsControl.ItemsSourceProperty, - new Binding(nameof(RegionContext.Items)) - { - Source = _context - }); - - control.SetBinding(Selector.SelectedItemProperty, - new Binding(nameof(RegionContext.Selected)) - { - Source = _context, - Mode = BindingMode.TwoWay - }); - - var dataTemplate = new DataTemplate - { - VisualTree = new FrameworkElementFactory(typeof(ContentPresenter)) - }; - dataTemplate.VisualTree.SetBinding(ContentPresenter.ContentProperty, - new Binding("IndicatorHost.Value.Host")); - - control.ItemTemplate = dataTemplate; - }); - EnableViewCache = useCache ?? false; IsSinglePageRegion = false; } @@ -49,6 +20,34 @@ public override NavigationPipelineMode NavigationPipelineMode { get => NavigationPipelineMode.RenderFirst; } + + protected override void InitializeOnRegionCreated(ItemsControl control) + { + base.InitializeOnRegionCreated(control); + control.Tag = this; + control.SetBinding(ItemsControl.ItemsSourceProperty, + new Binding(nameof(RegionContext.Items)) + { + Source = _context + }); + + control.SetBinding(Selector.SelectedItemProperty, + new Binding(nameof(RegionContext.Selected)) + { + Source = _context, + Mode = BindingMode.TwoWay + }); + + var dataTemplate = new DataTemplate + { + VisualTree = new FrameworkElementFactory(typeof(ContentPresenter)) + }; + dataTemplate.VisualTree.SetBinding(ContentPresenter.ContentProperty, + new Binding("IndicatorHost.Value.Host")); + + control.ItemTemplate = dataTemplate; + } + public override void Dispose() { base.Dispose(); diff --git a/src/AsyncNavigation.Wpf/TabRegion.cs b/src/AsyncNavigation.Wpf/TabRegion.cs index 3bfefa8..138c6ef 100644 --- a/src/AsyncNavigation.Wpf/TabRegion.cs +++ b/src/AsyncNavigation.Wpf/TabRegion.cs @@ -13,34 +13,6 @@ public TabRegion(string name, IServiceProvider serviceProvider, bool? useCache = null) : base(name, tabControl, serviceProvider) { - ArgumentNullException.ThrowIfNull(tabControl); - ArgumentNullException.ThrowIfNull(serviceProvider); - - RegionControlAccessor.ExecuteOn(control => - { - control.Tag = this; - control.SetBinding(ItemsControl.ItemsSourceProperty, - new Binding(nameof(RegionContext.Items)) - { - Source = _context - }); - - control.SetBinding(Selector.SelectedItemProperty, - new Binding(nameof(RegionContext.Selected)) - { - Source = _context, - Mode = BindingMode.TwoWay - }); - - var dataTemplate = new DataTemplate - { - VisualTree = new FrameworkElementFactory(typeof(ContentPresenter)) - }; - dataTemplate.VisualTree.SetBinding(ContentPresenter.ContentProperty, - new Binding("IndicatorHost.Value.Host")); - - control.ContentTemplate = dataTemplate; - }); EnableViewCache = useCache ?? false; IsSinglePageRegion = false; } @@ -48,6 +20,34 @@ public override NavigationPipelineMode NavigationPipelineMode { get => NavigationPipelineMode.ResolveFirst; } + + protected override void InitializeOnRegionCreated(TabControl control) + { + base.InitializeOnRegionCreated(control); + control.Tag = this; + control.SetBinding(ItemsControl.ItemsSourceProperty, + new Binding(nameof(RegionContext.Items)) + { + Source = _context + }); + + control.SetBinding(Selector.SelectedItemProperty, + new Binding(nameof(RegionContext.Selected)) + { + Source = _context, + Mode = BindingMode.TwoWay + }); + + var dataTemplate = new DataTemplate + { + VisualTree = new FrameworkElementFactory(typeof(ContentPresenter)) + }; + dataTemplate.VisualTree.SetBinding(ContentPresenter.ContentProperty, + new Binding("IndicatorHost.Value.Host")); + + control.ContentTemplate = dataTemplate; + } + public override void Dispose() { base.Dispose(); diff --git a/src/AsyncNavigation/Abstractions/IRegionAdapter.cs b/src/AsyncNavigation/Abstractions/IRegionAdapter.cs index 325d17b..cfb2538 100644 --- a/src/AsyncNavigation/Abstractions/IRegionAdapter.cs +++ b/src/AsyncNavigation/Abstractions/IRegionAdapter.cs @@ -2,6 +2,10 @@ public interface IRegionAdapter { + /// + /// The priority of the adapter.Higher values indicate higher priority. + /// + uint Priority { get; } bool IsAdapted(object control); IRegion CreateRegion(string name, object control, IServiceProvider serviceProvider, bool? useCache); } diff --git a/src/AsyncNavigation/Abstractions/IRegionFactory.cs b/src/AsyncNavigation/Abstractions/IRegionFactory.cs index 5861638..06b27db 100644 --- a/src/AsyncNavigation/Abstractions/IRegionFactory.cs +++ b/src/AsyncNavigation/Abstractions/IRegionFactory.cs @@ -2,6 +2,10 @@ public interface IRegionFactory { + /// + /// Registers a region adapter with the specified priority. + /// + /// The region adapter to register. Cannot be . void RegisterAdapter(IRegionAdapter adapter); IRegion CreateRegion(string name, object control, diff --git a/src/AsyncNavigation/RegionAdapterBase{T}.cs b/src/AsyncNavigation/RegionAdapterBase{T}.cs index 2faa354..9b9340e 100644 --- a/src/AsyncNavigation/RegionAdapterBase{T}.cs +++ b/src/AsyncNavigation/RegionAdapterBase{T}.cs @@ -4,6 +4,8 @@ namespace AsyncNavigation; public abstract class RegionAdapterBase : IRegionAdapter { + public virtual uint Priority => 0; + public virtual bool IsAdapted(T control) { if (control == null) return false; diff --git a/src/AsyncNavigation/RegionBase.cs b/src/AsyncNavigation/RegionBase.cs index 71423ad..f6ceba5 100644 --- a/src/AsyncNavigation/RegionBase.cs +++ b/src/AsyncNavigation/RegionBase.cs @@ -21,6 +21,9 @@ public RegionBase(string name, TControl control, IServiceProvider serviceProvide _controlAccessor = new WeakRegionControlAccessor(control); _regionNavigationService = serviceProvider.GetRequiredService().Create((this as TRegion)!); _navigationHistory = serviceProvider.GetRequiredService(); + + RegionControlAccessor.ExecuteOn(InitializeOnRegionCreated); + } IRegionPresenter IRegion.RegionPresenter => this; @@ -48,6 +51,17 @@ Task IRegion.CanGoBackAsync() return Task.FromResult(_navigationHistory.CanGoBack); } + /// + /// Performs initialization logic when a region is created and associated with the specified control. + /// Binding logic or setup tasks related to the control can be implemented in this method. + /// + /// This method is intended to be overridden in a derived class to provide custom initialization + /// logic. The base implementation does not perform any actions. + /// The control associated with the newly created region. This parameter cannot be null. + protected virtual void InitializeOnRegionCreated(TControl control) + { + + } public async Task GoBackAsync(CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/AsyncNavigation/RegionFactory.cs b/src/AsyncNavigation/RegionFactory.cs index 34746aa..72f7fca 100644 --- a/src/AsyncNavigation/RegionFactory.cs +++ b/src/AsyncNavigation/RegionFactory.cs @@ -1,29 +1,56 @@ -using AsyncNavigation.Abstractions; +using System.Collections.Immutable; +using AsyncNavigation.Abstractions; namespace AsyncNavigation; internal sealed class RegionFactory : IRegionFactory { - private readonly HashSet _adapters; + private ImmutableArray _adapters = ImmutableArray.Empty; public RegionFactory(IEnumerable adapters) { - _adapters = [.. adapters]; + if (adapters != null && adapters.Any()) + { + _adapters = [.. adapters]; + } + else + { + _adapters = []; + } } + /// + /// Registers a region adapter for use in the application. + /// + /// The region adapter to register. Cannot be . public void RegisterAdapter(IRegionAdapter adapter) { - _adapters.Add(adapter); + ArgumentNullException.ThrowIfNull(adapter); + _adapters = _adapters.Add(adapter); } - public IRegion CreateRegion(string name, - object control, - IServiceProvider serviceProvider, + private IRegionAdapter? GetAdapter(object control) + { + var snapshot = _adapters; + + return snapshot + .OrderByDescending(a => a.Priority) + .ThenBy(a => a.GetType().FullName) + .FirstOrDefault(a => a.IsAdapted(control)); + } + + public IRegion CreateRegion( + string name, + object control, + IServiceProvider serviceProvider, bool? useCache = null) { - var adapter = _adapters.FirstOrDefault(a => a.IsAdapted(control)); - return adapter == null? - throw new NotSupportedException($"Unsupported control: {control.GetType()}"): - adapter.CreateRegion(name, control, serviceProvider, useCache); + ArgumentNullException.ThrowIfNull(control); + + var adapter = GetAdapter(control) + ?? throw new NotSupportedException( + $"No adapter found for control type: {control.GetType().Name}"); + + return adapter.CreateRegion(name, control, serviceProvider, useCache); } } From 8719cc03224d8417b53f36d77deece5c5c572436 Mon Sep 17 00:00:00 2001 From: Easley Date: Thu, 27 Nov 2025 17:46:10 +0800 Subject: [PATCH 2/3] remove selectingitemsregionadapter --- samples/Sample.Avalonia/Regions/ListBoxRegion.cs | 3 +-- samples/Sample.Avalonia/Views/BView.axaml | 14 +++++++------- .../DependencyInjectionExtensions.cs | 1 - src/AsyncNavigation.Avalonia/ItemsRegionBase.cs | 4 +++- .../SelectingItemsRegion.cs | 6 +++++- .../SelectingItemsRegionAdapter.cs | 13 ------------- src/AsyncNavigation/Core/RegionContext.cs | 1 + 7 files changed, 17 insertions(+), 25 deletions(-) delete mode 100644 src/AsyncNavigation.Avalonia/SelectingItemsRegionAdapter.cs diff --git a/samples/Sample.Avalonia/Regions/ListBoxRegion.cs b/samples/Sample.Avalonia/Regions/ListBoxRegion.cs index ca04a3a..017e3cb 100644 --- a/samples/Sample.Avalonia/Regions/ListBoxRegion.cs +++ b/samples/Sample.Avalonia/Regions/ListBoxRegion.cs @@ -1,5 +1,4 @@ -using AsyncNavigation; -using AsyncNavigation.Avalonia; +using AsyncNavigation.Avalonia; using Avalonia.Controls; using System; diff --git a/samples/Sample.Avalonia/Views/BView.axaml b/samples/Sample.Avalonia/Views/BView.axaml index a1345d3..cf106f6 100644 --- a/samples/Sample.Avalonia/Views/BView.axaml +++ b/samples/Sample.Avalonia/Views/BView.axaml @@ -53,13 +53,13 @@ Tag="AView" /> - - - - - - - + + + + + + + diff --git a/src/AsyncNavigation.Avalonia/DependencyInjectionExtensions.cs b/src/AsyncNavigation.Avalonia/DependencyInjectionExtensions.cs index 71f00af..3b9cd3d 100644 --- a/src/AsyncNavigation.Avalonia/DependencyInjectionExtensions.cs +++ b/src/AsyncNavigation.Avalonia/DependencyInjectionExtensions.cs @@ -53,7 +53,6 @@ public static IServiceCollection AddNavigationSupport(this IServiceCollection se .RegisterRegionAdapter() .RegisterRegionAdapter() .RegisterRegionAdapter() - .RegisterRegionAdapter() .AddTransient() .AddSingleton() .RegisterDialogContainer(NavigationConstants.DEFAULT_DIALOG_WINDOW_KEY) diff --git a/src/AsyncNavigation.Avalonia/ItemsRegionBase.cs b/src/AsyncNavigation.Avalonia/ItemsRegionBase.cs index 1bd131e..f67b689 100644 --- a/src/AsyncNavigation.Avalonia/ItemsRegionBase.cs +++ b/src/AsyncNavigation.Avalonia/ItemsRegionBase.cs @@ -2,6 +2,7 @@ using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Threading; namespace AsyncNavigation.Avalonia; @@ -46,7 +47,8 @@ public override void ProcessActivate(NavigationContext navigationContext) _context.Items.Add(navigationContext); _context.Selected = navigationContext; - + // https://github.com/AvaloniaUI/Avalonia/issues/17347 + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); RegionControlAccessor.ExecuteOn(control => { control.ScrollIntoView(navigationContext); diff --git a/src/AsyncNavigation.Avalonia/SelectingItemsRegion.cs b/src/AsyncNavigation.Avalonia/SelectingItemsRegion.cs index 2d4ee24..6412253 100644 --- a/src/AsyncNavigation.Avalonia/SelectingItemsRegion.cs +++ b/src/AsyncNavigation.Avalonia/SelectingItemsRegion.cs @@ -4,7 +4,11 @@ namespace AsyncNavigation.Avalonia; -public class SelectingItemsRegion : ItemsRegionBase +/// +/// Make this class abstract to avoid direct usage. +/// Because https://github.com/AvaloniaUI/Avalonia/issues/11139 +/// +public abstract class SelectingItemsRegion : ItemsRegionBase { public SelectingItemsRegion(string name, SelectingItemsControl selectingItemsControl, diff --git a/src/AsyncNavigation.Avalonia/SelectingItemsRegionAdapter.cs b/src/AsyncNavigation.Avalonia/SelectingItemsRegionAdapter.cs deleted file mode 100644 index 87c2123..0000000 --- a/src/AsyncNavigation.Avalonia/SelectingItemsRegionAdapter.cs +++ /dev/null @@ -1,13 +0,0 @@ -using AsyncNavigation.Abstractions; -using Avalonia.Controls.Primitives; - -namespace AsyncNavigation.Avalonia; - -public class SelectingItemsRegionAdapter : RegionAdapterBase -{ - public override uint Priority => 1; - public override IRegion CreateRegion(string name, SelectingItemsControl control, IServiceProvider serviceProvider, bool? useCache) - { - return new SelectingItemsRegion(name, control, serviceProvider, useCache); - } -} diff --git a/src/AsyncNavigation/Core/RegionContext.cs b/src/AsyncNavigation/Core/RegionContext.cs index 120f371..c055073 100644 --- a/src/AsyncNavigation/Core/RegionContext.cs +++ b/src/AsyncNavigation/Core/RegionContext.cs @@ -29,3 +29,4 @@ public void Clear() Selected = null; } } + From a25c55aab601ee2c6ab110eb8ae8d2c194074575 Mon Sep 17 00:00:00 2001 From: Easley Date: Thu, 27 Nov 2025 18:29:38 +0800 Subject: [PATCH 3/3] add test for regionfactory --- .../SelectingItemsRegion.cs | 2 +- src/AsyncNavigation/RegionFactory.cs | 2 +- .../AsyncNavigation.Tests.csproj | 1 + .../Infrastructure/Extensions.cs | 1 + .../AsyncNavigation.Tests/Mocks/TestRegion.cs | 4 + .../RegionFactoryTests.cs | 180 ++++++++++++++++++ 6 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 tests/AsyncNavigation.Tests/RegionFactoryTests.cs diff --git a/src/AsyncNavigation.Avalonia/SelectingItemsRegion.cs b/src/AsyncNavigation.Avalonia/SelectingItemsRegion.cs index 6412253..bf8177e 100644 --- a/src/AsyncNavigation.Avalonia/SelectingItemsRegion.cs +++ b/src/AsyncNavigation.Avalonia/SelectingItemsRegion.cs @@ -10,7 +10,7 @@ namespace AsyncNavigation.Avalonia; /// public abstract class SelectingItemsRegion : ItemsRegionBase { - public SelectingItemsRegion(string name, + protected SelectingItemsRegion(string name, SelectingItemsControl selectingItemsControl, IServiceProvider serviceProvider, bool? useCache) : base(name, selectingItemsControl, serviceProvider, useCache) diff --git a/src/AsyncNavigation/RegionFactory.cs b/src/AsyncNavigation/RegionFactory.cs index 72f7fca..31b7a00 100644 --- a/src/AsyncNavigation/RegionFactory.cs +++ b/src/AsyncNavigation/RegionFactory.cs @@ -7,7 +7,7 @@ internal sealed class RegionFactory : IRegionFactory { private ImmutableArray _adapters = ImmutableArray.Empty; - public RegionFactory(IEnumerable adapters) + public RegionFactory(IEnumerable? adapters) { if (adapters != null && adapters.Any()) { diff --git a/tests/AsyncNavigation.Tests/AsyncNavigation.Tests.csproj b/tests/AsyncNavigation.Tests/AsyncNavigation.Tests.csproj index 1492db4..d819018 100644 --- a/tests/AsyncNavigation.Tests/AsyncNavigation.Tests.csproj +++ b/tests/AsyncNavigation.Tests/AsyncNavigation.Tests.csproj @@ -25,6 +25,7 @@ all + all diff --git a/tests/AsyncNavigation.Tests/Infrastructure/Extensions.cs b/tests/AsyncNavigation.Tests/Infrastructure/Extensions.cs index f69248a..e022eaf 100644 --- a/tests/AsyncNavigation.Tests/Infrastructure/Extensions.cs +++ b/tests/AsyncNavigation.Tests/Infrastructure/Extensions.cs @@ -11,6 +11,7 @@ public static IServiceCollection AddNavigationTestSupport(this IServiceCollectio return serviceDescriptors .RegisterNavigationFramework(navigationOptions) .AddTransient() + .AddSingleton() .AddSingleton(); } } diff --git a/tests/AsyncNavigation.Tests/Mocks/TestRegion.cs b/tests/AsyncNavigation.Tests/Mocks/TestRegion.cs index 8078a6b..f825ffa 100644 --- a/tests/AsyncNavigation.Tests/Mocks/TestRegion.cs +++ b/tests/AsyncNavigation.Tests/Mocks/TestRegion.cs @@ -9,6 +9,10 @@ public class TestRegion : RegionBase, IRegionPresenter public TestRegion(string name, object control, IServiceProvider serviceProvider) : base(name, control, serviceProvider) { + } + public static TestRegion GetOne(IServiceProvider serviceProvider) + { + return new TestRegion("TestRegion", new object(), serviceProvider); } public bool IsActive { get; private set; } public NavigationContext? Current { get; private set; } diff --git a/tests/AsyncNavigation.Tests/RegionFactoryTests.cs b/tests/AsyncNavigation.Tests/RegionFactoryTests.cs new file mode 100644 index 0000000..9398f06 --- /dev/null +++ b/tests/AsyncNavigation.Tests/RegionFactoryTests.cs @@ -0,0 +1,180 @@ +using AsyncNavigation; +using AsyncNavigation.Abstractions; +using AsyncNavigation.Tests.Mocks; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Xunit; + +namespace AsyncNavigation.Tests; + +public class RegionFactoryTests +{ + [Fact] + public void Constructor_WithNullAdapters_ShouldInitializeEmptyList() + { + // Act + var factory = new RegionFactory(null); + + // Assert + var adapters = GetAdaptersField(factory); + Assert.Empty(adapters); + } + + [Fact] + public void Constructor_WithEmptyAdapters_ShouldInitializeEmptyList() + { + // Arrange + var adapters = Enumerable.Empty(); + + // Act + var factory = new RegionFactory(adapters); + + // Assert + var internalAdapters = GetAdaptersField(factory); + Assert.Empty(internalAdapters); + } + + [Fact] + public void RegisterAdapter_Null_ThrowsArgumentNullException() + { + // Arrange + var factory = new RegionFactory(null); + + // Act & Assert + Assert.Throws(() => factory.RegisterAdapter(null!)); + } + + [Fact] + public void RegisterAdapter_ValidAdapter_AddsToInternalList() + { + // Arrange + var factory = new RegionFactory(null); + var mockAdapter = new Mock(); + mockAdapter.Setup(a => a.Priority).Returns((uint)0); + + // Act + factory.RegisterAdapter(mockAdapter.Object); + + // Assert + var adapters = GetAdaptersField(factory); + Assert.Single(adapters); + Assert.Same(mockAdapter.Object, adapters[0]); + } + + [Fact] + public void GetAdapter_SelectsHighestPriorityAndThenByName() + { + var control = new object(); + + var adapterLow = new Mock(); + adapterLow.Setup(a => a.Priority).Returns((uint)1); + adapterLow.Setup(a => a.IsAdapted(control)).Returns(true); + + var adapterHigh1 = new Mock(); + adapterHigh1.Setup(a => a.Priority).Returns((uint)2); + adapterHigh1.Setup(a => a.IsAdapted(control)).Returns(true); + + var adapterHigh2 = new Mock(); + adapterHigh2.Setup(a => a.Priority).Returns((uint)2); + adapterHigh2.Setup(a => a.IsAdapted(control)).Returns(true); + + var factory = new RegionFactory([ + adapterLow.Object, + adapterHigh1.Object, + adapterHigh2.Object + ]); + + var selected = GetSelectedAdapterViaReflection(factory, control); + + Assert.Same(adapterHigh1.Object, selected); + } + + + [Fact] + public void CreateRegion_NoMatchingAdapter_ThrowsNotSupportedException() + { + // Arrange + var factory = new RegionFactory(null); + var control = new object(); + + // Act & Assert + var ex = Assert.Throws(() => + factory.CreateRegion("test", control, Mock.Of())); + + Assert.Contains("No adapter found for control type: Object", ex.Message); + } + + [Fact] + public void CreateRegion_MatchingAdapter_CallsCreateRegionOnAdapter() + { + // Arrange + var services = new ServiceCollection(); + services.AddNavigationTestSupport(); + var control = new object(); + var mockAdapter = new Mock(); + mockAdapter.Setup(a => a.Priority).Returns((uint)0); + mockAdapter.Setup(a => a.IsAdapted(control)).Returns(true); + + var serviceProvider = services.BuildServiceProvider(); + var mockRegion = TestRegion.GetOne(serviceProvider); + + + mockAdapter.Setup(a => a.CreateRegion("test", control, serviceProvider, null)) + .Returns(mockRegion); + + var factory = new RegionFactory([mockAdapter.Object]); + + // Act + var region = factory.CreateRegion("test", control, serviceProvider); + + // Assert + Assert.Same(mockRegion, region); + mockAdapter.Verify(a => a.CreateRegion("test", control, serviceProvider, null), Times.Once); + } + + // --- Helper Methods --- + + private static ImmutableArray GetAdaptersField(RegionFactory factory) + { + var field = typeof(RegionFactory).GetField("_adapters", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + return (ImmutableArray)field!.GetValue(factory)!; + } + + private static IRegionAdapter? GetSelectedAdapterViaReflection(RegionFactory factory, object control) + { + var method = typeof(RegionFactory).GetMethod("GetAdapter", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + return (IRegionAdapter?)method!.Invoke(factory, new object[] { control }); + } + + // --- Dummy Adapter Implementations for Ordering Test --- + + private sealed class AdapterHighA : IRegionAdapter + { + public uint Priority => 2; + public bool IsAdapted(object control) => true; + public IRegion CreateRegion(string name, object control, IServiceProvider serviceProvider, bool? useCache) => + throw new NotImplementedException(); + } + + private sealed class AdapterHighB : IRegionAdapter + { + public uint Priority => 2; + public bool IsAdapted(object control) => true; + public IRegion CreateRegion(string name, object control, IServiceProvider serviceProvider, bool? useCache) => + throw new NotImplementedException(); + } + + private sealed class AdapterLow : IRegionAdapter + { + public uint Priority => 1; + public bool IsAdapted(object control) => true; + public IRegion CreateRegion(string name, object control, IServiceProvider serviceProvider, bool? useCache) => + throw new NotImplementedException(); + } +} \ No newline at end of file