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

使用 Microsoft 桌面堆栈 - 第三部分:在 MVVM 应用程序中使用 Entity Framework 4

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.73/5 (21投票s)

2011年4月17日

CPOL

25分钟阅读

viewsIcon

78266

downloadIcon

4955

本文展示了如何使用 MVVM 模式将 Entity Framework 4 集成到 WPF 应用程序中。

引言

过去几年,Microsoft 改造了其桌面应用程序堆栈,从 WinForms 转向 WPF,从 ADO.NET 转向 Entity Framework,从 Jet 数据库引擎转向 SQL Server Compact Edition。本系列文章解释了如何使用该堆栈并提供了实现它的清单。

本系列文章共三篇

第 1 部分和第 2 部分包含用于为桌面应用程序设置 SQL Compact 和 Entity Framework 的清单。如果您已经阅读了这些文章,您就会看到演示应用程序的关键组件。

本文是本系列的第三篇也是最后一篇,展示了如何使用 MVVM 模式将 Entity Framework 4 集成到 WPF 应用程序中。它提供了一个使用 WPF、Entity Framework 4 (EF 4) 和 SQL Server Compact Edition 4 (SQL Compact 4) 构建桌面应用程序的端到端解决方案。演示应用程序 *MsDesktopStackDemo* 围绕第 1 部分和第 2 部分中介绍的组件和过程构建。它是一个概念验证应用程序,旨在解决以下问题:

  • 如何创建 Repository 类,尽可能完全地封装 Entity Framework 4,并允许应用程序的业务层和表示层对所使用的 ORM 技术一无所知。
  • 如何创建一个专门的 ObservableCollection 类,该类可以与持久层协调,而无需知道该层中使用的特定 ORM 技术。
  • 如何将 Entity Framework 4 作为 SQL Compact 4 的 ORM 前端实现。
  • 如何将二进制大对象 (BLOB) 保存到 SQL Compact 4 作为 image 数据类型,以规避 varbinary 类型的 8K 大小限制。

此外,本文还演示了我建议的 MVVM 模式方法。该方法使用完全编码的 ICommand 类,旨在将命令代码与调用这些命令的视图模型分开。

演示应用程序显示了 Larry Niven 和 Jerry Pournelle 的部分书籍列表。其中一些书籍是两位作者合著的,另一些是单独撰写的。书籍的封面艺术用于演示如何使用 SQL Compact 4 的 image 数据类型存储 BLOB 对象。演示应用程序将实际图像存储到此数据类型,但它可用于存储任何序列化的 BLOB,例如序列化的 WPF FlowDocument

本文假设读者对 WPF、Entity Framework 4、MVVM 和 Repository 模式有基本的了解。网上有许多关于这些主题的文章、博客文章和论坛讨论。

设置演示应用程序

演示应用程序依赖于 SQL Compact 4.0 的“私有安装”。SQL Compact 二进制文件超过 5 MB,因此我在压缩演示应用程序上传之前将其删除。要运行演示应用程序,您首先需要将 SQL Compact 4.0 二进制文件复制到演示应用程序的 *Library* 文件夹。假设您的开发机器上已全局安装 SQL Compact 4.0,这应该不需要超过五分钟。您可以在本系列的第 1 部分中获取构建私有安装的完整分步说明,或者您可以在每个 *Library* 子文件夹中查找一个文本文件,其中包含需要从全局安装中复制的文件列表。

应用程序架构

该应用程序围绕 MVVM 和 Repository 模式设计。视图仅与视图模型通信,视图模型充当应用程序后端的 API。视图模型可以访问业务模型和服务层。视图模型仅限于协调器角色——它将其大部分工作委托给一组 ICommand 对象和服务方法。

应用程序的业务层由业务模型以及前面提到的 ICommand 和服务组成。业务模型由使用 Visual Studio 2010 中的模型优先开发创建的更改跟踪对象组成。更改跟踪对象的使用在业务模型和 Entity Framework 4 之间产生了一些耦合,这将在下面讨论。视图模型可以访问业务层中的所有对象。命令可以访问业务模型和服务,服务可以访问业务模型。

