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

支持 WPF 窗口关闭的行为

starIconstarIconstarIconstarIconstarIcon

5.00/5 (8投票s)

2016年2月2日

CPOL

7分钟阅读

viewsIcon

31397

downloadIcon

636

WPF MVVM 设计模式下从 ViewModel 关闭窗口存在问题。本文介绍了一种使用依赖属性来控制窗口关闭的方法。

引言

在使用 MVVM 模式创建 WPF 应用程序时,一个持续存在的问题是如何关闭窗口,以及如何将窗口关闭事件通知给 ViewModel。在我们之前的项目中,我们使用了 MVVMLite Messenger 来处理这类功能(我觉得我们过度使用了 Messenger)。在开始一个新项目时(实际上并不是完全从零开始,因为已经有大量的基础代码),我再次遇到了这个问题。经过一番思考,我决定研究使用一个行为(behavior)来支持 ViewViewModel 之间的这种交互。我总是自己编写行为,并且没有使用过任何框架提供的行为支持,包括 Expression Blend。我从未遇到过我认为这些基础设施提供了真正需要的东西来解决我需要用行为解决的问题。在大多数情况下,你只需要一个 DependencyProperty,而使用专门的行为会限制其在需要框架的项目中的使用。

第一部分实际上非常简单——将窗口关闭事件通知给 ViewModel。只有当用户可以在不与 ViewModel 交互的情况下关闭窗口,并且 ViewModel 需要知道 Window 已关闭时,才需要这样做。这很容易通过 ICommand 接口来处理。该行为有一个 ICommand 类型的 DependencyProperty。使用 ICommand 接口的好处是,可以检查 ICommandCanExecute 方法,如果返回 false,则可以取消关闭。

第二部分,也是最重要的一部分,是从 ViewModelView 发送信号,指示应执行 WindowClose 方法。没有直接的方法可以做到这一点,但可以使用一个属性来发出信号,表明应该执行 WindowClose 方法。这可以通过一个 bool 类型来实现。这样做需要将这个标志重置,以防同一个 ViewModel 被再次用于另一个窗口。至少对于这个行为来说是这样,因为它不应该无故限制其灵活性。

该行为还支持 Nullable<bool> 类型的 DialogResult。我这样做是因为有时返回一个标志来指示对话框是否保存了更改会很方便,比如在编辑对话框中,或者 UI 窗口有是/否或确定/取消的结果。这样,导致对话框显示的那个方法就可以放在一个 if 语句中,并且稍后不需要检查特殊的标志。为了支持这一点,该行为使用了一个 Nullable<bool> 来向行为发送关闭窗口的请求——null 是初始状态,绑定值被更改为 truefalse 来设置窗口的 DialogResult。行为处理完关闭请求的更改后,会将对话框结果设置为该值,并将关闭请求标志重置为 null,以便可以发出另一个关闭请求。

由于此行为也可以用于非 ShowDialog 方法打开的窗口,因此存在一个问题,因为当窗口使用 Show 方法打开时,无法设置 DialogResult——这会引发异常。一个简单的检查方法是,在设置 DialogResult 之前,检查 Owner 属性是否不为 null

该行为的一个不寻常的特点是它不必与 Window 相关联,而是可以应用于 Window 内的 FrameworkElement,包括 UserControl。如果 FrameworkElement 不是 Window,则使用 VisualTreeHelper 来查找父 Window。这样做的原因是提高可见性。在我的项目中,有一些标准的窗口被用作提供特定功能的 UserContorls 的容器。我的想法是,如果行为在窗口中指定,那么未来的程序员在处理代码时可能会比行为在 UserControl 中遇到更多困难。

关闭请求

用于信号以关闭窗口和设置 DialogResultDependencyProperty 是 object 类型。

public static readonly DependencyProperty CloseRequestProperty =
             DependencyProperty.RegisterAttached("CloseRequest", typeof(object),
             typeof(WindowCloseCommandBehaviour),
             new FrameworkPropertyMetadata(new object(),
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                    OnCloseRequestChanged));

这样做的原因是有一个初始值,可以检查绑定到此的 ViewModel 属性是否最初为 null。如果不为 null,则可以强制将其设置为 null。这是因为如果 ViewModel 被多次用于不同的窗口,属性将是最后一个设置的值 truefalse,而不是 null。可以使 ViewModel 负责处理,但最好让该行为来处理。

此外,还需要双向绑定,以允许行为将绑定属性强制设置为初始值 null,因此 DependencyProperty 被设置为具有双向绑定的默认值。

以下是 DependencyProperty 的回调代码。

private static void OnCloseRequestChanged(DependencyObject sender,
             DependencyPropertyChangedEventArgs e)
{
       Debug.Assert(e.NewValue == null || e.NewValue is bool?,
                           "Close Request must be nullable bool");
       if (e.OldValue == null && e.NewValue != null)
       {
             var newValue = (bool?)e.NewValue;
             sender.SetValue(CommandProperty, null);
             var window = sender as Window ?? sender.FindParent<Window>();
             if (window?.Owner != null && window?.DialogResult != newValue)
                    window.DialogResult = newValue;
             window?.Close();
       }
             else if (e.OldValue != null && e.NewValue != null)
       { //if did not find Window, then DataContext has changed
             sender.SetValue(CloseRequestProperty, null);
       }
}

