不幸的是,没有一个伟大的 MVVM 示例应用程序可以做所有事情,并且有很多不同的方法来做事。首先,您可能想熟悉其中一个应用程序框架(Prism 是一个不错的选择),因为它们为您提供了方便的工具,如依赖注入、命令、事件聚合等,可以轻松尝试适合您的不同模式.
棱镜释放:
http://www.codeplex.com/CompositeWPF
它包括一个相当不错的示例应用程序(股票交易员)以及许多较小的示例和操作方法。至少它很好地展示了人们用来使 MVVM 实际工作的几种常见子模式。我相信他们有 CRUD 和对话的例子。
Prism 不一定适用于每个项目,但熟悉它是件好事。
CRUD:
这部分非常简单,WPF 两种方式的绑定使得编辑大多数数据变得非常容易。真正的诀窍是提供一个易于设置 UI 的模型。至少您要确保您的 ViewModel(或业务对象)实现 INotifyPropertyChanged 以支持绑定,并且您可以将属性直接绑定到 UI 控件,但您可能还希望实现 IDataErrorInfo 以进行验证。通常,如果您使用某种 ORM 解决方案,则设置 CRUD 很容易。
本文演示了简单的 crud 操作:
http://dotnetslackers.com/articles/wpf/WPFDataBindingWithLINQ.aspx
它是基于 LinqToSql 构建的,但这与示例无关 - 重要的是您的业务对象实现 INotifyPropertyChanged(由 LinqToSql 生成的类执行)。 MVVM 不是该示例的重点,但我认为在这种情况下它并不重要。
本文演示数据验证
http://blogs.msdn.com/wpfsdk/archive/2007/10/02/data-validation-in-3-5.aspx
同样,大多数 ORM 解决方案生成的类已经实现了IDataErrorInfo,并且通常提供一种机制来轻松添加自定义验证规则。
大多数情况下,您可以获取由某个 ORM 创建的对象(模型)并将其包装在一个 ViewModel 中,该 ViewModel 包含它和用于保存/删除的命令 - 您已准备好将 UI 直接绑定到模型的属性。
视图看起来像这样(ViewModel 有一个属性 Item 保存模型,就像在 ORM 中创建的类):
<StackPanel>
<StackPanel DataContext=Item>
<TextBox Text="{Binding FirstName, Mode=TwoWay, ValidatesOnDataErrors=True}" />
<TextBox Text="{Binding LastName, Mode=TwoWay, ValidatesOnDataErrors=True}" />
</StackPanel>
<Button Command="{Binding SaveCommand}" />
<Button Command="{Binding CancelCommand}" />
</StackPanel>
对话框:
对话框和 MVVM 有点棘手。我更喜欢在对话框中使用 Mediator 方法的风格,您可以在 StackOverflow 问题中了解更多信息:
WPF MVVM dialog example
我常用的做法,不是很经典的MVVM,可以总结如下:
对话框 ViewModel 的基类,它公开提交和取消操作的命令,让视图知道对话框已准备好关闭的事件,以及所有对话框中需要的任何其他内容。
对话框的通用视图 - 这可以是一个窗口,也可以是一个自定义的“模态”覆盖类型控件。它的核心是我们将视图模型转储到的内容呈现器,它处理关闭窗口的连接 - 例如,在数据上下文更改时,您可以检查新的 ViewModel 是否从您的基类继承,如果是,订阅相关的关闭事件(处理程序将分配对话结果)。如果您提供替代的通用关闭功能(例如 X 按钮),您应该确保也在 ViewModel 上运行相关的关闭命令。
您需要为您的 ViewModel 提供数据模板的地方,它们可能非常简单,尤其是因为您可能有一个封装在单独控件中的每个对话框的视图。 ViewModel 的默认数据模板将如下所示:
<DataTemplate DataType="{x:Type vmodels:AddressEditViewModel}">
<views:AddressEditView DataContext="{Binding}" />
</DataTemplate>
对话框视图需要访问这些,否则它将不知道如何显示 ViewModel,除了共享对话框 UI 之外,它的内容基本上是这样的:
<ContentControl Content="{Binding}" />
隐式数据模板会将视图映射到模型,但谁启动它?
这是不那么 mvvm 部分。一种方法是使用全局事件。我认为更好的做法是使用通过依赖注入提供的事件聚合器类型设置 - 这样事件对于容器来说是全局的,而不是整个应用程序。 Prism 使用统一框架进行容器语义和依赖注入,总体来说我还是挺喜欢 Unity 的。
通常,根窗口订阅此事件是有意义的 - 它可以打开对话框并将其数据上下文设置为通过引发事件传入的 ViewModel。
以这种方式设置允许 ViewModel 要求应用程序打开一个对话框并在其中响应用户操作,而无需了解有关 UI 的任何信息,因此在大多数情况下 MVVM 保持完整。
但是,有时 UI 必须弹出对话框,这会使事情变得有点棘手。例如,如果对话框位置取决于打开它的按钮的位置。在这种情况下,您需要在请求打开对话框时提供一些特定于 UI 的信息。我通常会创建一个单独的类来保存 ViewModel 和一些相关的 UI 信息。不幸的是,那里的一些耦合似乎是不可避免的。
一个按钮处理程序的伪代码,它引发一个需要元素位置数据的对话框:
ButtonClickHandler(sender, args){
var vm = DataContext as ISomeDialogProvider; // check for null
var ui_vm = new ViewModelContainer();
// assign margin, width, or anything else that your custom dialog might require
...
ui_vm.ViewModel = vm.SomeDialogViewModel; // or .GetSomeDialogViewModel()
// raise the dialog show event
}
对话框视图将绑定到位置数据,并将包含的 ViewModel 传递给内部ContentControl。 ViewModel 本身对 UI 仍然一无所知。
一般来说,我不会使用ShowDialog() 方法的DialogResult 返回属性,也不会期望线程阻塞直到对话框关闭。非标准模态对话框并不总是这样工作,在复合环境中,您通常不希望事件处理程序无论如何都像那样阻塞。我更喜欢让 ViewModel 处理这个问题——ViewModel 的创建者可以订阅其相关事件、设置提交/取消方法等,因此无需依赖这种 UI 机制。
所以不是这个流程:
// in code behind
var result = somedialog.ShowDialog();
if (result == ...
我用:
// in view model
var vm = new SomeDialogViewModel(); // child view model
vm.CommitAction = delegate { this.DoSomething(vm); } // what happens on commit
vm.CancelAction = delegate { this.DoNothing(vm); } // what happens on cancel/close (optional)
// raise dialog request event on the container
我更喜欢这种方式,因为我的大多数对话框都是非阻塞的伪模态控件,并且这样做似乎比解决它更简单。也易于进行单元测试。