持久层由一组 Repository 组成,这些 Repository 封装了 Entity Framework 4 并将其与应用程序的其余部分隔离。Repository 充当应用程序中数据存储访问的 API。Entity Framework 4 充当应用程序的 ORM 技术,尽管也可以使用其他 ORM,例如 nHibernate。SQL Compact 4 充当数据存储;Repository 类已设置好,因此应用程序可以根据需要打开和关闭 SDF 数据文件。

表示层

表示层由单个窗体组成——应用程序的主窗口。主窗口由 XAML 标记和代码隐藏中的代码组成,用于根据需要显示消息框。控件数据绑定到视图模型中的属性。数据控件绑定到数据属性,命令控件(按钮)绑定到命令属性。

ViewModel

视图模型充当表示层与应用程序其余部分之间的协调器(有人说是中介)。表示层只知道视图模型,并通过视图模型进行所有与应用程序后端的通信。因此,只要更改不影响视图模型的接口,视图就免受应用程序后端更改的影响。

更重要的是,应用程序后端不受视图更改的影响。应用程序设计为视图了解其视图模型,但视图模型不了解其视图。这种方法为设计视图提供了更大的灵活性。它可以更改或完全替换,只要它符合视图模型的接口,它就会像以前一样工作。如果视图不符合视图模型接口,视图将无法工作,但这不会影响程序的任何其他部分。换句话说,我们隔离了视图的更改并保护了程序的其余部分免受这些更改的影响。这种保护是 MVVM 模式的主要动力。

视图模型基类:视图模型派生自 ViewModelBase 类,该类实现了两个标准的 WPF 接口

  • INotifyPropertyChanged:此接口必须由 MVVM 视图模型实现。该接口提供了一个 PropertyChanged 事件,该事件使 WPF 数据绑定以及订阅该事件的任何其他代码能够进行更改后通知。
  • INotifyPropertyChanging:MVVM 模式不需要此接口;它作为一项便利功能提供。它为订阅该事件的任何代码提供更改前通知,并有机会取消更改。该事件可用于对象验证或在属性更改之前可能需要执行的任何其他任务。

如果视图模型属性打算与 WPF 数据绑定一起使用,则必须引发 PropertyChanged 事件。属性通常包括视图模型中的所有数据属性。属性在其设置器中引发事件,如下所示:

public FsObservableCollection<Book> Books
{
    get { return p_Books; }

    set
    {
        base.RaisePropertyChangingEvent("Books");
        p_Books = value;
        base.RaisePropertyChangedEvent("Books");
    }
}

简单地引发事件足以在属性上启用 WPF 数据绑定。如果视图模型需要响应这些事件之一(例如,在属性更改之前执行对象验证),则它必须订阅该事件,如下所示:

private void Initialize()
{
    ...

    // Subscribe to events
    this.PropertyChanging += OnPropertyChanging;
    this.PropertyChanged += OnPropertyChanged;
    
    ...
}

命令:视图模型命令属性被实例化为完全编码的 ICommand 对象。一些 MVVM 实现使用快捷方式创建伪 ICommand,这些快捷方式链接到视图模型中的方法。在我看来,这会导致视图模型中的代码膨胀,并且可以将视图模型转变为分布式体系结构试图避免的那种整体控制器。

在演示应用程序中,命令属性由视图模型的 Initialize() 方法实例化,该方法由其构造函数调用。ICommand 类位于源代码的 *Commands* 文件夹中。

事件:视图模型提供一个事件 UserMessagePosted。此事件用于从应用程序向最终用户发布消息。主窗口订阅此事件;当它触发时,主窗口会显示一个消息框,其中包含事件参数携带的内容。

FsObservableCollection:EF 4 更改跟踪对单个对象有效,但对集合无效。当我更新对象中的数据时,EF 4 会跟踪更改并更新数据存储以反映更改。但 EF 4 无法知道我何时向 ObservableCollection 添加对象,或何时从集合中删除对象。这意味着,我必须将任何新对象添加到 EF 4 对象集,并且必须从 EF 4 对象集中删除任何已删除的对象。FsObservableCollection 类旨在促进这些任务。

FsObservableCollection 类派生自 WPF ObservableCollection 类。除了 WPF ObservableCollection 类的标准功能之外,它还提供以下功能:

  • Repository 意识:FsObservableCollection 在其构造函数中接受一个 IRepository 对象。当项目添加到集合或从集合中删除时,该类会调用 IRepository 方法以对数据存储执行相应的任务,从而使集合和数据存储保持同步。
  • 更改前通知:集合实现了一个 CollectionChanging 事件,该事件在项目添加到集合或从集合中删除时提供更改前通知。该事件支持对象验证,并有机会在验证失败时取消更改。

