From 30b9bdebf5307370292d004c18d68c087714cfed Mon Sep 17 00:00:00 2001 From: Easley Date: Sat, 15 Aug 2026 20:01:20 +0200 Subject: [PATCH 1/4] support winui --- src/AsyncNavigation.WinUI/AssemblyInfo.cs | 6 + .../AsyncNavigation.WinUI.csproj | 19 +++ src/AsyncNavigation.WinUI/ContentRegion.cs | 57 ++++++++ .../ContentRegionAdapter.cs | 16 +++ .../DefaultDialogContainer.xaml | 11 ++ .../DefaultDialogContainer.xaml.cs | 15 ++ .../DependencyInjectionExtensions.cs | 129 ++++++++++++++++++ .../IInnerIndicatorProvider.cs | 9 ++ .../IconDescriptorConverter.cs | 30 ++++ src/AsyncNavigation.WinUI/IconResolver.cs | 114 ++++++++++++++++ .../InnerIndicatorHost.cs | 84 ++++++++++++ src/AsyncNavigation.WinUI/ItemsRegion.cs | 72 ++++++++++ .../ItemsRegionAdapter.cs | 17 +++ src/AsyncNavigation.WinUI/PlatformService.cs | 78 +++++++++++ src/AsyncNavigation.WinUI/RegionManager.cs | 88 ++++++++++++ src/AsyncNavigation.WinUI/TabRegion.cs | 73 ++++++++++ src/AsyncNavigation.WinUI/TabRegionAdapter.cs | 16 +++ .../XamlTemplateHelper.cs | 19 +++ src/AsyncNavigation.slnx | 1 + 19 files changed, 854 insertions(+) create mode 100644 src/AsyncNavigation.WinUI/AssemblyInfo.cs create mode 100644 src/AsyncNavigation.WinUI/AsyncNavigation.WinUI.csproj create mode 100644 src/AsyncNavigation.WinUI/ContentRegion.cs create mode 100644 src/AsyncNavigation.WinUI/ContentRegionAdapter.cs create mode 100644 src/AsyncNavigation.WinUI/DefaultDialogContainer.xaml create mode 100644 src/AsyncNavigation.WinUI/DefaultDialogContainer.xaml.cs create mode 100644 src/AsyncNavigation.WinUI/DependencyInjectionExtensions.cs create mode 100644 src/AsyncNavigation.WinUI/IInnerIndicatorProvider.cs create mode 100644 src/AsyncNavigation.WinUI/IconDescriptorConverter.cs create mode 100644 src/AsyncNavigation.WinUI/IconResolver.cs create mode 100644 src/AsyncNavigation.WinUI/InnerIndicatorHost.cs create mode 100644 src/AsyncNavigation.WinUI/ItemsRegion.cs create mode 100644 src/AsyncNavigation.WinUI/ItemsRegionAdapter.cs create mode 100644 src/AsyncNavigation.WinUI/PlatformService.cs create mode 100644 src/AsyncNavigation.WinUI/RegionManager.cs create mode 100644 src/AsyncNavigation.WinUI/TabRegion.cs create mode 100644 src/AsyncNavigation.WinUI/TabRegionAdapter.cs create mode 100644 src/AsyncNavigation.WinUI/XamlTemplateHelper.cs diff --git a/src/AsyncNavigation.WinUI/AssemblyInfo.cs b/src/AsyncNavigation.WinUI/AssemblyInfo.cs new file mode 100644 index 0000000..b8c6d17 --- /dev/null +++ b/src/AsyncNavigation.WinUI/AssemblyInfo.cs @@ -0,0 +1,6 @@ +using Microsoft.UI.Xaml.Markup; + +[assembly: XmlnsDefinition("https://github.com/NeverMorewd/AsyncNavigation", "AsyncNavigation")] +[assembly: XmlnsDefinition("https://github.com/NeverMorewd/AsyncNavigation", "AsyncNavigation.WinUI")] +[assembly: XmlnsDefinition("https://github.com/NeverMorewd/AsyncNavigation", "AsyncNavigation.Abstractions")] +[assembly: XmlnsDefinition("https://github.com/NeverMorewd/AsyncNavigation", "AsyncNavigation.Core")] \ No newline at end of file diff --git a/src/AsyncNavigation.WinUI/AsyncNavigation.WinUI.csproj b/src/AsyncNavigation.WinUI/AsyncNavigation.WinUI.csproj new file mode 100644 index 0000000..6ad6107 --- /dev/null +++ b/src/AsyncNavigation.WinUI/AsyncNavigation.WinUI.csproj @@ -0,0 +1,19 @@ + + + net8.0-windows10.0.19041.0 + 10.0.17763.0 + AsyncNavigation.WinUI + true + false + false + true + enable + + + + + + + + + \ No newline at end of file diff --git a/src/AsyncNavigation.WinUI/ContentRegion.cs b/src/AsyncNavigation.WinUI/ContentRegion.cs new file mode 100644 index 0000000..d46fb3f --- /dev/null +++ b/src/AsyncNavigation.WinUI/ContentRegion.cs @@ -0,0 +1,57 @@ +using AsyncNavigation.Core; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Data; +using System; +using System.Threading.Tasks; + +namespace AsyncNavigation.WinUI; + +public partial class ContentRegion : RegionBase +{ + public ContentRegion(string name, + ContentControl contentControl, + IServiceProvider serviceProvider, + bool? useCache) : base(name, contentControl, serviceProvider) + { + EnableViewCache = useCache ?? true; + IsSinglePageRegion = true; + } + 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 + { + Path = new Microsoft.UI.Xaml.PropertyPath(nameof(RegionContext.Selected)), + Source = _context, + Mode = BindingMode.TwoWay + }); + control.ContentTemplate = XamlTemplateHelper.CreateIndicatorHostTemplate(); + } + public override void Dispose() + { + base.Dispose(); + _context.Selected = null; + RegionControlAccessor.ExecuteOn(control => + { + control.Content = null; + }); + } + + public override Task ProcessActivateAsync(NavigationContext navigationContext) + { + _context.Selected = navigationContext; + return Task.CompletedTask; + } + + public override Task ProcessDeactivateAsync(NavigationContext? navigationContext) + { + _context.Selected = null; + return Task.CompletedTask; + } +} diff --git a/src/AsyncNavigation.WinUI/ContentRegionAdapter.cs b/src/AsyncNavigation.WinUI/ContentRegionAdapter.cs new file mode 100644 index 0000000..2e12d6a --- /dev/null +++ b/src/AsyncNavigation.WinUI/ContentRegionAdapter.cs @@ -0,0 +1,16 @@ +using AsyncNavigation.Abstractions; +using Microsoft.UI.Xaml.Controls; +using System; + +namespace AsyncNavigation.WinUI; + +public class ContentRegionAdapter : RegionAdapterBase +{ + public override IRegion CreateRegion(string name, + ContentControl control, + IServiceProvider serviceProvider, + bool? useCache) + { + return new ContentRegion(name, control, serviceProvider, useCache); + } +} diff --git a/src/AsyncNavigation.WinUI/DefaultDialogContainer.xaml b/src/AsyncNavigation.WinUI/DefaultDialogContainer.xaml new file mode 100644 index 0000000..41548aa --- /dev/null +++ b/src/AsyncNavigation.WinUI/DefaultDialogContainer.xaml @@ -0,0 +1,11 @@ + diff --git a/src/AsyncNavigation.WinUI/DefaultDialogContainer.xaml.cs b/src/AsyncNavigation.WinUI/DefaultDialogContainer.xaml.cs new file mode 100644 index 0000000..803b06c --- /dev/null +++ b/src/AsyncNavigation.WinUI/DefaultDialogContainer.xaml.cs @@ -0,0 +1,15 @@ +using AsyncNavigation.Abstractions; +using System.Windows; + +namespace AsyncNavigation.WinUI; + +/// +/// Interaction logic for DefaultDialogContainer.xaml +/// +public partial class DefaultDialogContainer : Window, IDialogWindow +{ + public DefaultDialogContainer() + { + InitializeComponent(); + } +} diff --git a/src/AsyncNavigation.WinUI/DependencyInjectionExtensions.cs b/src/AsyncNavigation.WinUI/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..7983650 --- /dev/null +++ b/src/AsyncNavigation.WinUI/DependencyInjectionExtensions.cs @@ -0,0 +1,129 @@ +using AsyncNavigation; +using AsyncNavigation.Abstractions; +using AsyncNavigation.WinUI; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.UI.Xaml; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class DependencyInjectionExtensions +{ + /// + /// Registers the navigation framework services into the dependency injection container. + /// + /// + /// The used to register navigation-related services. + /// + /// + /// Optional navigation configuration. If provided, it will merge into the global . + /// + /// + /// The updated with navigation support registered. + /// + /// + /// This method will: + /// + /// + /// Merge the specified into . + /// + /// + /// Register as a singleton or transient service depending on + /// . + /// + /// + /// Register built-in region adapters ( and ). + /// + /// + /// Register region navigation, view factory, cache manager, indicator services, and . + /// + /// + /// This extension method is typically called during application startup: + /// + /// services.AddNavigationSupport(new NavigationOptions + /// { + /// NavigationTaskScope = NavigationTaskScope.Global + /// }); + /// + /// + + public static IServiceCollection AddNavigationSupport(this IServiceCollection serviceDescriptors, NavigationOptions? navigationOptions = null) + { + return serviceDescriptors + .RegisterNavigationFramework(navigationOptions) + .RegisterRegionAdapter() + .RegisterRegionAdapter() + .RegisterRegionAdapter() + .AddTransient() + .AddSingleton() + .AddTransient, IconResolver>() + .AddSingleton() + .AddSingleton(sp => sp.GetRequiredService()) + .RegisterDialogContainer(NavigationConstants.DEFAULT_DIALOG_WINDOW_KEY) + .AddSingleton(); + } + + /// + /// Registers a custom implementation into the dependency injection container. + /// + /// + /// The type of the to register. + /// Typically, this adapter is designed to work with a specific container control + /// (e.g.ContentControl,TabControl). + /// + /// The service collection to add the registration to. + /// The same instance for method chaining. + public static IServiceCollection RegisterRegionAdapter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(this IServiceCollection serviceDescriptors) + where T : class, IRegionAdapter + { + return serviceDescriptors.AddSingleton(); + } + + /// + /// Registers an implementation of in the DI container as a transient service. + /// + /// + /// The concrete type that implements . + /// + /// + /// The to add the service descriptor to. + /// + /// + /// The updated for method chaining. + /// + /// + /// This method uses TryAddTransient, which means: + /// + /// + /// If no service of type is registered, it will add . + /// + /// + /// If a service of type is already registered, this call will be ignored. + /// + /// + /// + /// ⚠ Important: This method should be called only once. Subsequent calls will not override the previous registration. + /// If you need to override an existing registration, consider using Replace from + /// . + /// + /// + public static IServiceCollection RegisterInnerIndicatorProvider<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(this IServiceCollection serviceDescriptors) + where T : class, IInnerIndicatorProvider + { + serviceDescriptors.TryAddTransient(); + return serviceDescriptors; + } + + + /// + /// Override default IconResolver + /// + /// + /// + /// + public static IServiceCollection RegisterIconResolver<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(this IServiceCollection serviceDescriptors) + where T : class, IIconResolver + { + return serviceDescriptors.AddTransient, T>(); + } +} diff --git a/src/AsyncNavigation.WinUI/IInnerIndicatorProvider.cs b/src/AsyncNavigation.WinUI/IInnerIndicatorProvider.cs new file mode 100644 index 0000000..80889e7 --- /dev/null +++ b/src/AsyncNavigation.WinUI/IInnerIndicatorProvider.cs @@ -0,0 +1,9 @@ +using AsyncNavigation.Abstractions; +using Microsoft.UI.Xaml; + +namespace AsyncNavigation.WinUI; + +public interface IInnerIndicatorProvider : IInnerIndicatorProviderBase +{ + +} diff --git a/src/AsyncNavigation.WinUI/IconDescriptorConverter.cs b/src/AsyncNavigation.WinUI/IconDescriptorConverter.cs new file mode 100644 index 0000000..a90d518 --- /dev/null +++ b/src/AsyncNavigation.WinUI/IconDescriptorConverter.cs @@ -0,0 +1,30 @@ +using AsyncNavigation.Abstractions; +using AsyncNavigation.Core; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Data; +using System; + +namespace AsyncNavigation.WinUI; + +public partial class IconDescriptorConverter : IValueConverter +{ + private readonly IIconResolver _iconResolver; + + public IconDescriptorConverter(IIconResolver iconResolver) + { + _iconResolver = iconResolver; + } + + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is not IconDescriptor descriptor) return null; + + var size = parameter is double d ? d : 24; + return _iconResolver.Resolve(descriptor, size)!; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/AsyncNavigation.WinUI/IconResolver.cs b/src/AsyncNavigation.WinUI/IconResolver.cs new file mode 100644 index 0000000..04e34d3 --- /dev/null +++ b/src/AsyncNavigation.WinUI/IconResolver.cs @@ -0,0 +1,114 @@ +using AsyncNavigation.Abstractions; +using AsyncNavigation.Core; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media.Imaging; +using System; +using System.Collections.Generic; +using System.IO; + +namespace AsyncNavigation.WinUI; + +public class IconResolver : IIconResolver +{ + private readonly Dictionary _fileCache = []; + + public FrameworkElement? Resolve(IconDescriptor descriptor, double size = 24) + { + return descriptor.Kind switch + { + IconKind.FilePath => ResolveFile(descriptor.Value, size), + IconKind.PathData => ResolvePathData(descriptor.Value, size), + IconKind.IconFont => ResolveThemeIcon(descriptor.Value, size), + IconKind.ResourceKey => ResolveResourceKey(descriptor.Value, size), + _ => null + }; + } + + protected virtual Image? ResolveFile(string path, double size) + { + var fullPath = System.IO.Path.Combine(AppContext.BaseDirectory, path); + if (!System.IO.File.Exists(fullPath)) return null; + + if (!_fileCache.TryGetValue(fullPath, out var bitmap)) + { + bitmap = new BitmapImage(new Uri(fullPath, UriKind.Absolute)); + _fileCache[fullPath] = bitmap; + } + + return new Image + { + Source = bitmap, + Width = size, + Height = size + }; + } + + protected virtual Path? ResolvePathData(string data, double size) + { + if (string.IsNullOrWhiteSpace(data)) return null; + + var path = new Path + { + Data = Geometry.Parse(data), + Width = size, + Height = size, + Stretch = Stretch.Uniform, + }; + + // Bind Fill to ancestor Foreground so it follows theme color automatically + path.SetBinding(Path.FillProperty, new Binding("Foreground") + { + RelativeSource = new RelativeSource( + RelativeSourceMode.FindAncestor, + typeof(Control), + 1) + }); + + return path; + } + + protected virtual TextBlock ResolveThemeIcon(string name, double size) + { + return new TextBlock + { + Text = name, + FontSize = size, + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Center, + }; + } + + protected virtual FrameworkElement? ResolveResourceKey(string resourceKey, double size) + { + if (Application.Current is null) return null; + + var resource = Application.Current.TryFindResource(resourceKey); + if (resource is null) return null; + + return resource switch + { + Geometry geometry => new Path + { + Data = geometry, + Width = size, + Height = size, + Stretch = Stretch.Uniform, + }, + DrawingImage drawing => new Image + { + Source = drawing, + Width = size, + Height = size + }, + ImageSource imageSource => new Image + { + Source = imageSource, + Width = size, + Height = size + }, + string pathData => ResolvePathData(pathData, size), + _ => null + }; + } +} diff --git a/src/AsyncNavigation.WinUI/InnerIndicatorHost.cs b/src/AsyncNavigation.WinUI/InnerIndicatorHost.cs new file mode 100644 index 0000000..dca2b85 --- /dev/null +++ b/src/AsyncNavigation.WinUI/InnerIndicatorHost.cs @@ -0,0 +1,84 @@ +using AsyncNavigation.Abstractions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using System; +using System.Threading.Tasks; + +namespace AsyncNavigation.WinUI; + +internal sealed class InnerIndicatorHost : IInnerRegionIndicatorHost, IInnerIndicatorProvider +{ + private readonly ContentControl _host; + private readonly IInnerIndicatorProvider _innerIndicatorProvider; + public InnerIndicatorHost(IServiceProvider serviceProvider) + { + _host = new ContentControl(); + var innerIndicatorProvider = serviceProvider.GetService(); + if (innerIndicatorProvider != null) + { + _innerIndicatorProvider = innerIndicatorProvider; + } + else + { + _innerIndicatorProvider = this; + } + } + + public object Host => _host; + + public UIElement GetErrorIndicator(NavigationContext navigationContext) + { + return new Grid(); + } + + public UIElement GetLoadingIndicator(NavigationContext navigationContext) + { + return new Grid(); + } + + public bool HasErrorIndicator(NavigationContext navigationContext) + { + return false; + } + + public bool HasLoadingIndicator(NavigationContext navigationContext) + { + return false; + } + + Task IRegionIndicator.OnCancelledAsync(NavigationContext context) + { + _host.Content = null; + return Task.CompletedTask; + } + + Task IRegionIndicator.OnLoadedAsync(NavigationContext context) + { + return Task.CompletedTask; + } + + Task IInnerRegionIndicatorHost.ShowContentAsync(NavigationContext context) + { + _host.Content = context.Target.Value; + return Task.CompletedTask; + } + + Task IRegionIndicator.ShowErrorAsync(NavigationContext context, Exception? innerException) + { + if (_innerIndicatorProvider.HasErrorIndicator(context)) + { + _host.Content = _innerIndicatorProvider.GetErrorIndicator(context); + } + return Task.CompletedTask; + } + + Task IRegionIndicator.ShowLoadingAsync(NavigationContext context) + { + if (_innerIndicatorProvider.HasLoadingIndicator(context)) + { + _host.Content = _innerIndicatorProvider.GetLoadingIndicator(context); + } + return Task.CompletedTask; + } +} diff --git a/src/AsyncNavigation.WinUI/ItemsRegion.cs b/src/AsyncNavigation.WinUI/ItemsRegion.cs new file mode 100644 index 0000000..a02e11d --- /dev/null +++ b/src/AsyncNavigation.WinUI/ItemsRegion.cs @@ -0,0 +1,72 @@ +using AsyncNavigation.Core; +using Microsoft.UI.Xaml.Controls; +using System; +using System.Threading.Tasks; + +namespace AsyncNavigation.WinUI; + +public partial class ItemsRegion : RegionBase +{ + public ItemsRegion(string name, + ItemsControl itemsControl, + IServiceProvider serviceProvider, + bool? useCache) : base(name, itemsControl, serviceProvider) + { + EnableViewCache = useCache ?? false; + IsSinglePageRegion = false; + } + 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(); + _context.Clear(); + } + public override Task ProcessActivateAsync(NavigationContext navigationContext) + { + if (!_context.Items.Contains(navigationContext)) + _context.Items.Add(navigationContext); + _context.Selected = navigationContext; + + return Task.CompletedTask; + } + + public override Task ProcessDeactivateAsync(NavigationContext? navigationContext) + { + var target = navigationContext ?? _context.Selected; + if (target == null) + return Task.CompletedTask; + _ = _context.Items.Remove(target); + return Task.CompletedTask; + } +} diff --git a/src/AsyncNavigation.WinUI/ItemsRegionAdapter.cs b/src/AsyncNavigation.WinUI/ItemsRegionAdapter.cs new file mode 100644 index 0000000..8fc9c16 --- /dev/null +++ b/src/AsyncNavigation.WinUI/ItemsRegionAdapter.cs @@ -0,0 +1,17 @@ +using AsyncNavigation.Abstractions; +using Microsoft.UI.Xaml.Controls; +using System; + +namespace AsyncNavigation.WinUI; + +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 = null) + { + return new ItemsRegion(name, control, serviceProvider, useCache); + } +} diff --git a/src/AsyncNavigation.WinUI/PlatformService.cs b/src/AsyncNavigation.WinUI/PlatformService.cs new file mode 100644 index 0000000..bfa43c7 --- /dev/null +++ b/src/AsyncNavigation.WinUI/PlatformService.cs @@ -0,0 +1,78 @@ +using AsyncNavigation.Core; +using Microsoft.UI.Xaml; +using System; +using System.Threading.Tasks; + +namespace AsyncNavigation.WinUI; + +internal class PlatformService : PlatformServiceBase +{ + public override T WaitOnDispatcher(Task task) + { + return WaitOnDispatcherFrame(task); + } + + public override void WaitOnDispatcher(Task task) + { + WaitOnDispatcherFrame(task); + } + public override void Show(Window window, bool isModal) + { + ArgumentNullException.ThrowIfNull(window); + + if (isModal) + { + window.ShowDialog(); + } + else + { + window.Show(); + } + } + + public override Task ShowAsync(Window window, bool isModal) + { + Show(window, isModal); + return Task.CompletedTask; + } + + private static T WaitOnDispatcherFrame(Task task) + { + if (!task.IsCompleted) + { + var frame = new DispatcherFrame(); + task.ContinueWith(static (_, s) => ((DispatcherFrame)s!).Continue = false, frame); + Dispatcher.PushFrame(frame); + } + return task.GetAwaiter().GetResult(); + } + + private static void WaitOnDispatcherFrame(Task task) + { + if (!task.IsCompleted) + { + var frame = new DispatcherFrame(); + task.ContinueWith(static (_, s) => ((DispatcherFrame)s!).Continue = false, frame); + Dispatcher.PushFrame(frame); + } + task.GetAwaiter().GetResult(); + } + + public override Action AttachClosingCore(Window window, Action handler) + { + System.ComponentModel.CancelEventHandler wrapper = (s, e) => + { + var args = new WindowClosingEventArgs { Cancel = e.Cancel }; + handler(s, args); + e.Cancel = args.Cancel; + }; + window.Closing += wrapper; + return () => window.Closing -= wrapper; + } + + public override void ShowMainWindow(Window mainWindow) + { + Application.Current.MainWindow = mainWindow; + mainWindow.Show(); + } +} diff --git a/src/AsyncNavigation.WinUI/RegionManager.cs b/src/AsyncNavigation.WinUI/RegionManager.cs new file mode 100644 index 0000000..27c4d4d --- /dev/null +++ b/src/AsyncNavigation.WinUI/RegionManager.cs @@ -0,0 +1,88 @@ +using AsyncNavigation.Abstractions; +using System; +using System.Windows; + +namespace AsyncNavigation.WinUI; + +public sealed class RegionManager : RegionManagerBase +{ + #region RegionName + public static readonly DependencyProperty RegionNameProperty = + DependencyProperty.RegisterAttached( + "RegionName", + typeof(string), + typeof(RegionManager), + new PropertyMetadata(null, OnRegionNameChanged)); + + private static void OnRegionNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var name = e.NewValue as string; + var oldName = e.OldValue as string; + + if (name == oldName) + return; + + if (string.IsNullOrEmpty(name)) + { + if (!string.IsNullOrEmpty(oldName)) + OnRemoveRegionNameCore(oldName); + return; + } + + var serviceProvider = GetServiceProvider(d); + var preferCache = GetPreferCache(d); + OnAddRegionNameCore(name, d, serviceProvider, preferCache); + } + + public static string GetRegionName(DependencyObject obj) + { + return (string)obj.GetValue(RegionNameProperty); + } + + public static void SetRegionName(DependencyObject obj, string value) + { + obj.SetValue(RegionNameProperty, value); + } + #endregion + + #region ServiceProvider + public static readonly DependencyProperty ServiceProviderProperty = + DependencyProperty.RegisterAttached("ServiceProvider", + typeof(IServiceProvider), + typeof(RegionManager), + new PropertyMetadata(null)); + + public static IServiceProvider GetServiceProvider(DependencyObject obj) + { + return (IServiceProvider)obj.GetValue(ServiceProviderProperty); + } + + public static void SetServiceProvider(DependencyObject obj, IServiceProvider value) + { + obj.SetValue(ServiceProviderProperty, value); + } + #endregion + + #region PreferCache + public static readonly DependencyProperty PreferCacheProperty = + DependencyProperty.RegisterAttached("PreferCache", + typeof(bool?), + typeof(RegionManager), + new PropertyMetadata(null)); + + public static bool? GetPreferCache(DependencyObject obj) + { + return (bool?)obj.GetValue(PreferCacheProperty); + } + + public static void SetPreferCache(DependencyObject obj, bool? value) + { + obj.SetValue(PreferCacheProperty, value); + } + #endregion + + public RegionManager(IRegionFactory regionFactory, + IServiceProvider serviceProvider) : base(regionFactory, serviceProvider) + { + } +} diff --git a/src/AsyncNavigation.WinUI/TabRegion.cs b/src/AsyncNavigation.WinUI/TabRegion.cs new file mode 100644 index 0000000..01795ed --- /dev/null +++ b/src/AsyncNavigation.WinUI/TabRegion.cs @@ -0,0 +1,73 @@ +using AsyncNavigation.Core; + +namespace AsyncNavigation.WinUI; + +public class TabRegion : RegionBase +{ + public TabRegion(string name, + TabControl tabControl, + IServiceProvider serviceProvider, + bool? useCache = null) : base(name, tabControl, serviceProvider) + { + EnableViewCache = useCache ?? false; + IsSinglePageRegion = false; + } + 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(); + _context.Clear(); + } + + public override Task ProcessActivateAsync(NavigationContext navigationContext) + { + if (!_context.Items.Contains(navigationContext)) + _context.Items.Add(navigationContext); + + _context.Selected = navigationContext; + return Task.CompletedTask; + } + + public override Task ProcessDeactivateAsync(NavigationContext? navigationContext) + { + var target = navigationContext ?? _context.Selected; + if (target == null) + return Task.CompletedTask; + + _ = _context.Items.Remove(target); + return Task.CompletedTask; + } +} + + diff --git a/src/AsyncNavigation.WinUI/TabRegionAdapter.cs b/src/AsyncNavigation.WinUI/TabRegionAdapter.cs new file mode 100644 index 0000000..74d115d --- /dev/null +++ b/src/AsyncNavigation.WinUI/TabRegionAdapter.cs @@ -0,0 +1,16 @@ +using AsyncNavigation.Abstractions; +using System.Windows.Controls; + +namespace AsyncNavigation.WinUI; + +public class TabRegionAdapter : RegionAdapterBase +{ + public override bool IsAdapted(TabControl control) + { + return base.IsAdapted(control); + } + public override IRegion CreateRegion(string name, TabControl control, IServiceProvider serviceProvider, bool? useCache = null) + { + return new TabRegion(name, control, serviceProvider, useCache); + } +} diff --git a/src/AsyncNavigation.WinUI/XamlTemplateHelper.cs b/src/AsyncNavigation.WinUI/XamlTemplateHelper.cs new file mode 100644 index 0000000..9382144 --- /dev/null +++ b/src/AsyncNavigation.WinUI/XamlTemplateHelper.cs @@ -0,0 +1,19 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Markup; + +namespace AsyncNavigation.WinUI; + +internal static class XamlTemplateHelper +{ + public static DataTemplate CreateIndicatorHostTemplate() + { + const string xaml = """ + + + + """; + + return (DataTemplate)XamlReader.Load(xaml); + } +} diff --git a/src/AsyncNavigation.slnx b/src/AsyncNavigation.slnx index 0f11ea8..749e7b5 100644 --- a/src/AsyncNavigation.slnx +++ b/src/AsyncNavigation.slnx @@ -33,6 +33,7 @@ + From b5a927a0838888b7c713f1941ee877f6c9cc7bf3 Mon Sep 17 00:00:00 2001 From: Easley Date: Thu, 27 Aug 2026 12:40:54 +0200 Subject: [PATCH 2/4] feat: complete WinUI 3 navigation support --- samples/Directory.Packages.props | 3 + samples/Sample.Common/MainWindowViewModel.cs | 6 + samples/Sample.Common/Sample.Common.csproj | 2 +- samples/Sample.WinUI/App.xaml | 15 +++ samples/Sample.WinUI/App.xaml.cs | 74 ++++++++++ .../Converters/BoolToVisibilityConverter.cs | 15 +++ .../Sample.WinUI/InnerIndicatorProvider.cs | 39 ++++++ samples/Sample.WinUI/MainPage.xaml | 42 ++++++ samples/Sample.WinUI/MainPage.xaml.cs | 11 ++ samples/Sample.WinUI/MainWindow.cs | 7 + .../MessageBoxIndicatorProvider.cs | 61 +++++++++ samples/Sample.WinUI/Regions/ListBoxRegion.cs | 18 +++ .../Regions/ListBoxRegionAdapter.cs | 14 ++ samples/Sample.WinUI/Sample.WinUI.csproj | 24 ++++ samples/Sample.WinUI/Views/AWindow.cs | 13 ++ .../Views/ChildContentRegionView.xaml | 3 + .../Views/ChildContentRegionView.xaml.cs | 4 + samples/Sample.WinUI/Views/HeavyView.xaml | 3 + samples/Sample.WinUI/Views/HeavyView.xaml.cs | 4 + .../Sample.WinUI/Views/ItemsRegionView.xaml | 3 + .../Views/ItemsRegionView.xaml.cs | 4 + samples/Sample.WinUI/Views/LightView.xaml | 3 + samples/Sample.WinUI/Views/LightView.xaml.cs | 4 + .../Sample.WinUI/Views/ListBoxRegionView.xaml | 3 + .../Views/ListBoxRegionView.xaml.cs | 4 + .../Views/NavigationViewRegionView.xaml | 26 ++++ .../Views/NavigationViewRegionView.xaml.cs | 23 ++++ samples/Sample.WinUI/Views/TabRegionView.xaml | 3 + .../Sample.WinUI/Views/TabRegionView.xaml.cs | 15 +++ src/AsyncNavigation.WinUI/AssemblyInfo.cs | 7 +- src/AsyncNavigation.WinUI/ContentRegion.cs | 2 + .../DefaultDialogContainer.xaml | 4 +- .../DefaultDialogContainer.xaml.cs | 31 ++++- .../DependencyInjectionExtensions.cs | 2 + .../IconDescriptorConverter.cs | 4 +- src/AsyncNavigation.WinUI/IconResolver.cs | 44 +++--- src/AsyncNavigation.WinUI/ItemsRegion.cs | 22 +-- .../ItemsRegionAdapter.cs | 2 +- .../NavigationViewRegion.cs | 127 ++++++++++++++++++ .../NavigationViewRegionAdapter.cs | 15 +++ src/AsyncNavigation.WinUI/PlatformService.cs | 46 +++---- src/AsyncNavigation.WinUI/RegionManager.cs | 12 +- src/AsyncNavigation.WinUI/TabRegion.cs | 30 ++--- src/AsyncNavigation.WinUI/TabRegionAdapter.cs | 9 +- .../XamlTemplateHelper.cs | 21 ++- src/AsyncNavigation.slnx | 3 + .../Abstractions/IPlatformService.cs | 1 + src/AsyncNavigation/AssemblyInfo.cs | 3 +- src/AsyncNavigation/DialogService.cs | 21 +-- 49 files changed, 739 insertions(+), 113 deletions(-) create mode 100644 samples/Sample.WinUI/App.xaml create mode 100644 samples/Sample.WinUI/App.xaml.cs create mode 100644 samples/Sample.WinUI/Converters/BoolToVisibilityConverter.cs create mode 100644 samples/Sample.WinUI/InnerIndicatorProvider.cs create mode 100644 samples/Sample.WinUI/MainPage.xaml create mode 100644 samples/Sample.WinUI/MainPage.xaml.cs create mode 100644 samples/Sample.WinUI/MainWindow.cs create mode 100644 samples/Sample.WinUI/MessageBoxIndicatorProvider.cs create mode 100644 samples/Sample.WinUI/Regions/ListBoxRegion.cs create mode 100644 samples/Sample.WinUI/Regions/ListBoxRegionAdapter.cs create mode 100644 samples/Sample.WinUI/Sample.WinUI.csproj create mode 100644 samples/Sample.WinUI/Views/AWindow.cs create mode 100644 samples/Sample.WinUI/Views/ChildContentRegionView.xaml create mode 100644 samples/Sample.WinUI/Views/ChildContentRegionView.xaml.cs create mode 100644 samples/Sample.WinUI/Views/HeavyView.xaml create mode 100644 samples/Sample.WinUI/Views/HeavyView.xaml.cs create mode 100644 samples/Sample.WinUI/Views/ItemsRegionView.xaml create mode 100644 samples/Sample.WinUI/Views/ItemsRegionView.xaml.cs create mode 100644 samples/Sample.WinUI/Views/LightView.xaml create mode 100644 samples/Sample.WinUI/Views/LightView.xaml.cs create mode 100644 samples/Sample.WinUI/Views/ListBoxRegionView.xaml create mode 100644 samples/Sample.WinUI/Views/ListBoxRegionView.xaml.cs create mode 100644 samples/Sample.WinUI/Views/NavigationViewRegionView.xaml create mode 100644 samples/Sample.WinUI/Views/NavigationViewRegionView.xaml.cs create mode 100644 samples/Sample.WinUI/Views/TabRegionView.xaml create mode 100644 samples/Sample.WinUI/Views/TabRegionView.xaml.cs create mode 100644 src/AsyncNavigation.WinUI/NavigationViewRegion.cs create mode 100644 src/AsyncNavigation.WinUI/NavigationViewRegionAdapter.cs diff --git a/samples/Directory.Packages.props b/samples/Directory.Packages.props index 71d7ad5..fe1691e 100644 --- a/samples/Directory.Packages.props +++ b/samples/Directory.Packages.props @@ -14,11 +14,14 @@ + + + diff --git a/samples/Sample.Common/MainWindowViewModel.cs b/samples/Sample.Common/MainWindowViewModel.cs index 435295d..f5761c8 100644 --- a/samples/Sample.Common/MainWindowViewModel.cs +++ b/samples/Sample.Common/MainWindowViewModel.cs @@ -155,6 +155,12 @@ private void ShowWindow(string param) var result = _dialogService.ShowWindowDialog(param); } + [ReactiveCommand(CanExecute = nameof(SupportDialog))] + private async Task AsyncShowWindow(string param) + { + await _dialogService.ShowWindowDialogAsync(param); + } + [ReactiveCommand] private async Task GoForward() { diff --git a/samples/Sample.Common/Sample.Common.csproj b/samples/Sample.Common/Sample.Common.csproj index 19ee1c4..625e956 100644 --- a/samples/Sample.Common/Sample.Common.csproj +++ b/samples/Sample.Common/Sample.Common.csproj @@ -1,7 +1,7 @@  - net10.0 + net8.0;net10.0 enable enable diff --git a/samples/Sample.WinUI/App.xaml b/samples/Sample.WinUI/App.xaml new file mode 100644 index 0000000..3e07b22 --- /dev/null +++ b/samples/Sample.WinUI/App.xaml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/samples/Sample.WinUI/App.xaml.cs b/samples/Sample.WinUI/App.xaml.cs new file mode 100644 index 0000000..50c9f98 --- /dev/null +++ b/samples/Sample.WinUI/App.xaml.cs @@ -0,0 +1,74 @@ +using AsyncNavigation; +using AsyncNavigation.Core; +using AsyncNavigation.WinUI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.UI.Xaml; +using ReactiveUI.Builder; +using Sample.Common; +using Sample.WinUI.Views; +using Sample.WinUI.Regions; + +namespace Sample.WinUI; + +public partial class App : Application +{ + private Window? _window; + + public App() + { + InitializeComponent(); + } + + protected override void OnLaunched(LaunchActivatedEventArgs args) + { + RxAppBuilder.CreateReactiveUIBuilder() + .WithWinUI() + .BuildApp(); + var services = new ServiceCollection(); + services.AddNavigationSupport() + .AddSingleton() + .RegisterRegionAdapter() + .RegisterView(nameof(LightView)) + .RegisterView(nameof(ItemsRegionView)) + .RegisterView(nameof(ChildContentRegionView)) + .RegisterView(nameof(TabRegionView)) + .RegisterView(nameof(NavigationViewRegionView)) + .RegisterView(nameof(HeavyView)) + .RegisterDialogWindow(nameof(AWindow)) + .RegisterInnerIndicatorProvider() + .RegisterRegionIndicatorProvider() + .RegisterView(nameof(ListBoxRegionView)) + .RegisterRouter((mapper, _) => + { + mapper.MapNavigation("Path_ChildHeavyView", + new NavigationTarget("MainRegion", nameof(ChildContentRegionView)), + new NavigationTarget("ChildContentRegion", nameof(HeavyView))); + mapper.MapNavigation("Path_ChildAView", + new NavigationTarget("MainRegion", nameof(ChildContentRegionView)), + new NavigationTarget("ChildContentRegion", nameof(LightView))); + mapper.MapNavigation("Path_TabHeavyView", + new NavigationTarget("MainRegion", nameof(TabRegionView)), + new NavigationTarget("TabRegion", nameof(HeavyView))); + mapper.MapNavigation("Tab.Tab_A", + new NavigationTarget("MainRegion", nameof(TabRegionView)), + new NavigationTarget("TabRegion", nameof(LightView))) + .WithSegments("Tab", "Tab_A"); + mapper.MapNavigation("Path_UnknownView", new NavigationTarget("UnknownRegion", "UnknownView")) + .WithFallback(new NavigationTarget("MainRegion", nameof(LightView))); + }); + + var serviceProvider = services.BuildServiceProvider(); + Resources[nameof(IconDescriptorConverter)] = serviceProvider.GetRequiredService(); + + _window = new MainWindow + { + Content = new MainPage + { + DataContext = serviceProvider.GetRequiredService() + } + }; + _window.Title = "AsyncNavigation · WinUI 3 Sample"; + _window.AppWindow.Resize(new Windows.Graphics.SizeInt32(1100, 720)); + _window.Activate(); + } +} diff --git a/samples/Sample.WinUI/Converters/BoolToVisibilityConverter.cs b/samples/Sample.WinUI/Converters/BoolToVisibilityConverter.cs new file mode 100644 index 0000000..6675b13 --- /dev/null +++ b/samples/Sample.WinUI/Converters/BoolToVisibilityConverter.cs @@ -0,0 +1,15 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Data; + +namespace Sample.WinUI.Converters; + +public sealed class BoolToVisibilityConverter : IValueConverter +{ + public bool Invert { get; set; } + + public object Convert(object value, Type targetType, object parameter, string language) + => value is bool visible && visible != Invert ? Visibility.Visible : Visibility.Collapsed; + + public object ConvertBack(object value, Type targetType, object parameter, string language) + => throw new NotSupportedException(); +} diff --git a/samples/Sample.WinUI/InnerIndicatorProvider.cs b/samples/Sample.WinUI/InnerIndicatorProvider.cs new file mode 100644 index 0000000..fc2fac8 --- /dev/null +++ b/samples/Sample.WinUI/InnerIndicatorProvider.cs @@ -0,0 +1,39 @@ +using AsyncNavigation; +using AsyncNavigation.WinUI; +using Microsoft.UI; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; + +namespace Sample.WinUI; + +internal sealed class InnerIndicatorProvider : IInnerIndicatorProvider +{ + public bool HasErrorIndicator(NavigationContext context) => true; + public bool HasLoadingIndicator(NavigationContext context) => true; + + public UIElement GetLoadingIndicator(NavigationContext context) + { + var cancel = new Button { Content = "Cancel", HorizontalAlignment = HorizontalAlignment.Center }; + cancel.Click += async (_, _) => + { + cancel.IsEnabled = false; + cancel.Content = "Cancelling…"; + await context.CancelAndWaitAsync(); + }; + var panel = new StackPanel { Spacing = 12, VerticalAlignment = VerticalAlignment.Center }; + panel.Children.Add(new TextBlock { Text = "Loading…", FontSize = 24, HorizontalAlignment = HorizontalAlignment.Center }); + panel.Children.Add(new TextBlock { Text = context.ToString(), TextWrapping = TextWrapping.Wrap, HorizontalAlignment = HorizontalAlignment.Center }); + panel.Children.Add(new ProgressBar { IsIndeterminate = true, Width = 260 }); + panel.Children.Add(cancel); + return new Border { Padding = new Thickness(24), Child = panel }; + } + + public UIElement GetErrorIndicator(NavigationContext context) + { + var panel = new StackPanel { Spacing = 12, VerticalAlignment = VerticalAlignment.Center }; + panel.Children.Add(new TextBlock { Text = "Navigation failed", FontSize = 24, Foreground = new SolidColorBrush(Colors.Red), HorizontalAlignment = HorizontalAlignment.Center }); + panel.Children.Add(new TextBlock { Text = context.ToString(), TextWrapping = TextWrapping.Wrap, HorizontalAlignment = HorizontalAlignment.Center }); + return new Border { Padding = new Thickness(24), Child = panel }; + } +} diff --git a/samples/Sample.WinUI/MainPage.xaml b/samples/Sample.WinUI/MainPage.xaml new file mode 100644 index 0000000..fcc361e --- /dev/null +++ b/samples/Sample.WinUI/MainPage.xaml @@ -0,0 +1,42 @@ + + + + + + +