使用 WPF 和 MVVM 关闭窗口和应用程序






4.68/5 (12投票s)
本文示例了一种符合MVVM规范的应用程序及其对话框的启动和关闭序列的实现。
引言
本文介绍了一种符合MVVM规范的应用程序的实现,该应用程序演示了如何
- 应用程序的启动/关闭和
- 窗口的打开/关闭
可以实现。
我研究了WPF中关闭窗口的各种解决方案,发现了一些方法(起初令人困惑)。互联网上有一些建议[2],其中在应用程序的MainWindow
中使用一个附加行为,通过一个附加属性[3]来关闭窗口,该属性绑定到一个ViewModel
。我的示例应用程序利用了该解决方案并在此基础上进行了扩展。该示例实现了
- 关闭应用程序的
MainWindow
(无论是MainWindow
、ViewModel
还是Windows操作系统发起的,这都没有区别——App
对象始终完全控制着局面) - 一个特定于对话框的
ViewModel
,可用于驱动对话框并接收DialogResult
对话框
ViewModel
还提供了一种评估用户输入并产生相应消息(信息、警告、错误等)的功能,如果某些输入不符合预期的质量。
Using the Code
应用程序的启动和关闭
应用程序启动的顺序是
- 实例化一个
App
类的对象(在WPF项目中默认生成),并在该对象中使用Application_Startup
方法来- 实例化一个
AppViewModel
(有时也称为工作区) - 实例化一个应用程序的
MainWindow
- 将
ViewModel
附加到MainWindow
的DataContext
- 并
Show
MainWindow
- 实例化一个
private void Application_Startup(object sender, StartupEventArgs e)
{
AppViewModel tVM = new AppViewModel(); // Construct ViewModel and MainWindow
this.win = new MainWindow();
this.win.Closing += this.OnClosing;
// When the ViewModel asks to be closed, it closes the window via attached behaviour.
// We use this event to shut down the remaining parts of the application
tVM.RequestClose += delegate
{
// Make sure close down event is processed only once
if (this.mRequestClose == false)
{
this.mRequestClose = true;
// Save session data and close application
this.OnClosed(this.win.DataContext as ViewModel.AppViewModel, this.win);
}
};
this.win.DataContext = tVM; // Attach ViewModel to DataContext of ViewModel
this.InitMainWindowCommandBinding(this.win); // Initialize RoutedCommand bindings
this.win.Show(); // SHOW ME DA WINDOW!
}
这个启动顺序的优点是View
和ViewModel
之间没有互相引用,但仍然可以完美地协同工作。这对于您需要维护多个MainWindow
,并且需要根据当前使用的配置或命令行选项实例化不同的MainWindow
的情况尤其有利。命令行参数也可以在Application_Startup
方法中进行评估。然后,该方法可以相应地准备AppViewModel
,并以预期的方式启动应用程序(尽管示例中没有实现命令行参数的评估)。
App
类代表一个围绕应用程序的View
和ViewModel
实例的包装器。该包装器在应用程序启动或关闭时激活。
应用程序关闭顺序是启动顺序的逆过程。也就是说,MainWindow
告诉AppViewModel
它已收到关闭应用程序的请求,ViewModel
根据当前情况进行评估,并拒绝或确认该请求。
关闭顺序是否可以进行,通过App
类进行路由
this.win.Closing += this.OnClosing;
private void OnClosing(object sender, System.ComponentModel.CancelEventArgs e)
{
e.Cancel = this.OnSessionEnding();
}
private bool OnSessionEnding()
{
ViewModel.AppViewModel tVM = this.MainWindow.DataContext as ViewModel.AppViewModel;
if (tVM != null)
{
if (tVM.IsReadyToClose == false)
{
MessageBox.Show("Application is not ready to exit.\n" +
"Hint: Check the checkbox in the MainWindow before exiting the application.",
"Cannot exit application", MessageBoxButton.OK);
return !tVM.IsReadyToClose; // Cancel close down request if ViewModel is not ready, yet
}
tVM.OnRequestClose(false);
return !tVM.IsReadyToClose; // Cancel close down request if ViewModel is not ready, yet
}
return true;
}
当用户单击MainWindow
关闭按钮时,会发生此事件级联。当用户选择“文件”>“退出”菜单项或按下ALT+F4键时,情况略有不同。在这种情况下,会在App.xaml.cs中执行路由命令绑定
win.CommandBindings.Add(new CommandBinding(AppCommand.Exit,
(s, e) =>
{
e.Handled = true;
((AppViewModel)win.DataContext).ExitExecuted();
}));
...这将调用AppViewModel
类中的ExitExecuted()
方法
if (this.mShutDownInProgress == false)
{
this.mShutDownInProgress = true;
if (this.OnSessionEnding != null)
{
if (this.OnSessionEnding() == true)
{
this.mShutDownInProgress = false;
return;
}
}
this.WindowCloseResult = true; // Close the MainWindow and tell outside world
// that we are closed down.
EventHandler handler = this.RequestClose;
if (handler != null)
handler(this, EventArgs.Empty);
}
...上面的代码通过同名的委托方法属性调用App
类中的OnSessionEnding
方法。这样做之所以如此迂回,是因为我想确保所有的关闭请求都由一个方法(App.OnSessionEnding()
)处理。
关闭过程通过设置this.WindowCloseResult = true;
进行,这反过来又会调用附加的DialogCloser
属性,该属性通过绑定的布尔属性关闭MainWindow
(参见MainWindow.xaml)。
还有第三种执行路径可以关闭应用程序,那就是关闭Windows操作系统。该事件会遍历每个应用程序,询问它们是否可以关闭。我们在App.xaml代码中设置了SessionEnding="App_SessionEnding"
属性,以便在Windows关闭时调用OnSessionEnding
方法。这样,如果OnSessionEnding
方法发出信号表明我们仍有数据需要保存到磁盘(当然,除非用户强制Windows关机),Windows将不再继续关机。
总结本节。关闭主窗口,进而关闭应用程序有几种执行路径。所有这些路径都基于相同的评估函数(App
类中的OnSessionEnding
),并最终汇集到一个关闭函数OnClosed
,以确保在应用程序永久关闭之前可以保存会话数据。
- 主窗口可以通过
App
类中的OnClosing
方法发送Closing
事件来检查关闭是否正常。 - 主窗口可以通过(通过“文件”>“退出”或Alt+F4)执行
AppCommand.Exit
路由命令,通过AppViewModel
类中的ExitExecuted()
方法来关闭应用程序。 - Windows操作系统可以通过调用
App
类中的App_SessionEnding
方法来正常关闭我们的应用程序。
对话框的打开和关闭
上一节描述了一种情况,其中使用附加属性DialogCloser
通过将WindowCloseResult
属性设置为true
来关闭MainWindow
。这个附加行为(AB)实际上被设计成可以像DialogResult
一样使用。它默认为null
,当用户通过“取消”或“确定”关闭应用程序时,它被设置为true
或false
。
我正在使用此行为以符合MVVM的方式实现一个驱动对话框(或其他视图)的ViewModel
,这样对话框就不会关闭,除非用户输入了符合预期质量的数据。这里的重要主题是,我们有一个ViewModel
,它通过其完整的生命周期驱动视图,而无需使用任何代码隐藏。要做到这一点,我们需要构造一个ViewModel
和View
对象,将前者附加到后者的DataContext
,并订阅视图的Closing
事件。此外,View
和ViewModel
应该实现OK
和Cancel
命令,以便在用户单击其中一个按钮时执行相应的处理。
这似乎是很多工作——我们必须为每个驱动对话框的ViewModel
这样做吗?我听到您在说,这太痛苦了。有些人已经说过,这太复杂了[2],声称在某些情况下,代码隐藏是可以接受的。有些人说得有道理,但如果能将复杂性抽象到一个漂亮的类中,然后可以在需要对话框(或其他视图)来基于OK
或Cancel
处理输入时,在ViewModel
中使用它呢?
DialogViewModelBase
类实现了上述项目(以及更多),这些项目对于通过其生命周期驱动对话框是必需的。该类可用于使用户输入验证成为一项琐碎的任务。
这个对话框通过AppCommand
类中的ShowDialog
路由命令显示,该命令绑定到AppViewModel
类中的ShowDialog
方法。该方法调用Util
类中的ShowDialog
方法。在这里,我们实例化对话框,附加UsernameViewModel
对象的副本,并初始化OpenCloseView
属性。
UsernameViewModel
类在其OpenCloseView
属性中实例化DialogViewModel
类。OpenCloseView
属性在UsernameDialog.xaml中绑定。
因此,当您在UsernameDialog
中单击“确定”或“取消”(或使用Escape键)时,您实际上是在执行DialogViewModel
类中相应的命令。现在,执行“确定”命令会调用PerformInputDataEvaluation
方法,该方法又会调用绑定到EvaluateInputData
委托属性的外部方法(ValidateData
)。
该方法的签名是返回true
或false
,取决于用户输入是否存在问题。如果存在问题,这些问题可以被详细说明、分类,并与Msg
对象一起作为out List<msg>
参数返回。然后,此消息列表被馈送到ObservableCollection<msg>
类型的ListMessages
属性中。这些消息会显示出来,并带有图标,因为对话框的XAML使用了CountToVisibilityHiddenConverter
来隐藏消息StackPanel
(当没有消息时)。它还使用MsgTypeToResourceConverter
将消息的类别转换为可以显示在消息列表中的图像资源[4]。
可以通过简单地将EvaluateInputData
委托属性设置为null
来禁用此输入评估和消息列表。然后,当IsReadyToClose
为true
时,对话框将通过ViewModel
关闭。
第2部分 使用DialogService
我发布这篇文章的第一个版本时收到了充满争议的反馈。有些人缺少一个对话框服务实现(我在本节中会介绍),而另一些人则认为这种实现对话框的方法是过度设计。回顾一下,并且看到了其他关于类似主题的文章[5],我意识到我掉进了一个“CodeProject陷阱”,因为有些主题,例如MVVM,定义不那么清晰,而有些模式在应用于解决不同需求时会有所不同。
我最初的想法是简单地发布一个可以用于实现简单WPF应用程序的小代码示例。虽然,需求
- 单元测试
- 代码重用,以及
- 关注点分离
[6] 这些问题很有用,但它们不是我最初的关注点。此后,我审阅了MSDN、CodeProject和其他地方的大量文章,并得出结论,对话框服务是一个非常有用的练习。
需要了解的关键术语是“控制反转”(IOC)和“依赖注入”(DI)。只需在此处的CodeProject或其他地方的搜索引擎中输入“IOC”、“Dependency Injection”或“DialogService”,您就会找到无数关于该主题的解释。
我遵循了IOC的路线,发现了大量的框架:Unity、MEF、StructureMap、Castle.Windsor、AutoFac、Chinch、LinFu、Hiro和Ninject,这解释了为什么人们厌倦了看到又一个框架 " />。
我不会在这篇文章中实现和记录另一个框架。相反,我们将研究一个简化的DialogService
实现,以解释和记录IOC服务模式,并提供一个示例实现。我在CodeProject[5]找到了这样一个示例实现,对其进行了进一步简化,并将其应用于我的示例代码,如下所述。
Disore的原始实现[5]实现了一些服务来定义接口,用于
- 从XML读取
Person
信息 - 显示
MessageBox
内容 - 使用常用对话框,例如打开文件或浏览文件夹,以及
- 使用带有
DialogService
的ViewModel
我只对最后一点“使用带有‘DialogService
’的ViewModel
”感兴趣。因此,我下载了他的源代码,并删除了对DialogService
实现不必要的其他所有内容。这给我留下了“Service”和“WindowViewModelMapping”文件夹中的类。
我们喜欢简单的事物,对吧?事实证明,第一次使用这些类很简单(这要归功于Disore的文章和源代码)。但以一种不吓人的方式理解它们正确的应用则一点也不简单。所以,让我们一步步地看代码,看看我们能学到什么。
ServiceLocator
类是static
的,这意味着当.NET Framework加载相应的命名空间时,它就会被初始化和加载。对话框服务在其早期存在于App
类的App.Application_Startup
中。以下行
// Configure service locator
ServiceLocator.RegisterSingleton<IDialogService, DialogService>();
ServiceLocator.RegisterSingleton<IWindowViewModelMappings, WindowViewModelMappings>();
初始化两个服务
- 一个
DialogService
——用于创建和显示ViewModel
的View
,以及 - 一个
WindowViewModelMappings
服务,用于将ViewModel
类与其相应的View
类关联(映射)。
我们可以以任何我们想要的方式注册这些服务,但显然,除非存在两个类的映射(关联),否则我们无法实例化一个View
来匹配一个ViewModel
。Disore[5]在他的文章中展示了创建此映射的各种方法。然而,在我们的例子中,我们使用以下映射...
public WindowViewModelMappings()
{
mappings = new Dictionary<type,>
{
////{ typeof(AppViewModel), typeof(string) } ////,
{ typeof(UsernameViewModel), typeof(UsernameDialog)}
};
}
...在运行时将UsernameViewModel
类与UsernameDialog
类关联。这在下面的AppViewModel
函数中体现出来
public void ShowUserNameDialog()
{
UsernameViewModel dlgVM = null;
try
{
dlgVM = new UsernameViewModel(this.mTestDialogViewModel);
// It is important to either:
// 1> Use the InitDialogInputData method here or
// 2> Reset the WindowCloseResult=null property
// because otherwise ShowDialog will not work twice
// (Symptom: The dialog is closed immediately by the attached behaviour)
dlgVM.InitDialogInputData();
// Showing the dialog, alternative 1.
// Showing a specified dialog. This doesn't require any form of mapping using
// IWindowViewModelMappings.
dialogService.ShowDialog(this, dlgVM);
// Copy input if user OK'ed it. This could also be done by a method,
// equality operator, or copy constructor
if (dlgVM.OpenCloseView.WindowCloseResult == true)
{
Console.WriteLine("Dialog was OK'ed.");
this.mTestDialogViewModel.FirstName = dlgVM.FirstName;
this.mTestDialogViewModel.LastName = dlgVM.LastName;
}
else
Console.WriteLine("Dialog was Cancel'ed.");
}
catch (Exception exc)
{
MessageBox.Show(exc.ToString());
}
}
调用InitDialogInputData()
可以抽象到对话框服务的接口中,但我不会深入探讨,因为如果您对这个概念感到满意,您应该只使用上面列出的10个IOC容器之一。
总结一下这个概念——有一个static
类,即ServiceLocator
[7],用于注册DialogService
类和WindowViewModelMappings
类。该注册的结果存储在ServiceLocator
类中的一个dictionary
对象中。
private static Dictionary<Type, ServiceInfo> services = new Dictionary<Type, ServiceInfo>();
调用dialogService
dialogService.ShowDialog(this, dlgVM);
然后从ServiceLocator
字典中取出映射服务,该服务又从WindowViewModelMappings
字典中取出相应的视图。
public bool? ShowDialog(object ownerViewModel, object viewModel)
{
Type dialogType = windowViewModelMappings.GetWindowTypeFromViewModelType(viewModel.GetType());
return ShowDialog(ownerViewModel, viewModel, dialogType);
}
现在我们有了一个实现,可以确保View
和ViewModel
彼此不知道。这有利于单元测试——事实证明它比您想象的要简单。例如,只需获取Disore的源代码,下载并安装NUnit设置,然后使用图形GUI在5分钟内开始进行单元测试。
使用这样的实现还可以揭示其他可能性。例如,考虑一个具有高级视图和基本视图的应用程序——或者一个需要查看模式或编辑模式的应用程序。您可以通过在运行时映射/重新映射视图来轻松实现此类运行时更改。
关注点
我学会了使用委托方法在复杂的系统中执行灵活的代码片段,从而精确地公开我需要公开的内容,同时隐藏整个系统的复杂性。我还了解到,通用函数,例如通过生命周期驱动对话框,可以封装在一个类中,当显示该对话框(或视图)时可以实例化该类。使用ServiceLocator
可以将这种开发进一步推向专业的实现。现在我有一个可以简单应用的模式,而不必关心窗口关闭、正在关闭或其他任何事件,因为我只需要实现
OpenCloseView
属性、输入评估方法和ShowDialog
方法- 加上必需的
XAML
这样,另一个对话框就完成了。哦,所有这些都是以符合MVVM的方式完成的。那么,这为什么不是一个好方法呢?
参考文献
- [1]使用模型-视图-视图模型设计模式的WPF应用程序
http://msdn.microsoft.com/en-us/magazine/dd419663.aspx
- [2]WPF:如果Carlsberg做了MVVM框架:第3部分
https://codeproject.org.cn/Articles/38440/WPF-If-Carlsberg-did-MVVM-Frameworks-Part-3-of-n#PopServ
WPF MVVM新手 - ViewModel应该如何关闭表单?
http://stackoverflow.com/questions/501886/wpf-mvvm-newbie-how-should-the-viewmodel-close-the-form - [3]WPF中附加行为的介绍(Josh Smith)
https://codeproject.org.cn/Articles/28959/Introduction-to-Attached-Behaviors-in-WPF - [4]在WPF中使用ValueConverter和MultiValueConverter
https://codeproject.org.cn/Articles/298950/Using-ValueConverter-and-MultiValueConverter-in-WP - [5]使用MVVM模式显示对话框 - 作者:disore
https://codeproject.org.cn/Articles/36745/Showing-Dialogs-When-Using-the-MVVM-Pattern - [6]DI/IOCs - 作者:Sacha Barber
https://codeproject.org.cn/Articles/32190/DI-IOCs - [7]使用服务定位器在MVVM应用程序中处理MessageBox(Josh Smith)
https://codeproject.org.cn/Articles/70223/Using-a-Service-Locator-to-Work-with-MessageBoxes
历史
- 2012年6月30日:初始版本
- 2012年7月3日(修复了
OnClosing
方法中的一个bug,并将util.ShowDialog
移出了ViewModel
命名空间) - 2012年8月9日:添加了
DialogService
实现部分和高级代码示例