增加ViewModelBase 生命周期控制View

This commit is contained in:
chfei committed 2026-11-13 11:07:41 +08:00
1 parent 30a6a5ea24
commit 9b9e4cbd82
8 files changed
+163 -8

No files matched your search

@@ -32,7 +32,7 @@ public static class IDialogServiceExtension
/// <summary>
/// 确定取消对话框 ViewModel
/// </summary>
public class ConfirmDialogViewModel : BindableBase, IDialogAware
public class ConfirmDialogViewModel : ViewModelBase, IDialogAware
{
public ConfirmDialogViewModel()
{
@@ -286,4 +286,4 @@ public abstract class ConfirmDialogViewModel<TParameter, TResult> : ConfirmDialo
//public abstract class ConfirmDialogParameterViewModel : ConfirmDialogViewModel<ConfirmDialogParameterViewModel.Parameter>
//{
// public abstract class Parameter;
//}
//}
@@ -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();
}
}
@@ -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() { }
}
@@ -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;
@@ -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;
@@ -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; }
@@ -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;
@@ -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; }