WPF-NHibernate 工具包






4.98/5 (21投票s)
使 NHibernate 类能够运行在 WPF 中

引言
WPF 很棒——真的。NHibernate 也很棒。但它们根本不喜欢彼此。在最近的一次 Microsoft 培训课程中,有人向讲师提问关于将 WPF 与 NHibernate 结合使用的问题。他的回答是:“祝你好运,能让它工作起来!” 嗯,在本文中,我们将为您提供技能和工具,让它工作起来,并且让它变得容易。
WPF 最棒的地方之一是它的数据绑定能力。我不知道 WPF 是否真的是数据绑定的第三版,但 Microsoft 终于做对了。但是要使用 WPF 数据绑定,您需要以某种方式设置您的域模型。单个属性需要实现 PropertyChanged
事件,集合需要引发 CollectionChanged
事件。这看起来是一个相当无害的要求,但使用领域驱动设计(DDD)的开发人员讨厌它。您看,对于 DDD 开发人员来说,WPF 污染了域模型并破坏了关注点分离。从 DDD 纯粹主义者的角度来看,WPF 只是另一种糟糕的 Microsoft 技术。
NHibernate 确实不喜欢集合约束。虽然 WPF 首选的集合类型是 ObservableCollection<T>
,但 NHibernate 绝对要求域集合的类型是 IList<T>
(或 IESI.ISet<T>
),它们不提供任何更改通知。如果您使用 NHibernate 作为您的 ORM,传统的观点认为您几乎无法使用 WPF,至少在此之前是这样。
那么,我们如何让 WPF 和 NHibernate 友好地协同工作呢?我们又如何让 WPF 受到 DDD 开发人员的欢迎呢?DDD 纯粹主义者提供了这种方法:将您想要在视图中公开的域模型对象进行包装——将它们包装在视图模型包装器(VM 包装器)中,这些类提供了 WPF 所需的功能。这听起来足够直接。但是,如果您曾经尝试过实际操作,您就会知道这项任务是多么令人麻木、迟钝、耗时的苦差事。至于其他人,只需想想牙科治疗。
本文展示了一个工具包,它让所有组件和谐共处,并使创建 VM 包装器类的任务变得几乎微不足道。该工具包的核心是 VM Wrapper Express 2.0,一个可以在眨眼间生成包装器类的实用程序。本文还包含一个演示应用程序 VM Collection Demo,它展示了包装器类如何工作以及如何在您自己的应用程序中使用它们。
自本文的原始版本发布以来,VM Wrapper Express 已进行了更新。本文主体讨论了从 1.0 版到 2.0 版的变化。但让我们首先介绍一些背景知识——看看 NHibernate 和 WPF 组合带来的关注点分离问题。
2.1 版更新
VM Wrapper Express 已更新到 2.1 版本。在使用该应用程序时,我发现当它与 VMWE 生成的包装器类一起使用时,WPF DataGrid
变得不可编辑。2.1 版本修复了这个问题;我将在下面讨论这个问题和修复方案。
VM Wrapper Express 有一个安装项目。如果您只想安装程序,可以在 SetupVmWrapperExpress\bin\Release 文件夹中找到安装程序。
关注点分离
关注点分离原则指出,一个类应该只做一件事,并把它做好。视图类应该关注视图,数据访问类应该关注数据访问,而域模型应该关注问题域的建模。域模型不应该关注数据访问或显示——如果我们需要扭曲域模型以满足我们的 ORM 或显示技术的要求,那么我们正在用无关的关注点污染我们的域模型。
在 DDD 开发人员中(除了真正的纯粹主义者,他们吓坏了我们其他人),似乎普遍认为 NHibernate 要求所有集合都是 IList<T>
或 IESI.ISet<T>
集合,这并没有真正地损害域模型的关注点分离。IList<T>
是 .NET 集合的最低公分母接口,ISet<T>
也是。即使应用程序不使用 NHibernate,很有可能无论使用哪种集合类型,它都会实现 IList<T>
。所以,我们在这里没有真正的顾虑。
WPF 要求单个对象实现 PropertyChanged
事件,并且它要求集合属性实现 CollectionChanged
事件。实际上,这些要求意味着与 WPF 交互的类必须实现 INotifyPropertyChanged
接口,并且它们的集合属性必须是 ObservableCollection<T>
类型。可以说,这些约束并不比 NHibernate 对 IList<T>
的约束更繁重。也许是,也许不是。但是,我们都可以同意,一个与 WPF 协同工作的类绝对不会与 NHibernate 协同工作。
这重要吗?
即使没有 NHibernate 的问题,我也会担心 WPF 在多大程度上会扭曲域模型。面向对象编程(和关注点分离)的整个理念是尽量减少程序不同部分之间的耦合。如果我将我的域模型(和我的其他业务逻辑)与 WPF 和 NHibernate 分离,那么我可以在以后需要时替换它们中的任何一个,而无需对其他两个组件进行重大更改。如果 Microsoft 在 Entity Framework 的第 2 版中大获成功,我应该能够在不拆散我的域模型或用户界面的情况下替换 NHibernate。而且,如果另一种显示技术在未来取代 WPF,我应该能够在不破坏我的应用程序后端的情况下更新用户界面。
进行这些更改的能力使我确信,一些额外的努力是非常值得的。这完全取决于保持关注点分离需要多少工作。WPF-NHibernate 工具包旨在最大限度地减少将用户界面与程序后端分离所需的额外工作。
有必要这么复杂吗?
当这篇文章首次发表时,Paulo Quicoli 提出了一个很好的观点。Paulo 是 WPF + M-V-VM + NHibernate + NHibernate Validator = Fantastic Four! 的作者。这是一篇很棒的文章,我向任何学习 WPF 的人推荐。总之,Paulo 实际上问,是否可以使用以下方法更轻松地实现相同的结果:
- 在您的单个域对象上实现
INotifyPropertyChanged
- 将您的 NHibernate 集合设为
IList<T>
集合 - 对于 WPF,使用接受
IList<T>
的构造函数将这些集合注入到ObservableCollection<T>
集合中 - 要持久化回数据库,只需将
ObservableCollection<T>
强制转换为IList<T>
正如 Paulo 指出的那样,这个解决方案是可行的,正如他在文章中演示的那样。
Paulo 完全正确,在一个小规模的应用程序中,这种方法会非常有效。然而,在一个大规模的应用程序中,它会导致性能下降。原因如下:NHibernate 会对您检索的对象进行快照,并将快照缓存到 ISession
对象中。当您持久化回数据库时,NHibernate 会将您对象的当前状态与其缓存中的快照进行比较。它只写入实际已更改的对象。
为了使 NHibernate 能够实现这个技巧,您必须在整个会话中保持对象标识。换句话说,您必须将 NHibernate 最初传递给您的相同对象再传回给它。当您将 IList<T>
注入 ObservableCollection<T>
时,您正在创建一个新集合,这会破坏对象标识。因此,当您将 ObservableCollection<T>
转换回 IList<T>
时,您将拥有原始集合的忠实副本,但您不会拥有原始集合。结果,NHibernate 将回退到将所有内容写回数据库。因此,即使您只更改了一个包含一千个对象的集合中的一个对象,整个集合也会被写回数据库。
值得注意的是,如果您决定在单个对象上实现 INotifyPropertyChanged
,那么就不需要包装这些对象了。因此,您可能会简化 VmCollectionBase<VM, DM>
,因为它将能够访问域对象,而不是包装器。我喜欢尽可能保持我的域模型干净,所以目前,我正在包装我所有的单个域对象。但这很难说——我能理解实现 INotifyPropertyChanged
不会以任何有意义的方式污染我的域模型的论点的优点。
模型-视图-视图模型
大多数 WPF 专家建议使用模型-视图-视图模型 (MVVM) 模式来组织 WPF 应用程序。MVVM 的思想是为应用程序中的每个视图创建一个视图模型类。视图模型提供了视图所需的命令和数据的抽象表示,以及驱动显示所需的编程逻辑。这种模式的巨大优势在于 WPF 可以以一种非常简单、易于实现的方式绑定命令和数据。
本文不会花时间讨论 MVVM。网上有许多关于 MVVM 的好文章。WPF 开发实验室 Lab 49 的 Jason Dolinger 也制作了一个非常好的关于该主题的截屏视频,可以在 Lab 49 博客上找到。但是,在继续之前,我想就 MVVM 提出几点:
- 视图模型不封装整个域模型。相反,它封装了域模型中选定的类——那些将在特定视图中公开的类。
- 视图模型不是一个单一的类。视图模型通常由许多类组成,包括包装器类、
IConverter
类和实用程序类。然而,这些类通常由一个单一的视图模型类组织,该类被设置为 WPF 窗口(或控件)的DataContext
,并作为其他类的外观。
在本文中,当我们提及视图模型(小写)时,我们指的是构成特定视图视图模型的类组。当我们提及 ViewModel 时,我们指的是外观类。现在,让我们进入工具包。
领域对象的封装
VM Wrapper Express 2.0 版本的设计目标之一是改善域对象的封装。理想情况下,包装器对象应该向视图隐藏域对象。换句话说,视图应该只看到视图模型中的包装器对象,而不是包装器内部的域对象。这种方法可以防止开发人员错误地访问域对象并破坏 UI 中的数据绑定。在 VM Wrapper Express 的原始版本中,被包装的对象通过一个 public
属性 DomainObject
暴露了域对象。因此,域对象并未被其包装器向视图隐藏。
对 public DomainObject
属性的需求源于 VmCollectionBase
类需要访问包装器的域对象这一事实。新的 VM 对象可以通过数据绑定操作在视图中创建——例如,请参阅 VM Wrapper Demo 应用程序中的“添加”和“删除”按钮。这些新对象通常被添加到 VM 集合中。在这种情况下,VmCollectionBase
类响应 VM 类中的更改并将更改推送到域模型。为此,VmCollectionBase
类需要访问新创建或删除的 VM 中的域对象,以将更改推送到域模型。
版本 2.0 通过将 DomainObject
属性设为 internal
来改善了这种情况。只要视图模型与视图在单独的程序集中编译,VM 对象就会向视图隐藏域对象,同时仍可供 VmCollectionBase
类访问。VM Wrapper Demo 解决方案采用了这种方法。
然而,如果视图和视图模型包含在同一个程序集中,DomainObject
属性将对视图可见。因此,虽然版本 2.0 改进了 VM Wrapper Express 的原始版本,但我对结果并不完全满意。如果任何读者可以推荐一种更好的封装域对象的方法,我将非常乐意接受建议。
请注意,在 VM Wrapper Demo 应用程序中,我们打破了隐藏域对象的规则。事实上,视图模型将整个域模型作为属性公开,视图可以很容易地访问它。这样做是为了让视图可以检查域对象,从而演示更改已推送到域模型。请注意,我不建议将此作为一般做法。正如我们上面讨论的,视图模型应该为视图提供访问域模型属性的唯一方式。
工具包
工具有两部分:
- VM Wrapper Express:一个为领域对象和集合生成 VM 包装器的实用程序。
- VM Wrapper Demo:一个简单的演示程序,展示了如何使用 VM Wrapper Express 生成的类。
VM Wrapper Express 不会包装域模型中包含的所有对象。相反,它只包装您希望在视图模型中公开的那些对象。这些包装器是四人帮适配器模式的实现。
包装器类
VM 包装器分为两类:
- 对象包装器:这些类包装单个领域类,例如演示应用程序中的
Customer
和Order
类。对象包装器对于它们所包装的领域类是唯一的。 - 集合包装器:这些类包装单个集合类,例如
Customers
和Orders
集合类。这些集合包装器是骨架类,它们的所有功能都派生自VmCollectionBase<VM, DM>
类。我将在下面讨论这种方法的原因。
在 2.1 版本之前,VM Wrapper Express 不会为集合类生成单独的包装器类。相反,它使用一个泛型 VmCollection<VM, DM>
类。为了包装一个集合,开发人员只需将一个属性声明为 VmCollection<VM, DM>
类型(VM
和 DM
在下面解释)。这在大多数情况下都运行良好。然而,由于某些尚不完全清楚的原因,当 WPF DataGrid
绑定到这种类型的属性时,这种方法会导致 DataGrid
变得不可编辑。出于某种原因,当 WPF DataGrid
绑定到具有多个泛型参数的泛型类时,它会将 IEditableCollectionView.CanAddNew
属性设置为 false
。
解决方案原来相当简单。我们只需为每个集合类声明一个骨架集合包装器。该包装器派生自 VmCollectionBase<VM, DM>
类。集合包装器不需要实现任何单独的功能,因为其唯一目的是解决 WPF DataGrid
的错误。因此,从 2.1 版本开始,我们有了一个 VmCollectionBase<VM, DM>
类,以及从该基类派生的各个集合包装器。新基类的行为与旧的泛型类完全相同。
VmCollectionBase
类有两个泛型类型变量(它们的行为与旧泛型类中的对应变量相同)
VM
:这个泛型类型变量表示集合中包含的对象包装器的类型。DM
:这个泛型类型变量表示由 VM 类包装的领域对象的类型。
在讨论集合包装器时,我们给“包装”加上了引号,因为 VmCollectionBase<VM, DM>
并没有真正包装 IList<T>
集合。相反,它复制了它,以容纳一个对象包装器集合。但是,效果与它包装了集合一样。
对象和集合包装器的设计目的是,对包装器对象属性所做的任何更改都会流回被包装域对象中相应的属性。涉及两种类型的更改:
- 对象属性更改:由于对象包装器属性直接连接到其在被包装域对象中的对应属性,因此包装器对象中的任何更改都会立即传递给域对象。
- 集合更改:当对象被添加到集合或从集合中删除时,
VmCollectionBase<VM, DM>
处理其自己的CollectionChanged
事件。在这些情况下,相同的操作会推回到被包装的域集合——添加一个新域对象(带有相应的对象包装器),或者将与被删除的对象包装器对应的域对象从域集合中删除。
这些方法使视图模型和域模型集合保持同步。
请注意,VM Wrapper Express 为主窗口中选定的每个领域对象创建一个单独的对象包装器和集合包装器。生成的包装器放置在主窗口中选定的文件夹下的三个子文件夹中:BaseClasses、CollectionWrappers 和 ObjectWrappers。一旦您使用 VM Wrapper Express 生成了包装器类,您就可以决定在您的视图模型中需要使用哪些包装器。在我的应用程序中,我通常将类生成到视图模型项目中的 Wrappers 文件夹中,然后将基类从该文件夹移动到项目的其他基类存储位置。这是 VmWrapperDemo 应用程序中使用的方法。
包装器类中更改的方向
请注意,在 2.0 版中,更改的流向仍然是单向的;也就是说,对包装对象或集合的更改会流向被包装的域对象或集合,反之则不然。因此,在包装域对象或集合之后,对域对象或集合的更改不会流向它们的包装器。这种方法基于这样一种想法:一旦对象被包装,它就应该只能通过其包装器访问。演示应用程序的“添加客户”命令反映了这种方法。一个新的对象包装器(其中包含一个新的域对象)被添加到视图模型集合中;然后包装器集合将新的包装对象推送到被包装的集合中。
之前版本的错误修复
我在 VM Wrapper Express 1.0 版本中发现了一个 bug,每当域类有一个类型为另一个域类的属性时,就会出现这个 bug。假设您有一个域类,其所有属性都是简单的 .NET 类型;字符串、整数等等。当 UI 调用一个被包装的属性时,它需要该属性包装的字符串或其值。但是,如果一个域类有一个属性是另一个域类型呢?例如,一个 Customer
类可能有一个 Address
属性,其类型为名为 Address
的域类。后一个类具有 StreetNumber
、StreetName
、City
、State
和 Zip
属性(假设是美国地址)。在这种情况下,UI 需要的是被包装的地址对象,然后它可以从中获取所需的信息。
原始版本 VM Wrapper Express 生成的对象包装器没有区分这一点——如果一个域类属性的类型是另一个域类,它只会尝试返回第二个域对象。VM Wrapper Express 2.x 版本在将第二个域对象传递给调用者之前,将其包装在一个 VM 包装器中,这是预期的行为。
CodeProject 成员 Skendrot 在对原始文章的回应中发帖指出,原始解决方案没有考虑惰性加载。他提出了一个我非常喜欢的修复方案,以至于我在 2.0 版本中原封不动地采用了它。您可以在本文末尾找到他的原始消息。谢谢,Skendrot!
演示应用程序
演示应用程序在 2.1 版中略有更改——它现在使用单独的集合包装器,而不是 2.0 版中的泛型集合包装器。它的行为与 2.0 版相同。该演示展示了包装器类在简单应用程序中的使用方法:

