如何使用C#优雅地在ViewModel中关闭Windows

Posted by JP on January 22, 2021

前言

当你在使用MVVM设计模式开发应用程序时,第一条 原则 就是 “ViewModel中没有UI元素”,也就是UI与业务逻辑分离,以便于后期即使要修改UI,也无需更改ViewModel。好吧,如果不允许我们在ViewModel中使用UI元素,那么到底如何从ViewModel中关闭窗口呢?这时候就需要充分利用面向接口编程的优势了。

正文

这篇Blog将教您如何利用接口的优势,从ViewModel中抽象出Window对象,并且仍然能够从ViewModel中关闭Windows。

不仅如此,我们还可以利用抽象的优点,在ViewModel中使用业务逻辑,甚至在单击Window的关闭按钮(Window顶部右角的X)时也可以防止Window关闭。

我们也可以通过抽象将接口信息合并到附加属性(AttachedProperty)中来减少重复重复的代码,该属性可以在应用程序中的任何Window上使用。真正做到 「Write once Run anywhere」

废话不多说了,代码直接撸起来。

首先在窗口中定义一个关闭窗口的按钮

MainWindow.xaml


<Window
    x:Class="CloseWindowMvvm.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:CloseWindowMvvm"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">

    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>

    <Grid>
        <Button
            Width="125"
            Height="45"
            Command="{Binding CloseCommand}"
            Content="关闭" />
    </Grid>
</Window>


接着需要定义一个ICloseWindows接口,用来解耦View和ViewModel。这个接口中定义了一个关闭窗口的委托,和一个是否允许关闭窗口的方法。

ICloseWindows.cs

  interface ICloseWindows
  {
  
     Action Close { get; set; }

     bool CanClose();
  }

MainWindow的ViewModel定义如下:

MainWindowViewModel.cs

 public class MainWindowViewModel : ICloseWindows
 {
     public Action Close { get; set; }

     private ICommand _closeCommand;
     public ICommand CloseCommand => _closeCommand ?? (_closeCommand = new DelegateCommand(CloseWindow));

     private void CloseWindow()
     {
         Close?.Invoke();
     }

     public bool CanClose()
     {
         MessageBoxResult messageBoxResult = 
             MessageBox.Show("确定要关闭吗?", "提示", MessageBoxButton.OKCancel);

         return messageBoxResult == MessageBoxResult.OK;
     }
 }

接着我们在MainWindow的后台,窗体的Loaded事件中,先判断当前窗口的DataContext是不是ICloseWindows,由于我们在Window的Xaml代码中指定了当前Window的ViewModel,并且ViewModel继承自ICloseWindows。所以当窗体加载的时候,会注册ViewModel的Close委托,以及当前窗口的Closing事件。

当点击窗口中的 “关闭” 按钮时,首先会执行ViewModel中定义的Close委托的匿名方法,关闭当前窗口,接着会触发当前窗口的Closing事件,会执行 e.Cancel=!vm.CanClose(); 这一行代码,所以,我们就可以在 ViewModel 中通过设置 CanClose() 方法来控制当前窗口是否允许关闭了。

using System.Windows;

namespace CloseWindowMvvm
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            Loaded += MainWindow_Loaded;
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            if (DataContext is ICloseWindows vm)
            {
                vm.Close += () =>
                {
                    Close();
                };

                Closing += (s, e) =>
                {
                    e.Cancel = !vm.CanClose();
                };
            }
        }
    }
}

到这里,你会发现,没有在 ViewModel 中直接引用UI元素,也没有直接依赖ViewModel,真正做到了解耦,是不是感觉得很amazing。
我们终于实现了我们想要的效果,但如果我有多个窗口,那我就需要在每个窗口的VM中写一遍上述的代码,很重复。有没有一种方式把业务逻辑相同的代码拿来复用呢,答案是有的,这时候就可以考虑下 附加属性 了。

利用附加属性进一步优化

新建一个叫WindowCloser的类,并在此类中添加一个附加属性

WindowCloser.cs

 public class WindowCloser
 {
     public static bool GetEnableWindowClosing(DependencyObject obj)
     {
         return (bool)obj.GetValue(EnableWindowClosingProperty);
     }

     public static void SetEnableWindowClosing(DependencyObject obj, bool value)
     {
         obj.SetValue(EnableWindowClosingProperty, value);
     }

     // Using a DependencyProperty as the backing store for EnableWindowClosing.  This enables animation, styling, binding, etc...
     public static readonly DependencyProperty EnableWindowClosingProperty =
         DependencyProperty.RegisterAttached("EnableWindowClosing", typeof(bool), typeof(WindowCloser), new PropertyMetadata(false, OnEnableWindowClosingChanged));

     private static void OnEnableWindowClosingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
     {
         if (d is Window window)
         {
             window.Loaded += (s, e) =>
             {
                 if (window.DataContext is ICloseWindows vm)
                 {
                     vm.Close += () =>
                     {
                         window.Close();
                     };

                     window.Closing += (s, e) =>
                     {
                         e.Cancel = !vm.CanClose();
                     };
                 }
             };
         }
      }
 }

在MainWindow.xaml的Window节点下中新增一行代码

<Window

    /*
     * Other code ...
     */
     
    local:WindowCloser.EnableWindowClosing="True"/>

    /*
     * Other code ...
     */
    
    
</Window>

MainWindow的后台代码中,恢复到默认创建MainWindow类时的样子。只留下下图所示代码,其余的全部删除掉

MainWindow.cs

using System.Windows;

namespace CloseWindowMvvm
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
	{
		public MainWindow()
		{
			InitializeComponent();
		}
    }
}


再次运行,你会发现利用附加属性,实现了相同的效果,并且利用WindowCloser附件属性,也可以在别的窗口中复用,是不是很酷呢?

感谢

Attached properties

Prism