FsObservableCollection 类位于 *ViewModel\BaseClasses* 文件夹中。支持 CollectionChanging 事件的对象位于 *ViewModel\Events* 文件夹中。

服务类:视图模型使用一个服务类,该类位于 *ViewModel\Services* 文件夹中。我尽可能地利用服务类,以使实际视图模型尽可能精简。仅服务于视图模型的类位于 *ViewModel\Services* 文件夹中,而同时服务于视图模型和命令的类位于项目根目录的 *Services* 文件夹中。

业务模型

演示应用程序的业务模型非常简单

该模型是在 Visual Studio 2010 中的实体数据建模器中创建的。

更改跟踪对象与 POCO:演示应用程序的业务对象创建为 EF 4 更改跟踪对象。领域驱动设计 (Domain-Driven Design) 的支持者可能更喜欢使用“POCO”(Plain Old C# Objects,普通旧式 C# 对象),理由是使用更改跟踪会将业务模型与 Entity Framework 4 耦合。这些异议有一些道理;我使用更改跟踪对象的原因有几个:

  • Entity Framework 4.0 不提供对 POCO 的内置支持。一个提供 POCO 支持的 Entity Framework 升级目前处于 CTP 开发阶段,预计将作为 Entity Framework 4.1 发布。CTP 通过所谓的“代码优先”开发支持 POCO。
  • 更改跟踪对象提供了易用性,作为其带来的与 Entity Framework 的耦合的交换。单个对象不必在每次更改时都发送到 Repository;EF 4 会自动跟踪更改,并在数据上下文保存时更新数据存储以反映所有此类更改。

我倾向于不是一个 DDD 纯粹主义者,我愿意接受业务模型和 ORM 之间的一些耦合,假设这种耦合在便利性方面提供了足够的权衡。然而,在这个问题上没有普遍的规则,我建议在应用程序的初始设计阶段仔细考虑“POCO 与跟踪对象”的问题。

Commands

如上所述,视图模型的命令是完全编码的 ICommand 类。我推荐这种方法有几个原因:

  • 它将代码从视图模型中移出,减少了视图模型中的代码膨胀,并导致功能在协作对象之间更均匀地分布,而不是单一的、庞大的控制器。
  • 我发现单个 ICommand 类中的代码比视图模型中大量方法中的代码更容易理解。
  • 单个 ICommand 类使实现撤消更容易。关于命令模式的在线讨论解释了如何在这些类中实现撤消。

服务

演示应用程序有一个服务类 FileServices,它位于项目根目录的 *Services* 文件夹中。FileServices 类包含获取演示应用程序数据文件路径以及包含用于将封面艺术加载到数据库中的 PNG 文件的文件夹的方法。

我的生产应用程序相当依赖这些服务类。一般来说,如果一段代码被多个命令或视图模型调用,它就会进入根目录 *Services* 文件夹中的一个服务类。我发现这种方法使组织和查找代码更容易,并避免了代码重复。

存储库

演示应用程序的 Repository 类位于项目根目录的 *Persistence* 文件夹中。Repository 类旨在将 EF 4 与应用程序的其余部分完全隔离。当然,完全隔离需要使用 POCO 对象,但演示应用程序通过使用更改跟踪对象而违反了这一点。尽管如此,Repository 旨在完全封装 EF 4,并且业务层不需要了解 EF 4。

IRepository 接口:*Persistence\Interfaces* 文件夹包含 IRepository<T> 接口,该接口指定了 Repository 的通用契约。RepositoryBase<T> 类实现了此接口。

RepositoryBase<T> 类:Repository 的大部分工作实际上由 RepositoryBase<T> 类执行,该类本质上封装了 EF 4 功能。该类位于 *Persistence\BaseClasses* 文件夹中。RepositoryBase<T> 有两个构造函数:

  • 拥有的对象上下文:第一个构造函数接受 SQL Compact 4 (SDF) 数据文件的文件路径和实体数据模型的名称。如果使用此构造函数,Repository 将创建自己的 EF 4 对象上下文,并在 Repository 被处置时处置它。
  • 共享对象上下文:第二个构造函数接受一个 EF 4 对象上下文,适用于 Repository 将使用共享对象上下文的情况。当使用此构造函数时,Repository 在 Repository 被处置时不会处置对象上下文。

在第二种情况下,应用程序的持久层将需要一个 Repository 工厂类来持有共享对象上下文,并在创建新 Repository 时提供给它们。演示应用程序使用拥有的对象上下文,因此它不需要 Repository 工厂。

我们正在使用的 RepositoryBase<T> 构造函数需要两个来自 EF4 的参数

protected RepositoryBase(string filePath, Type contextType, string edmName)
{

乍一看,我们似乎未能将应用程序的其余部分与 EF4 隔离。任何调用此类的都必须了解 EF4。但我们实际上所做的是将这些任务委托给具体的派生类型。它们是唯一可以调用构造函数的类型。您将在下面看到,EF4 信息已硬编码到具体的存储库中。因此,我们已经将 EF4 封装在存储库中,因为具体类的调用者只需要知道文件路径。

BookRepository 类BookRepository 类是具体类。它位于 *Persistence* 文件夹中。调用者将文件路径传递给它,BookRepository 调用 RepositoryBase<T>,传递文件路径以及上下文类型和 EDM 名称的硬编码值。

public BookRepository(string filePath) : 
       base(filePath, typeof(BooksContainer), "Model.Books")
{
}

BookRepository 类不需要任何超出基类提供的功能,因此类体为空。RepositoryBase<T> 完成所有工作。基类只是为了进行基构造函数调用。显然,它也可以用于保存具体存储库独有的任何方法或属性。

另请注意,与其基类不同,BookRepository 不提供“共享对象上下文”构造函数。虽然 RepositoryBase<T> 类旨在在任何应用程序中无需修改即可重用,但 BookRepository 类被设计为具体实现,并专用于演示应用程序。

DataFileInfo 类:应用程序不使用 DataFileInfo 类,仅包含它以演示如何在生产应用程序中使用轻量级对象来维护有关当前数据文件的基本信息。在生产应用程序中,打开数据文件的方法会将文件路径存储在此对象中,该对象还可用于保存有关数据文件状态所需的任何其他信息。

日志记录

演示应用程序包含日志记录功能,使用开源 log4Net 日志记录框架。log4Net 配置为文本文件日志记录,它使用 SpecialFolderPatternConverter 类(在 *Utility* 文件夹中找到)将其日志放置在 *c:\ProgramData* 文件夹中。配置在演示应用程序的 *App.config* 文件中完成。

演示应用演练

当您第一次运行演示应用程序时,它会打开一个没有内容的窗口。演示应用程序窗口有三个按钮

  • 加载书籍:此按钮从 SQL Compact 4 数据库加载书籍列表。书名列表将出现在应用程序窗口左上角的框中。单击列表中的一本书,书名作者将出现在左下角的框中。同时,书籍的封面艺术将出现在窗口右侧的框中。
  • 设置封面:此按钮将每本书的封面艺术从 PNG 文件复制到 SQL Compact 4 数据库。封面已加载,因此该按钮被禁用。它旨在演示如何将 BLOB 对象存储在 SQL Compact 4 数据库中,并将在下面讨论。
  • 删除书籍:此按钮用于演示如何对集合执行数据验证。当您单击该按钮时,DeleteButtonCommand 将尝试删除选定的书籍。视图模型订阅 Books 集合的 CollectionChanging 事件,并在其 OnBooksCollectionChanging() 方法中处理该事件。当事件触发时,事件处理程序模拟对象验证失败。然后它向用户发送错误消息并取消待处理的更改。

请注意,演示应用程序的数据文件存储在安装演示应用程序的用户文档文件夹中。我不认为这是一种最佳方法——我使用它是由于 Visual Studio 部署项目的限制。这个问题在本系列的第 1 部分中讨论过。

应用程序初始化

视图和视图模型通过添加到 *App.xaml.cs* 的 OnStartup() 重写进行初始化。通过将视图模型设置为视图的数据上下文,将视图绑定到视图模型

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);

    var mainWindow = new MainWindow();
    var mainWindowViewModel = new MainWindowViewModel();
    mainWindow.DataContext = mainWindowViewModel;
    mainWindow.Show();
}

