diff --git a/Common/Adapters/ContentControlRegionAdapter.cs b/Common/Adapters/ContentControlRegionAdapter.cs
new file mode 100644
index 0000000..c632970
--- /dev/null
+++ b/Common/Adapters/ContentControlRegionAdapter.cs
@@ -0,0 +1,26 @@
+using SimpleNavigation.Interface.Adapters;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace SimpleNavigation.Common.Adapters;
+
+internal sealed class ContentControlRegionAdapter : IContentRegionHostAdapter
+{
+ public RegionHostKind Kind => RegionHostKind.Content;
+
+ public bool CanHandle(FrameworkElement region)
+ => region is ContentControl && region is not Frame;
+
+ public void Present(FrameworkElement host, FrameworkElement content)
+ {
+ if (host is not ContentControl contentControl || host is Frame)
+ {
+ throw new ArgumentException(
+ $"Host type '{host.GetType().FullName}' must be a non-Frame ContentControl.",
+ nameof(host));
+ }
+
+ host.Dispatcher.VerifyAccess();
+ contentControl.Content = content;
+ }
+}
diff --git a/Common/Adapters/FrameRegionAdapter.cs b/Common/Adapters/FrameRegionAdapter.cs
new file mode 100644
index 0000000..a51f71c
--- /dev/null
+++ b/Common/Adapters/FrameRegionAdapter.cs
@@ -0,0 +1,30 @@
+using SimpleNavigation.Interface.Adapters;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace SimpleNavigation.Common.Adapters;
+
+internal sealed class FrameRegionAdapter : IPageRegionHostAdapter
+{
+ public RegionHostKind Kind => RegionHostKind.Page;
+
+ public bool CanHandle(FrameworkElement region) => region is Frame;
+
+ public bool Navigate(Frame frame, Page page)
+ {
+ frame.Dispatcher.VerifyAccess();
+ return frame.Navigate(page);
+ }
+
+ public bool CanGoBack(Frame frame)
+ {
+ frame.Dispatcher.VerifyAccess();
+ return frame.CanGoBack;
+ }
+
+ public void GoBack(Frame frame)
+ {
+ frame.Dispatcher.VerifyAccess();
+ frame.GoBack();
+ }
+}
diff --git a/Common/Adapters/RegionHostAdapter.cs b/Common/Adapters/RegionHostAdapter.cs
new file mode 100644
index 0000000..fdc0ee2
--- /dev/null
+++ b/Common/Adapters/RegionHostAdapter.cs
@@ -0,0 +1,49 @@
+using SimpleNavigation.Interface.Adapters;
+using System.Windows;
+
+namespace SimpleNavigation.Common.Adapters;
+
+///
+/// 区域宿主可接受的内容类型
+///
+internal enum RegionHostKind
+{
+ Page,
+ Content,
+}
+
+///
+/// 获取适配导航元素的 Adapter
+///
+internal static class RegionHostAdapterResolver
+{
+ private static readonly IRegionHostAdapter[] HostAdapters =
+ {
+ new FrameRegionAdapter(),
+ new ContentControlRegionAdapter(),
+ };
+
+ ///
+ /// 根据 附加对象的类型,筛选相匹配的 Adapter
+ ///
+ /// 附加对象
+ /// 相匹配的 Adapter
+ ///
+ ///
+ public static IRegionHostAdapter GetRequired(FrameworkElement attachedRegion)
+ {
+ if (attachedRegion == null)
+ throw new ArgumentNullException(nameof(attachedRegion));
+
+ foreach (var adapter in HostAdapters)
+ {
+ if (adapter.CanHandle(attachedRegion))
+ return adapter;
+ }
+
+ var regionType = attachedRegion.GetType();
+ throw new ArgumentException(
+ $"Region host type '{regionType.FullName}' is not supported.",
+ nameof(attachedRegion));
+ }
+}
diff --git a/Common/DialogManager.cs b/Common/Managers/DialogManager.cs
similarity index 93%
rename from Common/DialogManager.cs
rename to Common/Managers/DialogManager.cs
index 697556c..863dbbd 100644
--- a/Common/DialogManager.cs
+++ b/Common/Managers/DialogManager.cs
@@ -1,8 +1,8 @@
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
-using SimpleNavigation.Interface;
+using SimpleNavigation.Interface.Managers;
-namespace SimpleNavigation.Common
+namespace SimpleNavigation.Common.Managers
{
public class DialogManager : IDialogManager
{
diff --git a/Common/Managers/RegionManager.cs b/Common/Managers/RegionManager.cs
new file mode 100644
index 0000000..24f97e0
--- /dev/null
+++ b/Common/Managers/RegionManager.cs
@@ -0,0 +1,274 @@
+using System.Windows;
+using SimpleNavigation.Common.Adapters;
+using SimpleNavigation.Interface.Managers;
+using SimpleNavigation.Services;
+
+namespace SimpleNavigation.Common.Managers
+{
+ public sealed class RegionManager : IRegionManager, IDisposable
+ {
+ private readonly Dictionary regions = new(StringComparer.Ordinal);
+ private readonly Action declarationSubscriber;
+ private readonly List pendingDeclarationChanges = new();
+ private readonly object syncRoot = new();
+ private bool isImportingDeclarations = true;
+ private bool isDisposed;
+
+ public RegionManager()
+ {
+ declarationSubscriber = OnDeclarationChanged;
+ Region.Subscribe(declarationSubscriber);
+
+ try
+ {
+ var snapshot = Region.GetActiveSnapshot();
+
+ lock (syncRoot)
+ {
+ foreach (var change in snapshot)
+ ApplyDeclarationChangeUnderLock(change);
+
+ foreach (var change in pendingDeclarationChanges)
+ ApplyDeclarationChangeUnderLock(change);
+
+ pendingDeclarationChanges.Clear();
+ isImportingDeclarations = false;
+ }
+ }
+ catch
+ {
+ lock (syncRoot)
+ {
+ isDisposed = true;
+ isImportingDeclarations = false;
+ pendingDeclarationChanges.Clear();
+ regions.Clear();
+ }
+
+ Region.Unsubscribe(declarationSubscriber);
+ throw;
+ }
+ }
+
+ public void RegisterRegion(string regionName, FrameworkElement region)
+ {
+ ValidateRegionName(regionName);
+ if (region == null) throw new ArgumentNullException(nameof(region));
+
+ RegionHostAdapterResolver.GetRequired(region); // 无匹配的Adapter则抛出异常
+
+ lock (syncRoot)
+ {
+ ThrowIfDisposedUnderLock();
+
+ if (TryGetLiveEntryUnderLock(regionName, out var existingEntry, out var existingRegion))
+ {
+ if (ReferenceEquals(existingRegion, region))
+ {
+ existingEntry.HasProgrammaticOwnership = true;
+ return;
+ }
+
+ throw new InvalidOperationException($"Region '{regionName}' is already registered.");
+ }
+
+ regions[regionName] = new RegionEntry(region)
+ {
+ HasProgrammaticOwnership = true,
+ };
+ }
+ }
+
+ public bool UnregisterRegion(string regionName, FrameworkElement region)
+ {
+ ValidateRegionName(regionName);
+
+ if (region == null)
+ {
+ throw new ArgumentNullException(nameof(region));
+ }
+
+ lock (syncRoot)
+ {
+ ThrowIfDisposedUnderLock();
+
+ if (!TryGetLiveEntryUnderLock(regionName, out var existingEntry, out var existingRegion))
+ {
+ return false;
+ }
+
+ if (!ReferenceEquals(existingRegion, region) ||
+ !existingEntry.HasProgrammaticOwnership)
+ {
+ return false;
+ }
+
+ existingEntry.HasProgrammaticOwnership = false;
+ if (existingEntry.AttachedActivationTokens.Count == 0)
+ {
+ regions.Remove(regionName);
+ }
+
+ return true;
+ }
+ }
+
+ public FrameworkElement? GetRegion(string regionName)
+ {
+ ValidateRegionName(regionName);
+
+ lock (syncRoot)
+ {
+ ThrowIfDisposedUnderLock();
+
+ if (TryGetLiveEntryUnderLock(regionName, out _, out var region))
+ {
+ return region;
+ }
+
+ return null;
+ }
+ }
+
+ public TRegion? GetRegion(string regionName) where TRegion : FrameworkElement
+ => GetRegion(regionName) as TRegion;
+
+ public void Dispose()
+ {
+ lock (syncRoot)
+ {
+ if (isDisposed)
+ {
+ return;
+ }
+
+ isDisposed = true;
+ isImportingDeclarations = false;
+ pendingDeclarationChanges.Clear();
+ regions.Clear();
+ }
+
+ Region.Unsubscribe(declarationSubscriber);
+ }
+
+ ///
+ /// 当宿主容器属性发生变化事件回调
+ ///
+ /// 变化事件携带信息
+ private void OnDeclarationChanged(RegionDeclarationChange change)
+ {
+ lock (syncRoot)
+ {
+ if (isDisposed)
+ return;
+
+ if (isImportingDeclarations)
+ {
+ pendingDeclarationChanges.Add(change);
+ return;
+ }
+
+ ApplyDeclarationChangeUnderLock(change);
+ }
+ }
+
+ private void ApplyDeclarationChangeUnderLock(RegionDeclarationChange change)
+ {
+ if (change.Kind == RegionDeclarationChangeKind.Add)
+ {
+ AddAttachedOwnershipUnderLock(change);
+ return;
+ }
+
+ RemoveAttachedOwnershipUnderLock(change);
+ }
+
+ private void AddAttachedOwnershipUnderLock(RegionDeclarationChange change)
+ {
+ var host = change.Host;
+
+ if (TryGetLiveEntryUnderLock(change.Name, out var existingEntry, out var existingHost))
+ {
+ if (!ReferenceEquals(existingHost, host))
+ {
+ throw new InvalidOperationException($"Region '{change.Name}' is already registered.");
+ }
+
+ existingEntry.AttachedActivationTokens.Add(change.ActivationToken);
+ return;
+ }
+
+ var entry = new RegionEntry(host);
+ entry.AttachedActivationTokens.Add(change.ActivationToken);
+ regions[change.Name] = entry;
+ }
+
+ private void RemoveAttachedOwnershipUnderLock(RegionDeclarationChange change)
+ {
+ if (!TryGetLiveEntryUnderLock(change.Name, out var existingEntry, out var existingHost) ||
+ !ReferenceEquals(existingHost, change.Host))
+ {
+ return;
+ }
+
+ existingEntry.AttachedActivationTokens.Remove(change.ActivationToken);
+ if (!existingEntry.HasProgrammaticOwnership &&
+ existingEntry.AttachedActivationTokens.Count == 0)
+ {
+ regions.Remove(change.Name);
+ }
+ }
+
+ ///
+ /// 获取活跃的
+ ///
+ ///
+ ///
+ ///
+ ///
+ private bool TryGetLiveEntryUnderLock(string regionName, out RegionEntry entry, out FrameworkElement region)
+ {
+ if (!regions.TryGetValue(regionName, out entry!))
+ {
+ region = null!;
+ return false;
+ }
+
+ if (entry.Host.TryGetTarget(out region!))
+ return true;
+
+ regions.Remove(regionName);
+ entry = null!;
+ region = null!;
+ return false;
+ }
+
+ private void ThrowIfDisposedUnderLock()
+ {
+ if (isDisposed)
+ throw new ObjectDisposedException(nameof(RegionManager));
+ }
+
+ private static void ValidateRegionName(string regionName)
+ {
+ if (string.IsNullOrWhiteSpace(regionName))
+ throw new ArgumentException("Region name cannot be null, empty, or whitespace.", nameof(regionName));
+ }
+ }
+
+ internal sealed class RegionEntry
+ {
+ public RegionEntry(FrameworkElement host)
+ {
+ Host = new WeakReference(host);
+ }
+
+ public WeakReference Host { get; }
+
+ public bool HasProgrammaticOwnership { get; set; }
+
+ public HashSet AttachedActivationTokens { get; } = new();
+ }
+}
+
+
diff --git a/Common/NavigationAwareNotifier.cs b/Common/NavigationAwareNotifier.cs
new file mode 100644
index 0000000..4172348
--- /dev/null
+++ b/Common/NavigationAwareNotifier.cs
@@ -0,0 +1,38 @@
+using SimpleNavigation.Interface.Awares;
+using System.Windows;
+
+namespace SimpleNavigation.Common
+{
+ ///
+ /// 对于Page或Content类型的导航回调
+ ///
+ internal static class NavigationAwareNotifier
+ {
+ public static void Notify(FrameworkElement target, DialogParameters? parameters)
+ {
+ if (target is IViewInitializeAware view && !view.IsViewInitialized)
+ {
+ view.Initialize();
+ view.IsViewInitialized = true;
+ }
+
+ var dataContext = target.DataContext;
+
+ if (dataContext is IViewInitializeAware contextViewAware
+ && !ReferenceEquals(contextViewAware, target)
+ && !contextViewAware.IsViewInitialized)
+ {
+ contextViewAware.Initialize();
+ contextViewAware.IsViewInitialized = true;
+ }
+
+ if (target is INavigationAware targetAware)
+ targetAware.OnNavigated(parameters);
+
+ if (dataContext is INavigationAware contextAware && !ReferenceEquals(dataContext, target))
+ contextAware.OnNavigated(parameters);
+ }
+ }
+}
+
+
diff --git a/Common/NavigationRouteRegistry.cs b/Common/NavigationRouteRegistry.cs
new file mode 100644
index 0000000..6601191
--- /dev/null
+++ b/Common/NavigationRouteRegistry.cs
@@ -0,0 +1,80 @@
+namespace SimpleNavigation.Common
+{
+ ///
+ /// 被导航对象的类型
+ ///
+ internal enum NavigationRouteKind
+ {
+ Page,
+ Content,
+ }
+
+ ///
+ /// 导航对象
+ ///
+ internal sealed class NavigationRouteRegistration
+ {
+ public NavigationRouteKind Kind { get; }
+
+ public string Key { get; }
+
+ public Type TargetType { get; }
+
+ public NavigationRouteRegistration(NavigationRouteKind kind, string key, Type targetType)
+ {
+ Kind = kind;
+ Key = key;
+ TargetType = targetType;
+ }
+ }
+
+ internal sealed class NavigationRouteRegistry
+ {
+ private readonly IReadOnlyDictionary pages;
+ private readonly IReadOnlyDictionary contents;
+
+ public NavigationRouteRegistry(IEnumerable registrations)
+ {
+ pages = BuildRoute(registrations, NavigationRouteKind.Page);
+ contents = BuildRoute(registrations, NavigationRouteKind.Content);
+ }
+
+ public Type GetRequiredPageType(string key) => GetRequiredTarget(pages, key, "page");
+
+ public Type GetRequiredContentType(string key) => GetRequiredTarget(contents, key, "content");
+
+ ///
+ /// Build 导航路由
+ ///
+ /// 被导航对象
+ /// 被导航对象的类型
+ ///
+ private IReadOnlyDictionary BuildRoute(
+ IEnumerable registrations,
+ NavigationRouteKind kind)
+ {
+ var routes = new Dictionary(StringComparer.Ordinal);
+
+ foreach (var registration in registrations.Where(item => item.Kind == kind))
+ routes.Add(registration.Key, registration.TargetType);
+
+ return routes;
+ }
+
+ private Type GetRequiredTarget(IReadOnlyDictionary routes, string key, string category)
+ {
+ if (string.IsNullOrWhiteSpace(key))
+ {
+ throw new ArgumentException(
+ "Route key cannot be null or whitespace.",
+ nameof(key));
+ }
+
+ if (routes.TryGetValue(key, out var targetType))
+ return targetType;
+
+ throw new KeyNotFoundException(
+ $"No {category} route is registered for key '{key}'.");
+ }
+ }
+}
diff --git a/Extensions/NavigationExtensions.cs b/Extensions/NavigationExtensions.cs
index 756a2fd..5f1f0c0 100644
--- a/Extensions/NavigationExtensions.cs
+++ b/Extensions/NavigationExtensions.cs
@@ -1,20 +1,119 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using SimpleNavigation.Common;
-using SimpleNavigation.Interface;
+using SimpleNavigation.Common.Managers;
+using SimpleNavigation.Interface.Managers;
+using SimpleNavigation.Interface.Services;
using SimpleNavigation.Services;
+using System.Windows;
+using System.Windows.Controls;
namespace SimpleNavigation.Extensions
{
- public static class NavigationExtensions
- {
- public static IServiceCollection RegisterNavigationService(this IServiceCollection serviceCollection)
- {
- serviceCollection.TryAddSingleton();
- serviceCollection.TryAddSingleton();
- serviceCollection.TryAddSingleton();
-
- return serviceCollection;
- }
- }
+ public static class NavigationExtensions
+ {
+ public static IServiceCollection RegisterNavigationService(
+ this IServiceCollection serviceCollection)
+ {
+ serviceCollection.TryAddSingleton();
+ serviceCollection.TryAddSingleton();
+ serviceCollection.TryAddSingleton();
+ serviceCollection.TryAddSingleton();
+ serviceCollection.TryAddSingleton();
+ serviceCollection.TryAddSingleton(provider =>
+ new NavigationRouteRegistry(
+ provider.GetServices()));
+
+ return serviceCollection;
+ }
+
+ public static IServiceCollection AddPage(this IServiceCollection services, string key)
+ where TPage : Page
+ {
+ AddRoute(services, NavigationRouteKind.Page, key, typeof(TPage));
+ services.TryAddSingleton();
+ return services;
+ }
+
+ public static IServiceCollection AddPage(this IServiceCollection services)
+ where TPage : Page where TViewModel : class
+ {
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ return services;
+ }
+
+ public static IServiceCollection AddPage(this IServiceCollection services, string key)
+ where TPage : Page where TViewModel : class
+ {
+ AddRoute(services, NavigationRouteKind.Page, key, typeof(TPage));
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ return services;
+ }
+
+ public static IServiceCollection AddContent(this IServiceCollection services,string key)
+ where TView : FrameworkElement
+ {
+ ValidateContentType(typeof(TView));
+ AddRoute(services, NavigationRouteKind.Content, key, typeof(TView));
+ services.TryAddSingleton();
+ return services;
+ }
+
+ public static IServiceCollection AddContent(this IServiceCollection services)
+ where TView : FrameworkElement where TViewModel : class
+ {
+ ValidateContentType(typeof(TView));
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ return services;
+ }
+
+ public static IServiceCollection AddContent(this IServiceCollection services, string key)
+ where TView : FrameworkElement where TViewModel : class
+ {
+ ValidateContentType(typeof(TView));
+ AddRoute(services, NavigationRouteKind.Content, key, typeof(TView));
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ return services;
+ }
+
+ private static void AddRoute(IServiceCollection services, NavigationRouteKind kind, string key, Type targetType)
+ {
+ if (string.IsNullOrWhiteSpace(key))
+ {
+ throw new ArgumentException(
+ "Route key cannot be null or whitespace.",
+ nameof(key));
+ }
+
+ var duplicate = services.Any(descriptor =>
+ descriptor.ServiceType == typeof(NavigationRouteRegistration) &&
+ descriptor.ImplementationInstance is NavigationRouteRegistration route &&
+ route.Kind == kind &&
+ string.Equals(route.Key, key, StringComparison.Ordinal));
+
+ if (duplicate)
+ {
+ throw new ArgumentException(
+ $"A {kind.ToString().ToLowerInvariant()} route with key '{key}' is already registered.",
+ nameof(key));
+ }
+
+ services.AddSingleton(new NavigationRouteRegistration(kind, key, targetType));
+ }
+
+ private static void ValidateContentType(Type targetType)
+ {
+ if (typeof(Page).IsAssignableFrom(targetType) ||
+ typeof(Window).IsAssignableFrom(targetType))
+ {
+ throw new ArgumentException(
+ $"Content type '{targetType.FullName}' cannot derive from Page or Window.",
+ nameof(targetType));
+ }
+ }
+ }
}
diff --git a/Interface/Adapters/IContentRegionHostAdapter.cs b/Interface/Adapters/IContentRegionHostAdapter.cs
new file mode 100644
index 0000000..e0f2158
--- /dev/null
+++ b/Interface/Adapters/IContentRegionHostAdapter.cs
@@ -0,0 +1,17 @@
+锘縰sing System.Windows;
+
+namespace SimpleNavigation.Interface.Adapters
+{
+ ///
+ /// 瀹圭撼 瀵硅薄鐨勫涓婚傞厤鍣
+ ///
+ internal interface IContentRegionHostAdapter : IRegionHostAdapter
+ {
+ ///
+ /// 瀵艰埅鏍稿績瀹炵幇鏂规硶锛氬皢闇瑕佸鑸殑鍐呭鍔犲叆鍒板涓荤殑瑙嗚鏍
+ ///
+ /// 瀹夸富
+ /// 闇瑕佸鑸殑鍐呭
+ void Present(FrameworkElement host, FrameworkElement content);
+ }
+}
diff --git a/Interface/Adapters/IPageRegionHostAdapter.cs b/Interface/Adapters/IPageRegionHostAdapter.cs
new file mode 100644
index 0000000..c750074
--- /dev/null
+++ b/Interface/Adapters/IPageRegionHostAdapter.cs
@@ -0,0 +1,16 @@
+锘縰sing System.Windows.Controls;
+
+namespace SimpleNavigation.Interface.Adapters
+{
+ ///
+ /// 瀹圭撼 瀵硅薄鐨勫涓婚傞厤鍣
+ ///
+ internal interface IPageRegionHostAdapter : IRegionHostAdapter
+ {
+ bool Navigate(Frame frame, Page page);
+
+ bool CanGoBack(Frame frame);
+
+ void GoBack(Frame frame);
+ }
+}
diff --git a/Interface/Adapters/IRegionHostAdapter.cs b/Interface/Adapters/IRegionHostAdapter.cs
new file mode 100644
index 0000000..acb0507
--- /dev/null
+++ b/Interface/Adapters/IRegionHostAdapter.cs
@@ -0,0 +1,24 @@
+锘縰sing SimpleNavigation.Common.Adapters;
+using System.Windows;
+
+namespace SimpleNavigation.Interface.Adapters
+{
+ ///
+ /// Region 瀹夸富閫傞厤鍣
+ /// 鑻ヨ瀹炵幇鍏朵粬绫诲瀷鎺т欢浣滀负瀹夸富鐨勮兘鍔涳紝蹇呴』瀹炵幇姝ゆ帴鍙
+ ///
+ internal interface IRegionHostAdapter
+ {
+ ///
+ /// 瀹夸富绫诲瀷
+ ///
+ RegionHostKind Kind { get; }
+
+ ///
+ /// 鏍囧織褰撳墠 Adapter 鍙互澶勭悊鐨勫厓绱
+ ///
+ /// 褰撳墠 Adapter 鎵鎷ユ湁锛堝搴旓級鐨 Region 瀹炰緥
+ ///
+ bool CanHandle(FrameworkElement region);
+ }
+}
diff --git a/Interface/Awares/IDialogAware.cs b/Interface/Awares/IDialogAware.cs
new file mode 100644
index 0000000..1aab2a2
--- /dev/null
+++ b/Interface/Awares/IDialogAware.cs
@@ -0,0 +1,22 @@
+锘縰sing SimpleNavigation.Common;
+
+namespace SimpleNavigation.Interface.Awares
+{
+ ///
+ /// Window瀵艰埅鍥炶皟鎺ュ彛锛
+ /// 鑻ヨ瀹炵幇Window瀵艰埅鍚庢垨鍏抽棴鍚庢墽琛屽洖璋冿紙浼犻掑弬鏁帮級锛孷iew鎴朧iewModel蹇呴』瀹炵幇姝ゆ帴鍙
+ ///
+ public interface IDialogAware : INavigationAware
+ {
+ ///
+ /// 褰揇ialog璇锋眰鍏抽棴鏃惰皟鐢紝鍙紶閫掑弬鏁
+ ///
+ Action? RequestClose { get; set; }
+
+ /////
+ ///// 褰揇ialog瀵艰埅瀹屾垚鏃剁殑鍥炶皟鏂规硶
+ /////
+ ///// 瀵艰埅鍙傛暟
+ //void OnNavigated(DialogParameters? parameters);
+ }
+}
diff --git a/Interface/Awares/INavigationAware.cs b/Interface/Awares/INavigationAware.cs
new file mode 100644
index 0000000..2ecff3f
--- /dev/null
+++ b/Interface/Awares/INavigationAware.cs
@@ -0,0 +1,19 @@
+using SimpleNavigation.Common;
+
+namespace SimpleNavigation.Interface.Awares
+{
+ ///
+ /// 导航完成后的回调接口,
+ /// 若需导航后执行回调(传递参数),View 或 ViewModel必须实现此接口
+ ///
+ public interface INavigationAware
+ {
+ ///
+ /// 导航后的回调方法
+ ///
+ /// 传递的参数
+ void OnNavigated(DialogParameters? parameters);
+ }
+}
+
+
diff --git a/Interface/Awares/IPageAware.cs b/Interface/Awares/IPageAware.cs
new file mode 100644
index 0000000..7a062c6
--- /dev/null
+++ b/Interface/Awares/IPageAware.cs
@@ -0,0 +1,17 @@
+锘縰sing SimpleNavigation.Common;
+
+namespace SimpleNavigation.Interface.Awares
+{
+ ///
+ /// Page瀵艰埅鍥炶皟鎺ュ彛锛
+ /// 鑻ヨ瀹炵幇Page瀵艰埅鍚庢墽琛屽洖璋冿紙浼犻掑弬鏁帮級锛孷iew鎴朧iewModel蹇呴』瀹炵幇姝ゆ帴鍙
+ ///
+ public interface IPageAware : INavigationAware
+ {
+ ///
+ /// 褰撻〉闈㈡帴鏀舵秷鎭椂鐨勫洖璋冩柟娉
+ ///
+ /// 娑堟伅鍙傛暟
+ event Action? Receive;
+ }
+}
diff --git a/Interface/Awares/IViewInitializeAware.cs b/Interface/Awares/IViewInitializeAware.cs
new file mode 100644
index 0000000..97eb5fc
--- /dev/null
+++ b/Interface/Awares/IViewInitializeAware.cs
@@ -0,0 +1,18 @@
+锘縩amespace SimpleNavigation.Interface.Awares
+{
+ ///
+ /// View 鍒濆鍖栧畬姣曞悗锛岃嫢闇瑕佸垵濮嬪寲UI锛屽垯寤鸿缁ф壙璇ユ帴鍙o紝灏哢I鍒濆鍖栭昏緫锛堝瀵艰埅銆佹樉绀哄唴瀹癸級鍐欏埌Initialize鏂规硶涓
+ ///
+ public interface IViewInitializeAware
+ {
+ ///
+ /// 鐢ㄤ簬琛ㄧず鏄惁宸茶繘琛岃繃鍒濆鍖
+ ///
+ bool IsViewInitialized { get; set; }
+
+ ///
+ /// View 鍒濆鍖栧畬姣曞悗锛岃嫢闇瑕佸垵濮嬪寲UI锛屽垯寤鸿缁ф壙璇ユ帴鍙o紝灏哢I鍒濆鍖栭昏緫锛堝瀵艰埅銆佹樉绀哄唴瀹癸級鍐欏埌Initialize鏂规硶涓
+ ///
+ void Initialize();
+ }
+}
diff --git a/Interface/IDialogAware.cs b/Interface/IDialogAware.cs
deleted file mode 100644
index 271218a..0000000
--- a/Interface/IDialogAware.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-锘縰sing SimpleNavigation.Common;
-
-namespace SimpleNavigation.Interface
-{
- public interface IDialogAware
- {
- ///
- /// 褰揇ialog璇锋眰鍏抽棴鏃惰皟鐢
- ///
- Action? RequestClose { get; set; }
-
- ///
- /// 褰揇ialog瀵艰埅瀹屾垚鏃剁殑鍥炶皟鏂规硶
- ///
- /// 瀵艰埅鍙傛暟
- void OnNavigated(DialogParameters? parameters);
- }
-}
diff --git a/Interface/IPageAware.cs b/Interface/IPageAware.cs
deleted file mode 100644
index 17b302b..0000000
--- a/Interface/IPageAware.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-锘縰sing SimpleNavigation.Common;
-
-namespace SimpleNavigation.Interface
-{
- public interface IPageAware
- {
- ///
- /// 褰撻〉闈㈡帴鏀舵秷鎭椂鐨勫洖璋冩柟娉
- ///
- /// 娑堟伅鍙傛暟
- event Action? Receive;
-
- ///
- /// 褰撻〉闈㈠鑸畬鎴愭椂鐨勫洖璋冩柟娉
- ///
- /// 瀵艰埅鍙傛暟
- void OnNavigated(DialogParameters? parameters);
- }
-}
diff --git a/Interface/IPageService.cs b/Interface/IPageService.cs
deleted file mode 100644
index cefd01a..0000000
--- a/Interface/IPageService.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-锘縰sing SimpleNavigation.Common;
-using System.Windows.Controls;
-
-namespace SimpleNavigation.Interface
-{
- public interface IPageService
- {
- ///
- /// 瀵艰埅鍒版寚瀹氶〉闈
- ///
- /// 椤甸潰绫诲瀷
- /// 鍖哄煙鍚嶇О
- /// 椤甸潰鍙傛暟
- void Navigate(string regionName, DialogParameters? parameters = null) where T : Page;
-
- ///
- /// 瀵艰埅鍒版寚瀹氶〉闈
- ///
- /// 鍖哄煙鍚嶇О
- /// 鐩爣绫诲瀷
- /// 椤甸潰鍙傛暟
- void Navigate(string regionName, Type targetType, DialogParameters? parameters = null);
-
- ///
- /// 鑾峰彇Page鐨勭埗瀹瑰櫒
- ///
- /// 鍖哄煙鍚嶇О
- /// Page鐨勭埗瀹瑰櫒
- Frame? GetRegion(string regionName);
-
- ///
- /// 瀵艰埅杩斿洖涓婁竴椤
- ///
- /// 鍖哄煙鍚嶇О
- void Goback(string region);
- }
-}
diff --git a/Interface/IDialogManager.cs b/Interface/Managers/IDialogManager.cs
similarity index 75%
rename from Interface/IDialogManager.cs
rename to Interface/Managers/IDialogManager.cs
index 84fcddd..9d0801c 100644
--- a/Interface/IDialogManager.cs
+++ b/Interface/Managers/IDialogManager.cs
@@ -1,7 +1,10 @@
using System.Windows;
-namespace SimpleNavigation.Interface
+namespace SimpleNavigation.Interface.Managers
{
+ ///
+ /// Dialog 绠$悊瀵硅薄
+ ///
public interface IDialogManager
{
///
diff --git a/Interface/Managers/IRegionManager.cs b/Interface/Managers/IRegionManager.cs
new file mode 100644
index 0000000..d8ab1c4
--- /dev/null
+++ b/Interface/Managers/IRegionManager.cs
@@ -0,0 +1,42 @@
+using System.Windows;
+
+namespace SimpleNavigation.Interface.Managers
+{
+ ///
+ /// Region管理对象,负责Region的注册、注销、获取
+ ///
+ public interface IRegionManager
+ {
+ ///
+ /// 将指定控件注册为 Region
+ ///
+ /// Region名称
+ /// Region 对象(指定为 Region 的实例控件)
+ void RegisterRegion(string regionName, FrameworkElement region);
+
+ ///
+ /// 将指定控件从 Region 中注销
+ ///
+ /// Region 名称
+ /// Region 对象(指定为 Region 的实例控件)
+ /// 是否成功注销
+ bool UnregisterRegion(string regionName, FrameworkElement region);
+
+ ///
+ /// 获取指定 Region 实例
+ ///
+ /// Region 名称
+ /// 获得的 Region 实例
+ FrameworkElement? GetRegion(string regionName);
+
+ ///
+ /// 获取指定类型的 Region 实例
+ ///
+ /// 指定的控件类型
+ /// Region 名称
+ /// 指定类型控件的实例
+ TRegion? GetRegion(string regionName) where TRegion : FrameworkElement;
+ }
+}
+
+
diff --git a/Interface/Services/IContentService.cs b/Interface/Services/IContentService.cs
new file mode 100644
index 0000000..1413fa0
--- /dev/null
+++ b/Interface/Services/IContentService.cs
@@ -0,0 +1,20 @@
+using SimpleNavigation.Common;
+using System.Windows;
+
+namespace SimpleNavigation.Interface.Services
+{
+ ///
+ /// Content 导航接口
+ ///
+ public interface IContentService
+ {
+ void Navigate(string regionName, DialogParameters? parameters = null)
+ where TContent : FrameworkElement;
+
+ void Navigate(string regionName, Type targetType, DialogParameters? parameters = null);
+
+ void Navigate(string regionName, string key, DialogParameters? parameters = null);
+ }
+}
+
+
diff --git a/Interface/IDialogService.cs b/Interface/Services/IDialogService.cs
similarity index 94%
rename from Interface/IDialogService.cs
rename to Interface/Services/IDialogService.cs
index 8f4beb6..c6c6b1c 100644
--- a/Interface/IDialogService.cs
+++ b/Interface/Services/IDialogService.cs
@@ -1,7 +1,7 @@
using SimpleNavigation.Common;
using System.Windows;
-namespace SimpleNavigation.Interface
+namespace SimpleNavigation.Interface.Services
{
///
/// 绐楀彛鏈嶅姟鎺ュ彛锛岀敤浜庢墦寮鏂扮獥鍙
diff --git a/Interface/Services/IPageService.cs b/Interface/Services/IPageService.cs
new file mode 100644
index 0000000..a5698d1
--- /dev/null
+++ b/Interface/Services/IPageService.cs
@@ -0,0 +1,22 @@
+using SimpleNavigation.Common;
+using System.Windows.Controls;
+
+namespace SimpleNavigation.Interface.Services
+{
+ ///
+ /// Page 导航接口
+ ///
+ public interface IPageService
+ {
+ void Navigate(string regionName, DialogParameters? parameters = null) where TPage : Page;
+
+ void Navigate(string regionName, Type targetType, DialogParameters? parameters = null);
+
+ void Navigate(string regionName, string key, DialogParameters? parameters = null);
+
+ void GoBack(string regionName);
+
+ [Obsolete("Use GoBack instead.")]
+ void Goback(string regionName);
+ }
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index fe130ad..db6ec93 100644
--- a/README.md
+++ b/README.md
@@ -2,327 +2,281 @@
[](https://www.nuget.org/packages/Junevy.SimpleNavigation/)
-涓涓秴杞婚噺绾х殑 WPF 瀵艰埅妗嗘灦锛屽熀浜 `Microsoft.Extensions.DependencyInjection` 鏋勫缓锛岃 WPF 搴旂敤鐨勯〉闈㈠鑸拰绐楀彛绠$悊鍙樺緱绠鍗曠洿瑙傘
+SimpleNavigation 鏄竴涓熀浜 `Microsoft.Extensions.DependencyInjection` 鐨勮交閲忕骇 WPF 瀵艰埅绫诲簱锛屾敮鎸 .NET 8 WPF 涓 .NET Framework 4.8銆
----
+## 鍔熻兘姒傝
-## 椤圭洰浠嬬粛
+- `PageService`锛氬湪鍛藉悕鐨 `Frame` 鍖哄煙涓鑸 `Page`锛屾敮鎸佽繑鍥炰笂涓椤点
+- `ContentService`锛氬湪鍛藉悕鐨勯潪 `Frame` `ContentControl` 鍖哄煙涓樉绀 `UserControl`銆佽嚜瀹氫箟 `ContentControl` 鎴栧叾浠栭潪 `Page`銆侀潪 `Window` 鐨 `FrameworkElement`銆
+- `DialogService`锛氶氳繃 DI 鍒涘缓骞舵樉绀 `Window`锛涘綋鍓嶅彲闈犺矾寰勬槸闈炴ā鎬佹樉绀恒佸弬鏁颁紶閫掍笌鍏抽棴璇锋眰銆
+- `RegionManager`锛氱粺涓绠$悊 XAML 澹版槑鎴栦唬鐮佹敞鍐岀殑鍛藉悕鍖哄煙锛屽苟浠ュ急寮曠敤淇濆瓨鍖哄煙瀹夸富銆
-SimpleNavigation 鏄竴涓彈 Prism 鍚彂鐨 WPF 瀵艰埅搴擄紝浣嗗幓闄や簡 Prism 鐨勫鏉傛э紝浠呬繚鐣欐渶鏍稿績鐨勫鑸兘鍔涖傛暣涓鏋朵粎鍖呭惈 11 涓 C# 婧愭枃浠讹紝闆堕澶栦緷璧栵紙鍙緷璧栧井杞畼鏂圭殑 DI 瀹瑰櫒锛夛紝鎻愪緵涓夊ぇ鏍稿績鍔熻兘锛
-
-- **鍖哄煙锛圧egion锛夐〉闈㈠鑸** 鈥 鍦ㄥ懡鍚 `Frame` 鍖哄煙鍐呰繘琛 `Page` 鐨勫鑸笌鍥為
-- **绐楀彛锛圖ialog锛夌鐞** 鈥 鎵撳紑 WPF `Window`锛屾敮鎸佹ā鎬/闈炴ā鎬併佸弬鏁颁紶閫掍笌缁撴灉杩斿洖
-- **瀵艰埅鍙傛暟浼犻** 鈥 閫氳繃 `DialogParameters` 鍦ㄩ〉闈㈠拰绐楀彛涔嬮棿浼犻掑己绫诲瀷鍙傛暟
-
-閰嶅悎 `CommunityToolkit.Mvvm` 浣跨敤鏁堟灉鏇翠匠銆
-
----
+瀵艰埅鐩爣鍏ㄩ儴鐢卞簲鐢ㄧ殑 DI 瀹瑰櫒鍒涘缓銆傜被搴撲笉璁剧疆 `DataContext`锛屽洜姝 View 涓 ViewModel 鐨勬瀯閫犳敞鍏ュ拰缁戝畾鏂瑰紡浠嶇敱搴旂敤鍐冲畾銆
## 鐜瑕佹眰
-| 鐩爣妗嗘灦 | 璇存槑 |
-|----------|------|
-| `.NET 8.0` (net8.0-windows) | 鐜颁唬 .NET WPF 搴旂敤 |
-| `.NET Framework 4.8` (net48) | 浼犵粺 .NET Framework WPF 搴旂敤 |
-
-寮鍙戠幆澧冿細Visual Studio 2022 (v17.14+)锛孋# 13銆
-
----
-
-## 渚濊禆淇℃伅
-
-### NuGet 鍖
-
-```
-Junevy.SimpleNavigation
-```
+| 鐩爣妗嗘灦 | DI 渚濊禆 |
+| --- | --- |
+| `net8.0-windows` | `Microsoft.Extensions.DependencyInjection` 8.0.0 |
+| `net48` | `Microsoft.Extensions.DependencyInjection` 6.0.1 |
-### 妗嗘灦渚濊禆
-
-| 鐩爣妗嗘灦 | 渚濊禆椤 | 鐗堟湰 |
-|----------|--------|------|
-| net8.0-windows | `Microsoft.Extensions.DependencyInjection` | 8.0.0 |
-| net48 | `Microsoft.Extensions.DependencyInjection` | 6.0.1 |
-
-浠呬緷璧栧井杞畼鏂圭殑 DI 瀹瑰櫒锛屾棤闇 Prism 鎴栧叾浠栭噸鍨嬫鏋躲
-
----
-
-## 鍔熻兘绠浠嬩笌婕旂ず
-
-### 1. 椤圭洰缁撴瀯
-
-```
-SimpleNavigation/
-鈹溾攢鈹 Common/
-鈹 鈹溾攢鈹 DialogManager.cs # 绐楀彛鐢熷懡鍛ㄦ湡绠$悊锛學eakReference 缂撳瓨闃叉鍐呭瓨娉勬紡
-鈹 鈹斺攢鈹 DialogParameters.cs # 瀵艰埅鍙傛暟瀵硅薄锛屾敮鎸佸瓧鍏/绱㈠紩/閿间笁绉嶆瀯閫
-鈹溾攢鈹 Extensions/
-鈹 鈹斺攢鈹 NavigationExtensions.cs # DI 瀹瑰櫒娉ㄥ唽鎵╁睍鏂规硶
-鈹溾攢鈹 Interface/
-鈹 鈹溾攢鈹 IPageService.cs # 椤甸潰瀵艰埅鏈嶅姟鎺ュ彛
-鈹 鈹溾攢鈹 IPageAware.cs # 椤甸潰鎰熺煡鎺ュ彛锛堟帴鏀跺鑸簨浠讹級
-鈹 鈹溾攢鈹 IDialogService.cs # 绐楀彛鏈嶅姟鎺ュ彛
-鈹 鈹溾攢鈹 IDialogAware.cs # 绐楀彛鎰熺煡鎺ュ彛锛堟帴鏀跺鑸簨浠 + 璇锋眰鍏抽棴锛
-鈹 鈹斺攢鈹 IDialogManager.cs # 绐楀彛瀹炰緥绠$悊鎺ュ彛
-鈹斺攢鈹 Services/
- 鈹溾攢鈹 PageService.cs # 椤甸潰瀵艰埅瀹炵幇
- 鈹溾攢鈹 DialogService.cs # 绐楀彛绠$悊瀹炵幇
- 鈹斺攢鈹 RegionService.cs # 鍖哄煙闄勫姞灞炴э紙XAML 澹版槑寮忔敞鍐岋級
-```
-
-### 2. 蹇熷紑濮
-
-#### 2.1 瀹夎
-
-閫氳繃 NuGet 瀹夎锛
+瀹夎 NuGet 鍖咃細
```bash
dotnet add package Junevy.SimpleNavigation
```
-鎴栦娇鐢 Package Manager锛
+## 椤圭洰缁撴瀯
-```
-Install-Package Junevy.SimpleNavigation
+```text
+SimpleNavigation/
+ Interface/
+ IRegionManager.cs # 鍖哄煙娉ㄥ唽涓庢煡璇
+ IPageService.cs # Page 瀵艰埅
+ IContentService.cs # FrameworkElement 鍐呭瀵艰埅
+ INavigationAware.cs # 瀵艰埅瀹屾垚閫氱煡
+ IPageAware.cs # 鍏煎鏃т唬鐮侊紝缁ф壙 INavigationAware
+ IDialogService.cs # Window 鏄剧ず鏈嶅姟
+ IDialogAware.cs # Window 瀵艰埅涓庡叧闂氱煡
+ IDialogManager.cs # Window 瀹炰緥绠$悊
+ Services/
+ Region.cs # RegionName 闄勫姞灞炴
+ PageService.cs # Frame/Page 瀵艰埅瀹炵幇
+ ContentService.cs # ContentControl 鍐呭瀵艰埅瀹炵幇
+ DialogService.cs # Window 鏄剧ず瀹炵幇
+ Common/
+ RegionManager.cs # 鍛藉悕鍖哄煙绠$悊
+ DialogManager.cs # Window 瀹炰緥绠$悊
+ DialogParameters.cs # 瀵艰埅鍙傛暟
+ Extensions/
+ NavigationExtensions.cs # DI 涓庤矾鐢辨敞鍐屾墿灞
```
-#### 2.2 娉ㄥ唽瀵艰埅鏈嶅姟
-
-鍦 `App.xaml.cs` 涓娇鐢ㄤ竴琛屼唬鐮佸畬鎴愭墍鏈夋湇鍔℃敞鍐岋細
+## 娉ㄥ唽鏈嶅姟鍜岀洰鏍
```csharp
using Microsoft.Extensions.DependencyInjection;
using SimpleNavigation.Extensions;
-public partial class App : Application
-{
- public IServiceProvider Provider { get; private set; }
-
- public void InitialProvider()
- {
- var container = new ServiceCollection();
+var services = new ServiceCollection();
+services.RegisterNavigationService();
- // 娉ㄥ唽瀵艰埅鏈嶅姟锛堜竴琛屾敞鍐 IDialogService / IPageService / IDialogManager锛
- container.RegisterNavigationService();
+// 鏅 DI 娉ㄥ唽瓒充互鏀寔娉涘瀷瀵艰埅鍜 Type 瀵艰埅銆
+services.AddTransient();
+services.AddSingleton();
- // 娉ㄥ唽浣犵殑椤甸潰銆佺獥鍙e拰 ViewModel
- container.AddTransient();
- container.AddSingleton();
- container.AddTransient();
- container.AddSingleton();
- container.AddSingleton((p) => new TestPage() { ShowsNavigationUI = false });
+// 璺敱鎵╁睍鍙悓鏃舵敞鍐 View/ViewModel锛屽苟鍙夋嫨娣诲姞瀛楃涓插埆鍚嶃
+services.AddPage("settings");
+services.AddPage();
+services.AddPage("reports");
+services.AddContent("help");
+services.AddContent();
+services.AddContent("status");
- Provider = container.BuildServiceProvider();
- }
-}
+var provider = services.BuildServiceProvider();
```
-#### 2.3 娉ㄥ唽瀵艰埅鍖哄煙锛圧egion锛
+`AddPage` 涓 `AddContent` 浣跨敤 `TryAddTransient` 娉ㄥ唽 View 鍜屽彲閫夌殑 ViewModel锛屼笉浼氳鐩栧湪瀹冧滑涔嬪墠娣诲姞鐨勬敞鍐屻傞渶瑕佽嚜瀹氫箟鐢熷懡鍛ㄦ湡鎴栧伐鍘傛椂锛屽簲鍏堜娇鐢ㄦ爣鍑 DI 鏂规硶娉ㄥ唽瀵瑰簲鏈嶅姟銆傝繖浜涙墿灞曟柟娉曚笉浼氬垱寤烘垨璁剧疆 View 鐨 `DataContext`銆
-鍦 XAML 涓氳繃闄勫姞灞炴у0鏄庡鑸尯鍩燂細
+鍙屾硾鍨嬫棤 key 鐨勯噸杞藉彧娉ㄥ唽 View 鍜 ViewModel锛涘崟娉涘瀷鍔 key 鐨勯噸杞芥敞鍐 View 涓庤矾鐢卞埆鍚嶏紱鍙屾硾鍨嬪姞 key 鐨勯噸杞藉悓鏃舵敞鍐 View銆乂iewModel 涓庤矾鐢卞埆鍚嶃侾age 涓 Content 鐨 key 绌洪棿鐩镐簰鐙珛銆佸尯鍒嗗ぇ灏忓啓锛屽悓涓绌洪棿鍐呬笉鑳介噸澶嶆敞鍐 key銆
-```xml
-
+## 澹版槑瀵艰埅鍖哄煙
+```xml
+
-
+
-
+
-
-
+
```
-> `RegionService.RegionName` 鍙兘闄勫姞鍒 `Frame` 鎺т欢涓婏紝鍚﹀垯浼氭姏鍑哄紓甯搞
+`PageService` 鐨勫尯鍩熷涓诲繀椤绘槸 `Frame`銆俙ContentService` 褰撳墠瑕佹眰鍖哄煙瀹夸富鏄潪 `Frame` 鐨 `ContentControl`銆備篃鍙互閫氳繃 `IRegionManager.RegisterRegion(...)` 鏄惧紡娉ㄥ唽瀹夸富锛屽苟閫氳繃 `GetRegion(...)` 鏌ヨ銆
-### 3. 椤甸潰瀵艰埅
+## Page 瀵艰埅
-#### 3.1 瀵艰埅鍒版寚瀹氶〉闈
+`IPageService` 鎻愪緵娉涘瀷銆乣Type` 鍜屽瓧绗︿覆 key 涓夌瀵艰埅鏂瑰紡锛
```csharp
-using SimpleNavigation.Interface;
using SimpleNavigation.Common;
+using SimpleNavigation.Interface;
-public class MainWindowViewModel
+public sealed class MainViewModel
{
private readonly IPageService pageService;
- public MainWindowViewModel(IPageService pageService)
+ public MainViewModel(IPageService pageService)
{
this.pageService = pageService;
}
- [RelayCommand]
- public void RegionTest()
+ public void OpenPages()
{
- // 娉涘瀷鏂瑰紡瀵艰埅
- pageService.Navigate("Main");
+ var parameters = new DialogParameters("id", 42);
- // 甯﹀弬鏁板鑸
- var parameters = new DialogParameters("key", "value");
- pageService.Navigate("Main", parameters);
-
- // Type 鏂瑰紡瀵艰埅
- pageService.Navigate("Main", typeof(TestPage), parameters);
+ pageService.Navigate("Pages", parameters);
+ pageService.Navigate("Pages", typeof(HomePage), parameters);
+ pageService.Navigate("Pages", "settings", parameters);
}
- [RelayCommand]
- public void GoBack()
+ public void Back()
{
- // 杩斿洖涓婁竴椤
- pageService.Goback("Main");
+ pageService.GoBack("Pages");
}
}
```
-#### 3.2 椤甸潰鎺ユ敹鍙傛暟
+娉涘瀷閲嶈浇鐩存帴浠 DI 瑙f瀽娉涘瀷绫诲瀷锛宍Type` 閲嶈浇鐩存帴浠 DI 瑙f瀽浼犲叆绫诲瀷銆傚彧鏈夊瓧绗︿覆閲嶈浇鍏堜粠 Page 璺敱瀛楀吀鍙栧緱鏈缁堢被鍨嬶紱鍙栧緱绫诲瀷鍚庝粛閫氳繃鏅 DI 瑙f瀽 Page 瀹炰緥銆
+
+## Content 瀵艰埅
-璁 ViewModel 瀹炵幇 `IPageAware` 鎺ュ彛锛
+`IContentService` 鍚屾牱鎻愪緵涓夌瀵艰埅鏂瑰紡锛
```csharp
using SimpleNavigation.Common;
using SimpleNavigation.Interface;
-public class TestViewModel : IPageAware
+public sealed class ShellViewModel
{
- public event Action? Receive;
+ private readonly IContentService contentService;
- public void OnNavigated(DialogParameters? parameters)
+ public ShellViewModel(IContentService contentService)
{
- if (parameters != null)
- {
- var value = parameters.Get("key");
- // 澶勭悊瀵艰埅鍙傛暟
- }
+ this.contentService = contentService;
+ }
+
+ public void OpenContent()
+ {
+ var parameters = new DialogParameters("id", 42);
+
+ contentService.Navigate("Content", parameters);
+ contentService.Navigate("Content", typeof(DashboardView), parameters);
+ contentService.Navigate("Content", "status", parameters);
}
}
```
-### 4. 绐楀彛锛圖ialog锛夌鐞
+娉涘瀷涓 `Type` 閲嶈浇涓嶈鍙栬矾鐢辫〃銆傚彧鏈夊瓧绗︿覆閲嶈浇浠 Content 璺敱瀛楀吀鍙栧緱鏈缁堢被鍨嬶紝闅忓悗閫氳繃鏅 DI 瑙f瀽瀹炰緥銆傚鑸洰鏍囧彲浠ユ槸 `UserControl`銆佹櫘閫氭垨鑷畾涔 `ContentControl`銆乣Grid`銆乣StackPanel` 绛 `FrameworkElement`锛屼絾涓嶈兘鏄 `Page` 鎴 `Window`銆
-#### 4.1 鎵撳紑绐楀彛
+## 瀵艰埅閫氱煡
+
+Page 鎴 Content 瀵艰埅鎴愬姛鍚庯紝鐩爣 View 鍙婂叾 `DataContext` 涓疄鐜颁簡 `INavigationAware` 鐨勫璞¢兘浼氭敹鍒伴氱煡锛涘鏋滀簩鑰呮槸鍚屼竴瀹炰緥锛屽垯鍙氱煡涓娆°
```csharp
-using SimpleNavigation.Interface;
using SimpleNavigation.Common;
+using SimpleNavigation.Interface;
-public class MainWindowViewModel
+public sealed class StatusViewModel : INavigationAware
{
- private readonly IDialogService dialogService;
-
- public MainWindowViewModel(IDialogService dialogService)
- {
- this.dialogService = dialogService;
- }
-
- [RelayCommand]
- public void OpenWindow()
- {
- // 闈炴ā鎬佹墦寮绐楀彛
- var param = new DialogParameters("key", "value");
- dialogService.Show(param);
- }
-
- [RelayCommand]
- public void OpenModalDialog()
+ public void OnNavigated(DialogParameters? parameters)
{
- // 妯℃佹墦寮绐楀彛锛岀瓑寰呯敤鎴峰叧闂苟杩斿洖缁撴灉
- var param = new DialogParameters("key", "value");
- var result = dialogService.ShowDialog(param);
-
- if (result != null)
- {
- var returnValue = result.Get("resultKey");
- }
+ var id = parameters?.Get("id");
}
}
```
-#### 4.2 绐楀彛鎺ユ敹鍙傛暟骞惰繑鍥炵粨鏋
+绫诲簱鍙彂閫侀氱煡锛屼笉璐熻矗鎶 `StatusViewModel` 璁剧疆涓 View 鐨 `DataContext`銆
-璁 ViewModel 鎴 Window 瀹炵幇 `IDialogAware` 鎺ュ彛锛
+## DialogService
+
+Window 鍙婂叾渚濊禆闇瑕佸厛娉ㄥ唽鍒 DI锛
```csharp
-using SimpleNavigation.Common;
-using SimpleNavigation.Interface;
+services.AddTransient();
+services.AddTransient();
+```
+
+浣跨敤 `IDialogService` 鏄剧ず闈炴ā鎬佺獥鍙o細
+
+```csharp
+var input = new DialogParameters("id", 42);
-public class TestViewModel : IDialogAware
+dialogService.Show(input);
+```
+
+瀵逛簬 `Show`锛學indow 鎴栧畠鐨 `DataContext` 鍙互瀹炵幇 `IDialogAware` 鏉ユ帴鏀跺弬鏁板拰璇锋眰鍏抽棴锛
+
+```csharp
+public sealed class TestViewModel : IDialogAware
{
public Action? RequestClose { get; set; }
public void OnNavigated(DialogParameters? parameters)
{
- if (parameters != null)
- {
- var value = parameters.Get("key");
- // 澶勭悊浼犲叆鍙傛暟
- }
+ var id = parameters?.Get("id");
}
- [RelayCommand]
public void Close()
{
- // 鍏抽棴绐楀彛骞惰繑鍥炵粨鏋
- var result = new DialogParameters("resultKey", "resultValue");
- RequestClose?.Invoke(result);
+ RequestClose?.Invoke(null);
}
}
```
-> 妗嗘灦鏀寔 `IDialogAware` 鍚屾椂瀹炵幇鍦 Window 鑷韩锛坈ode-behind锛夋垨 DataContext锛圴iewModel锛変笂锛屼袱鑰呭彲鍏卞瓨銆
+`ShowDialog` 褰撳墠鐨勫叧闂/缁撴灉娴佺▼瀛樺湪宸茬煡闄愬埗锛氬叾 `Closing` 澶勭悊浼氬彇娑堝叧闂紝骞跺彲鑳藉湪鍏抽棴璇锋眰涓啀娆¤皟鐢 `Close`銆傚湪 `DialogService` 淇鍓嶏紝涓嶅簲渚濊禆璇ユ柟娉曞畬鎴愭ā鎬佸叧闂垨杩斿洖缁撴灉銆
-### 5. DialogParameters 鍙傛暟瀵硅薄
+`DialogManager` 浣跨敤寮卞紩鐢ㄧ紦瀛樺悓绫诲瀷 Window锛屽苟鍦 Window 鍏抽棴鍚庣Щ闄よ褰曘
+
+## DialogParameters
```csharp
-// 鏂瑰紡涓锛氶敭鍊煎鏋勯
-var p1 = new DialogParameters("key", "value");
+var byKey = new DialogParameters("key", "value");
-// 鏂瑰紡浜岋細瀛楀吀鏋勯
-var p2 = new DialogParameters(new Dictionary
+var byDictionary = new DialogParameters(new Dictionary
{
- { "id", 100 },
- { "name", "test" },
- { "data", someObject }
+ ["id"] = 100,
+ ["name"] = "test",
});
-// 鏂瑰紡涓夛細绱㈠紩鏋勯
-var p3 = new DialogParameters("hello", 123, DateTime.Now);
-var first = p3.Get("0"); // "hello"
-var second = p3.Get("1"); // 123
+var byIndex = new DialogParameters("hello", 123, DateTime.Now);
+var first = byIndex.Get("0");
+var second = byIndex.Get("1");
-// 鑾峰彇 / 璁剧疆鍙傛暟
-var value = p1.Get("key");
-p1.Set("key", "newValue"); // 鍏佽瑕嗙洊宸叉湁鍊
+byKey.Set("key", "newValue");
+var value = byKey.Get("key");
```
-### 6. 鏋舵瀯璁捐瑕佺偣
+## DI 鐢熷懡鍛ㄦ湡
+
+`RegisterNavigationService()` 榛樿鎶 `IRegionManager`銆乣IPageService`銆乣IContentService`銆乣IDialogService` 鍜 `IDialogManager` 娉ㄥ唽涓 singleton銆俙IRegionManager` 淇濆瓨鍛藉悕鍖哄煙瀹夸富鐨勫急寮曠敤锛屽苟鎸佹湁闈欐 `Region` 澹版槑璁㈤槄锛涘畠涓嶈В鏋 View 瀵硅薄鍥撅紝骞朵細鍦ㄦ墍灞 DI provider 閲婃斁鏃跺彇娑堣闃呫
+
+Page/Content 瀵艰埅鏈嶅姟涓 `DialogManager` 浼氭崟鑾峰垱寤哄畠浠殑 `IServiceProvider`锛堥氬父鏄牴瀹瑰櫒锛夛紱浠呭垱寤烘垨鎸佹湁涓涓瓙 scope 涓嶄細鎶婅繖浜 singleton 鐨勭洰鏍囪В鏋愬垏鎹㈠埌璇 scope銆
+
+鍥犳锛屽湪鍚敤 `ValidateScopes` 鏃讹紝浠庢牴瀹瑰櫒瑙f瀽 scoped View銆乂iewModel 鎴 Window 瀵硅薄鍥炬槸鏃犳晥鐨勶紱浠庢牴瀹瑰櫒瑙f瀽鐨 disposable transient 瀵硅薄浼氱敱 Microsoft DI 淇濈暀鍒版牴瀹瑰櫒閲婃斁銆傜被搴撲笉浼氫负瀵艰埅鎴栫獥鍙e垱寤恒佹寔鏈夋垨閲婃斁 scope锛屽洜涓哄凡鏄剧ず UI 鐨勭敓鍛藉懆鏈熷睘浜庡簲鐢ㄣ
+
+闇瑕 scoped 鎴 disposable View/ViewModel/Window 瀵硅薄鍥剧殑搴旂敤锛屽繀椤诲湪璋冪敤 `RegisterNavigationService()` 鍓嶈鐩栫浉鍏冲鑸湇鍔″拰绠$悊鍣ㄧ殑鐢熷懡鍛ㄦ湡鎴栬В鏋愮瓥鐣ワ紙鍏 `TryAdd` 娉ㄥ唽浼氫繚鐣欏厛鍓嶆敞鍐岋級锛屼粠搴旂敤鎸佹湁鐨 scope 瑙f瀽杩欎簺鏈嶅姟锛屽苟璁 scope 鐨勯噴鏀炬椂鏈轰笌瀵瑰簲 UI 鐢熷懡鍛ㄦ湡涓鑷淬
-- **DI 椹卞姩**锛氭墍鏈 Page 鍜 Window 鍧囩敱 DI 瀹瑰櫒瑙f瀽锛屾敮鎸佹瀯閫犲嚱鏁版敞鍏
-- **WeakReference 绐楀彛缂撳瓨**锛歚DialogManager` 浣跨敤 `WeakReference` 闃叉鍐呭瓨娉勬紡
-- **闄勫姞灞炴ф敞鍐屽尯鍩**锛氶氳繃 `RegionService.RegionName` 鍦 XAML 涓0鏄庡紡娉ㄥ唽瀵艰埅鍖哄煙
-- **绾跨▼瀹夊叏**锛歚PageService` 浣跨敤 `ConcurrentDictionary` 瀛樺偍鍖哄煙
-- **鍙岀粦瀹氭劅鐭**锛氭鏋跺悓鏃舵鏌 Window/Page 鑷韩鍙婂叾 `DataContext` 鏄惁瀹炵幇浜嗘劅鐭ユ帴鍙
-- **澶氱洰鏍囨鏋**锛氬悓鏃舵敮鎸 .NET Framework 4.8 鍜 .NET 8.0 Windows
+## 鍖哄煙瀹夸富鎵╁睍杈圭晫
-### 7. 鏁堟灉棰勮
+`Grid`銆乣StackPanel` 绛夊厓绱犵幇鍦ㄥ彲浠ヤ綔涓 Content 瀵艰埅鐩爣锛屼絾涓嶈兘浣滀负鍖哄煙瀹夸富锛沗TabControl` 涔熷皻鏈綔涓哄尯鍩熷涓诲惎鐢ㄣ
-
+鍐呴儴鍖哄煙閫傞厤鍣ㄤ互 `FrameworkElement` 鍜屽涓昏兘鍔涗负杈圭晫锛屽洜姝ゆ湭鏉ュ彲浠ュ湪 resolver 涓鍔犻傞厤鍣紝鑰屼笉蹇呬慨鏀 `ContentService`銆備笉杩囷紝鍦ㄦ敮鎸 `Panel` 鍓嶅繀椤绘槑纭尯鍩熸槸鍚︽嫢鏈夊叏閮ㄥ瓙鍏冪礌浠ュ強鏇挎崲/杩藉姞绛栫暐锛涘湪鏀寔 `TabControl` 鍓嶅繀椤绘槑纭爣绛惧垱寤恒侀夋嫨銆佸鐢ㄥ拰 header 绛栫暐銆
+
+## 鐮村潖鎬у崌绾ц縼绉
+
+鏈閲嶅缓閲囩敤浠ヤ笅 API 鏄犲皠锛
+
+```text
+RegionService.RegionName -> Region.RegionName
+IPageService.GetRegion(...) -> IRegionManager.GetRegion(...)
+IPageService.Goback(...) -> IPageService.GoBack(...)
+```
----
+`RegionService` 宸插垹闄ゃ俙Goback` 浠呬綔涓烘爣璁颁簡 `Obsolete` 鐨勮浆鍙戞柟娉曚繚鐣欙紝鐜版湁浠g爜搴旇縼绉诲埌 `GoBack`銆傜敱浜庨檮鍔犲睘鎬ф墍鏈夎呭彂鐢熷彉鍖栵紝寮曠敤鏃у睘鎬х殑宸茬紪璇 XAML/BAML 蹇呴』閲嶆柊鏋勫缓銆
## License
diff --git a/Services/ContentService.cs b/Services/ContentService.cs
new file mode 100644
index 0000000..4dc0000
--- /dev/null
+++ b/Services/ContentService.cs
@@ -0,0 +1,143 @@
+using Microsoft.Extensions.DependencyInjection;
+using SimpleNavigation.Common;
+using SimpleNavigation.Common.Adapters;
+using SimpleNavigation.Interface.Adapters;
+using SimpleNavigation.Interface.Managers;
+using SimpleNavigation.Interface.Services;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace SimpleNavigation.Services
+{
+ public class ContentService : IContentService
+ {
+ private readonly IServiceProvider provider;
+ private readonly IRegionManager regionManager;
+ private readonly NavigationRouteRegistry routes;
+
+ public ContentService(IServiceProvider provider, IRegionManager regionManager)
+ {
+ this.provider = provider;
+ this.regionManager = regionManager;
+ routes = provider.GetRequiredService();
+ }
+
+ public void Navigate(string regionName, DialogParameters? parameters = null)
+ where TContent : FrameworkElement
+ {
+ ValidateRegionName(regionName);
+ ValidateContentType(typeof(TContent));
+ NavigateCore(regionName, provider.GetRequiredService(), parameters);
+ }
+
+ public void Navigate(string regionName, Type targetType, DialogParameters? parameters = null)
+ {
+ ValidateRegionName(regionName);
+ ValidateContentType(targetType);
+ var content = provider.GetRequiredService(targetType) as FrameworkElement
+ ?? throw new InvalidOperationException(
+ $"Resolved service '{targetType.FullName}' is not a FrameworkElement.");
+ NavigateCore(regionName, content, parameters);
+ }
+
+ public void Navigate(string regionName, string key, DialogParameters? parameters = null)
+ {
+ ValidateRegionName(regionName);
+ var targetType = routes.GetRequiredContentType(key);
+ ValidateContentType(targetType);
+ var content = provider.GetRequiredService(targetType) as FrameworkElement
+ ?? throw new InvalidOperationException(
+ $"Resolved service '{targetType.FullName}' is not a FrameworkElement.");
+ NavigateCore(regionName, content, parameters);
+ }
+
+ ///
+ /// 导航功能的核心实现方法
+ ///
+ /// 指定的 Region 名称
+ /// 需要导航的UI控件或内容
+ /// 传递的参数
+ private void NavigateCore(string regionName, FrameworkElement content, DialogParameters? parameters)
+ {
+ var host = GetRequiredHost(regionName);
+ var hostAdapter = RegionHostAdapterResolver.GetRequired(host);
+ if (hostAdapter is not IContentRegionHostAdapter adapter)
+ throw CreateInvalidHostException(regionName, host);
+
+ adapter.Present(host, content);
+ NavigationAwareNotifier.Notify(content, parameters);
+ }
+
+ ///
+ /// 获取指定 Region 的实例
+ ///
+ /// Region 名称
+ /// Region 实例
+ /// 该宿主未注册或查找失败
+ private FrameworkElement GetRequiredHost(string regionName)
+ {
+ var region = regionManager.GetRegion(regionName);
+ if (region != null)
+ return region;
+
+ throw CreateInvalidHostException(regionName, null);
+ }
+
+ ///
+ /// 自定义异常:宿主未注册或查找失败
+ ///
+ /// Region 名称
+ /// 宿主实例
+ /// CreateInvalidHostException异常
+ private static InvalidOperationException CreateInvalidHostException(
+ string regionName,
+ FrameworkElement? host)
+ {
+ var actual = host?.GetType().FullName ?? "missing";
+ return new InvalidOperationException(
+ $"Region '{regionName}' must be a non-Frame ContentControl or another host " +
+ $"with a content navigation adapter but was '{actual}'.");
+ }
+
+ ///
+ /// 校验需要导航的 UI控件或内容 是否符合约束
+ ///
+ /// 需要导航的内容类型
+ ///
+ ///
+ private static void ValidateContentType(Type targetType)
+ {
+#if NET6_0_OR_GREATER
+ ArgumentNullException.ThrowIfNull(targetType);
+#elif NET46_OR_GREATER
+ if (targetType == null)
+ throw new ArgumentNullException(nameof(targetType));
+#endif
+ if (!typeof(FrameworkElement).IsAssignableFrom(targetType)
+ || typeof(Page).IsAssignableFrom(targetType)
+ || typeof(Window).IsAssignableFrom(targetType))
+ {
+ throw new ArgumentException(
+ $"Target type '{targetType.FullName}' must be a non-Page, non-Window FrameworkElement.",
+ nameof(targetType));
+ }
+ }
+
+ ///
+ /// 校验 Region 名称
+ ///
+ ///
+ ///
+ private static void ValidateRegionName(string regionName)
+ {
+ if (string.IsNullOrWhiteSpace(regionName))
+ {
+ throw new ArgumentException(
+ "Region name cannot be null or whitespace.",
+ nameof(regionName));
+ }
+ }
+ }
+}
+
+
diff --git a/Services/DialogService.cs b/Services/DialogService.cs
index c2a890f..b58c3d4 100644
--- a/Services/DialogService.cs
+++ b/Services/DialogService.cs
@@ -1,5 +1,7 @@
using SimpleNavigation.Common;
-using SimpleNavigation.Interface;
+using SimpleNavigation.Interface.Awares;
+using SimpleNavigation.Interface.Managers;
+using SimpleNavigation.Interface.Services;
using System.Windows;
namespace SimpleNavigation.Services
@@ -52,14 +54,10 @@ public void Show(DialogParameters? parameters = null) where T : Window
{
e.Cancel = true;
if (s is IDialogAware dialogAware)
- {
dialogAware.RequestClose?.Invoke(result);
- }
if (s is Window w && w.DataContext is IDialogAware dialogVmAware)
- {
dialogVmAware.RequestClose?.Invoke(result);
- }
};
if (window.DataContext is IDialogAware vm)
diff --git a/Services/PageService.cs b/Services/PageService.cs
index 60bfb45..e88fea6 100644
--- a/Services/PageService.cs
+++ b/Services/PageService.cs
@@ -1,76 +1,133 @@
-锘縰sing Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection;
using SimpleNavigation.Common;
-using SimpleNavigation.Interface;
-using System.Collections.Concurrent;
+using SimpleNavigation.Common.Adapters;
+using SimpleNavigation.Interface.Adapters;
+using SimpleNavigation.Interface.Managers;
+using SimpleNavigation.Interface.Services;
using System.Windows.Controls;
-namespace SimpleNavigation.Services
+namespace SimpleNavigation.Services;
+
+public class PageService : IPageService
{
- public class PageService : IPageService
+ private readonly IServiceProvider provider;
+ private readonly IRegionManager regionManager;
+ private readonly NavigationRouteRegistry routes;
+
+ public PageService(IServiceProvider provider, IRegionManager regionManager)
{
- private readonly ConcurrentDictionary regions = new();
- private readonly IServiceProvider provider;
+ this.provider = provider;
+ this.regionManager = regionManager;
+ routes = provider.GetRequiredService();
+ }
- public PageService(IServiceProvider provider)
- {
- this.provider = provider;
- RegionService.RegionRegisted += (regionName, frame) => RegisterRegion(regionName, frame);
- }
+ public void Navigate(string regionName, DialogParameters? parameters = null) where TPage : Page
+ {
+ ValidateRegionName(regionName);
+ NavigateCore(regionName, provider.GetRequiredService(), parameters);
+ }
- public void RegisterRegion(string regionName, Frame frame)
- {
- if (!string.IsNullOrWhiteSpace(regionName) && frame != null)
- regions[regionName] = frame;
- }
+ public void Navigate(string regionName, Type targetType, DialogParameters? parameters = null)
+ {
+ ValidateRegionName(regionName);
+ ValidatePageType(targetType);
+ var page = provider.GetRequiredService(targetType) as Page
+ ?? throw new InvalidOperationException(
+ $"Resolved service '{targetType.FullName}' is not a Page.");
+ NavigateCore(regionName, page, parameters);
+ }
- public Frame? GetRegion(string regionName)
- {
- regions.TryGetValue(regionName, out var frame);
- return frame;
- }
+ public void Navigate(string regionName, string key, DialogParameters? parameters = null)
+ {
+ ValidateRegionName(regionName);
+ var targetType = routes.GetRequiredPageType(key);
+ var page = provider.GetRequiredService(targetType) as Page
+ ?? throw new InvalidOperationException(
+ $"Resolved service '{targetType.FullName}' is not a Page.");
+ NavigateCore(regionName, page, parameters);
+ }
- public void Goback(string region)
- {
- if (regions.TryGetValue(region, out var frame))
- {
- if (frame.CanGoBack)
- frame.GoBack();
- }
- }
+ public void GoBack(string regionName)
+ {
+ var frame = GetRequiredFrame(regionName);
+ var adapter = (IPageRegionHostAdapter)
+ RegionHostAdapterResolver.GetRequired(frame);
+ if (adapter.CanGoBack(frame))
+ adapter.GoBack(frame);
+ }
- public void Navigate(string regionName, DialogParameters? parameters = null) where T : Page
- {
- var region = GetRegion(regionName);
- if (region != null)
- {
- var page = provider.GetRequiredService();
+ [Obsolete("Use GoBack instead.")]
+ public void Goback(string regionName)
+ {
+ GoBack(regionName);
+ }
+
+ ///
+ /// 导航功能的核心实现
+ ///
+ /// 指定的 Region 名称
+ /// 指定的Page
+ /// 传递的参数
+ private void NavigateCore(string regionName, Page page, DialogParameters? parameters)
+ {
+ var frame = GetRequiredFrame(regionName);
+ var adapter = (IPageRegionHostAdapter)
+ RegionHostAdapterResolver.GetRequired(frame);
+ if (adapter.Navigate(frame, page))
+ NavigationAwareNotifier.Notify(page, parameters);
+ }
- //var oldContent = region.Content;
+ ///
+ /// 获取指定 Region(Frame) 的实例
+ ///
+ /// Region 名称
+ /// Region 实例
+ /// Region 实例类型异常
+ private Frame GetRequiredFrame(string regionName)
+ {
+ ValidateRegionName(regionName);
+ var region = regionManager.GetRegion(regionName);
+ if (region is Frame frame)
+ return frame;
- region.Navigate(page);
+ var actual = region?.GetType().FullName ?? "missing";
+ throw new InvalidOperationException(
+ $"Region '{regionName}' must be a Frame but was '{actual}'.");
+ }
- if (page.DataContext is IPageAware pA && parameters != null)
- {
- pA.OnNavigated(parameters);
- }
- }
+ ///
+ /// 校验导航的目标类型是否负责约束
+ ///
+ /// 导航目标的类型
+ ///
+ private static void ValidatePageType(Type targetType)
+ {
+#if NET5_0_OR_GREATER
+ ArgumentNullException.ThrowIfNull(targetType);
+#elif NET46_OR_GREATER
+ if (targetType == null)
+ throw new ArgumentNullException(nameof(targetType));
+#endif
+ if (!typeof(Page).IsAssignableFrom(targetType))
+ {
+ throw new ArgumentException(
+ $"Target type '{targetType.FullName}' must derive from Page.",
+ nameof(targetType));
}
+ }
- public void Navigate(string regionName, Type targetType, DialogParameters? parameters = null)
+ ///
+ /// 校验 Region 名称是否负责规则
+ ///
+ ///
+ ///
+ private static void ValidateRegionName(string regionName)
+ {
+ if (string.IsNullOrWhiteSpace(regionName))
{
- if (targetType.IsSubclassOf(typeof(Page)))
- {
- var region = GetRegion(regionName);
- if (region != null)
- {
- var page = provider.GetRequiredService(targetType) as Page;
- region.Navigate(page);
- if (page?.DataContext is IPageAware pA && parameters != null)
- {
- pA.OnNavigated(parameters);
- }
- }
- }
+ throw new ArgumentException(
+ "Region name cannot be null or whitespace.",
+ nameof(regionName));
}
}
}
diff --git a/Services/Region.cs b/Services/Region.cs
new file mode 100644
index 0000000..aa90fbf
--- /dev/null
+++ b/Services/Region.cs
@@ -0,0 +1,638 @@
+using SimpleNavigation.Common.Adapters;
+using System.Runtime.ExceptionServices;
+using System.Windows;
+
+namespace SimpleNavigation.Services
+{
+ public static class Region
+ {
+ private static readonly object SyncRoot = new();
+ private static readonly object PublicationGate = new();
+ private static readonly List Declarations = new();
+ private static readonly List>> Subscribers = new();
+ private static long nextActivationToken;
+
+ #region Attached property
+ public static readonly DependencyProperty RegionNameProperty =
+ DependencyProperty.RegisterAttached(
+ "RegionName",
+ typeof(string),
+ typeof(Region),
+ new PropertyMetadata(null, OnRegionNameChanged));
+
+ public static string? GetRegionName(DependencyObject obj)
+ {
+ if (obj == null)
+ throw new ArgumentNullException(nameof(obj));
+
+ return (string?)obj.GetValue(RegionNameProperty);
+ }
+
+ public static void SetRegionName(DependencyObject obj, string value)
+ {
+ if (obj == null) throw new ArgumentNullException(nameof(obj));
+
+ // Before set value to confirm:
+ // 1) The region name is valid;
+ ValidateRegionName(value);
+ // 2) The container is valid (container must be FrameworkElement);
+ var host = GetRequiredFrameworkElement(obj);
+ // 3) The adapter has been existed (the container has been have a matching adapter);
+ RegionHostAdapterResolver.GetRequired(host);
+ // 4) The region name and container is valid;
+ ValidateAttachedNameAvailability(host, value);
+
+ obj.SetValue(RegionNameProperty, value);
+ }
+
+ ///
+ /// 附加属性 RegionName 注册回调
+ ///
+ /// 被附加的宿主容器
+ /// 事件信息
+ private static void OnRegionNameChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs eventArgs)
+ {
+ if (eventArgs.NewValue == null)
+ {
+ ClearDeclaration(dependencyObject); // Region name为空,移除该宿主容器
+ return;
+ }
+
+ var regionName = (string)eventArgs.NewValue;
+ try
+ {
+ ValidateRegionName(regionName);
+ var host = GetRequiredFrameworkElement(dependencyObject);
+ RegionHostAdapterResolver.GetRequired(host); // 获取匹配的 Adapter
+ ApplyRegionName(host, regionName);
+ }
+ catch (Exception exception)
+ {
+ var originalFailure = ExceptionDispatchInfo.Capture(exception);
+
+ if (!HasMatchingDeclaration(dependencyObject, regionName))
+ {
+ try
+ {
+ RestoreDependencyPropertyValue(dependencyObject, eventArgs.OldValue); // 恢复发生异常前的 Region name
+ }
+ catch
+ {
+ // Preserve the validation or registration failure that caused the rollback.
+ }
+ }
+
+ originalFailure.Throw();
+ throw;
+ }
+ }
+ #endregion
+
+ ///
+ /// 注册回调方法
+ ///
+ ///
+ ///
+ internal static void Subscribe(Action subscriber)
+ {
+ if (subscriber == null)
+ throw new ArgumentNullException(nameof(subscriber));
+
+ lock (SyncRoot)
+ {
+ RemoveDeadSubscribersUnderLock();
+ Subscribers.Add(new WeakReference>(subscriber));
+ }
+ }
+
+ internal static void Unsubscribe(Action subscriber)
+ {
+ if (subscriber == null)
+ return;
+
+ lock (SyncRoot)
+ {
+ for (var index = Subscribers.Count - 1; index >= 0; index--)
+ {
+ if (!Subscribers[index].TryGetTarget(out var existingSubscriber) ||
+ existingSubscriber.Equals(subscriber))
+ {
+ Subscribers.RemoveAt(index);
+ }
+ }
+ }
+ }
+
+ ///
+ /// 获取所有未取消注册的宿主容器
+ ///
+ ///
+ internal static IReadOnlyList GetActiveSnapshot()
+ {
+ lock (SyncRoot)
+ {
+ RemoveDeadDeclarationsUnderLock();
+
+ var snapshot = new List();
+ foreach (var declaration in Declarations)
+ {
+ if (declaration.IsActive && declaration.Host.TryGetTarget(out var host))
+ {
+ snapshot.Add(CreateChange(
+ declaration,
+ host,
+ RegionDeclarationChangeKind.Add));
+ }
+ }
+
+ return snapshot;
+ }
+ }
+
+ ///
+ /// 添加宿主容器,并指定 Region name
+ ///
+ /// 注册的宿主容器
+ /// Region name
+ private static void ApplyRegionName(FrameworkElement host, string regionName)
+ {
+ lock (PublicationGate)
+ {
+ ApplyRegionNameUnderPublicationGate(host, regionName);
+ }
+ }
+
+ private static void ApplyRegionNameUnderPublicationGate(FrameworkElement host, string regionName)
+ {
+ Declaration? createdDeclaration = null;
+ List changes;
+ Action[] subscribers;
+
+ lock (SyncRoot)
+ {
+ RemoveDeadDeclarationsUnderLock();
+ ValidateAttachedNameAvailabilityUnderLock(host, regionName); // 校验Region name是否合法 与 宿主容器是否重复注册
+
+ var declaration = FindDeclarationUnderLock(host); // 查找宿主容器实例是否存在
+
+ // 宿主容器已注册,且其Region name与 预注册的Region name相同,不执行任何操作
+ if (declaration != null && string.Equals(declaration.Name, regionName, StringComparison.Ordinal))
+ return;
+
+ changes = new List(2);
+
+ if (declaration == null) // 宿主容器未注册
+ {
+ // 创建新的宿主容器
+ declaration = new Declaration(
+ new WeakReference(host),
+ regionName,
+ isActive: true,
+ GetNextActivationTokenUnderLock());
+ Declarations.Add(declaration);
+ createdDeclaration = declaration;
+ }
+ else // 宿主容器已存在,更新宿主容器信息
+ {
+ if (declaration.IsActive)
+ {
+ changes.Add(CreateChange(
+ declaration,
+ host,
+ RegionDeclarationChangeKind.Remove));
+ }
+
+ declaration.Name = regionName; //更新 Region name
+ declaration.IsActive = true;
+ declaration.ActivationToken = GetNextActivationTokenUnderLock();
+ }
+
+ changes.Add(CreateChange(
+ declaration,
+ host,
+ RegionDeclarationChangeKind.Add));
+ subscribers = GetLiveSubscribersUnderLock();
+ }
+
+ if (createdDeclaration != null)
+ {
+ try
+ {
+ AttachLifecycleHandlers(host); // 订阅Loaded 和 UnLoaded 事件
+ }
+ catch
+ {
+ DetachLifecycleHandlers(host); // 取消订阅Loaded 和 UnLoaded 事件
+
+ lock (SyncRoot)
+ {
+ Declarations.Remove(createdDeclaration); // 移除宿主容器
+ }
+
+ throw;
+ }
+ }
+
+ PublishChanges(changes, subscribers);
+ }
+
+ ///
+ /// Remove 指定的宿主容器
+ ///
+ ///
+ private static void ClearDeclaration(DependencyObject dependencyObject)
+ {
+ if (dependencyObject is not FrameworkElement host)
+ return;
+
+ lock (PublicationGate)
+ {
+ ClearDeclarationUnderPublicationGate(host);
+ }
+ }
+
+ private static void ClearDeclarationUnderPublicationGate(FrameworkElement host)
+ {
+ Declaration? removedDeclaration;
+ List changes;
+ Action[] subscribers;
+
+ lock (SyncRoot)
+ {
+ RemoveDeadDeclarationsUnderLock();
+ removedDeclaration = FindDeclarationUnderLock(host);
+ if (removedDeclaration == null)
+ {
+ return;
+ }
+
+ changes = new List(1);
+ if (removedDeclaration.IsActive)
+ {
+ changes.Add(CreateChange(
+ removedDeclaration,
+ host,
+ RegionDeclarationChangeKind.Remove));
+ }
+
+ Declarations.Remove(removedDeclaration);
+ subscribers = GetLiveSubscribersUnderLock();
+ }
+
+ DetachLifecycleHandlers(host);
+ PublishChanges(changes, subscribers);
+ }
+
+ #region 宿主容器Loaded 和 UnLoaded 事件
+ private static void OnHostLoaded(object sender, RoutedEventArgs eventArgs)
+ {
+ if (sender is not FrameworkElement host)
+ return;
+
+ lock (PublicationGate)
+ {
+ ActivateHostUnderPublicationGate(host);
+ }
+ }
+
+ private static void ActivateHostUnderPublicationGate(FrameworkElement host)
+ {
+ RegionDeclarationChange? change = null;
+ Action[] subscribers = Array.Empty>();
+
+ lock (SyncRoot)
+ {
+ RemoveDeadDeclarationsUnderLock();
+ var declaration = FindDeclarationUnderLock(host);
+ if (declaration == null || declaration.IsActive)
+ {
+ return;
+ }
+
+ declaration.IsActive = true;
+ declaration.ActivationToken = GetNextActivationTokenUnderLock();
+ change = CreateChange(declaration, host, RegionDeclarationChangeKind.Add);
+ subscribers = GetLiveSubscribersUnderLock();
+ }
+
+ PublishChanges(new[] { change }, subscribers);
+ }
+
+ private static void OnHostUnloaded(object sender, RoutedEventArgs eventArgs)
+ {
+ if (sender is not FrameworkElement host)
+ return;
+
+ lock (PublicationGate)
+ {
+ DeactivateHostUnderPublicationGate(host);
+ }
+ }
+
+ private static void DeactivateHostUnderPublicationGate(FrameworkElement host)
+ {
+ RegionDeclarationChange? change = null;
+ Action[] subscribers = Array.Empty>();
+
+ lock (SyncRoot)
+ {
+ RemoveDeadDeclarationsUnderLock();
+ var declaration = FindDeclarationUnderLock(host);
+ if (declaration == null || !declaration.IsActive) // 容器已失效并被移除 或 容器不存在
+ return;
+
+ declaration.IsActive = false;
+ change = CreateChange(declaration, host, RegionDeclarationChangeKind.Remove); // 创建移除容器事件
+ subscribers = GetLiveSubscribersUnderLock();
+ }
+
+ PublishChanges(new[] { change }, subscribers); // 发布事件
+ }
+ #endregion
+
+ ///
+ /// 订阅宿主容器Loaded 和 UnLoaded 事件
+ ///
+ ///
+ private static void AttachLifecycleHandlers(FrameworkElement host)
+ {
+ host.Loaded += OnHostLoaded;
+ host.Unloaded += OnHostUnloaded;
+ }
+
+ ///
+ /// 取消订阅宿主容器Loaded 和 UnLoaded 事件
+ ///
+ ///
+ private static void DetachLifecycleHandlers(FrameworkElement host)
+ {
+ host.Loaded -= OnHostLoaded;
+ host.Unloaded -= OnHostUnloaded;
+ }
+
+ private static void ValidateAttachedNameAvailability(FrameworkElement host, string regionName)
+ {
+ lock (SyncRoot)
+ {
+ RemoveDeadDeclarationsUnderLock();
+ ValidateAttachedNameAvailabilityUnderLock(host, regionName);
+ }
+ }
+
+ ///
+ /// 查询是否存在符合条件的宿主容器
+ ///
+ /// 宿主容器对象
+ /// Region name
+ /// 是否存在符合条件的宿主容器
+ private static bool HasMatchingDeclaration(DependencyObject dependencyObject, string regionName)
+ {
+ if (dependencyObject is not FrameworkElement host)
+ return false;
+
+ lock (SyncRoot)
+ {
+ var declaration = FindDeclarationUnderLock(host);
+ return declaration != null &&
+ string.Equals(declaration.Name, regionName, StringComparison.Ordinal);
+ }
+ }
+
+ ///
+ /// 在注册 Region name 发生异常时,回滚 Region name
+ ///
+ /// 预被注册的宿主容器
+ /// 发生异常前的Region name
+ private static void RestoreDependencyPropertyValue(DependencyObject dependencyObject, object? oldValue)
+ {
+ if (oldValue == null || ReferenceEquals(oldValue, DependencyProperty.UnsetValue))
+ {
+ dependencyObject.ClearValue(RegionNameProperty);
+ return;
+ }
+
+ dependencyObject.SetValue(RegionNameProperty, oldValue);
+ }
+
+ ///
+ /// 检查 Region name 与 预注册的宿主容器是否重复注册
+ ///
+ /// 宿主容器
+ /// Region name
+ /// 宿主容器重复注册或Region name已存在
+ private static void ValidateAttachedNameAvailabilityUnderLock(FrameworkElement host, string regionName)
+ {
+ foreach (var declaration in Declarations)
+ {
+ if (!string.Equals(declaration.Name, regionName, StringComparison.Ordinal)
+ || !declaration.Host.TryGetTarget(out var existingHost)
+ || ReferenceEquals(existingHost, host))
+ {
+ continue;
+ }
+
+ throw new InvalidOperationException(
+ $"Region '{regionName}' is already declared on another host.");
+ }
+ }
+
+ ///
+ /// 将附加对象转换为 FrameworkElement
+ ///
+ /// 需要转换的附加对象
+ /// 转为为 FrameworkElement 的附加对象
+ /// 转换失败抛出异常
+ private static FrameworkElement GetRequiredFrameworkElement(DependencyObject obj)
+ {
+ if (obj is FrameworkElement host)
+ return host;
+
+ throw new ArgumentException(
+ $"Region host type '{obj.GetType().FullName}' is not a FrameworkElement.",
+ nameof(obj));
+ }
+
+ ///
+ /// 校验 Region name 是否合法
+ ///
+ private static void ValidateRegionName(string regionName)
+ {
+ if (string.IsNullOrWhiteSpace(regionName))
+ {
+ throw new ArgumentException(
+ "Region name cannot be null, empty, or whitespace.",
+ nameof(regionName));
+ }
+ }
+
+ ///
+ /// 循环查找当前宿主容器是否已经存在并返回
+ /// 当前程序集中存在 Content 宿主容器集 和 Page宿主容器集,他们互相独立,且Region name 可相同
+ ///
+ /// 预被注册为宿主容器的控件(元素)
+ /// 返回已被注册为宿主容器的控件,如果未被注册,则返回 null
+ private static Declaration? FindDeclarationUnderLock(FrameworkElement host)
+ {
+ foreach (var declaration in Declarations)
+ {
+ if (declaration.Host.TryGetTarget(out var existingHost) && ReferenceEquals(existingHost, host))
+ return declaration;
+ }
+
+ return null;
+ }
+
+ ///
+ /// 移除失效的宿主容器
+ ///
+ private static void RemoveDeadDeclarationsUnderLock()
+ {
+ for (var index = Declarations.Count - 1; index >= 0; index--)
+ {
+ if (!Declarations[index].Host.TryGetTarget(out _))
+ {
+ Declarations.RemoveAt(index);
+ }
+ }
+ }
+
+ ///
+ /// 移除失效的订阅者
+ ///
+ private static void RemoveDeadSubscribersUnderLock()
+ {
+ for (var index = Subscribers.Count - 1; index >= 0; index--)
+ {
+ if (!Subscribers[index].TryGetTarget(out _))
+ {
+ Subscribers.RemoveAt(index);
+ }
+ }
+ }
+
+ ///
+ /// 获取活跃(未取消注册)的订阅者
+ ///
+ ///
+ private static Action[] GetLiveSubscribersUnderLock()
+ {
+ var liveSubscribers = new List>(Subscribers.Count);
+
+ for (var index = 0; index < Subscribers.Count;)
+ {
+ if (Subscribers[index].TryGetTarget(out var subscriber))
+ {
+ liveSubscribers.Add(subscriber);
+ index++;
+ }
+ else
+ {
+ Subscribers.RemoveAt(index);
+ }
+ }
+
+ return liveSubscribers.ToArray();
+ }
+
+ ///
+ /// 生成下一个宿主容器的 Token(Id)
+ ///
+ /// 宿主容器Id
+ private static long GetNextActivationTokenUnderLock() => ++nextActivationToken;
+
+ private static RegionDeclarationChange CreateChange(
+ Declaration declaration,
+ FrameworkElement host,
+ RegionDeclarationChangeKind kind)
+ {
+ return new RegionDeclarationChange(
+ declaration.Name,
+ host,
+ declaration.ActivationToken,
+ kind);
+ }
+
+ private static void PublishChanges(
+ IEnumerable changes,
+ IReadOnlyList> subscribers)
+ {
+ ExceptionDispatchInfo? firstFailure = null;
+
+ foreach (var change in changes)
+ {
+ foreach (var subscriber in subscribers)
+ {
+ try
+ {
+ subscriber(change);
+ }
+ catch (Exception exception)
+ {
+ firstFailure ??= ExceptionDispatchInfo.Capture(exception);
+ }
+ }
+ }
+
+ firstFailure?.Throw();
+ }
+
+ ///
+ /// 宿主容器对象
+ ///
+ private sealed class Declaration
+ {
+ public Declaration(WeakReference host, string name, bool isActive, long activationToken)
+ {
+ Host = host;
+ Name = name;
+ IsActive = isActive;
+ ActivationToken = activationToken;
+ }
+
+ public WeakReference Host { get; }
+
+ public string Name { get; set; }
+
+ public bool IsActive { get; set; }
+
+ public long ActivationToken { get; set; }
+ }
+ }
+
+ ///
+ /// Region 发生变化时的类型
+ ///
+ internal enum RegionDeclarationChangeKind
+ {
+ Add,
+ Remove,
+ }
+
+ ///
+ /// Region发生变化时的对象
+ ///
+ internal sealed class RegionDeclarationChange
+ {
+ public RegionDeclarationChange(
+ string name,
+ FrameworkElement host,
+ long activationToken,
+ RegionDeclarationChangeKind kind)
+ {
+ Name = name;
+ Host = host;
+ ActivationToken = activationToken;
+ Kind = kind;
+ }
+
+ public string Name { get; }
+
+ public FrameworkElement Host { get; }
+
+ public long ActivationToken { get; }
+
+ public RegionDeclarationChangeKind Kind { get; }
+ }
+}
+
+
diff --git a/Services/RegionService.cs b/Services/RegionService.cs
deleted file mode 100644
index 34455ab..0000000
--- a/Services/RegionService.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-锘縰sing System.Windows;
-using System.Windows.Controls;
-
-namespace SimpleNavigation.Services
-{
- ///
- /// 缁存姢鍖哄煙鍔熻兘
- ///
- public static class RegionService
- {
- // 鍏佽鍦╔AML涓娇鐢≧egionService娉ㄥ唽瀵艰埅鍖哄煙锛屾敞鍐屽畬鎴愬悗浼氳Е鍙浜嬩欢锛屽鑸湇鍔′細璁㈤槄璇ヤ簨浠朵互瀹屾垚鍖哄煙鐨勬敞鍐屻
- public static event Action? RegionRegisted;
-
- public static string GetRegionName(DependencyObject obj)
- {
- return (string)obj.GetValue(RegionNameProperty);
- }
-
- public static void SetRegionName(DependencyObject obj, string value)
- {
- obj.SetValue(RegionNameProperty, value);
- }
-
- // 浣跨敤闄勫姞灞炴х殑鏂瑰紡鍏佽鍦╔AML涓负鎺т欢鎸囧畾RegionName锛孯egionService浼氱洃鍚灞炴х殑鍙樺寲浠ュ畬鎴愬鑸尯鍩熺殑娉ㄥ唽銆
- public static readonly DependencyProperty RegionNameProperty =
- DependencyProperty.RegisterAttached("RegionName", typeof(string), typeof(RegionService), new PropertyMetadata("", OnRegionNameChanged));
-
-
- private static void OnRegionNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
- {
- if (d is not Frame frame) throw new InvalidOperationException("RegionName can only be attached to Frame controls.");
-
- var regionName = e.NewValue as string;
-
- if (string.IsNullOrWhiteSpace(regionName)) throw new ArgumentException("Region name cannot be null or whitespace.", nameof(regionName));
-
- RegionRegisted?.Invoke(regionName, frame);
- }
- }
-}
diff --git a/SimpleNavigation.csproj b/SimpleNavigation.csproj
index 99a88a9..f881e58 100644
--- a/SimpleNavigation.csproj
+++ b/SimpleNavigation.csproj
@@ -8,9 +8,10 @@
13
true
Junevy.SimpleNavigation
- 1.0.1
+ 2.0.1
Junevy
Recommand to use it in conjunction with Communication.Toolkit.Mvvm. The package copywriting for Prism.
+ $(DefaultItemExcludesInProjectFolder);Tests/**
diff --git a/SimpleNavigation.sln b/SimpleNavigation.sln
index 48ccf79..5ba1bd4 100644
--- a/SimpleNavigation.sln
+++ b/SimpleNavigation.sln
@@ -5,6 +5,10 @@ VisualStudioVersion = 17.14.36401.2 d17.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleNavigation", "SimpleNavigation.csproj", "{78216B02-64FB-4739-A012-3422014F0B26}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleNavigation.Tests", "Tests\SimpleNavigation.Tests\SimpleNavigation.Tests.csproj", "{BCFBF2DD-8CDD-49E5-80AB-2537B6698EAC}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -15,10 +19,17 @@ Global
{78216B02-64FB-4739-A012-3422014F0B26}.Debug|Any CPU.Build.0 = Debug|Any CPU
{78216B02-64FB-4739-A012-3422014F0B26}.Release|Any CPU.ActiveCfg = Release|Any CPU
{78216B02-64FB-4739-A012-3422014F0B26}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BCFBF2DD-8CDD-49E5-80AB-2537B6698EAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BCFBF2DD-8CDD-49E5-80AB-2537B6698EAC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BCFBF2DD-8CDD-49E5-80AB-2537B6698EAC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BCFBF2DD-8CDD-49E5-80AB-2537B6698EAC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {BCFBF2DD-8CDD-49E5-80AB-2537B6698EAC} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
+ EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BCCE3619-D9CD-41B6-99FD-C9B8BA074CD4}
EndGlobalSection
diff --git a/Tests/SimpleNavigation.Tests/ContentServiceTests.cs b/Tests/SimpleNavigation.Tests/ContentServiceTests.cs
new file mode 100644
index 0000000..6b26ac4
--- /dev/null
+++ b/Tests/SimpleNavigation.Tests/ContentServiceTests.cs
@@ -0,0 +1,341 @@
+using Microsoft.Extensions.DependencyInjection;
+using SimpleNavigation.Common;
+using SimpleNavigation.Extensions;
+using SimpleNavigation.Interface.Awares;
+using SimpleNavigation.Interface.Managers;
+using SimpleNavigation.Interface.Services;
+using SimpleNavigation.Tests.TestInfrastructure;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace SimpleNavigation.Tests;
+
+public class ContentServiceTests
+{
+ [Fact]
+ public void ContentHostAdapterContractAcceptsAnyFrameworkElementHost()
+ {
+ var adapterType = typeof(IContentService).Assembly.GetType(
+ "SimpleNavigation.Common.IContentRegionHostAdapter",
+ throwOnError: true)!;
+ var present = adapterType.GetMethod("Present");
+
+ Assert.NotNull(present);
+ Assert.Equal(
+ typeof(FrameworkElement),
+ present!.GetParameters()[0].ParameterType);
+ }
+
+ [Fact]
+ public void GenericAndTypeNavigationDoNotRequireRoutes()
+ {
+ StaTest.Run(() =>
+ {
+ var services = new ServiceCollection();
+ services.RegisterNavigationService();
+ services.AddTransient();
+ services.AddTransient();
+ using var provider = services.BuildServiceProvider();
+ var host = RegisterContentHost(provider, "main");
+ var service = provider.GetRequiredService();
+
+ service.Navigate("main");
+ Assert.IsType(host.Content);
+
+ service.Navigate("main", typeof(Grid));
+ Assert.IsType(host.Content);
+ });
+ }
+
+ [Fact]
+ public void StringNavigationUsesCaseSensitiveRouteThenOrdinaryDi()
+ {
+ StaTest.Run(() =>
+ {
+ var expected = new TestContent();
+ var services = new ServiceCollection();
+ services.RegisterNavigationService();
+ services.AddSingleton(expected);
+ services.AddContent("content");
+ using var provider = services.BuildServiceProvider();
+ var host = RegisterContentHost(provider, "main");
+ var service = provider.GetRequiredService();
+
+ service.Navigate("main", "content");
+
+ Assert.Same(expected, host.Content);
+ Assert.Throws(() => service.Navigate("main", "Content"));
+ });
+ }
+
+ [Fact]
+ public void ViewThenDistinctDataContextReceiveTheSameParameters()
+ {
+ StaTest.Run(() =>
+ {
+ var calls = new List();
+ var viewModel = new RecordingAware("context", calls);
+ var content = new RecordingAwareContent("view", calls)
+ {
+ DataContext = viewModel,
+ };
+ var parameters = new DialogParameters("id", 9);
+ var services = new ServiceCollection();
+ services.RegisterNavigationService();
+ services.AddSingleton(content);
+ using var provider = services.BuildServiceProvider();
+ var host = RegisterContentHost(provider, "main");
+
+ provider.GetRequiredService()
+ .Navigate("main", parameters);
+
+ Assert.Equal(new[] { "view", "context" }, calls);
+ Assert.Same(parameters, content.Parameters);
+ Assert.Same(parameters, viewModel.Parameters);
+ Assert.Same(content, host.Content);
+ });
+ }
+
+ [Fact]
+ public void NullParametersStillNotifyAndSameInstanceIsDeduplicated()
+ {
+ StaTest.Run(() =>
+ {
+ var content = new AwareContent();
+ content.DataContext = content;
+ var services = new ServiceCollection();
+ services.RegisterNavigationService();
+ services.AddSingleton(content);
+ using var provider = services.BuildServiceProvider();
+ var host = RegisterContentHost(provider, "main");
+
+ provider.GetRequiredService()
+ .Navigate("main");
+
+ Assert.Equal(1, content.CallCount);
+ Assert.Null(content.Parameters);
+ Assert.Same(content, host.Content);
+ });
+ }
+
+ [Fact]
+ public void InvalidInputsAndUnsupportedTargetsFailFast()
+ {
+ StaTest.Run(() =>
+ {
+ var services = new ServiceCollection();
+ services.RegisterNavigationService();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ using var provider = services.BuildServiceProvider();
+ var frame = new Frame();
+ provider.GetRequiredService().RegisterRegion("frame", frame);
+ var service = provider.GetRequiredService();
+
+ Assert.Throws(() => service.Navigate(" "));
+ Assert.Throws(() => service.Navigate("frame", (Type)null!));
+ Assert.Throws(() => service.Navigate("frame", typeof(string)));
+ Assert.Throws(() => service.Navigate("frame"));
+ Assert.Throws(() => service.Navigate("frame", typeof(Window)));
+ var exception = Assert.Throws(
+ () => service.Navigate("frame"));
+ Assert.Contains("frame", exception.Message);
+ Assert.Contains(typeof(Frame).FullName!, exception.Message);
+ Assert.Contains("non-Frame ContentControl", exception.Message);
+ GC.KeepAlive(frame);
+ });
+ }
+
+ [Fact]
+ public void MissingRegionUnknownKeyAndMissingDiFailFast()
+ {
+ StaTest.Run(() =>
+ {
+ var services = new ServiceCollection();
+ services.RegisterNavigationService();
+ services.AddTransient();
+ using var provider = services.BuildServiceProvider();
+ var host = RegisterContentHost(provider, "main");
+ var service = provider.GetRequiredService();
+
+ var missingRegionException = Assert.Throws(
+ () => service.Navigate("missing"));
+ Assert.Contains("Region 'missing'", missingRegionException.Message);
+ Assert.Contains("was 'missing'", missingRegionException.Message);
+ Assert.Contains("non-Frame ContentControl", missingRegionException.Message);
+ Assert.Throws(
+ () => service.Navigate("main", "unknown"));
+ var exception = Assert.Throws(
+ () => service.Navigate("main", typeof(Grid)));
+ Assert.Contains(typeof(Grid).FullName!, exception.Message);
+ GC.KeepAlive(host);
+ });
+ }
+
+ [Fact]
+ public void DoubleGenericRegistrationWithoutKeyDoesNotCreateARoute()
+ {
+ StaTest.Run(() =>
+ {
+ var services = new ServiceCollection();
+ services.RegisterNavigationService();
+ services.AddContent();
+ using var provider = services.BuildServiceProvider();
+ var host = RegisterContentHost(provider, "main");
+
+ Assert.Throws(() =>
+ provider.GetRequiredService()
+ .Navigate("main", "content"));
+ GC.KeepAlive(host);
+ });
+ }
+
+ [Fact]
+ public void AwarenessExceptionPropagatesAfterPresentation()
+ {
+ StaTest.Run(() =>
+ {
+ var content = new ThrowingAwareContent();
+ var services = new ServiceCollection();
+ services.RegisterNavigationService();
+ services.AddSingleton(content);
+ using var provider = services.BuildServiceProvider();
+ var host = RegisterContentHost(provider, "main");
+
+ var exception = Assert.Throws(() =>
+ provider.GetRequiredService()
+ .Navigate("main"));
+
+ Assert.Equal("awareness failed", exception.Message);
+ Assert.Same(content, host.Content);
+ });
+ }
+
+ [Fact]
+ public void HostPresentationExceptionPropagatesWithoutNotifyingAwareness()
+ {
+ StaTest.Run(() =>
+ {
+ var content = new AwareContent();
+ var services = new ServiceCollection();
+ services.RegisterNavigationService();
+ services.AddSingleton(content);
+ using var provider = services.BuildServiceProvider();
+ var host = RegisterContentHost(
+ provider,
+ "main",
+ new ThrowingContentControl());
+ var regionManager = provider.GetRequiredService();
+
+ try
+ {
+ var exception = Assert.Throws(() =>
+ provider.GetRequiredService()
+ .Navigate("main"));
+
+ Assert.Equal("presentation failed", exception.Message);
+ Assert.Equal(0, content.CallCount);
+ }
+ finally
+ {
+ regionManager.UnregisterRegion("main", host);
+ }
+ });
+ }
+
+ [Fact]
+ public void NavigationRequiresTheHostDispatcherThread()
+ {
+ ServiceProvider? provider = null;
+ IContentService? service = null;
+ ContentControl? host = null;
+ StaTest.Run(() =>
+ {
+ var services = new ServiceCollection();
+ services.RegisterNavigationService();
+ services.AddSingleton(new TestContent());
+ provider = services.BuildServiceProvider();
+ host = RegisterContentHost(provider, "main");
+ service = provider.GetRequiredService();
+ });
+
+ try
+ {
+ Assert.Throws(() =>
+ service!.Navigate("main"));
+ GC.KeepAlive(host);
+ }
+ finally
+ {
+ provider!.Dispose();
+ }
+ }
+
+ private static ContentControl RegisterContentHost(
+ IServiceProvider provider,
+ string name)
+ {
+ return RegisterContentHost(provider, name, new ContentControl());
+ }
+
+ private static TContentControl RegisterContentHost(
+ IServiceProvider provider,
+ string name,
+ TContentControl host)
+ where TContentControl : ContentControl
+ {
+ provider.GetRequiredService().RegisterRegion(name, host);
+ return host;
+ }
+
+ private sealed class ThrowingContentControl : ContentControl
+ {
+ protected override void OnContentChanged(
+ object oldContent,
+ object newContent)
+ {
+ throw new InvalidOperationException("presentation failed");
+ }
+ }
+
+ private sealed class RecordingAwareContent : UserControl, INavigationAware
+ {
+ private readonly string name;
+ private readonly IList calls;
+
+ public RecordingAwareContent(string name, IList calls)
+ {
+ this.name = name;
+ this.calls = calls;
+ }
+
+ public DialogParameters? Parameters { get; private set; }
+
+ public void OnNavigated(DialogParameters? parameters)
+ {
+ calls.Add(name);
+ Parameters = parameters;
+ }
+ }
+
+ private sealed class RecordingAware : INavigationAware
+ {
+ private readonly string name;
+ private readonly IList calls;
+
+ public RecordingAware(string name, IList calls)
+ {
+ this.name = name;
+ this.calls = calls;
+ }
+
+ public DialogParameters? Parameters { get; private set; }
+
+ public void OnNavigated(DialogParameters? parameters)
+ {
+ calls.Add(name);
+ Parameters = parameters;
+ }
+ }
+}
diff --git a/Tests/SimpleNavigation.Tests/NavigationExtensionsTests.cs b/Tests/SimpleNavigation.Tests/NavigationExtensionsTests.cs
new file mode 100644
index 0000000..3d5a8b6
--- /dev/null
+++ b/Tests/SimpleNavigation.Tests/NavigationExtensionsTests.cs
@@ -0,0 +1,313 @@
+using Microsoft.Extensions.DependencyInjection;
+using SimpleNavigation.Common.Managers;
+using SimpleNavigation.Extensions;
+using SimpleNavigation.Interface.Managers;
+using SimpleNavigation.Interface.Services;
+using SimpleNavigation.Services;
+using SimpleNavigation.Tests.TestInfrastructure;
+using System.Reflection;
+using System.Runtime.ExceptionServices;
+using System.Windows;
+
+namespace SimpleNavigation.Tests;
+
+public sealed class NavigationExtensionsTests
+{
+ [Fact]
+ public void SingleGenericKeyOverloads_RegisterTransientViewsAndCoreServices()
+ {
+ StaTest.Run(() =>
+ {
+ var services = new ServiceCollection();
+ services.RegisterNavigationService();
+ services.AddPage("page");
+ services.AddContent("content");
+ using var provider = services.BuildServiceProvider();
+
+ Assert.NotSame(
+ provider.GetRequiredService(),
+ provider.GetRequiredService());
+ Assert.NotSame(
+ provider.GetRequiredService(),
+ provider.GetRequiredService());
+ Assert.Same(
+ provider.GetRequiredService(),
+ provider.GetRequiredService());
+ });
+ }
+
+ [Fact]
+ public void RegisterNavigationService_RegistersConcreteRegionManagerAsSingleton()
+ {
+ var services = new ServiceCollection();
+ services.RegisterNavigationService();
+ using var provider = services.BuildServiceProvider();
+
+ var first = provider.GetRequiredService();
+ var second = provider.GetRequiredService();
+
+ Assert.IsType(first);
+ Assert.Same(first, second);
+ }
+
+ [Fact]
+ public void RegisterNavigationService_RegistersInternalRouteRegistryAsSingleton()
+ {
+ var services = new ServiceCollection();
+ services.RegisterNavigationService();
+ var descriptor = Assert.Single(
+ services,
+ item => item.ServiceType.FullName ==
+ "SimpleNavigation.Common.NavigationRouteRegistry");
+ using var provider = services.BuildServiceProvider();
+
+ var first = provider.GetRequiredService(descriptor.ServiceType);
+ var second = provider.GetRequiredService(descriptor.ServiceType);
+
+ Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime);
+ Assert.Same(first, second);
+ }
+
+ [Fact]
+ public void RegisterNavigationService_PreservesEarlierCoreRegistrations()
+ {
+ var services = new ServiceCollection();
+ using var regionManager = new RegionManager();
+ services.AddSingleton(regionManager);
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ var existingDescriptors = services.ToArray();
+
+ services.RegisterNavigationService();
+ using var provider = services.BuildServiceProvider();
+
+ Assert.Equal(existingDescriptors.Length + 1, services.Count);
+ foreach (var descriptor in existingDescriptors)
+ {
+ Assert.Same(
+ descriptor,
+ Assert.Single(services, item => item.ServiceType == descriptor.ServiceType));
+ }
+
+ Assert.Same(regionManager, provider.GetRequiredService());
+ }
+
+ [Fact]
+ public void RouteRegistry_MapsRoutesAddedBeforeAndAfterCoreRegistration()
+ {
+ var services = new ServiceCollection();
+ services.AddPage("shared");
+ services.RegisterNavigationService();
+ services.AddContent("shared");
+ services.AddPage("second");
+ using var provider = services.BuildServiceProvider();
+ var registry = GetRouteRegistry(services, provider);
+
+ Assert.Equal(
+ typeof(FirstPage),
+ InvokeRouteLookup(registry, "GetRequiredPageType", "shared"));
+ Assert.Equal(
+ typeof(TestContent),
+ InvokeRouteLookup(registry, "GetRequiredContentType", "shared"));
+ Assert.Equal(
+ typeof(SecondPage),
+ InvokeRouteLookup(registry, "GetRequiredPageType", "second"));
+ }
+
+ [Fact]
+ public void RouteRegistry_UsesOrdinalCaseSensitiveKeys()
+ {
+ var services = new ServiceCollection();
+ services.AddPage("main");
+ services.AddPage("Main");
+ services.RegisterNavigationService();
+ using var provider = services.BuildServiceProvider();
+ var registry = GetRouteRegistry(services, provider);
+
+ Assert.Equal(
+ typeof(FirstPage),
+ InvokeRouteLookup(registry, "GetRequiredPageType", "main"));
+ Assert.Equal(
+ typeof(SecondPage),
+ InvokeRouteLookup(registry, "GetRequiredPageType", "Main"));
+ }
+
+ [Fact]
+ public void RouteRegistry_UnknownPageAndContentKeysThrowKeyNotFoundException()
+ {
+ var services = new ServiceCollection();
+ services.RegisterNavigationService();
+ using var provider = services.BuildServiceProvider();
+ var registry = GetRouteRegistry(services, provider);
+
+ Assert.Throws(
+ () => InvokeRouteLookup(registry, "GetRequiredPageType", "missing"));
+ Assert.Throws(
+ () => InvokeRouteLookup(registry, "GetRequiredContentType", "missing"));
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void RouteRegistry_InvalidPageAndContentKeysThrowArgumentException(string? key)
+ {
+ var services = new ServiceCollection();
+ services.RegisterNavigationService();
+ using var provider = services.BuildServiceProvider();
+ var registry = GetRouteRegistry(services, provider);
+
+ Assert.Throws(
+ () => InvokeRouteLookup(registry, "GetRequiredPageType", key));
+ Assert.Throws(
+ () => InvokeRouteLookup(registry, "GetRequiredContentType", key));
+ }
+
+ [Fact]
+ public void DoubleGenericOverloads_RegisterViewAndViewModelWithoutSettingDataContext()
+ {
+ StaTest.Run(() =>
+ {
+ var services = new ServiceCollection();
+ services.AddPage();
+ services.AddContent();
+ using var provider = services.BuildServiceProvider();
+
+ var page = provider.GetRequiredService();
+ var content = provider.GetRequiredService();
+
+ Assert.Null(page.DataContext);
+ Assert.Null(content.DataContext);
+ Assert.NotSame(
+ provider.GetRequiredService(),
+ provider.GetRequiredService());
+ });
+ }
+
+ [Fact]
+ public void RegistrationHelpers_PreserveEarlierSingletonRegistrations()
+ {
+ StaTest.Run(() =>
+ {
+ var page = new FirstPage();
+ var content = new TestContent();
+ var viewModel = new TestViewModel();
+ var services = new ServiceCollection();
+ services.AddSingleton(page);
+ services.AddSingleton(content);
+ services.AddSingleton(viewModel);
+
+ services.AddPage("page");
+ services.AddContent("content");
+ using var provider = services.BuildServiceProvider();
+
+ Assert.Same(page, provider.GetRequiredService());
+ Assert.Same(content, provider.GetRequiredService());
+ Assert.Same(viewModel, provider.GetRequiredService());
+ });
+ }
+
+ [Fact]
+ public void DuplicateKeyWithinOneCategory_IsRejectedUsingOrdinalMatching()
+ {
+ var services = new ServiceCollection();
+ services.AddPage("main");
+ services.AddPage("Main");
+
+ var exception = Assert.Throws(
+ () => services.AddPage("main"));
+
+ Assert.Equal("key", exception.ParamName);
+ }
+
+ [Fact]
+ public void DuplicateContentKey_IsRejected()
+ {
+ var services = new ServiceCollection();
+ services.AddContent("main");
+
+ var exception = Assert.Throws(
+ () => services.AddContent("main"));
+
+ Assert.Equal("key", exception.ParamName);
+ }
+
+ [Fact]
+ public void SameKeyAcrossPageAndContentCategories_IsAllowed()
+ {
+ var services = new ServiceCollection();
+
+ services.AddPage("main");
+ services.AddContent("main");
+ }
+
+ [Fact]
+ public void ContentRegistration_RejectsPageAndWindowTypes()
+ {
+ var services = new ServiceCollection();
+
+ Assert.Throws(() => services.AddContent("page"));
+ Assert.Throws(() => services.AddContent("window"));
+ Assert.Throws(() => services.AddContent());
+ Assert.Throws(() => services.AddContent("window-vm"));
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void InvalidRouteKey_IsRejected(string? key)
+ {
+ var exception = Assert.Throws(
+ () => new ServiceCollection().AddPage(key!));
+
+ Assert.Equal("key", exception.ParamName);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void InvalidContentRouteKey_IsRejected(string? key)
+ {
+ var exception = Assert.Throws(
+ () => new ServiceCollection().AddContent(key!));
+
+ Assert.Equal("key", exception.ParamName);
+ }
+
+ private static object GetRouteRegistry(
+ IServiceCollection services,
+ IServiceProvider provider)
+ {
+ var descriptor = Assert.Single(
+ services,
+ item => item.ServiceType.FullName ==
+ "SimpleNavigation.Common.NavigationRouteRegistry");
+ return provider.GetRequiredService(descriptor.ServiceType);
+ }
+
+ private static Type InvokeRouteLookup(
+ object registry,
+ string methodName,
+ string? key)
+ {
+ var method = registry.GetType().GetMethod(
+ methodName,
+ BindingFlags.Instance | BindingFlags.Public);
+ Assert.NotNull(method);
+
+ try
+ {
+ return Assert.IsAssignableFrom(
+ method!.Invoke(registry, new object?[] { key }));
+ }
+ catch (TargetInvocationException exception) when (exception.InnerException != null)
+ {
+ ExceptionDispatchInfo.Capture(exception.InnerException).Throw();
+ throw;
+ }
+ }
+}
diff --git a/Tests/SimpleNavigation.Tests/PageServiceTests.cs b/Tests/SimpleNavigation.Tests/PageServiceTests.cs
new file mode 100644
index 0000000..d0e943b
--- /dev/null
+++ b/Tests/SimpleNavigation.Tests/PageServiceTests.cs
@@ -0,0 +1,239 @@
+using Microsoft.Extensions.DependencyInjection;
+using SimpleNavigation.Common;
+using SimpleNavigation.Extensions;
+using SimpleNavigation.Interface.Managers;
+using SimpleNavigation.Interface.Services;
+using SimpleNavigation.Tests.TestInfrastructure;
+using System.Windows.Controls;
+using System.Windows.Navigation;
+
+namespace SimpleNavigation.Tests;
+
+public class PageServiceTests
+{
+ [Fact]
+ public void GenericAndTypeNavigationDoNotRequireRoutes()
+ {
+ StaTest.Run(() =>
+ {
+ var services = new ServiceCollection();
+ services.RegisterNavigationService();
+ services.AddTransient();
+ services.AddTransient();
+ using var provider = services.BuildServiceProvider();
+ var frame = RegisterFrame(provider, "main");
+ var service = provider.GetRequiredService();
+
+ service.Navigate("main");
+ StaTest.PumpUntil(() => frame.Content is FirstPage);
+ Assert.IsType(frame.Content);
+
+ service.Navigate("main", typeof(SecondPage));
+ StaTest.PumpUntil(() => frame.Content is SecondPage);
+ Assert.IsType(frame.Content);
+ });
+ }
+
+ [Fact]
+ public void StringNavigationUsesCaseSensitiveRouteThenOrdinaryDi()
+ {
+ StaTest.Run(() =>
+ {
+ var expected = new FirstPage();
+ var services = new ServiceCollection();
+ services.RegisterNavigationService();
+ services.AddSingleton(expected);
+ services.AddPage("first");
+ using var provider = services.BuildServiceProvider();
+ var frame = RegisterFrame(provider, "main");
+ var service = provider.GetRequiredService();
+
+ service.Navigate("main", "first");
+
+ StaTest.PumpUntil(() => ReferenceEquals(frame.Content, expected));
+ Assert.Same(expected, frame.Content);
+ Assert.Throws(() => service.Navigate("main", "First"));
+ });
+ }
+
+ [Fact]
+ public void ViewAndDataContextReceiveParametersOnceEach()
+ {
+ StaTest.Run(() =>
+ {
+ var viewModel = new AwareViewModel();
+ var page = new AwarePage { DataContext = viewModel };
+ var parameters = new DialogParameters("id", 7);
+ var services = new ServiceCollection();
+ services.RegisterNavigationService();
+ services.AddSingleton(page);
+ using var provider = services.BuildServiceProvider();
+ var frame = RegisterFrame(provider, "main");
+
+ provider.GetRequiredService()
+ .Navigate("main", parameters);
+
+ Assert.Equal(1, page.CallCount);
+ Assert.Equal(1, viewModel.CallCount);
+ Assert.Same(parameters, page.Parameters);
+ Assert.Same(parameters, viewModel.Parameters);
+ GC.KeepAlive(frame);
+ });
+ }
+
+ [Fact]
+ public void NullParametersStillTriggerAwarenessAndSameInstanceIsDeduplicated()
+ {
+ StaTest.Run(() =>
+ {
+ var page = new AwarePage();
+ page.DataContext = page;
+ var services = new ServiceCollection();
+ services.RegisterNavigationService();
+ services.AddSingleton(page);
+ using var provider = services.BuildServiceProvider();
+ var frame = RegisterFrame(provider, "main");
+
+ provider.GetRequiredService().Navigate("main");
+
+ Assert.Equal(1, page.CallCount);
+ Assert.Null(page.Parameters);
+ GC.KeepAlive(frame);
+ });
+ }
+
+ [Fact]
+ public void InvalidInputsFailFast()
+ {
+ StaTest.Run(() =>
+ {
+ var services = new ServiceCollection();
+ services.RegisterNavigationService();
+ services.AddTransient();
+ using var provider = services.BuildServiceProvider();
+ var contentHost = new ContentControl();
+ provider.GetRequiredService().RegisterRegion("content", contentHost);
+ var service = provider.GetRequiredService