练习翻译,翻译的很滥,大家骂吧,顺便指出我的错误,不胜感激.但这篇文章对于做WPF和silverlight的开发人员是很值得一看的.
原文:http://kentb.blogspot.com/2009/03/view-models-pocos-versus.html
这篇文章是我在做MVVM架构时一个短系列中文章的其中一篇,在这个系列中,我将会与大家分享一些思想和代码,以帮助在应用程序中写出更加清晰和简洁的MVVM代码.
相关文章:
- POCOs 对比 DependencyObject
- ViewModel
- DelegateCommand
- ActiveAwareCommand
如果你在WPF/Silverlight 开发中衡量过MVVM模式的话,你很快将面临一个抉择即View model该如何实现:它们应该是DependencyObject
还是POCO (Plain Old CLR Objects)?我见过可运行的程序同时使用两个方案.
这篇文章的目的就是讨论这两种方案.它肯定不会涉及到所有的问题及这两个方案的细微差别.但其会覆盖到我所发现的主要问题.我会从中列出其中较为有意义的问题.
性能
我很犹豫会提到这一点,因为我没有做过任何的测量并没发现这将会是一个问题所在. 在View model中使用DependencyObject
,理论上有2个能带来性能效益的原因:
- 更低的内存使用.
- 更快的绑定性能.
假设你的View model有很多属性的前提下,这些属性将会给它们赋默认值.WPF的依赖属性系统对于此进行过优化.一个典型的WPF控件拥有几十个甚至几百个的属性,它们多数都设置为默认值.若属性在不使用默认值的情况下才会使用额外的内存。如果View model按照相似的模式,那么你可能在使用DependencyObject
过程中会获得一些内存使用上的效益.但是如果真的是这样,我也会质疑你View model的设计.
第二点更加紧密关联,一个View model首要的工作就是提供属性以使得这个View可以绑定.比起绑定到CLR属性,绑定到DependencyObject时
WPF的绑定系统可能表现的更好.很可能是这样(再次说明,我没有测量过),但是这些不同点是可以忽视的,因为到目前为止这并不能证明将会是一个问题。
相关View Model属性编程通知
假设你有两个相关联的view model。比如一个ParentViewModel和
ChildViewModel。就说
ChildViewModel拥有一个引用的
ParentViewModel。当
Savings属性在
ParentViewModel发生改变时,我们需要在
ChildViewModel中更新继承的属性。使用
DependencyObject
充当view model,我们可以直接使用数据绑定来实现:
//this code is in the ChildViewModel constructor var binding = new Binding("Savings") { Source = parent }; BindingOperations.SetBinding(this, ChildViewModel.InheritanceProperty, binding);
使用POCOs的话,我们需要做多一点的工作。通常你的POCO view models会实现INotifyPropertyChanged接口,那么代码看起来会这样:
//this code is in the ChildViewModel constructor parent.PropertyChanged += delegate(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == "Savings") { Inheritance = parent.Savings; } };
这两种方案都特别的吸引我,但是通过绑定到DependencyObject
的方式则更加灵活和更少输入(指敲代码).
两者都具有着一个魔幻的字符("Savings", 在这种情况下).
我认识到这在使用POCO view model 将会成为一个问题.前阵子在我的类库中经过忙碌的工作并解决了这个问题. 为刺激你的欲望,使用我的类库允许你对任何的POCO做如下动作:
//this code is in the ChildViewModel constructor var binding = new TypedBinding<ParentViewModel, ChildViewModel>(parent, x => x.Savings, this, x => x.Inheritance); bindingManager.Bindings.Add(binding);
注意强类型的绑定使用了lambda 表达式. 另外请注意我打算用一个更简洁和流畅的接口. 比如:
bindingManager.Bind(parent).Property(x => x.Savings).To(this).Property(x => x.Inheritance);
View Model的序列化
有些时候你想要序列化你的view model,比如你可能想要实现IEditableObject接口,这样发生更改后的view model就可以回滚。一个简单的方法就是序列化一个view model的快照,当有需要的时候则回滚:
[Serializable] public class MyViewModel : IEditableObject { [NonSerialized] private object[] _copy; public MyViewModel() { Name = string.Empty; } public int Age { get; set; } public string Name { get; set; } public void BeginEdit() { //take a copy of current state var members = FormatterServices.GetSerializableMembers(GetType()); _copy = FormatterServices.GetObjectData(this, members); } public void CancelEdit() { //roll back to copy var members = FormatterServices.GetSerializableMembers(GetType()); FormatterServices.PopulateObjectMembers(this, members, _copy); } public void EndEdit() { //discard copy _copy = null; } }
这项工作对于POCO view model来说则没问题,但对于DependencyObject则不然.让我们回忆一下,若一个对象可以序列化,那么它以及它的父类都需要标记可序列化的attribute.
DependencyObject并没有标记为可序列化。
顺便提一下,你可能按照这种模式在一个可以序列化的结构中包装你的数据并序列化或者恢复这个结构以代替view model 本身.这个工作对于POCO来说可以运行的很好,但是再次,如果你使用DependencyObject你将会再次陷入头疼.
还有一些其他的原因你可能想要序列化你的view model.也许你想要克隆它.或者也许你想要保存某几个view model对象以应对应用程序重启.当有这项需求时,基于
DependencyObject的view model将会给你带来无限的痛苦.基本上,你唯一的选择就是用实现序列化来替代或者是使用非
IFormatter的序列化机制
同等性和哈希化
比较view model的同等性非常有用,或将其放在dictionary中. 比如,假设你有一个ReportsViewModel,其职责是管理和暴露一系列的
ReportViewModel(注意这些复数和单数的命名)
每个ReportViewModel
均包含报告的名字,该参数报告以及其执行结果:
现在假设你要缓存执行的报告。如果用户使用相同的参数来运行相同的报告,那么只需给出现存的结果.为了实现它,你自然而然地会在ReportViewModel
类(实现IEquatable<
)中去尝试重写ReportViewModel
>接口Equals()
和 GetHashCode()这两个方法。但是如果
ReportViewModel继承自
DependencyObject,你将会发现无法这么做。
DependencyObject
重写并密封了Equals()
and GetHashCode()这两个方法.
在某些场景下,view model将受到限制,变得无能为力.比如,你可以实现IEqualityComparer<ReportViewModel>的一个类并在适当的情况下使用它。不过,这将很快导致混乱
当然如果你的view model是一个POCO,它将不会面临这个问题。你只需要在你的view model类中提供最适当的实现
View Model的线程关联
view model其中的一项职责通常是在后台线程中执行繁重的工作.比如,假设你在UI里有一个刷新的Button导致一堆的Widget数据要从数据中重新加载并显示在列表中。那么当在获取数据时,你很少会停止UI的响应,所以你将决定把这项工作放在后台线程中:
public class MyViewModel { //this gets called when the user clicks the refresh button - we'll worry about how that happens in a later post public void LoadWidgets() { //do the heavy lifting in a BG thread ThreadPool.QueueUserWorkItem(delegate { var widgets = new List<Widget>(); //pretend I execute a database query here, would you kindly? var dataReader = command.ExecuteReader(); while (dataReader.Read()) { widgets.Add(new WidgetViewModel(dataReader["Name"])); } //now we have all our widgets read from the DB, so assign to the collection that the UI is bound to Dispatcher.Invoke(delegate { Widgets.Clear(); Widgets.AddRange(widgets); }); }); } }
总之我们可以在后台线程中做尽可能多的工作,只有在最后一步的时候我们需要切换到受限的UI线程去更新集合(使用正确的技术,甚至其是可选的)
此外,这对于POCO view model来说非常好,但当面对DependencyObject
时,就不那么顺利了.
DependencyObject
具有线程关联性-它只能通过被创建的线程上访问。
当我们在后台线程创建一堆的WidgetViewModel时,这些view model只能通过该线程访问。因此,只要在UI线程中试图去访问他们(通过绑定),那么将会抛出异常。
这里唯一的方案就是在UI线程上创建每个view model。这看起来非常丑陋,且容易出错,首先就否定了在后台线程带来的好处。
如果我们需要创建很多的view model,那么UI线程顿时就会花费很多时间就构造和初始化那些view model
代码可读性
你会注意到所有的这些问题都可以工作,对于这点,我并不怀疑.然而,所有这些变通的做法将导致代码一片混乱.代码可读性也是其众多重要品质之一.使用DependencyObject
充当view model导致支持的代码爆炸进而隐藏了代码的意图.此外,这些变通的变法并没给你带来任何东西
对于我来说,使用DependencyObject
s充当view model.好比最终钉在棺材里.起初在xaml中使用MVVM,我想要我的view models是可读的并且是可维护的.与其为view model扫除View中的丑陋的代码.我则更喜欢完全的放弃它.
总结
我认为POCO view model的优势超过DependencyObject
view model 在于其清晰和可信服的.而且,使用POCOs的不利条件几乎为0,同样的,我总是在我的项目中选择POCO。那么说,所有我所提到的问题都可以工作,当然你可能有一个说服力的论据去这么做(请在评论里让我知道如果你这么做).但是,到目前为止,我将继续坚持使用POCO 充当我的view model
我的总结:说实话,我也没有用过View Model继承自DependencyObject,因为给我第一感觉就是不好,如果都是DependencyObject绑定DependencyObject,岂不是UI和业务逻辑都是依赖于WPF了,这是我想到的一点.当然好处就是DependencyObject本身的好处了.上面说到了,更小的内存占用,更快的数据绑定.但是这么多的坏处,我是真的没有想过.
如序列化,重写Equals和GetHashCode,线程关联(这个我认为很重要,是需要注意的).还有一点讲到的就是代码的可读性,确实的DependencyObject的代码可读性比较差,这是一个事实,不过我认为这倒是其次,好的东西我们总是要接受的,不能因为麻烦就不用,主要看适用不适用.
经过上面的分析,大部分情况下还是使用实现INotifyPropertyChanged接口的类的. 而DependencyObject绑DependencyObject多数则用于控件与控件之间的绑定(因为本身这就是WPF技术的基础特性),对于控件而言,这也是非常之常用的。而非用于ViewModel.
我还真是想不出非要用ViewModel继承自DependencyObject的场景.如果你有用过的话,也请告诉我