支持 WPF 窗口关闭的行为





5.00/5 (8投票s)
WPF MVVM 设计模式下从 ViewModel 关闭窗口存在问题。本文介绍了一种使用依赖属性来控制窗口关闭的方法。
引言
在使用 MVVM 模式创建 WPF 应用程序时,一个持续存在的问题是如何关闭窗口,以及如何将窗口关闭事件通知给 ViewModel
。在我们之前的项目中,我们使用了 MVVMLite 的 Messenger
来处理这类功能(我觉得我们过度使用了 Messenger)
。在开始一个新项目时(实际上并不是完全从零开始,因为已经有大量的基础代码),我再次遇到了这个问题。经过一番思考,我决定研究使用一个行为(behavior)来支持 View
和 ViewModel
之间的这种交互。我总是自己编写行为,并且没有使用过任何框架提供的行为支持,包括 Expression Blend。我从未遇到过我认为这些基础设施提供了真正需要的东西来解决我需要用行为解决的问题。在大多数情况下,你只需要一个 DependencyProperty
,而使用专门的行为会限制其在需要框架的项目中的使用。
第一部分实际上非常简单——将窗口关闭事件通知给 ViewModel
。只有当用户可以在不与 ViewModel
交互的情况下关闭窗口,并且 ViewModel
需要知道 Window
已关闭时,才需要这样做。这很容易通过 ICommand
接口来处理。该行为有一个 ICommand
类型的 DependencyProperty
。使用 ICommand
接口的好处是,可以检查 ICommand
的 CanExecute
方法,如果返回 false
,则可以取消关闭。
第二部分,也是最重要的一部分,是从 ViewModel
向 View
发送信号,指示应执行 Window
的 Close
方法。没有直接的方法可以做到这一点,但可以使用一个属性来发出信号,表明应该执行 Window
的 Close
方法。这可以通过一个 bool
类型来实现。这样做需要将这个标志重置,以防同一个 ViewModel
被再次用于另一个窗口。至少对于这个行为来说是这样,因为它不应该无故限制其灵活性。
该行为还支持 Nullable<bool>
类型的 DialogResult
。我这样做是因为有时返回一个标志来指示对话框是否保存了更改会很方便,比如在编辑对话框中,或者 UI 窗口有是/否或确定/取消的结果。这样,导致对话框显示的那个方法就可以放在一个 if 语句中,并且稍后不需要检查特殊的标志。为了支持这一点,该行为使用了一个 Nullable<bool>
来向行为发送关闭窗口的请求——null
是初始状态,绑定值被更改为 true
或 false
来设置窗口的 DialogResult
。行为处理完关闭请求的更改后,会将对话框结果设置为该值,并将关闭请求标志重置为 null,以便可以发出另一个关闭请求。
由于此行为也可以用于非 ShowDialog
方法打开的窗口,因此存在一个问题,因为当窗口使用 Show
方法打开时,无法设置 DialogResult
——这会引发异常。一个简单的检查方法是,在设置 DialogResult
之前,检查 Owner
属性是否不为 null
。
该行为的一个不寻常的特点是它不必与 Window
相关联,而是可以应用于 Window
内的 FrameworkElement
,包括 UserControl
。如果 FrameworkElement
不是 Window
,则使用 VisualTreeHelper
来查找父 Window
。这样做的原因是提高可见性。在我的项目中,有一些标准的窗口被用作提供特定功能的 UserContorls 的容器。我的想法是,如果行为在窗口中指定,那么未来的程序员在处理代码时可能会比行为在 UserControl
中遇到更多困难。
关闭请求
用于信号以关闭窗口和设置 DialogResult
的 DependencyProperty
是 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
被多次用于不同的窗口,属性将是最后一个设置的值 true
或 false
,而不是 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
,新值必须是布尔值 true
或 false
)。如果是这种情况,则将 DialogResult
强制设置为与 CloseRequestProperty
相同的值,前提是 Window
的 Owner
不为 null
(要执行 ShowDialog
方法,必须设置 Owner
属性)。然后,如果新值不为 null
且 Close
未执行,则将 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
被禁用,则将 CancelEventArgs
的 Cancel
属性设置为 false
,否则,执行 ICommand
,窗口将自动关闭。
使用行为
该行为可能适用于任何控件,但我只将其用于 Window
和 UserControl
。此外,可以设置 CloseRequest
或 CloseRequest
和 CloseCommand
。我怀疑 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>
示例
该示例有一个主窗口,其中有两个按钮来打开两个不同的窗口:一个带有 View
和 ViewModel
,它使用该行为的 CloseCommand
和 CloseRequest
;另一个只使用该行为的 CloseRequest
。绑定到 CloseCommand
的子窗口有一个 CheckBox
,需要勾选才能关闭子窗口。标记为 X 和 Close 的按钮都使用一个触发 Window
Close
方法来关闭窗口的事件。此外,主窗口还使用该行为来关闭主窗口。主窗口还能够知道子窗口何时关闭,以及窗口关闭时的 DialogResult
。主窗口会显示一个 MessageBox
来响应此 DialogResult
。
历史
2016/02/02:初始版本