From 9b9e4cbd820f92c069234965c237c8095292f922 Mon Sep 17 00:00:00 2001 From: chfei Date: Fri, 13 Nov 2026 11:07:41 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0ViewModelBase=20=E7=94=9F?= =?UTF-8?q?=E5=91=BD=E5=91=A8=E6=9C=9F=E6=8E=A7=E5=88=B6View?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controls/ConfirmDialog.cs | 4 +- .../Controls/ViewLifecycle.cs | 85 +++++++++++++++++++ .../Controls/ViewModelBase.cs | 14 +++ .../FrameworkApplication.cs | 2 + .../ViewModels/FrameworkTestPageViewModel.cs | 4 +- .../ViewModels/MainViewModel.cs | 4 +- .../ViewModels/UserManagementPageViewModel.cs | 6 +- .../Program.cs | 52 +++++++++++- 8 files changed, 163 insertions(+), 8 deletions(-) create mode 100644 Source/Store.Management.Framework/Controls/ViewLifecycle.cs create mode 100644 Source/Store.Management.Framework/Controls/ViewModelBase.cs 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; }