加载书籍

主窗口中的 *Load Books* 按钮在 XAML 中数据绑定到视图模型中的 LoadBooks 属性。该属性又被实例化为一个 LoadBooksCommand 对象。当单击按钮时,它会调用命令,该命令使用 BookRepository 类从数据存储中加载书籍。

加载后,书籍列表也在 XAML 中通过书籍列表的 SelectedItem 属性绑定到视图模型中的 SelectedBook 属性

因此,当在书籍列表中选择一本书时,它也被设置为当前的 SelectedBook

视图模型在其 Initialize() 方法中订阅自己的 PropertyChanged 事件。当设置新的 SelectedBook 时,OnPropertyChanged() 事件处理程序调用服务方法来加载书籍的作者和封面艺术

void OnPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
    switch(e.PropertyName)
    {
        case "SelectedBook":

            /* When we select a book, we need to configure the view model
             * to display the selected book's author(s) and cover art. */

            ViewModelServices.ConfigureSelectedBookProperties(this);
            break;

        ...
    }
}

服务方法将新选择的书籍作者加载到视图左下角的列表框中,并将封面艺术从数据库加载到右侧的 Image 控件中。该代码展示了如何使用 EF 4 从 SQLCE 加载 BLOB 对象,并将在下面讨论。

设置封面(使用 BLOB)

