diff --git a/readme.md b/readme.md
index 2359e4a..63d6e23 100644
--- a/readme.md
+++ b/readme.md
@@ -6,6 +6,7 @@
[](https://www.nuget.org/packages/AsyncNavigation)
[](https://www.nuget.org/packages/AsyncNavigation.Avalonia)
[](https://www.nuget.org/packages/AsyncNavigation.Wpf)
+[](samples/Sample.WinUI)
[](LICENSE)
[](https://dotnet.microsoft.com)
@@ -31,6 +32,19 @@
---
+## Platform Support
+
+| Platform | Status | Package / Sample |
+|---|---|---|
+| Avalonia | Stable | `AsyncNavigation.Avalonia` |
+| WPF | Stable | `AsyncNavigation.Wpf` |
+| WinUI 3 | **In development** | [`Sample.WinUI`](samples/Sample.WinUI) |
+
+> [!WARNING]
+> WinUI 3 support is under active development. It includes content, items, tab, `NavigationView`, dialog, window, and indicator support, but its APIs and behavior may still change. Test it carefully before using it in production.
+
+---
+
## Installation
```bash
@@ -41,6 +55,8 @@ dotnet add package AsyncNavigation.Avalonia
dotnet add package AsyncNavigation.Wpf
```
+WinUI 3 is currently available from source and through the repository sample while development continues.
+
---
## Quick Start
diff --git a/readme_zh-cn.md b/readme_zh-cn.md
index e5e6773..90be970 100644
--- a/readme_zh-cn.md
+++ b/readme_zh-cn.md
@@ -6,6 +6,7 @@
[](https://www.nuget.org/packages/AsyncNavigation)
[](https://www.nuget.org/packages/AsyncNavigation.Avalonia)
[](https://www.nuget.org/packages/AsyncNavigation.Wpf)
+[](samples/Sample.WinUI)
[](LICENSE)
[](https://dotnet.microsoft.com)
@@ -31,6 +32,19 @@
---
+## 平台支持
+
+| 平台 | 状态 | 包 / 示例 |
+|---|---|---|
+| Avalonia | 稳定 | `AsyncNavigation.Avalonia` |
+| WPF | 稳定 | `AsyncNavigation.Wpf` |
+| WinUI 3 | **开发中** | [`Sample.WinUI`](samples/Sample.WinUI) |
+
+> [!WARNING]
+> WinUI 3 支持仍处于积极开发阶段。目前已包含 Content、Items、Tab、`NavigationView`、对话框、窗口和加载指示器支持,但 API 与行为仍可能发生变化。在生产环境使用前请充分测试。
+
+---
+
## 安装
```bash
@@ -41,6 +55,8 @@ dotnet add package AsyncNavigation.Avalonia
dotnet add package AsyncNavigation.Wpf
```
+WinUI 3 目前可通过源码和仓库中的示例项目进行体验,正式包仍在开发中。
+
---
## 快速开始
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/Sample.WinUI/MainPage.xaml.cs b/samples/Sample.WinUI/MainPage.xaml.cs
new file mode 100644
index 0000000..6cf012b
--- /dev/null
+++ b/samples/Sample.WinUI/MainPage.xaml.cs
@@ -0,0 +1,11 @@
+using Microsoft.UI.Xaml.Controls;
+
+namespace Sample.WinUI;
+
+public sealed partial class MainPage : Page
+{
+ public MainPage()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/samples/Sample.WinUI/MainWindow.cs b/samples/Sample.WinUI/MainWindow.cs
new file mode 100644
index 0000000..aa7539b
--- /dev/null
+++ b/samples/Sample.WinUI/MainWindow.cs
@@ -0,0 +1,7 @@
+using Microsoft.UI.Xaml;
+
+namespace Sample.WinUI;
+
+public sealed class MainWindow : Window
+{
+}
diff --git a/samples/Sample.WinUI/MessageBoxIndicatorProvider.cs b/samples/Sample.WinUI/MessageBoxIndicatorProvider.cs
new file mode 100644
index 0000000..2d7bbec
--- /dev/null
+++ b/samples/Sample.WinUI/MessageBoxIndicatorProvider.cs
@@ -0,0 +1,61 @@
+using AsyncNavigation;
+using AsyncNavigation.Abstractions;
+using Microsoft.UI;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Media;
+
+namespace Sample.WinUI;
+
+internal sealed class MessageBoxIndicatorProvider : IRegionIndicatorProvider
+{
+ private readonly MessageBoxIndicator _indicator = new();
+ public bool HasIndicator(string regionName) => true;
+ public IRegionIndicator GetIndicator(string regionName) => _indicator;
+}
+
+internal sealed class MessageBoxIndicator : IRegionIndicator
+{
+ private Window? _loadingWindow;
+
+ public Task ShowLoadingAsync(NavigationContext context)
+ {
+ _loadingWindow = CreateWindow("Loading", context.ToString(), Colors.Orange);
+ _loadingWindow.Activate();
+ return Task.CompletedTask;
+ }
+
+ public Task ShowErrorAsync(NavigationContext context, Exception? innerException = null)
+ {
+ var window = CreateWindow("Navigation error", $"{context}\n\n{innerException}", Colors.Red);
+ window.Activate();
+ return Task.CompletedTask;
+ }
+
+ public Task OnLoadedAsync(NavigationContext context) => CloseLoadingWindow();
+ public Task OnCancelledAsync(NavigationContext context) => CloseLoadingWindow();
+
+ private Task CloseLoadingWindow()
+ {
+ _loadingWindow?.Close();
+ _loadingWindow = null;
+ return Task.CompletedTask;
+ }
+
+ private static Window CreateWindow(string title, string? message, Windows.UI.Color color)
+ {
+ var window = new Window { Title = title };
+ window.Content = new ScrollViewer
+ {
+ Content = new TextBlock
+ {
+ Margin = new Thickness(24),
+ Text = message,
+ TextWrapping = TextWrapping.Wrap,
+ Foreground = new SolidColorBrush(color)
+ }
+ };
+ window.AppWindow.Resize(new Windows.Graphics.SizeInt32(480, 240));
+ return window;
+ }
+}
diff --git a/samples/Sample.WinUI/Regions/ListBoxRegion.cs b/samples/Sample.WinUI/Regions/ListBoxRegion.cs
new file mode 100644
index 0000000..ee5a799
--- /dev/null
+++ b/samples/Sample.WinUI/Regions/ListBoxRegion.cs
@@ -0,0 +1,18 @@
+using AsyncNavigation;
+using Microsoft.UI.Xaml.Controls;
+
+namespace Sample.WinUI.Regions;
+
+public sealed class ListBoxRegion : AsyncNavigation.WinUI.ItemsRegion
+{
+ private readonly ListBox _listBox;
+
+ public ListBoxRegion(string name, ListBox listBox, IServiceProvider serviceProvider, bool? useCache)
+ : base(name, listBox, serviceProvider, useCache) => _listBox = listBox;
+
+ public override async Task ProcessActivateAsync(NavigationContext navigationContext)
+ {
+ await base.ProcessActivateAsync(navigationContext);
+ _listBox.ScrollIntoView(navigationContext);
+ }
+}
diff --git a/samples/Sample.WinUI/Regions/ListBoxRegionAdapter.cs b/samples/Sample.WinUI/Regions/ListBoxRegionAdapter.cs
new file mode 100644
index 0000000..c4a8eb3
--- /dev/null
+++ b/samples/Sample.WinUI/Regions/ListBoxRegionAdapter.cs
@@ -0,0 +1,14 @@
+using AsyncNavigation;
+using AsyncNavigation.Abstractions;
+using AsyncNavigation.WinUI;
+using Microsoft.UI.Xaml.Controls;
+
+namespace Sample.WinUI.Regions;
+
+internal sealed class ListBoxRegionAdapter : RegionAdapterBase
+{
+ public override uint Priority => 100;
+
+ public override IRegion CreateRegion(string name, ListBox control, IServiceProvider serviceProvider, bool? useCache)
+ => new ListBoxRegion(name, control, serviceProvider, useCache);
+}
diff --git a/samples/Sample.WinUI/Sample.WinUI.csproj b/samples/Sample.WinUI/Sample.WinUI.csproj
new file mode 100644
index 0000000..557a537
--- /dev/null
+++ b/samples/Sample.WinUI/Sample.WinUI.csproj
@@ -0,0 +1,24 @@
+
+
+ WinExe
+ net10.0-windows10.0.19041.0
+ 10.0.17763.0
+ Sample.WinUI
+ true
+ None
+ true
+ win-x64
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/Sample.WinUI/Views/AWindow.cs b/samples/Sample.WinUI/Views/AWindow.cs
new file mode 100644
index 0000000..deb2ec7
--- /dev/null
+++ b/samples/Sample.WinUI/Views/AWindow.cs
@@ -0,0 +1,13 @@
+using AsyncNavigation.WinUI;
+
+namespace Sample.WinUI.Views;
+
+public sealed class AWindow : DefaultDialogContainer
+{
+ public AWindow()
+ {
+ Title = nameof(AWindow);
+ Content = new LightView();
+ AppWindow.Resize(new Windows.Graphics.SizeInt32(800, 450));
+ }
+}
diff --git a/samples/Sample.WinUI/Views/ChildContentRegionView.xaml b/samples/Sample.WinUI/Views/ChildContentRegionView.xaml
new file mode 100644
index 0000000..65396da
--- /dev/null
+++ b/samples/Sample.WinUI/Views/ChildContentRegionView.xaml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/samples/Sample.WinUI/Views/ChildContentRegionView.xaml.cs b/samples/Sample.WinUI/Views/ChildContentRegionView.xaml.cs
new file mode 100644
index 0000000..8867ca8
--- /dev/null
+++ b/samples/Sample.WinUI/Views/ChildContentRegionView.xaml.cs
@@ -0,0 +1,4 @@
+using AsyncNavigation.Abstractions;
+using Microsoft.UI.Xaml.Controls;
+namespace Sample.WinUI.Views;
+public sealed partial class ChildContentRegionView : UserControl, IView { public ChildContentRegionView() => InitializeComponent(); }
diff --git a/samples/Sample.WinUI/Views/HeavyView.xaml b/samples/Sample.WinUI/Views/HeavyView.xaml
new file mode 100644
index 0000000..73e7cfe
--- /dev/null
+++ b/samples/Sample.WinUI/Views/HeavyView.xaml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/samples/Sample.WinUI/Views/HeavyView.xaml.cs b/samples/Sample.WinUI/Views/HeavyView.xaml.cs
new file mode 100644
index 0000000..529d86d
--- /dev/null
+++ b/samples/Sample.WinUI/Views/HeavyView.xaml.cs
@@ -0,0 +1,4 @@
+using AsyncNavigation.Abstractions;
+using Microsoft.UI.Xaml.Controls;
+namespace Sample.WinUI.Views;
+public sealed partial class HeavyView : UserControl, IView { public HeavyView() => InitializeComponent(); }
diff --git a/samples/Sample.WinUI/Views/ItemsRegionView.xaml b/samples/Sample.WinUI/Views/ItemsRegionView.xaml
new file mode 100644
index 0000000..d45c494
--- /dev/null
+++ b/samples/Sample.WinUI/Views/ItemsRegionView.xaml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/samples/Sample.WinUI/Views/ItemsRegionView.xaml.cs b/samples/Sample.WinUI/Views/ItemsRegionView.xaml.cs
new file mode 100644
index 0000000..3e7172d
--- /dev/null
+++ b/samples/Sample.WinUI/Views/ItemsRegionView.xaml.cs
@@ -0,0 +1,4 @@
+using AsyncNavigation.Abstractions;
+using Microsoft.UI.Xaml.Controls;
+namespace Sample.WinUI.Views;
+public sealed partial class ItemsRegionView : UserControl, IView { public ItemsRegionView() => InitializeComponent(); }
diff --git a/samples/Sample.WinUI/Views/LightView.xaml b/samples/Sample.WinUI/Views/LightView.xaml
new file mode 100644
index 0000000..4e97b77
--- /dev/null
+++ b/samples/Sample.WinUI/Views/LightView.xaml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/samples/Sample.WinUI/Views/LightView.xaml.cs b/samples/Sample.WinUI/Views/LightView.xaml.cs
new file mode 100644
index 0000000..01bc396
--- /dev/null
+++ b/samples/Sample.WinUI/Views/LightView.xaml.cs
@@ -0,0 +1,4 @@
+using AsyncNavigation.Abstractions;
+using Microsoft.UI.Xaml.Controls;
+namespace Sample.WinUI.Views;
+public sealed partial class LightView : UserControl, IView { public LightView() => InitializeComponent(); }
diff --git a/samples/Sample.WinUI/Views/ListBoxRegionView.xaml b/samples/Sample.WinUI/Views/ListBoxRegionView.xaml
new file mode 100644
index 0000000..a00bcf7
--- /dev/null
+++ b/samples/Sample.WinUI/Views/ListBoxRegionView.xaml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/samples/Sample.WinUI/Views/ListBoxRegionView.xaml.cs b/samples/Sample.WinUI/Views/ListBoxRegionView.xaml.cs
new file mode 100644
index 0000000..331bafc
--- /dev/null
+++ b/samples/Sample.WinUI/Views/ListBoxRegionView.xaml.cs
@@ -0,0 +1,4 @@
+using AsyncNavigation.Abstractions;
+using Microsoft.UI.Xaml.Controls;
+namespace Sample.WinUI.Views;
+public sealed partial class ListBoxRegionView : UserControl, IView { public ListBoxRegionView() => InitializeComponent(); }
diff --git a/samples/Sample.WinUI/Views/NavigationViewRegionView.xaml b/samples/Sample.WinUI/Views/NavigationViewRegionView.xaml
new file mode 100644
index 0000000..3fba791
--- /dev/null
+++ b/samples/Sample.WinUI/Views/NavigationViewRegionView.xaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/Sample.WinUI/Views/NavigationViewRegionView.xaml.cs b/samples/Sample.WinUI/Views/NavigationViewRegionView.xaml.cs
new file mode 100644
index 0000000..5426ccd
--- /dev/null
+++ b/samples/Sample.WinUI/Views/NavigationViewRegionView.xaml.cs
@@ -0,0 +1,23 @@
+using AsyncNavigation.Abstractions;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+
+namespace Sample.WinUI.Views;
+
+public sealed partial class NavigationViewRegionView : UserControl, IView
+{
+ private readonly IRegionManager _regionManager;
+
+ public NavigationViewRegionView(IRegionManager regionManager)
+ {
+ _regionManager = regionManager;
+ InitializeComponent();
+ Loaded += OnLoaded;
+ }
+
+ private async void OnLoaded(object sender, RoutedEventArgs e)
+ {
+ Loaded -= OnLoaded;
+ await _regionManager.RequestNavigateAsync("NavigationViewRegion", "LightView");
+ }
+}
diff --git a/samples/Sample.WinUI/Views/TabRegionView.xaml b/samples/Sample.WinUI/Views/TabRegionView.xaml
new file mode 100644
index 0000000..393548d
--- /dev/null
+++ b/samples/Sample.WinUI/Views/TabRegionView.xaml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/samples/Sample.WinUI/Views/TabRegionView.xaml.cs b/samples/Sample.WinUI/Views/TabRegionView.xaml.cs
new file mode 100644
index 0000000..b17105d
--- /dev/null
+++ b/samples/Sample.WinUI/Views/TabRegionView.xaml.cs
@@ -0,0 +1,15 @@
+using AsyncNavigation.Abstractions;
+using Microsoft.UI.Xaml.Controls;
+using AsyncNavigation;
+using Sample.Common;
+namespace Sample.WinUI.Views;
+public sealed partial class TabRegionView : UserControl, IView
+{
+ public TabRegionView() => InitializeComponent();
+
+ private async void OnTabCloseRequested(TabView sender, TabViewTabCloseRequestedEventArgs args)
+ {
+ if (args.Item is NavigationContext context && context.TryResolveNavigationAware(out var aware) && aware is ViewModelBase viewModel)
+ await viewModel.RequestUnloadAsync(CancellationToken.None);
+ }
+}
diff --git a/src/AsyncNavigation.Avalonia/PlatformService.cs b/src/AsyncNavigation.Avalonia/PlatformService.cs
index 357a185..380b6d1 100644
--- a/src/AsyncNavigation.Avalonia/PlatformService.cs
+++ b/src/AsyncNavigation.Avalonia/PlatformService.cs
@@ -150,4 +150,9 @@ private static bool TryGetDesktopLifetime([MaybeNullWhen(false)]out IClassicDesk
classicDesktopStyleApplicationLifetime = null;
return false;
}
+
+ public override void PostToUIThread(Action action)
+ {
+ Dispatcher.UIThread.Post(action);
+ }
}
diff --git a/src/AsyncNavigation.WinUI/AssemblyInfo.cs b/src/AsyncNavigation.WinUI/AssemblyInfo.cs
new file mode 100644
index 0000000..ab79f5f
--- /dev/null
+++ b/src/AsyncNavigation.WinUI/AssemblyInfo.cs
@@ -0,0 +1 @@
+// WinUI 3 does not support WPF's XmlnsDefinitionAttribute assembly mappings.
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..8a142ae
--- /dev/null
+++ b/src/AsyncNavigation.WinUI/ContentRegion.cs
@@ -0,0 +1,59 @@
+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.HorizontalContentAlignment = Microsoft.UI.Xaml.HorizontalAlignment.Stretch;
+ control.VerticalContentAlignment = Microsoft.UI.Xaml.VerticalAlignment.Stretch;
+ 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..1c7f965
--- /dev/null
+++ b/src/AsyncNavigation.WinUI/DefaultDialogContainer.xaml
@@ -0,0 +1,9 @@
+
diff --git a/src/AsyncNavigation.WinUI/DefaultDialogContainer.xaml.cs b/src/AsyncNavigation.WinUI/DefaultDialogContainer.xaml.cs
new file mode 100644
index 0000000..a778d4a
--- /dev/null
+++ b/src/AsyncNavigation.WinUI/DefaultDialogContainer.xaml.cs
@@ -0,0 +1,44 @@
+using AsyncNavigation.Abstractions;
+using Microsoft.UI.Xaml;
+using System;
+
+namespace AsyncNavigation.WinUI;
+
+///
+/// Interaction logic for DefaultDialogContainer.xaml
+///
+public partial class DefaultDialogContainer : Window, IDialogWindow
+{
+ public DefaultDialogContainer()
+ {
+ InitializeComponent();
+ Closed += (_, _) => DialogClosed?.Invoke(this, EventArgs.Empty);
+ }
+
+ object? IDialogWindowBase.Content
+ {
+ get => Content;
+ set => Content = value as UIElement
+ ?? throw new ArgumentException("Dialog content must be a WinUI UIElement.", nameof(value));
+ }
+
+ object? IDialogWindowBase.DataContext
+ {
+ get => (Content as FrameworkElement)?.DataContext;
+ set
+ {
+ if (Content is FrameworkElement element)
+ element.DataContext = value;
+ }
+ }
+
+ event EventHandler? IDialogWindowBase.Closed
+ {
+ add => DialogClosed += value;
+ remove => DialogClosed -= value;
+ }
+
+ void IDialogWindowBase.Show() => Activate();
+
+ private event EventHandler? DialogClosed;
+}
diff --git a/src/AsyncNavigation.WinUI/DependencyInjectionExtensions.cs b/src/AsyncNavigation.WinUI/DependencyInjectionExtensions.cs
new file mode 100644
index 0000000..1e691ec
--- /dev/null
+++ b/src/AsyncNavigation.WinUI/DependencyInjectionExtensions.cs
@@ -0,0 +1,131 @@
+using AsyncNavigation;
+using AsyncNavigation.Abstractions;
+using AsyncNavigation.Core;
+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()
+ .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..83c40a3
--- /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();
+ }
+}
diff --git a/src/AsyncNavigation.WinUI/IconResolver.cs b/src/AsyncNavigation.WinUI/IconResolver.cs
new file mode 100644
index 0000000..5872af2
--- /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.Markup;
+using Microsoft.UI.Xaml.Media;
+using Microsoft.UI.Xaml.Media.Imaging;
+using Microsoft.UI.Xaml.Shapes;
+using System;
+using System.Collections.Generic;
+
+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 Microsoft.UI.Xaml.Shapes.Path? ResolvePathData(string data, double size)
+ {
+ if (string.IsNullOrWhiteSpace(data)) return null;
+
+ var geometry = (Geometry)XamlReader.Load($"{data}");
+ var path = new Microsoft.UI.Xaml.Shapes.Path
+ {
+ Data = geometry,
+ Width = size,
+ Height = size,
+ Stretch = Stretch.Uniform,
+ Fill = ResolvePathFill(),
+ };
+
+ 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;
+
+ if (!Application.Current.Resources.TryGetValue(resourceKey, out var resource)) return null;
+
+ return resource switch
+ {
+ Geometry geometry => new Microsoft.UI.Xaml.Shapes.Path
+ {
+ Data = geometry,
+ Width = size,
+ Height = size,
+ Stretch = Stretch.Uniform,
+ Fill = ResolvePathFill(),
+ },
+ ImageSource imageSource => new Image
+ {
+ Source = imageSource,
+ Width = size,
+ Height = size
+ },
+ string pathData => ResolvePathData(pathData, size),
+ _ => null
+ };
+ }
+
+ private static Brush ResolvePathFill()
+ {
+ if (Application.Current?.Resources.TryGetValue("TextFillColorPrimaryBrush", out var resource) == true
+ && resource is Brush brush)
+ {
+ return brush;
+ }
+
+ return new SolidColorBrush(Microsoft.UI.Colors.Black);
+ }
+}
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..b47cc22
--- /dev/null
+++ b/src/AsyncNavigation.WinUI/ItemsRegion.cs
@@ -0,0 +1,72 @@
+using AsyncNavigation.Core;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Controls.Primitives;
+using Microsoft.UI.Xaml.Data;
+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.HorizontalContentAlignment = Microsoft.UI.Xaml.HorizontalAlignment.Stretch;
+ control.VerticalContentAlignment = Microsoft.UI.Xaml.VerticalAlignment.Stretch;
+ control.SetBinding(ItemsControl.ItemsSourceProperty,
+ new Binding
+ {
+ Path = new Microsoft.UI.Xaml.PropertyPath(nameof(RegionContext.Items)),
+ Source = _context
+ });
+
+ if (control is Selector selector)
+ selector.SetBinding(Selector.SelectedItemProperty,
+ new Binding
+ {
+ Path = new Microsoft.UI.Xaml.PropertyPath(nameof(RegionContext.Selected)),
+ Source = _context,
+ Mode = BindingMode.TwoWay
+ });
+
+ control.ItemTemplate = XamlTemplateHelper.CreateIndicatorHostTemplate();
+ }
+
+ 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..e4a057c
--- /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 control is not null;
+ }
+ 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/NavigationViewRegion.cs b/src/AsyncNavigation.WinUI/NavigationViewRegion.cs
new file mode 100644
index 0000000..252416a
--- /dev/null
+++ b/src/AsyncNavigation.WinUI/NavigationViewRegion.cs
@@ -0,0 +1,127 @@
+using AsyncNavigation.Abstractions;
+using AsyncNavigation.Core;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using System.Diagnostics;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace AsyncNavigation.WinUI;
+
+public sealed class NavigationViewRegion : RegionBase
+{
+ private readonly NavigationView _navigationView;
+ private readonly IRegionManager _regionManager;
+ private string? _activeViewName;
+
+ public NavigationViewRegion(
+ string name,
+ NavigationView navigationView,
+ IServiceProvider serviceProvider,
+ bool? useCache)
+ : base(name, navigationView, serviceProvider)
+ {
+ _navigationView = navigationView;
+ _regionManager = serviceProvider.GetRequiredService();
+ EnableViewCache = useCache ?? true;
+ IsSinglePageRegion = true;
+
+ _navigationView.HorizontalContentAlignment = HorizontalAlignment.Stretch;
+ _navigationView.VerticalContentAlignment = VerticalAlignment.Stretch;
+ _navigationView.ItemInvoked += OnItemInvoked;
+ _navigationView.BackRequested += OnBackRequested;
+ Navigated += OnNavigated;
+ }
+
+ public override NavigationPipelineMode NavigationPipelineMode => NavigationPipelineMode.RenderFirst;
+
+ public override async Task ProcessActivateAsync(NavigationContext navigationContext)
+ {
+ _activeViewName = navigationContext.ViewName;
+ _navigationView.Content = navigationContext.IndicatorHost.Value?.Host;
+ _navigationView.Header = navigationContext.ViewName;
+
+ var matchingItem = FindMenuItem(navigationContext.ViewName);
+ if (matchingItem is not null)
+ _navigationView.SelectedItem = matchingItem;
+
+ await UpdateBackButtonAsync();
+ }
+
+ public override async Task ProcessDeactivateAsync(NavigationContext? navigationContext)
+ {
+ _activeViewName = null;
+ _navigationView.Content = null;
+ await UpdateBackButtonAsync();
+ }
+
+ public override void Dispose()
+ {
+ _navigationView.ItemInvoked -= OnItemInvoked;
+ _navigationView.BackRequested -= OnBackRequested;
+ Navigated -= OnNavigated;
+ _navigationView.Content = null;
+ base.Dispose();
+ }
+
+ private async void OnItemInvoked(NavigationView sender, NavigationViewItemInvokedEventArgs args)
+ {
+ if (args.IsSettingsInvoked || args.InvokedItemContainer?.Tag is not string viewName || string.IsNullOrWhiteSpace(viewName))
+ return;
+ if (string.Equals(_activeViewName, viewName, StringComparison.Ordinal))
+ return;
+
+ try
+ {
+ await _regionManager.RequestNavigateAsync(Name, viewName);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"NavigationView navigation to '{viewName}' failed: {ex}");
+ }
+ }
+
+ private async void OnBackRequested(NavigationView sender, NavigationViewBackRequestedEventArgs args)
+ {
+ try
+ {
+ if (await ((IRegion)this).CanGoBackAsync())
+ await GoBackAsync();
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"NavigationView back navigation failed: {ex}");
+ }
+ finally
+ {
+ await UpdateBackButtonAsync();
+ }
+ }
+
+ private async Task UpdateBackButtonAsync()
+ => _navigationView.IsBackEnabled = await ((IRegion)this).CanGoBackAsync();
+
+ private async void OnNavigated(object? sender, NavigationEventArgs e)
+ => await UpdateBackButtonAsync();
+
+ private NavigationViewItem? FindMenuItem(string viewName)
+ => FindMenuItem(_navigationView.MenuItems.Concat(_navigationView.FooterMenuItems), viewName);
+
+ private static NavigationViewItem? FindMenuItem(IEnumerable