演示应用程序基于 Model-View-ViewModel 模式构建;后台代码保持在最低限度,大多数控制逻辑包含在应用程序的视图模型类中,这些类位于 view model 文件夹中。
主窗口中的每个按钮都演示了对域模型的不同类型的操作。当您单击一个按钮时,演示应用程序将操作应用于包装器类(而不是被包装的域类)。列表框将立即更新,这表明操作已从包装器类传播到视图。然后,应用程序会显示一个消息框,其中显示从相应的包装器类属性以及相应的域类属性读取的操作结果。来自两个源的结果应该相同,这表明操作已从包装器类传播到被包装的域类。
域模型故意简化。演示模型提供最基本的客户-订单模型,仅足以展示多级域对象包装如何工作。域对象可以在 Domain 文件夹中找到。应用程序的主窗口可以在视图文件夹中找到。
视图模型由许多不同的类组成。MainWindowViewModel
(外观类)充当其中许多类的容器;它在启动时(在 App.xaml.cs 中)绑定到主窗口的 DataContext
属性。Commands 文件夹包含应用程序命令的 ICommand
对象。这些对象作为 ICommand
属性在视图模型类中公开。Converters 文件夹包含几个在视图和视图模型之间的数据绑定操作中使用的 IValueConverter
类。BaseClasses 文件夹保存视图模型及其包装器使用的基类:
ViewModelBase
作为视图模型类的基类。它提供了 WPF 数据绑定中使用的INotifyPropertyChanged
接口的实现。VmWrapperBase
作为对象包装器的基类。它派生自ViewModelBase
并向包装器对象提供INotifyPropertyChanged
接口。此外,它还提供了VmCollectionBase
类用于将视图模型集合更改推送到域模型的内部DomainObject
属性。VMCollectionBase
作为集合包装器的基类。
最后,Wrappers 文件夹包含视图模型中公开的领域对象的对象包装器。这些包装器由 VM Wrapper Express 工具生成。
演示应用程序在 App.xaml.cs 类中,通过重写 OnStartup()
事件处理程序进行初始化。演示应用程序模拟 NHibernate,而不是在任何程度上实现它。它只是创建了几个 Customer
对象,每个对象都有几个 Order
对象,然后将这些集合包装在 IList<T>
集合中,以模拟我们从 NHibernate 获得的结果。如果您需要了解如何使用 NHibernate 从数据库中获取这些对象,我建议您阅读我的文章 NHibernate Made Simple,该文章也在 CodeProject 上。
当演示应用程序启动时,它会呈现一个相当普通的主从视图,由两个列表框组成。顶部列表框列出了客户,底部列表框列出了选定客户的订单。列表框下方显示了一系列按钮;每个按钮都演示了涉及包装对象的不同任务。
按钮绑定到演示应用程序的 ICommand
对象,这些对象(如上所述)作为命令属性在视图模型上公开。命令逻辑都相当简单,因此我在此不再赘述。
在演示应用程序中包装领域对象
主要的领域对象包装是在 App.xaml.cs 中完成的。OnStartup()
重写创建了几个 Customer
对象并用 Order
对象填充它们;然后,它将创建的 IList<Customer>
传递给视图模型。
// Create view model
MainWindowview model mainWindowview model = new MainWindowview model(customers);
视图模型构造函数将其 Customers
属性初始化为新的 CustomersVM
集合类,并将 IList<Customer>
传递给集合的构造函数。
// Initialize collections
this.Customers = new CustomersVM(customers);
此时,VmCollectionBase
开始工作:
// Populate this collection with VM objects
VM wrapperObject = default(VM);
foreach (DM domainObject in domainCollection)
{
wrapperObject = new VM();
wrapperObject.DomainObject = domainObject;
this.Add(wrapperObject);
}
代码遍历 IList<Customer>
,为每个 Customer
对象创建一个对象包装器,将领域对象注入到包装器的构造函数中,并将这些包装器对象添加到其内部集合中。实际上,这是非常简单的。这个过程以递归链的形式继续,确保整个对象图都被包装到最深层。
运行演示
演示或多或少是自解释的。主从数据绑定在 XAML 中完成,使用两个列表框的 ItemsSource
属性。顶部列表框绑定到视图模型的 Customers
属性,如下所示:
<ListBox x:Name="listBoxCustomers" ItemsSource="{Binding Path=Customers}" … >
然后,订单列表框绑定到当前的 Customer.Orders
属性,如下所示:
<ListBox x:Name="listBoxOrders" ItemsSource="{Binding Path=Customers/Orders}" … >
注意,两个列表框的 IsSynchronizedWithCurrentItem
属性都设置为“True
”。这确保了两个列表框都设置为相同的 Customer
对象。
您可能会注意到 UI 有创建新对象的按钮。这些按钮调用一个操作,该操作会反转包装器对象的正常创建。通常,域对象优先,并被注入到包装器对象的构造函数中。但是,当从 UI 调用包装器对象创建时,没有预先存在的域对象。我们需要创建一个新的域对象来包装在 VM 包装器中。UI 通过在 Wrapper
对象上调用第二个无参数构造函数来完成此操作。此构造函数实例化一个相应类型的新域对象,并将新域对象注入到其自己的主构造函数中。
保持灵活性
您会注意到,我们将两个列表框的 SelectedIndex
属性绑定到视图模型属性。这些属性被几个 ICommand
用于获取当前选定的对象(请参阅“添加订单”和“删除订单”ICommand
)。乍一看,这似乎是一个不必要的复杂化——为什么不直接让视图模型读取 ListBox 属性呢?
在 Model-View-ViewModel 设计中,视图模型不应直接依赖于视图。事实上,在生产应用程序中,我将所有视图类放在一个项目中,将所有视图模型类放在一个单独的视图模型项目中。.NET 编译器不允许循环引用,我的视图项目显然引用了视图模型项目。因此,.NET 编译器不允许我在视图模型中创建对视图的引用,这意味着我不能从视图模型直接依赖视图。这种方法在很大程度上使我的视图模型与它们所绑定的视图保持解耦。
额外的努力值得吗?假设我可以在视图模型中调用 listBoxCustomer.SelectedIndex
。现在,假设一个设计师在我完成后出现,并希望用更吸引人的东西替换我相当平淡的列表框。不幸的是,他们将无法这样做,因为我的视图模型是硬编码到列表框的。为了让设计师能够进行更改,我将不得不重新打开我的代码并将其绑定到任何新的控件。
如果我消除了对列表框控件的这种依赖,我就增加了设计的灵活性。因此,我不是将视图模型绑定到列表框,而是让视图绑定到视图模型中的一个属性,该属性提供我想要的对象索引。并且,我将视图和视图模型分离到不同的项目中。这样,设计师就可以用他们想要的任何东西替换列表框,只要他们向视图模型提供 Customers
集合中当前选定客户的索引。这意味着无论设计师想做什么,我都不必重新打开我的代码。
VM Wrapper Express 实用程序
VM Wrapper Express 实用程序为域模型类生成自定义对象包装器。这是主窗口,显示了 VmWrapperDemo 项目的域类:

