diff --git a/Source/Store.Management.Framework/Controls/ConfirmDialog.cs b/Source/Store.Management.Framework/Controls/ConfirmDialog.cs
index f7e691a..8060816 100644
--- a/Source/Store.Management.Framework/Controls/ConfirmDialog.cs
+++ b/Source/Store.Management.Framework/Controls/ConfirmDialog.cs
@@ -32,7 +32,7 @@ public static class IDialogServiceExtension
///
/// 确定取消对话框 ViewModel
///
-public class ConfirmDialogViewModel : BindableBase, IDialogAware
+public class ConfirmDialogViewModel : ViewModelBase, IDialogAware
{
public ConfirmDialogViewModel()
{
@@ -286,4 +286,4 @@ public abstract class ConfirmDialogViewModel : ConfirmDialo
//public abstract class ConfirmDialogParameterViewModel : ConfirmDialogViewModel
//{
// public abstract class Parameter;
-//}
\ No newline at end of file
+//}
diff --git a/Source/Store.Management.Framework/Controls/ViewLifecycle.cs b/Source/Store.Management.Framework/Controls/ViewLifecycle.cs
new file mode 100644
index 0000000..2187f70
--- /dev/null
+++ b/Source/Store.Management.Framework/Controls/ViewLifecycle.cs
@@ -0,0 +1,85 @@
+using Prism.Services.Dialogs;
+using System.ComponentModel;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace Store.Management.Framework.Controls;
+
+internal static class ViewLifecycle
+{
+ // 卸载时 DataContext 可能已改变,记录实际收到 OnLoaded 的实例。
+ private static readonly DependencyProperty LoadedViewModelProperty =
+ DependencyProperty.RegisterAttached("LoadedViewModel", typeof(ViewModelBase), typeof(ViewLifecycle));
+
+ internal static void Register()
+ {
+ foreach (Type type in new[] { typeof(UserControl), typeof(Window) })
+ {
+ FrameworkElement.DataContextProperty.OverrideMetadata(type,
+ new FrameworkPropertyMetadata(null, OnDataContextChanged));
+ }
+ }
+
+ private static void OnDataContextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
+ {
+ if (sender is Window window)
+ {
+ if (e.OldValue is ViewModelBase)
+ {
+ window.Activated -= OnActivated;
+ window.Deactivated -= OnDeactivated;
+ window.Closing -= OnClosing;
+ window.Closed -= OnClosed;
+ }
+ if (e.NewValue is ViewModelBase)
+ {
+ window.Activated += OnActivated;
+ window.Deactivated += OnDeactivated;
+ window.Closing += OnClosing;
+ window.Closed += OnClosed;
+ }
+ if (window is IDialogWindow) return; // Prism 弹窗由内容页接收加载与卸载。
+ }
+ var view = (FrameworkElement)sender;
+ if (e.OldValue is ViewModelBase)
+ {
+ view.Loaded -= OnLoaded;
+ view.Unloaded -= OnUnloaded;
+ }
+ if (e.NewValue is ViewModelBase)
+ {
+ view.Loaded += OnLoaded;
+ view.Unloaded += OnUnloaded;
+ }
+ if (view.IsLoaded || view.GetValue(LoadedViewModelProperty) != null)
+ UpdateViewModel(view);
+ }
+
+ private static void OnLoaded(object sender, RoutedEventArgs e)
+ => UpdateViewModel((FrameworkElement)sender);
+
+ private static void OnActivated(object? sender, EventArgs e) => (((Window)sender!).DataContext as ViewModelBase)?.OnActivated();
+ private static void OnDeactivated(object? sender, EventArgs e) => (((Window)sender!).DataContext as ViewModelBase)?.OnDeactivated();
+ private static void OnClosing(object? sender, CancelEventArgs e) => (((Window)sender!).DataContext as ViewModelBase)?.OnClosing(e);
+ private static void OnClosed(object? sender, EventArgs e) => (((Window)sender!).DataContext as ViewModelBase)?.OnClosed();
+
+ private static void UpdateViewModel(FrameworkElement view)
+ {
+ // 共享父级 DataContext 的页眉、导航栏等子控件不拥有页面生命周期。
+ var source = DependencyPropertyHelper.GetValueSource(view, FrameworkElement.DataContextProperty);
+ var next = source.BaseValueSource == BaseValueSource.Inherited ? null : view.DataContext as ViewModelBase;
+ var previous = (ViewModelBase?)view.GetValue(LoadedViewModelProperty);
+ if (ReferenceEquals(previous, next)) return;
+ view.SetValue(LoadedViewModelProperty, next);
+ previous?.OnUnloaded();
+ next?.OnLoaded();
+ }
+
+ private static void OnUnloaded(object sender, RoutedEventArgs e)
+ {
+ var view = (FrameworkElement)sender;
+ var previous = (ViewModelBase?)view.GetValue(LoadedViewModelProperty);
+ view.ClearValue(LoadedViewModelProperty);
+ previous?.OnUnloaded();
+ }
+}
diff --git a/Source/Store.Management.Framework/Controls/ViewModelBase.cs b/Source/Store.Management.Framework/Controls/ViewModelBase.cs
new file mode 100644
index 0000000..51faa20
--- /dev/null
+++ b/Source/Store.Management.Framework/Controls/ViewModelBase.cs
@@ -0,0 +1,14 @@
+using Prism.Mvvm;
+using System.ComponentModel;
+
+namespace Store.Management.Framework.Controls;
+
+public abstract class ViewModelBase : BindableBase
+{
+ protected internal virtual void OnLoaded() { }
+ protected internal virtual void OnUnloaded() { }
+ protected internal virtual void OnActivated() { }
+ protected internal virtual void OnDeactivated() { }
+ protected internal virtual void OnClosing(CancelEventArgs e) { }
+ protected internal virtual void OnClosed() { }
+}
diff --git a/Source/Store.Management.Framework/FrameworkApplication.cs b/Source/Store.Management.Framework/FrameworkApplication.cs
index 4d548b1..c1ac7b3 100644
--- a/Source/Store.Management.Framework/FrameworkApplication.cs
+++ b/Source/Store.Management.Framework/FrameworkApplication.cs
@@ -26,6 +26,8 @@ public abstract class FrameworkApplication : PrismApplication
private IStatusBarService StatusBarService;
private IMessageBoxService MessageBoxService;
private static bool FatalShutdown = false;
+ static FrameworkApplication() => Controls.ViewLifecycle.Register();
+
protected FrameworkApplication()
{
DispatcherUnhandledException += OnDispatcherUnhandledException;
diff --git a/Source/Store.Management/ViewModels/FrameworkTestPageViewModel.cs b/Source/Store.Management/ViewModels/FrameworkTestPageViewModel.cs
index f2ec28a..f5031a6 100644
--- a/Source/Store.Management/ViewModels/FrameworkTestPageViewModel.cs
+++ b/Source/Store.Management/ViewModels/FrameworkTestPageViewModel.cs
@@ -2,12 +2,12 @@ using Prism.Commands;
using MAX.Notification;
using Store.Management.Notifications;
using Prism.Ioc;
-using Prism.Mvvm;
+using Store.Management.Framework.Controls;
using Prism.Services.Dialogs;
using Store.Management.Services;
using Store.Management.Views;
namespace Store.Management.ViewModels;
-public sealed class FrameworkTestPageViewModel : BindableBase
+public sealed class FrameworkTestPageViewModel : ViewModelBase
{
private readonly IContainerProvider container;
private readonly SingletonProbe singleton;
diff --git a/Source/Store.Management/ViewModels/MainViewModel.cs b/Source/Store.Management/ViewModels/MainViewModel.cs
index b33ed3f..32150ce 100644
--- a/Source/Store.Management/ViewModels/MainViewModel.cs
+++ b/Source/Store.Management/ViewModels/MainViewModel.cs
@@ -1,5 +1,5 @@
using Store.Management.Framework.Services;
-using Prism.Mvvm;
+using Store.Management.Framework.Controls;
using Prism.Regions;
using System;
using System.Collections.Generic;
@@ -9,7 +9,7 @@ using System.Threading.Tasks;
namespace Store.Management.ViewModels
{
- partial class MainViewModel : BindableBase
+ partial class MainViewModel : ViewModelBase
{
private readonly IRegionManager _regionManager;
public IStatusBarService StatusBar { get; }
diff --git a/Source/Store.Management/ViewModels/UserManagementPageViewModel.cs b/Source/Store.Management/ViewModels/UserManagementPageViewModel.cs
index 6130c30..fed595d 100644
--- a/Source/Store.Management/ViewModels/UserManagementPageViewModel.cs
+++ b/Source/Store.Management/ViewModels/UserManagementPageViewModel.cs
@@ -12,7 +12,7 @@ using System.Windows.Controls.Primitives;
namespace Store.Management.ViewModels;
-public sealed class UserManagementPageViewModel(IUserService userService, IDialogService dialogs, IStatusBarService statusBar) : BindableBase
+public sealed class UserManagementPageViewModel(IUserService userService, IDialogService dialogs, IStatusBarService statusBar) : ViewModelBase
{
public const int KeywordLimit = 100;
private string appliedKeyword = "";
@@ -37,6 +37,10 @@ public sealed class UserManagementPageViewModel(IUserService userService, IDialo
public AsyncDelegateCommand LoginCommand => field ??= new(LoginAsync);
+ protected override void OnLoaded()
+ {
+ base.OnLoaded();
+ }
private async Task LoginAsync()
{
ButtonResult result = ButtonResult.None;
diff --git a/Tests/Store.Management.App.SmokeTests/Program.cs b/Tests/Store.Management.App.SmokeTests/Program.cs
index 7e6aec6..b13e896 100644
--- a/Tests/Store.Management.App.SmokeTests/Program.cs
+++ b/Tests/Store.Management.App.SmokeTests/Program.cs
@@ -58,8 +58,46 @@ class Program
Console.WriteLine("PASS: LoginViewModel -> ILoginService -> AuthApi, retry, password contract and cancellation.");
return 0;
}
- App app = args.Contains("--status-bar") ? new App() : new TestApp(client);
+ App app = args.Contains("--status-bar") || args.Contains("--view-lifecycle") ? new App() : new TestApp(client);
app.InitializeComponent();
+ if (args.Contains("--view-lifecycle"))
+ {
+ app.ShutdownMode = ShutdownMode.OnExplicitShutdown;
+ var pageVm = new LifecycleProbe();
+ var windowVm = new LifecycleProbe();
+ var page = new UserControl { DataContext = pageVm, Content = new UserControl() };
+ var window = new Window { DataContext = windowVm, Content = page };
+ window.Show();
+ app.Dispatcher.Invoke(() => { }, DispatcherPriority.ApplicationIdle);
+ Check(pageVm.Loads == 1 && windowVm.Loads == 1, $"窗口和页面各加载一次:{windowVm.Loads}/{pageVm.Loads}");
+ window.Content = null;
+ app.Dispatcher.Invoke(() => { }, DispatcherPriority.ApplicationIdle);
+ Check(pageVm.Unloads == 1, "移出页面触发卸载");
+ window.Content = page;
+ app.Dispatcher.Invoke(() => { }, DispatcherPriority.ApplicationIdle);
+ Check(pageVm.Loads == 2, "重新加入页面再次加载");
+ var replacement = new LifecycleProbe();
+ page.DataContext = replacement;
+ Check(pageVm.Unloads == 2 && replacement.Loads == 1, "已加载的页面更换 ViewModel 后转移通知");
+ windowVm.CancelClose = true;
+ window.Close();
+ Check(window.IsVisible && windowVm.Closes == 0, "ViewModel 可以取消关闭");
+ windowVm.CancelClose = false;
+ window.Close();
+ Check(windowVm.Closes == 1, "窗口关闭后通知 ViewModel");
+ var dialogVm = new LifecycleProbe();
+ var dialog = new DialogWindow
+ {
+ DataContext = dialogVm,
+ Content = new UserControl { DataContext = dialogVm }
+ };
+ dialog.Show();
+ app.Dispatcher.Invoke(() => { }, DispatcherPriority.ApplicationIdle);
+ Check(dialogVm.Loads == 1, "Prism 弹窗宿主和内容共享 ViewModel 时只加载一次");
+ dialog.Close();
+ Console.WriteLine("PASS: view and window lifecycle.");
+ return 0;
+ }
int exitCode = 0;
app.Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(async () =>
{
@@ -227,6 +265,18 @@ class Program
static void Check(bool success, string message) { if (!success) throw new Exception(message); }
}
+sealed class LifecycleProbe : ViewModelBase
+{
+ public int Loads { get; private set; }
+ public int Unloads { get; private set; }
+ public int Closes { get; private set; }
+ public bool CancelClose { get; set; }
+ protected override void OnLoaded() => Loads++;
+ protected override void OnUnloaded() => Unloads++;
+ protected override void OnClosing(System.ComponentModel.CancelEventArgs e) => e.Cancel = CancelClose;
+ protected override void OnClosed() => Closes++;
+}
+
sealed class LoginHandler : HttpMessageHandler
{
public int RequestCount { get; private set; }