主窗口包含一个“设置封面”按钮,该按钮从 PNG 文件中读取数据库中每本书的封面艺术,并将图像加载到数据库“书籍”表中的图像列中。该按钮被禁用,因为封面只需设置一次,并且该任务已经执行。数据库预填充了封面艺术。如果您想执行该命令,可以通过将命令 CanExecute() 方法的返回值从 false 更改为 true 来重新启用“设置封面”按钮。

包含此按钮是为了演示如何使用 BLOB 对象。当与 SQL Compact 4 一起使用时,BLOB 会带来问题,因为其 varbinary 类型有 8K 的大小限制,这使得该类型对于存储大多数二进制对象而言毫无用处。幸运的是,SQL Compact 4 提供了一个 image 类型,该类型接受字节数组,并且最多可以存储 2^30-1 字节。我们可以将该类型与我们可以转换为字节数组的任何 BLOB 一起使用。

SetCoversCommand 将封面艺术读取到内存流中,将该流转换为字节数组,并将字节数组分配给 Book.Cover 属性

// Load cover image for each book into its Book object
foreach (var book in bookList)
{
    var fileName = String.Format("{0}.png", book.Title);
    var coverFilePath = Path.Combine(coversFolderPath, fileName);
    var stream = new FileStream(coverFilePath, FileMode.Open);
    var byteArray = ConvertStreamToByteArray(stream);
    book.Cover = byteArray;
}

如上所述,此技术可用于任何 BLOB,而不仅仅是图像。例如,WPF FlowDocument 可以序列化为内存流,此时我们可以以与演示应用程序中封面艺术完全相同的方式将其存储到图像列中。

LoadBooksCommand 调用 BooksRepository 将所有书籍加载到视图模型的 Books 属性中。表示每本书封面的字节数组被加载到 Book.Cover 属性中,该属性是 EF 4 Binary 类型。此类型接受字节数组,因此我们无需进行任何转换即可将封面艺术加载到视图模型中。

主窗口中的 Image 控件绑定到视图模型的 CoverArt 属性,该属性的类型为 BitmapImage。该类型适用于我们需要显示的封面艺术。如果我们要处理 FlowDocument,我们将改用该类型的视图模型属性。无论哪种情况,我们都需要将用于存储的字节数组转换为用于显示的适当类型。在演示应用程序中,我们需要将字节数组转换为 BitmapImage

它是这样工作的。如上所述,在书籍列表中选择一本书会设置视图模型的 SelectedBook 属性。更改所选书籍会触发 PropertyChanging 事件,该事件由视图模型的 OnPropertyChanged() 方法处理。该方法将其工作委托给视图模型服务类方法 ConfigureSelectedBookProperties()。服务方法在配置视图模型以获取新选择的书籍时执行所需的转换

public static void ConfigureSelectedBookProperties(MainWindowViewModel viewModel)
{
    ...

    // Show cover image for the newly-selected book
    using (var stream = new MemoryStream(viewModel.SelectedBook.Cover))
    {
        var coverArt = new BitmapImage();
        coverArt.BeginInit();
        coverArt.StreamSource = stream;
        coverArt.CacheOption = BitmapCacheOption.OnLoad;
        coverArt.EndInit();
        coverArt.Freeze();
        viewModel.CoverArt = coverArt;
    }
}