以下是使用 VM Wrapper Express 创建视图模型类的步骤:
- 单击应用程序工具栏上的“加载程序集”按钮,然后从出现的“打开文件”对话框中选择要加载的程序集,从而加载程序集(EXE 或 DLL 文件)。您将看到一个类似上述屏幕截图的显示。
- 通过单击其复选框来选择要包装的类。
- 输入 Express 在创建包装器类时应使用的命名空间。此时,“生成包装器”按钮应变为启用状态。
- 单击“生成包装器”按钮,然后从出现的文件夹浏览器中选择包装器类文件的目标位置。单击“确定”后,将生成文件。
在开发过程中,您可能会多次重新生成包装器。您可以通过单击“保存设置”按钮并在出现的“保存文件”对话框中保存当前设置来保存特定程序集的配置。下次需要为您的项目生成包装器时,您可以通过单击“打开设置”而不是“加载程序集”来重新打开之前的会话。Express 将加载程序集,选择之前选择的类,并为您输入视图模型命名空间。您所要做的就是单击“生成类”按钮。
使用包装器类
演示应用程序展示了如何使用 VM Wrapper Express 生成的包装器类。首先,将您需要的包装器类添加到您的开发项目——我通常将它们放在项目外部的“Wrappers”文件夹中,然后将我需要的包装器导入到项目中。在演示应用程序中,导入到项目中的包装器类包含在 View Model 文件夹中,分布在三个子文件夹中:BaseClasses、ObjectWrappers 和 CollectionWrappers。
VM Wrapper Express 生成所有包装器类和基类。因此,您只需将所需的类(应包括所有基类)导入到您的项目中。完成此操作后,您就可以像使用域类一样使用包装器类。关键区别在于,现在包装的类实现了 PropertyChanged
和 CollectionChanged
事件,因此 WPF 将很好地与它们进行数据绑定。
如上所述,被包装的域对象以及包含它们的域集合都保留了它们的身份和特性。换句话说,即使类被包装在视图模型包装器类中,被包装的域类也可以直接引用。只需保留对域类的引用即可。这样,当需要持久化域对象时,它们就可以以正常方式保存。
请注意,持久化对象不会破坏域对象与其包装器之间的链接——无需以任何方式提取被包装的域对象。因此,如果您需要在保存被包装对象后继续使用包装器,您可以这样做,而无需采取任何额外步骤。
事实上,演示应用程序展示了使用包装类的大部分方面。一旦您了解了它的工作原理,您应该能够轻松地将包装的域对象集成到您自己的开发流程中。
其他更改
VM Wrapper Express 的原始版本使用属性注入将域对象注入到新创建的 VM 包装器中。通常,人们会为此任务使用构造函数注入,但 .NET 不允许在使用 new
关键字创建新对象时使用参数化构造函数。VmCollectionBase
类的 2.x 版本使用 System.Reflection.Activator.CreateInstance()
(它允许参数化构造函数)来创建新的 VM 对象。因此,所有包装器类现在都提供了两个构造函数:
- 一个接受领域对象的参数化构造函数;以及
- 一个创建新领域对象的无参数构造函数。
第一个构造函数用于初始化视图模型;第二个构造函数用于当在视图中操作导致创建新的 VM 对象时。
结论
我们主要讨论了包装领域类的概念,作为一种将 NHibernate 友好的类适应为 WPF 友好的方式。然而,有一种观点认为,无论是否使用 NHibernate,包装领域对象都是一个好主意。这是因为包装器使领域模型摆脱了任何显示或数据绑定的关注点。这促进了关注点分离,而这始终是一件好事。如果 Microsoft 将来改变其显示或数据绑定技术(在某些时候,它们肯定会的),重写包装器类将比重写整个领域模型容易得多。
换句话说,VM 包装器类使得在域模型中使用“POCO”对象(普通旧 CLR 对象)成为可能。领域驱动设计倡导者会告诉您,这正是保持域模型灵活并专注于其建模问题域的唯一工作的原因。最终,这使得应用程序随着时间的推移更容易维护且成本更低。
请就您在演示应用程序或 VM Wrapper Express 中发现的任何错误,或任何改进这两个程序的建议发表评论。我将发布文章更新以纳入重要的建议或错误修复。我很乐意回答有关文章和所含应用程序的问题,但我无法帮助解决其他编程问题。我希望您发现这篇文章和 VM Wrapper Express 对您像对我一样有用。
历史
- 2009年5月1日:发布原始版本
- 2009年5月5日:添加了“有必要这么复杂吗?”
- 2009年8月17日:VM Wrapper Express 2.0 版修订
- 2009年10月26日:VM Wrapper Express 代码更新
- 2009年10月31日:修复了集合包装器中的 WPF
DataGrid
错误,添加了应用程序图标 - 2010年1月15日:修复了 VM Wrapper Express 安装项目中的错误