最初有一个 Debug Assert,强制用户使用 Nullable<bool> 值进行绑定。首先,在 Window 关闭之前,将 CloseRequest 重置为 null,以便在 ViewModel 重用时将绑定值重置为 null。接下来,检查旧值和新值是否为 null 和非 null(由于 Assert,新值必须是布尔值 truefalse)。如果是这种情况,则将 DialogResult 强制设置为与 CloseRequestProperty 相同的值,前提是 WindowOwner 不为 null(要执行 ShowDialog 方法,必须设置 Owner 属性)。然后,如果新值不为 nullClose 未执行,则将 DependencyProperty 设置为 null。我未能立即更改此属性为 null,这可能是因为窗口已被关闭。最初强制设置为 null 可能是最好的,因为它对 ViewModel 的要求更少。

关闭命令

CloseCommand 使用一个非常标准的 ICommand 类型的 DependencyProperty,并带有一个回调。

private static void OnCommandChanged(DependencyObject sender,
     DependencyPropertyChangedEventArgs e)
{
       if (sender is Window)
       {
             SetOnClosing(sender, e, (Window) sender);
       }
       else
       {
             var frameworkElement = sender as FrameworkElement;
             Debug.Assert(frameworkElement != null,
                    "Did not find FrameworkElement for control " +
                           sender.GetType().Name);
             frameworkElement.Loaded += (s, arg) =>
              {
                    var window = sender as Window ?? sender.FindParent<Window>();
                    Debug.Assert(window != null,
                           "Did not find window for control " +                                                  sender.GetType().Name);
                    SetOnClosing(sender, e, window);
             };
       }
}

其中一个可以看到的现象是,如果 sender 是一个 Window,可以立即调用 SetOnClosing 方法,在 FrameworkElement 加载后会尝试查找 Window,然后就可以调用 SetOnClosing 方法了。

SetOnCLosing 方法设置一个方法来观察 Closing 事件,并且还为 Window 控件设置一个 DependencyProperty 来维护对具有该行为的 Control 的引用,以便 Window 可以访问具有该行为的 DependencyProperties。这对于执行 ICommand 是必需的。

private static void SetOnClosing(DependencyObject sender,
             DependencyPropertyChangedEventArgs e, Window window)
{
       SetOriginalControl(window, sender);
       window.Closing -= OnClosing;
       if (e.NewValue is ICommand) window.Closing += OnClosing;
}

Closing 事件的处理器然后必须获取对具有该行为的 Control 的引用,以获取 ICommand 引用。如果 ICommand 引用为 null,则意味着窗口已被关闭,这会导致 DependencyProperty 被重置为 null。如果 ICommand 不是 null,则可以在继续之前检查 ICommand 是否已启用。

private static void OnClosing(object sender, System.ComponentModel.CancelEventArgs e)
{
       var window = (Window)sender;
       var originalControl = GetOriginalControl(window) as DependencyObject;
       var command = GetCommand(originalControl);
       if (command == null) return; since the window has been closed =>
// so command has been set back to null
       if (command.CanExecute(closeRequest)) command.Execute(closeRequest);
       else e.Cancel = true;
 }

如果 ICommand 被禁用,则将 CancelEventArgsCancel 属性设置为 false,否则,执行 ICommand,窗口将自动关闭。

使用行为

该行为可能适用于任何控件,但我只将其用于 WindowUserControl。此外,可以设置 CloseRequestCloseRequestCloseCommand。我怀疑 CloseCommand 可以单独使用,但同样,我没有测试过这种情况。以下是同时设置两者的示例:

<UserControl x:Class="CloseBehaviorWPF.MainUserControl"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:closeBehaviorWpf="clr-namespace:CloseBehaviorWPF"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        closeBehaviorWpf:WindowCloseCommandBehaviour.CloseRequest="{Binding CloseWindow}"
        closeBehaviorWpf:WindowCloseCommandBehaviour.Command="{Binding ClosingCommand}"
        mc:Ignorable="d">

    <!—Content-->

</UserControl>

示例

该示例有一个主窗口,其中有两个按钮来打开两个不同的窗口:一个带有 ViewViewModel,它使用该行为的 CloseCommandCloseRequest;另一个只使用该行为的 CloseRequest。绑定到 CloseCommand 的子窗口有一个 CheckBox,需要勾选才能关闭子窗口。标记为 X Close 的按钮都使用一个触发 Window Close 方法来关闭窗口的事件。此外,主窗口还使用该行为来关闭主窗口。主窗口还能够知道子窗口何时关闭,以及窗口关闭时的 DialogResult。主窗口会显示一个 MessageBox 来响应此 DialogResult

历史

2016/02/02:初始版本

© . All rights reserved.