这段代码相当直白。它创建一个新的 BitmapImage 并将字节数组流式传输到其中。当 BitmapImage 完成后,它将对象分配给 CoverArt 属性。如果我们要处理不同类型的 BLOB,例如序列化的 FlowDocument,我们会将字节数组转换回内存流并将其反序列化为适当的对象。

这项技术允许 SQL Compact 4 存储几乎任何类型的 BLOB。然而,正如您从演示应用程序数据库中看到的那样,BLOB 很快就会使 SQL Compact 数据库膨胀到无法管理的大小。因此,应谨慎使用此技术。

删除书籍

如上所述,提供“删除书籍”按钮是为了演示如何使用 FsObservableCollection 类中的 CollectionChanging 事件执行对象验证。OnBooksCollectionChanging() 方法通过模拟验证失败来处理该事件。它演示了如何通知用户并取消待删除操作。

基于命令的验证:请注意,CollectionChanging 事件并非严格要求执行对象验证。在 MVVM 架构中,命令对象可以在调用将更改对象或集合的操作之前执行对象验证。完全编码的 ICommand 类的好处之一是,如果您选择从 ICommand 对象进行验证,这些验证方法不会使视图模型变得混乱。

基于事件的验证:演示应用程序展示了另一种对象验证方法,称为基于事件的验证DeleteBookCommand 调用视图模型中 Books 属性上的 Remove() 方法。请注意,它在调用该方法之前不执行任何验证。Books 属性是一个 FsObservableCollection,因此它在执行删除之前会引发一个 CollectionChanging 事件。视图模型在其 Initialize() 方法中订阅此事件

private void Initialize()
{
    ...

    // Subscribe to events
    this.PropertyChanging += OnPropertyChanging;
    this.PropertyChanged += OnPropertyChanged;
    this.Books.CollectionChanging += OnBooksCollectionChanging;
}

订阅将事件处理委托给 OnBooksCollectionChanging() 方法。

事件处理程序是一个简单的调度程序,它调用一个服务类方法来向用户“发布”消息(有关发布用户消息,请参见下文)。服务方法发布用户消息后,事件处理程序通过将事件参数的 Cancel 属性设置为 true 来取消待处理的删除。

void OnBooksCollectionChanging(object sender, NotifyCollectionChangingEventArgs e)
{
    ViewModelServices.PostCancelMessage(this);
    e.Cancel = true;
}

事件处理程序将控制权返回给 FsObservableCollection.Remove() 方法,该方法检查 Cancel 属性。发现该属性设置为 true 后,该方法退出而不执行删除。

我对于基于命令的对象验证和基于事件的验证没有强烈偏好。提供 CollectionChanging 事件是为了让开发人员可以选择使用基于事件的验证。

订阅 CollectionChanging 事件:只要集合本身没有更改,视图模型只需在初始化时订阅该事件。但是,请注意,演示应用程序在启动时显示一个空的书籍列表。为此,我们需要将视图模型的 Books 属性初始化为一个空集合。因此,我们调用只接受 Repository 参数的集合构造函数,传递 null 表示我们还没有 Repository

private void Initialize()
{
    ...

    // Initialize data properties
    this.Books = new FsObservableCollection<Book>(null);

    // Subscribe to events
    ...
    this.Books.CollectionChanging += OnBooksCollectionChanging;
}

视图模型订阅此集合的 CollectionChanging 事件。

当用户点击“加载书籍”按钮时,LoadBooksCommand 会创建一个 BooksRepository,用它获取书籍列表,并将列表和 Repository 都传递给一个新的 FsObservableCollection。然后它将其 Books 属性设置为新的集合。请注意,当它这样做时,它会完全替换我们最初订阅的空集合。这意味着我们需要在旧集合被替换之前取消订阅,并在替换完成后订阅新集合。

视图模型使用其 PropertyChangingPropertyChanged 事件来执行这些任务。当 Books 集合被替换时,在事件执行之前触发 PropertyChanging 事件。OnPropertyChanging() 事件处理程序取消订阅旧集合

