65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.68/5 (12投票s)

2012年7月1日

CPOL

12分钟阅读

viewsIcon

242123

downloadIcon

5911

本文示例了一种符合MVVM规范的应用程序及其对话框的启动和关闭序列的实现。

引言

本文介绍了一种符合MVVM规范的应用程序的实现,该应用程序演示了如何

  • 应用程序的启动/关闭和
  • 窗口的打开/关闭

可以实现。

我研究了WPF中关闭窗口的各种解决方案,发现了一些方法(起初令人困惑)。互联网上有一些建议[2],其中在应用程序的MainWindow中使用一个附加行为,通过一个附加属性[3]来关闭窗口,该属性绑定到一个ViewModel。我的示例应用程序利用了该解决方案并在此基础上进行了扩展。该示例实现了

  • 关闭应用程序的MainWindow(无论是MainWindowViewModel还是Windows操作系统发起的,这都没有区别——App对象始终完全控制着局面)
  • 一个特定于对话框的ViewModel,可用于驱动对话框并接收DialogResult

    对话框ViewModel还提供了一种评估用户输入并产生相应消息(信息、警告、错误等)的功能,如果某些输入不符合预期的质量。

Using the Code

应用程序的启动和关闭

应用程序启动的顺序是

  • 实例化一个App类的对象(在WPF项目中默认生成),并在该对象中使用Application_Startup方法来
    • 实例化一个AppViewModel(有时也称为工作区)
    • 实例化一个应用程序的MainWindow
    • ViewModel附加到MainWindowDataContext
    • 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!
}

这个启动顺序的优点是ViewViewModel之间没有互相引用,但仍然可以完美地协同工作。这对于您需要维护多个MainWindow,并且需要根据当前使用的配置或命令行选项实例化不同的MainWindow的情况尤其有利。命令行参数也可以在Application_Startup方法中进行评估。然后,该方法可以相应地准备AppViewModel,并以预期的方式启动应用程序(尽管示例中没有实现命令行参数的评估)。

App类代表一个围绕应用程序的ViewViewModel实例的包装器。该包装器在应用程序启动或关闭时激活。

应用程序关闭顺序是启动顺序的逆过程。也就是说,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,当用户通过“取消”或“确定”关闭应用程序时,它被设置为truefalse

我正在使用此行为以符合MVVM的方式实现一个驱动对话框(或其他视图)的ViewModel,这样对话框就不会关闭,除非用户输入了符合预期质量的数据。这里的重要主题是,我们有一个ViewModel,它通过其完整的生命周期驱动视图,而无需使用任何代码隐藏。要做到这一点,我们需要构造一个ViewModelView对象,将前者附加到后者的DataContext,并订阅视图的Closing事件。此外,ViewViewModel应该实现OKCancel命令,以便在用户单击其中一个按钮时执行相应的处理。

这似乎是很多工作——我们必须为每个驱动对话框的ViewModel这样做吗?我听到您在说,这太痛苦了。有些人已经说过,这太复杂了[2],声称在某些情况下,代码隐藏是可以接受的。有些人说得有道理,但如果能将复杂性抽象到一个漂亮的类中,然后可以在需要对话框(或其他视图)来基于OKCancel处理输入时,在ViewModel中使用它呢?

DialogViewModelBase类实现了上述项目(以及更多),这些项目对于通过其生命周期驱动对话框是必需的。该类可用于使用户输入验证成为一项琐碎的任务。

这个对话框通过AppCommand类中的ShowDialog路由命令显示,该命令绑定到AppViewModel类中的ShowDialog方法。该方法调用Util类中的ShowDialog方法。在这里,我们实例化对话框,附加UsernameViewModel对象的副本,并初始化OpenCloseView属性。

UsernameViewModel类在其OpenCloseView属性中实例化DialogViewModel类。OpenCloseView属性在UsernameDialog.xaml中绑定。

因此,当您在UsernameDialog中单击“确定”或“取消”(或使用Escape键)时,您实际上是在执行DialogViewModel类中相应的命令。现在,执行“确定”命令会调用PerformInputDataEvaluation方法,该方法又会调用绑定到EvaluateInputData委托属性的外部方法(ValidateData)。

该方法的签名是返回truefalse,取决于用户输入是否存在问题。如果存在问题,这些问题可以被详细说明、分类,并与Msg对象一起作为out List<msg>参数返回。然后,此消息列表被馈送到ObservableCollection<msg>类型的ListMessages属性中。这些消息会显示出来,并带有图标,因为对话框的XAML使用了CountToVisibilityHiddenConverter来隐藏消息StackPanel(当没有消息时)。它还使用MsgTypeToResourceConverter将消息的类别转换为可以显示在消息列表中的图像资源[4]。

可以通过简单地将EvaluateInputData委托属性设置为null来禁用此输入评估和消息列表。然后,当IsReadyToClosetrue时,对话框将通过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、HiroNinject,这解释了为什么人们厌倦了看到又一个框架Smile | <img src= " />。

我不会在这篇文章中实现和记录另一个框架。相反,我们将研究一个简化的DialogService实现,以解释和记录IOC服务模式,并提供一个示例实现。我在CodeProject[5]找到了这样一个示例实现,对其进行了进一步简化,并将其应用于我的示例代码,如下所述。

Disore的原始实现[5]实现了一些服务来定义接口,用于

  • 从XML读取Person信息
  • 显示MessageBox内容
  • 使用常用对话框,例如打开文件或浏览文件夹,以及
  • 使用带有DialogServiceViewModel

我只对最后一点“使用带有‘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——用于创建和显示ViewModelView,以及
  • 一个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);
}

现在我们有了一个实现,可以确保ViewViewModel彼此不知道。这有利于单元测试——事实证明它比您想象的要简单。例如,只需获取Disore的源代码,下载并安装NUnit设置,然后使用图形GUI在5分钟内开始进行单元测试。

使用这样的实现还可以揭示其他可能性。例如,考虑一个具有高级视图和基本视图的应用程序——或者一个需要查看模式或编辑模式的应用程序。您可以通过在运行时映射/重新映射视图来轻松实现此类运行时更改。

关注点

我学会了使用委托方法在复杂的系统中执行灵活的代码片段,从而精确地公开我需要公开的内容,同时隐藏整个系统的复杂性。我还了解到,通用函数,例如通过生命周期驱动对话框,可以封装在一个类中,当显示该对话框(或视图)时可以实例化该类。使用ServiceLocator可以将这种开发进一步推向专业的实现。现在我有一个可以简单应用的模式,而不必关心窗口关闭、正在关闭或其他任何事件,因为我只需要实现

  • OpenCloseView属性、输入评估方法和ShowDialog方法
  • 加上必需的XAML

这样,另一个对话框就完成了。哦,所有这些都是以符合MVVM的方式完成的。那么,这为什么不是一个好方法呢?

参考文献

历史

  • 2012年6月30日:初始版本
  • 2012年7月3日(修复了OnClosing方法中的一个bug,并将util.ShowDialog移出了ViewModel命名空间)
  • 2012年8月9日:添加了DialogService实现部分和高级代码示例
© . All rights reserved.