在 MVVM 中实现对话框






4.82/5 (27投票s)
为 MVVM 应用程序添加全面的对话框支持
引言
任何 MVVM 纯粹主义者都会告诉你,在保持良好的关注点分离方面,对话框尤其成问题。对话框的历史可以追溯到 Windows 的早期版本,它们是良好架构设计的完全对立面。与 WPF 强大的数据绑定机制和实现视图模型与视图完全分离的能力不同,模态对话框是通过直接调用操作系统来调用的,此时与应用程序业务层的持续通信变得混乱,单元测试几乎不可能。
尽管存在问题,对话框仍然是现代程序员工具箱中的宝贵补充。它们为我们提供了一种方便的方法来显示弹出通知,提供常见应用程序任务的封装实现,并公开一组标准化接口以简化用户与我们应用程序的交互。
网上有许多资源展示了 MVVM/对话框问题的部分解决方案。有些采用控制反转来向服务提供商公开接口,有些则诉诸自定义 WPF 弹出窗口,还有一些完全抛弃了关注点分离,直接在代码背后硬编码解决方案。在本文中,我将探讨在 WPF/MVVM 应用程序中尝试显示对话框时遇到的问题,并提供一个解决方案,避免了其他技术中出现的许多问题。
任何全面的 MVVM 对话框解决方案,至少必须实现以下目标:
- 良好的关注点分离。更具体地说,对话框必须严格遵守 MVVM 架构模式,并由一个视图和通过 WPF 数据绑定机制绑定在一起的相应视图模型组成。如果项目需要,视图和视图模型也必须能够驻留在单独的项目中。
- 必须支持适当的顶级窗口对话框,而不是依赖于试图模仿真实对话框行为的自定义窗口解决方案。
- 必须提供完全控制自定义对话框视觉外观所有方面的能力,只要顶级 WPF 窗口允许。
- 必须支持模态和非模态对话框。
- 必须支持标准操作系统对话框(即
MessageBox
)以及常见的对话框,如OpenFile
/SaveFile
等。还必须支持第三方对话框框架。 - 必须支持任意数量的对话框同时显示在屏幕上,包括模态、非模态、系统/通用和第三方对话框的任何和所有组合。对话框还必须允许在不破坏 SOC 的情况下调用其他对话框的创建,包括作为其正常关闭过程的一部分创建确认/验证对话框。
- 必须允许对视图模型进行全面的单元测试,包括对依赖注入和模拟等相关技术的支持。
- 在应用程序层面必须非常易于使用。
本文的大部分内容都在论证以所选方式实现对话框的合理性,以实现所有这些目标,同时规避沿途出现的各种技术挑战。虽然对于对话框这样一个简单的功能来说,它可能被认为是过度设计,但一旦到位,它相对简单易行,而且非常容易使用。对底层机制的全面理解是有帮助的,但实际上并不是将对话框支持添加到您的项目所必需的;如果您的主要兴趣在于实际实现,那么我鼓励您直接跳到本文的最后一节(“MVVM 对话框的实际应用”)并查看在实际 MVVM 应用程序中使用对话框的具体示例。
打开对话框
让我们从最基本的示例开始:显示一个自定义对话框。MVVM 是一种高度数据驱动的架构,如果我们要遵循其架构哲学,那么我们的第一次尝试可能涉及向主视图模型添加一个带有更改通知的布尔属性
public class MainViewModel : ViewModelBase
{
private bool _DialogVisible = false;
public bool DialogVisible
{
get { return this._DialogVisible; }
set
{
if (this._DialogVisible != value)
{
this._DialogVisible = value;
RaisePropertyChanged(() => this.DialogVisible);
}
}
}
// etc.
当设置为 true
时,此属性应调用在屏幕上创建对话框。实现此功能并不太困难:我们可以简单地为我们的主窗口创建一个附加行为并将其绑定到此成员。当我们检测到该属性已设置为 true
时,我们创建对话框
public static class DialogBehavior
{
public static readonly DependencyProperty DialogVisibleProperty =
DependencyProperty.RegisterAttached(
"DialogVisible", typeof(bool), typeof(DialogBehavior),
new PropertyMetadata(false, OnDialogVisibleChange));
public static void SetDialogVisible(DependencyObject source, bool value)
{
source.SetValue(DialogVisibleProperty, value);
}
public static bool GetDialogVisible(DependencyObject source)
{
return (bool)source.GetValue(DialogVisibleProperty);
}
private static void OnDialogVisibleChange
(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var parent = d as Window;
if (parent == null)
return;
// when DialogVisible is set to true we create a new dialog
// box and set its DataContext to that of its parent
if ((bool)e.NewValue == true)
{
new CustomDialogWindow {DataContext = parent.DataContext}.ShowDialog();
}
}
}
要使用此行为,我们只需将 DialogVisible
附加属性绑定到主视图模型中的 DialogVisible
属性
<Window x:Class="MyDialogApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:behaviors="clr-namespace:MyDialogApp.Behaviors"
behaviors:DialogBehavior.DialogVisible="{Binding DialogVisible}"
现在将主视图模型的 DialogVisible
属性设置为 true
将导致 CustomDialogWindow
的实例立即作为模态对话框出现。
以这种方式实现对话框当然有些局限。最明显的问题是它只允许创建一种类型的对话框;对于每一种额外的类型,我们都需要创建一个单独的布尔标志。还有一个如何显示动态内容的问题;在没有任何其他架构功能的情况下,我们的对话框视图需要直接绑定到主视图模型中的属性。
为了论证起见,让我们假设我们一次只需要显示一个对话框。如果我们可以保证这一点,至少目前如此,那么为我们希望显示的每种对话框类型创建一个单独的视图模型,并将我们的布尔属性替换为类型为“object
”的泛型属性会更有用。为了使其工作,我们的附加行为现在必须提供我们每个对话框视图模型类型与在设置时必须创建的实际视图类之间的映射。在 WPF 框架内,此类映射通常通过数据模板实现。
数据模板:几乎是一个解决方案
乍一看,数据模板为我们提供了一个明显的机制,通过它可以在对话框视图模型及其相应视图之间进行映射,如果不是因为一个小问题:至少从技术上讲,顶级窗口不能构成数据模板的内容。假设我们决定通过以下方式实现我们的映射
<DataTemplate DataType="{x:Type local:DialogBoxViewModel}">
<local:DialogBoxWindow />
</DataTemplate>
如果 DialogBoxWindow
是一个顶级窗口类,则此代码将导致编译时错误。由于这是一个 XAML 错误,它实际上不会中断编译,并且在运行时通过调用 DataTemplate
的 LoadContent()
函数尝试创建此类的实例实际上会正常工作。也就是说,尽管如此,错误仍然存在,因为我们正在以数据模板未设计的方式使用它们。模板旨在表示子内容;顶级窗口是容器,而不是内容。
一种可能性是为所有对话框使用一个通用的顶级窗口,并根据视图模型的类型使用数据模板来填充该窗口的内容。这样做的问题是,我们不一定希望所有对话框窗口都采用相同的样式。有些可能需要调整大小,有些我们可能希望相对于父级出现,几乎所有都需要不同的初始大小和标题。我们可以使用常规数据绑定在我们的视图中提供所需的灵活性,但这将需要在我们的视图模型中增加额外的复杂性来设置样式参数,而我们实际上应该能够直接在 XAML 中设置它们。
与其尝试将对话框窗口声明为模板内容,不如将其视为容器本身,完全替换数据模板。要了解这如何工作,有助于更仔细地查看数据模板如何实现其功能。DataTemplate
类是可以在没有 x:Key
参数的情况下声明的几个类之一:它们提供的是一个 DataType
属性,该属性映射到相应的视图模型,并且使用 DictionaryKeyAttribute
属性将该属性设置为键。换句话说,视图模型类类型成为其自己的键,用于指定其相应视图内容的 DataTemplate
。
唉,由于 WPF 框架中的一个错误,此机制无法在我们的情况下使用,该错误阻止 DictionaryKeyAttribute
与自定义类一起工作。此错误于 2008 年首次报告,随后得到 Microsoft 确认,并且在撰写本文时仍存在于代码库中。然而,DictionaryKeyAttribute
对于 DataTemplate
的目的只是为指定资源键提供语法糖:没有什么可以阻止我们简单地在 XAML 中明确指定键。在这种情况下,我们像创建任何其他 WPF 窗口一样创建对话框,然后使用视图模型类型作为键在应用程序 XAML 中声明它的实例
<local:DialogBoxWindow x:Key="{x:Type local:DialogBoxViewModel}" x:Shared="False" />
请注意,此代码还将“x:Shared
”属性设置为“False
”。默认情况下,从 ResourceDictionary
索引的资源不是唯一的,每次请求特定资源的实例时,您都会获得对同一对象的引用。将“x:Shared
”属性设置为 False
会覆盖此行为,并导致每次请求该资源时都实例化对话框类的新实例。
此时,我们的行为类拥有它所需的一切,可以获取一个视图模型,并使用它来索引全局资源字典,实例化相应对话框窗口类的新实例,并将其自身设置为视图的 DataContext
。首先,我们将附加属性的名称更改为“DialogBox
”,并将其类型设置为“object
”。当设置为视图模型的实例时,我们让它在全局资源中查找该键,并创建关联类型的窗口
public static class DialogBehavior
{
public static readonly DependencyProperty DialogBoxProperty =
DependencyProperty.RegisterAttached(
"DialogBox",
typeof(object),
typeof(DialogBehavior),
new PropertyMetadata(null, OnDialogBoxChange));
public static void SetDialogBox(DependencyObject source, object value)
{
source.SetValue(DialogBoxProperty, value);
}
public static object GetDialogBox(DependencyObject source)
{
return (object)source.GetValue(DialogBoxProperty);
}
private static void OnDialogBoxChange
(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var parent = d as Window;
if (parent == null)
return;
// create an instance of the dialog box window that is keyed to this view model
if (e.NewValue == null)
return;
var resource = Application.Current.TryFindResource(e.NewValue.GetType());
if (resource == null)
return;
if (resource is Window)
{
(resource as Window).DataContext = e.NewValue;
(resource as Window).ShowDialog();
}
}
}
我们的视图模型也需要更新一个类型为 object
的新属性
private object _DialogBox = null;
public object DialogBox
{
get { return this._DialogBox; }
set
{
if (this._DialogBox != value)
{
this._DialogBox = value;
RaisePropertyChanged(() => this.DialogBox);
}
}
}
当然,XAML 中的附加属性绑定需要更新以反映新的属性名称
behaviors:DialogBehavior.DialogBox="{Binding DialogBox}"
现在创建对话框涉及创建视图模型的一个实例并将其分配给主视图模型的属性
this.DialogBox = new CustomDialogBoxViewModel(); // dialog box appears here
下一个需要解决的明显问题是同时显示多个对话框的要求,为此我们需要将视图模型属性切换到集合。
多个对话框
虽然我尚未解决关闭对话框的问题,但很明显,关闭对话框可以像关闭常规数据模板子内容一样实现,即,将相关视图模型属性设置回 null
。这样做的明显后果是,如果在一个对话框已经打开的情况下尝试打开第二个对话框,则会导致第一个对话框关闭。为了同时显示多个对话框,我们需要维护一个视图模型列表,我们可以监控其更改。幸运的是,WPF 为我们提供了完美的工具来做到这一点:ObservableCollection
。
ObservableCollection
是一个具有集合更改通知支持的项集合,这意味着我们可以让附加行为挂接到更改事件并相应地响应。如果将视图模型添加到集合中,则我们的附加行为可以为其创建一个对话框视图。同样,如果该视图模型被移除,则其对话框可以自动关闭。
一个真实的应用程序很可能需要在整个应用程序中创建对话框,但是对话框只能由像我们的主窗口这样的顶级窗口“拥有”。在实际应用程序中,您通常会创建一个单一的集合并将其或某种类型的接口公开给应用程序的其余部分,可能通过使用依赖注入框架。此类架构问题超出了本文的范围,我将其作为读者的练习,与此同时,我只是将 ObservableCollection
作为公开属性添加到我的演示应用程序的主视图模型中
private ObservableCollection<IDialogViewModel> _Dialogs =
new ObservableCollection<IDialogViewModel>();
public ObservableCollection<IDialogViewModel> Dialogs { get { return _Dialogs; } }
接下来,我们需要在我们的附加行为中创建一个相应的依赖属性,该属性将绑定到此集合
public static readonly DependencyProperty DialogViewModelsProperty =
DependencyProperty.RegisterAttached(
"DialogViewModels",
typeof(object),
typeof(DialogBehavior),
new PropertyMetadata(null, OnDialogViewModelsChange));
public static void SetDialogViewModels(DependencyObject source, object value)
{
source.SetValue(DialogViewModelsProperty, value);
}
public static object GetDialogViewModels(DependencyObject source)
{
return source.GetValue(DialogViewModelsProperty);
}
现在,我们通过向主应用程序窗口的 XAML 添加适当的代码来创建绑定本身
xmlns:dlgs="clr-namespace:MvvmDialogs.Behaviors;assembly=MvvmDialogs"
dlgs:DialogBehavior.DialogViewModels="{Binding Dialogs}"
最后,我们需要在我们的附加行为中添加代码,以便每当新的视图模型添加到我们的集合时就创建一个新的对话框。执行此操作的完整代码(即 DialogBehavior
中的 OnDialogViewModelsChange
方法)由于本文中进一步描述的附加功能而有些复杂,但基本行为是订阅集合更改通知,以便每当视图模型实例添加到集合时就创建一个新的对话框窗口
if (!ChangeNotificationHandlers.ContainsKey(parent))
ChangeNotificationHandlers[parent] = (sender, args) =>
{
var collection = sender as ObservableCollection<IDialogViewModel>;
if (collection != null)
{
if (args.Action == NotifyCollectionChangedAction.Add)
{
if (args.NewItems != null)
foreach (IDialogViewModel viewModel in args.NewItems)
{
AddDialog(viewModel, collection, d as Window);
}
}
}
};
现在我们拥有了轻松显示新对话框所需的一切,包括模态和非模态对话框,只需创建相关的视图模型并将其添加到对话框集合属性即可
this.Dialogs.Add(new CustomDialogBoxViewModel()); // dialog box appears here
只要我们也按照本文前面讨论的方式添加了视图模型和视图之间的映射,就会创建正确的窗口类并将其绑定到视图模型。
关闭对话框
对话框打开后,我们需要一种关闭它们的方法,但实现此目的的最佳方法并不像最初看起来那样简单。问题在于,在 Windows 应用程序中,有几种不同的方式可以关闭对话框。
对于无模式对话框,方法应该很明显:只需将它们从主视图模型的对话框集合中删除即可。我们的附加行为最初是为了响应视图模型添加到对话框集合而创建对话框的,因此,它在视图模型随后被移除时也应该负责关闭它,这似乎是完全合理的。
然而,模态对话框略有不同,因为它们通常有一个或多个用于关闭它们的按钮,即“确定”、“取消”等。在 MVVM 中,这些按钮通常会绑定到视图模型中的命令,因此第一个要解决的问题是视图模型是否应该获得对集合的引用,以便它可以响应这些命令从列表中删除自己。或者,视图模型是否应该始终在希望关闭时通知其父级,并依赖该父级进行实际移除?或者视图模型是否应该以某种方式通知行为它希望关闭,并依赖它来完成繁重的工作,从而将该任务从自身及其父级中解脱出来?毕竟,行为知道视图模型添加到的集合。
使问题进一步复杂化的是,有时行为需要启动关闭操作,最常见的例子是用户按下对话框标题栏上的取消按钮。通常,我们可以在代码背后添加处理程序来检测这些情况,但由于我们正在以 MVVM 方式行事,因此我们需要在行为中将通知处理程序附加到窗口,并在操作系统请求关闭时通知视图模型,以便它能够确定是否允许关闭。
为了为我们的对话框添加灵活的关闭行为,我们需要向基础用户对话框视图模型添加两个额外的成员
void RequestClose();
event EventHandler DialogClosing;
RequestClose
方法用于向对话框发出信号,表明我们希望它关闭。任何东西都可以调用此成员,包括其他视图模型,但我们的附加行为使用它来发出标题栏关闭按钮已被按下的信号。
我们对话框视图模型中 RequestClose()
的默认实现应该是引发 DialogClosing
事件,该事件反过来会向所有其他视图模型(包括其他视图模型)发出对话框正在关闭的信号。我们的附加行为可以订阅此事件,并在检测到时关闭相应的视图。在标题栏关闭按钮被按下的特殊情况下,该行为会检查此事件是否是响应 RequestClose
调用而引发的;如果不是,则该行为可以向操作系统发出信号以取消窗口关闭操作。这允许视图模型提供自定义行为,以确定是否允许自身关闭,例如,确认对话框或数据验证失败时的取消。
此功能还处理对话框希望自行关闭的情况。如果对话框有一个“确定”按钮,并且命令处理程序引发 DialogClosing
事件,则该行为将自动将其检测为关闭请求并响应,将其从集合中删除。此操作将导致与主视图模型或任何其他实体删除它时相同的行为:附加行为将检测到 CollectionChanged
事件并关闭对话框视图窗口。
执行流控制
让我们暂时绕道一下,检查使用本文描述的技术处理对话框时执行树会发生什么。
在“常规”Windows 代码中,创建对话框会有效地暂停调用它的代码的执行。实际上,真正发生的是 Windows 内部启动了一个单独的消息泵,以便应用程序消息继续传递,而不会导致整个应用程序锁定。在我们的案例中,我们正在使用附加行为来响应集合更改通知创建对话框,但在模态对话框的情况下,对调用函数执行的影响是相同的
this.Dialogs.Add(new CustomDialogBoxViewModel()); // dialog box appears here
// execution resumes here when the dialog box closes
幸运的是,WPF 数据绑定是多线程的,所以如果这种行为是一个问题,那么没有什么可以阻止您在单独的线程中进行赋值,以避免阻塞调用函数
Task.Run(() =>
{
lock (this.Dialogs)
this.Dialogs.Add(new CustomDialogBoxViewModel());
});
// code on the calling thread continues here immediately
此行为的另一个重要后果是,在正常情况下,模态对话框在其视图在屏幕上可见时不会收到通知。模态对话框可以在其构造函数中执行任何它喜欢的初始化,并且可以公开父类需要调用的任何附加函数,但它实际上直到被添加到 Dialogs
集合后才变得可见,此时执行会有效地暂停,直到对话框关闭。实际上,这意味着任何需要为后台任务或进度更新等创建的工作线程都必须在视图模型分配给对话框集合之前创建。实际上,这很少成为问题,毕竟视图只是视图模型的松散绑定表示,但如果您绝对必须知道对话框窗口本身何时在屏幕上可见,那么您将需要通过其他方式获取此通知。在 MVVM 中,这可以通过向窗口类添加交互触发器来实现,以响应适当的窗口事件调用视图模型中的命令
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<cmd:EventToCommand Command="{Binding LoadedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
与系统对话框和第三方框架集成
任何通用的对话框框架都必须能够支持不遵守 MVVM 架构模式的对话框。这包括像 MessageBox
这样的系统对话框,像 OpenFileDialog
/PrintDialog
这样的通用对话框,以及由第三方库(例如非常好的 Ookii Dialogs 包)提供的对话框。
与此架构中的所有其他对话框类型一样,添加对外部对话框的支持必须从创建视图模型开始。对于通用对话框,理论上可以使用 WPF 对话框类作为它们自己的视图模型,但这样做必然意味着破坏关注点分离,并要求视图模型引用它原本不需要的 Windows 库。我选择解决这个问题的方法是通过 IDialogPresenter<>
泛型。
IDialogPresenter<>
继承 IDialogViewModel
接口,并在视图模型和要显示的实际对话框之间形成“粘合剂”(如果你喜欢这样说的话)。它的唯一方法 Show()
接受类型化视图模型的实例,并负责使用适当的参数显示对话框。它还负责使用对话框可能返回的任何结果填充视图模型。视图模型和对话框属性之间的映射如何在实践中实现是一个实现问题,我留给读者,示例项目中的示例只是明确地复制了每个字段
public class OpenFileDialogPresenter : IDialogBoxPresenter<OpenFileDialogViewModel>
{
public void Show(OpenFileDialogViewModel vm)
{
var dlg = new OpenFileDialog();
dlg.Multiselect = vm.Multiselect;
dlg.ReadOnlyChecked = vm.ReadOnlyChecked;
dlg.ShowReadOnly = vm.ShowReadOnly;
// ... etc ...
var result = dlg.ShowDialog();
vm.Result = (result != null) && result.Value;
vm.Multiselect = dlg.Multiselect;
vm.ReadOnlyChecked = dlg.ReadOnlyChecked;
vm.ShowReadOnly = dlg.ShowReadOnly;
// ... etc ...
}
}
可以说,使用 IDialogPresenter<>
通过依赖视图模型和实际上是视图而破坏了关注点分离,我倾向于同意这一点。然而,我也会争辩说,鉴于对话框实现本身不遵循 MVVM 模式,这是不可避免的;在没有外部对话框代码本身使用的某种绑定机制的情况下,可能没有令人满意的方法完全规避这一点。然而,IDialogPresenter
确实尝试做的是隔离这种反模式并最小化对代码库其余部分的任何潜在影响。
与用户对话框一样,对话框演示器需要在视图模型和负责显示视图的对话框演示器之间进行映射,并且像以前一样,我们可以在 XAML 资源中通过声明演示器实例来实现,这些实例以其相应视图模型的类型为键
<local:MessageBoxPresenter x:Key="{x:Type vm:MessageBoxViewModel}" />
<local:OpenFileDialogPresenter x:Key="{x:Type vm:OpenFileDialogViewModel}" />
现在创建第三方对话框与创建无模式和模态对话框没有区别:只需创建相关视图模型的实例并将其添加到 Dialogs
集合。
单元测试
MVVM 的主要优点之一是它允许我们轻松地对业务逻辑和前端行为进行单元测试,而无需实际创建视图本身。实现商业质量的单元测试超出了本文的范围,但本文提供的演示项目确实包含了对主应用程序中创建的无模式和模态对话框行为的基本测试。
要了解单元测试如何与此系统配合工作,选择一个测试并逐行进行是很有帮助的。单元测试通常旨在测试每个测试的单个行为方面,因此它们被称为“单元”测试。我选择的测试旨在测试当用户尝试通过按下“取消”按钮关闭无模式对话框时出现的对话框的行为。当这种情况发生时,我们希望无模式对话框弹出一个消息框,询问用户是否真的要关闭无模式对话框,这就是我们将要测试的行为。我们从声明测试函数开始
[TestMethod]
[Description("Tests that clicking the 'Cancel' button on the modeless dialog
requests confirmation from the user that they actually want to close the dialog.")]
public void Modeless_Cancel_Requests_Confirmation()
{
// ... etc ...
}
此测试的设置代码需要创建我们的主视图模型实例以及一个无模式对话框
// == Arrange ==
var mainViewModel = new MainViewModel();
mainViewModel.NewModelessDialogCommand.Execute(null);
var dlg = mainViewModel.Dialogs[0] as CustomDialogViewModel;
在实际的单元测试中,我们通常不会像这样创建 MainViewModel
类的完整实例。我们最可能做的是创建一个模拟对象,该对象仅提供使无模式对话框出现所需的功能,我们将使用依赖注入框架或类似的东西获取此模拟对象的实例。然而,此演示中的视图模型非常简单,因此我们可以直接使用它们。
上面的代码应该非常不言自明:我们创建一个视图模型,然后通过直接调用其 Execute
函数来执行 NewModelessDialogCommand
,从而模拟用户单击已将 Command
属性绑定到它的按钮时发生的情况。我们的 MainViewModel
应该通过创建 CustomDialolgViewModel
的实例并将其添加到其 Dialogs
集合中来响应,但项目中的另一个测试对此进行了测试,因此在此测试中,我们可以假定它存在。
测试的下一部分包含我们的操作。在此测试中,我们正在测试当用户按下“取消”按钮时会发生什么,因此我们通过直接调用 CancelCommand
的 Execute
方法来模拟它
// == Act ==
dlg.CancelCommand.Execute(null);
如果我们的视图模型代码工作正常,那么我们现在应该已经创建了两个对话框:第一个应该是我们在设置代码中创建的无模式对话框,而第二个应该是要求用户确认的 MessageBox
。我们当然不会实际看到这些对话框,因为我们的单元测试框架不创建视图,但代表这些对话框的视图模型应该存在于主视图模型的对话框集合中。剩下要做的就是检查是否已添加了具有预期属性值的适当 MessageBoxViewModel
// == Assert ==
mainViewModel.Dialogs.Count.ShouldEqual(2);
var msg_dlg = mainViewModel.Dialogs[1] as MessageBoxViewModel;
msg_dlg.ShouldNotBeNull();
msg_dlg.Caption.ShouldEqual("Closing");
msg_dlg.Message.ShouldEqual("Are you sure you want to close this window?");
msg_dlg.Buttons.ShouldEqual(MessageBoxButton.YesNo);
msg_dlg.Image.ShouldEqual(MessageBoxImage.Question);
MVVM 对话框的实际应用
在本节中,我将总结文章中讨论的所有内容,并展示如何为任何 MVVM 应用程序添加对话框支持。
首先,您需要将 MvvmDialogs
项目或其文件添加到您自己的解决方案中,以获取各种接口以及行为类。接下来,您需要向您的主视图模型添加一个 ObservableCollection<IDialogViewModel>
,该集合将负责维护对话框视图模型的集合
private ObservableCollection<IDialogViewModel> _Dialogs =
new ObservableCollection<IDialogViewModel>();
public ObservableCollection<IDialogViewModel> Dialogs { get { return _Dialogs; } }
最后,您需要将 DialogBehaviour.DialogViewModels
附加行为添加到您的主窗口类,并将其绑定到您的 Dialogs
集合
dlgs:DialogBehavior.DialogViewModels="{Binding Dialogs}"
我们现在可以开始创建和显示对话框了。我们可以创建的最简洁的自定义对话框是继承 IUserDialogViewModel
的类
public class MinimalDialogViewModel : IUserDialogViewModel
{
public virtual bool IsModal { get { return true; } }
public virtual void RequestClose() { this.DialogClosing(this, null); }
public virtual event EventHandler DialogClosing;
}
在这个最小的例子中,我们希望对话框本身是模态的,并且如果任何东西请求它关闭(即用户单击标题栏上的“关闭”按钮),那么我们就引发 DialogClosing
事件以实际导致窗口关闭。将这些属性设为虚属性不是强制性的,但如果我们希望此类别构成应用程序中所有其他对话框的基础,并允许任何派生类覆盖此默认行为,则它很有用。
视图模型逻辑完成后,我们需要为视图创建一个相应的 Window,该视图将在屏幕上显示对话框
<Window x:Class="MvvmDialogs.Demo.View.MinimalDialogBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Minimal Dialog Box" Height="160" Width="300">
</Window>
最后,我们需要某种方法将视图模型映射到此视图。在处理子控件时,我们通常使用数据模板来完成此操作,但由于我们正在处理顶级窗口,因此我们改为创建 Window
类型的资源,用 x:Shared
附加属性修饰它,并使用视图模型类型作为键
<view:MinimalDialogBox x:Key="{x:Type vm:MinimalDialogViewModel}" x:Shared="False" />
我们现在创建了一个极简的对话框,只需将视图模型的实例添加到主视图模型中的 Dialogs
集合即可随时显示
this.Dialogs.Add(new MinimalDialogViewModel()); // modal dialog box appears
显示系统对话框(MessageBox
等)涉及类似的机制,但需要创建对话框演示器而不是窗口。此类需要继承 IDialogPresenter<>
泛型并实现“Show
”属性,以使用视图模型中的参数调用对话框。本文提供的演示应用程序展示了使用 IDialogPresenter
显示消息框的示例
public class MessageBoxPresenter : IDialogBoxPresenter<MessageBoxViewModel>
{
public void Show(MessageBoxViewModel vm)
{
vm.Result = MessageBox.Show(vm.Message, vm.Caption, vm.Buttons, vm.Image);
}
}
与常规对话框一样,我们需要某种方法将此类与其相应的视图模型关联起来,我们再次通过使用视图模型类型作为键的全局资源来实现这一点
<local:MessageBoxPresenter x:Key="{x:Type vm:MessageBoxViewModel}" />
(请注意,对话框演示器不需要 x:Shared
属性)。有了这个映射,我们现在可以轻松地从应用程序中的任何位置调用消息框
this.Dialogs.Add(new MessageBoxViewModel { Message = "Hello World!" }); // message box appears
我们可以通过添加 Show()
方法使此设计更适合方法级联。以下示例演示了如何使用此方法显示“文件打开”对话框,并使用消息框向用户报告结果
var dlg = new OpenFileDialogViewModel {
Title = "Select a file (I won't actually do anything with it)",
Filter="All files (*.*)|*.*",
Multiselect=false
};
if (dlg.Show(this.Dialogs))
new MessageBoxViewModel {
Message = "You selected the following file: " + dlg.FileName + "."
}.Show(this.Dialogs);
else
new MessageBoxViewModel { Message = "You didn't select a file." }.Show(this.Dialogs);
在实际应用程序中,您可能希望用对依赖注入框架的调用替换 new 运算符,以为您创建实例,该框架还将负责将对话框集合注入视图模型,这样您就不必将其作为参数传递到 Show
方法中。
本文的最后一个示例展示了不同的对话框协同工作。MVVM 对话框库中的行为类旨在允许在其他对话框正在关闭的同时创建新对话框。此代码片段利用该功能显示一个模态对话框,如果用户尝试通过标题栏上的关闭按钮关闭它,则会弹出一个确认消息框。父无模式对话框只有在用户在确认框中选择“确定”时才会关闭
new CustomDialogViewModel {
Message = "Hello World!",
Caption = "Modal Dialog Box",
OnCloseRequest = (sender) =>
{
if (new MessageBoxViewModel {
Caption = "Closing",
Message = "Are you sure you want to close this window?",
Buttons = MessageBoxButton.YesNo,
Image = MessageBoxImage.Question
}.Show(this.Dialogs) == MessageBoxResult.Yes)
sender.Close();
}
}.Show(this.Dialogs);
历史
- 2014年9月18日:初始版本