void OnPropertyChanging(object sender, System.ComponentModel.PropertyChangingEventArgs e)
{
    switch (e.PropertyName)
    {
        case "Books":
            this.Books.CollectionChanging -= OnBooksCollectionChanging;
            break;
    }
}

然后,在更改执行后,触发 PropertyChanged 事件。OnPropertyChanged() 事件处理程序订阅新集合

void OnPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
    switch(e.PropertyName)
    {
        ...

        case "Books":
            this.Books.CollectionChanging += OnBooksCollectionChanging;
            break;
    }
}

请注意,取消订阅步骤非常重要。如果您在完成对象使用后未能取消订阅其事件,.NET 垃圾回收可能会在这些对象上失败,最终导致应用程序崩溃或严重降低性能。

发布用户消息:如上所述,OnBooksCollectionChanging() 事件处理程序调用服务类方法 PostCancelMessage(),以创建在按下“删除书籍”按钮时向用户显示的消息。服务类方法配置 UserMessagePosted 事件参数的数据,并将其传回视图模型的 PostUserMessage() 方法。此方法创建事件参数并引发事件。

表示层订阅此事件,并使用事件参数创建消息框,然后将其显示给用户。主窗口在其数据上下文设置时订阅视图模型的 UserMessagePostedEvent。它在其代码隐藏类中使用两步过程来订阅该事件。

首先,主窗口的 Initialize() 方法(从其构造函数调用)订阅其自身的 DataContextChanged 事件

private void Initialize()
{
    // Subscribe to local events 
    this.DataContextChanged += OnDataContextChanged;
}

接下来,该事件的处理程序 OnDataContextChanged() 订阅 UserMessagePosted 事件

void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
    // Subscribe to view model events
    var viewModel = (MainWindowViewModel) this.DataContext;
    viewModel.UserMessagePosted += OnUserMessagePosted;
}

事件处理程序通过将其数据上下文转换为适当的类型来获取视图模型。然后它订阅事件。因此,当 OnStartup() 重写(在 *App.xaml.cs* 中)设置主窗口的数据上下文时,窗口会自动订阅 UserMessagePosted 事件。

MVVM 纯粹主义者可能会反对在主窗口中使用代码隐藏。严格的 MVVM 方法会规定永远不要使用代码隐藏。虽然这种方法可能有其优点,但我乐于使用一些最少的代码隐藏来实现视图的合法关注点。

MVVM 模式的目标是提供表示层和应用程序后端之间干净的分离。

  • 视图模型关注用户消息的内容;以及
  • 演示层关注消息的显示

这意味着消息的内容必须由视图模型生成并传递给视图进行显示。如上所述,演示应用程序的 MVVM 实现设计为视图模型对其视图一无所知。因此,事件(不要求发布者了解订阅者)提供了一种在不损害视图和视图模型之间分离的情况下传输内容的适当机制。

在生产应用程序中,可以预期几个不同的视图需要响应其视图模型引发的 UserMessagePosted 事件。因此,视图响应这些事件,但它们将消息的生成委托给表示层中的服务类。

部署项目

演示应用程序解决方案还包括一个部署项目,可用于在最终用户计算机上安装演示应用程序。该部署项目作为一个示例,展示如何为使用 Microsoft 桌面堆栈的应用程序创建桌面安装程序。安装程序包括本系列第 1 部分中讨论的“统一数据库”安装功能。

结论

希望您有足够的信息来按照相同的思路构建生产应用程序。本文中介绍的设计足以满足相对简单的应用程序,但人们会发现,即使是结构良好的 MVVM 应用程序,随着应用程序复杂性的增加,也可能变得臃肿和脆弱。因此,有效的应用程序分区和使用控制反转 (IOC) 容器是大多数生产应用程序的基本要素。

Microsoft Prism 4.0 提供了一个框架,用于将应用程序划分为独立的组件,这些组件可以组合成一个或多个窗口以供用户呈现。Prism 可与大多数流行的 IOC 容器配合使用,包括 .NET 可扩展性框架、Microsoft Unity 2.0、Castle Windsor 等。我建议将这些技术集成到除了最基本的生产应用程序之外的所有应用程序中。

一如既往,我欢迎您对改进本系列提出意见和建议。我发现 CodeProject 读者提供的同行评审非常宝贵,并且总是受到赞赏。

© . All rights reserved.