WPF中的MVVM – 如何提醒ViewModel的模型中的变化…或者我应该?
我正在浏览一些MVVM文章,主要是这个和这个 。
我的具体问题是: 如何将模型更改从模型传递到ViewModel?
在乔希的文章中,我没有看到他这样做。 ViewModel总是要求模型的属性。 在Rachel的例子中,她的模型实现了INotifyPropertyChanged
,并且引发了模型中的事件,但是它们被视图本身所使用(请参阅她的文章/代码以获取更多关于她为什么这么做的细节)。
我没有看到模型提示ViewModel对模型属性进行更改的示例。 这让我担心,也许这不是由于某种原因。 有没有一种模式来提醒ViewModel模型中的变化? 这似乎是必要的,因为(1)对于每个模型可以有多于1个ViewModel,并且(2)即使只有一个ViewModel,模型上的一些动作也可能导致其他属性被改变。
我怀疑可能会有“你为什么要这样做?”的forms的答案/评论。 评论,所以这里是我的程序的描述。 我是MVVM的新手,所以也许我的整个devise都是错误的。 我将简要描述一下。
我正在编写一些比“客户”或“产品”类更有趣的东西(至less对我来说!)。 我在编程BlackJack。
我有一个没有任何代码的View,只是依赖于绑定ViewModel中的属性和命令(参见Josh Smith的文章)。
无论是好还是坏,我都认为模型应该包含不仅仅是“ PlayingCard
,“ Deck
等类别,而且还包含保持整个游戏状态的BlackJackGame
类,并且知道玩家什么时候破产,庄家必须画牌,以及玩家和经销商当前得分是多less(小于21,21,胸围等)。
在BlackJackGame
我公开了像“DrawCard”这样的方法,并且在我看来,当绘制卡片时, CardScore
和IsBust
等属性应该被更新,并且这些新的值传递给ViewModel。 也许这是错误的思想?
有人可能会认为ViewModel叫做DrawCard()
方法,所以他应该知道要求更新的分数,看看他是不是破产了。 意见?
在我的ViewModel中,我有逻辑来获取纸牌的实际图像(基于套装,等级),并将其用于视图。 模型不应该关心这个(也许其他ViewModel只会使用数字,而不是纸牌图像)。 当然,也许有人会告诉我,该模型甚至不应该有一个BlackJack游戏的概念,应该在ViewModel中处理?
如果您希望模型向ViewModel发出更改提醒,则应实现INotifyPropertyChanged ,ViewModels应订阅接收PropertyChange通知。
你的代码可能看起来像这样:
// Attach EventHandler PlayerModel.PropertyChanged += PlayerModel_PropertyChanged; ... // When property gets changed in the Model, raise the PropertyChanged // event of the ViewModel copy of the property PlayerModel_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == "SomeProperty") RaisePropertyChanged("ViewModelCopyOfSomeProperty"); }
但是,通常只有在多个对象将对模型数据进行更改时才需要这样做,而这通常不是这种情况。
如果您曾经遇到实际上没有对Model属性的引用来将PropertyChanged事件附加到它的情况,那么您可以使用Prism的EventAggregator
或MVVM Light的Messenger
之类的Messenger
。
我在我的博客上简要概述了消息传递系统 ,但是总而言之,任何对象都可以广播一条消息,任何对象都可以订阅来收听特定的消息。 因此,您可以从一个对象广播PlayerScoreHasChangedMessage
,而另一个对象可以订阅,以侦听这些types的消息,并在听到一个消息时更新它的PlayerScore
属性。
但我不认为这是你所描述的系统所需要的。
在理想的MVVM世界中,您的应用程序由您的ViewModel组成,而您的模型只是用于构build应用程序的模块。 它们通常只包含数据,所以不会有像DrawCard()
这样的方法(在ViewModel中)
所以你可能会有这样的普通模型数据对象:
class CardModel { int Score; SuitEnum Suit; CardEnum CardValue; } class PlayerModel { ObservableCollection<Card> FaceUpCards; ObservableCollection<Card> FaceDownCards; int CurrentScore; bool IsBust { get { return Score > 21; } } }
你会有一个ViewModel对象
public class GameViewModel { ObservableCollection<CardModel> Deck; PlayerModel Dealer; PlayerModel Player; ICommand DrawCardCommand; void DrawCard(Player currentPlayer) { var nextCard = Deck.First(); currentPlayer.FaceUpCards.Add(nextCard); if (currentPlayer.IsBust) // Process next player turn Deck.Remove(nextCard); } }
(上面的对象都应该实现INotifyPropertyChanged
,但为了简单起见,我把它放在了外面)
简短的回答:这取决于具体情况。
在你的例子中,模型正在“自行更新”,当然这些变化需要以某种方式传播给观点。 由于视图只能直接访问视图模型,这意味着模型必须将这些变化传递给相应的视图模型。 这样做的机制当然是INotifyPropertyChanged
,这意味着你会得到这样一个工作stream程:
- Viewmodel创build并包装模型
- Viewmodel订阅模型的
PropertyChanged
事件 - Viewmodel被设置为视图的
DataContext
,属性被绑定等 - 查看触发器对viewmodel的操作
- Viewmodel调用模型的方法
- 模型更新自己
- Viewmodel处理模型的
PropertyChanged
并引发它自己的PropertyChanged
作为响应 - 视图反映其绑定的变化,closures反馈循环
另一方面,如果你的模型包含很less(或没有)业务逻辑,或者由于某种其他原因(例如获得事务性能力),你决定让每个视图模型“拥有”它的包装模型,然后对模型的所有修改都会通过这样的视图模式,这样的安排将不是必要的。
我在这里描述了另外一个MVVM问题的devise。
相当古老的线程,但经过大量的search,我想出了我自己的解决scheme:一个PropertyChangedProxy
通过这个类,您可以轻松注册到其他人的NotifyPropertyChanged,并在注册的属性被触发时采取适当的行动。
下面是当你有一个模型属性“状态”,它可以改变它自己,然后应该自动通知ViewModel触发它自己的PropertyChanged它的“状态”属性,这样可能看起来像样本,以便该视图也通知: )
public class MyModel : INotifyPropertyChanged { private string _status; public string Status { get { return _status; } set { _status = value; OnPropertyChanged(); } } // Default INotifyPropertyChanged public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { var handler = PropertyChanged; if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); } } public class MyViewModel : INotifyPropertyChanged { public string Status { get { return _model.Status; } } private PropertyChangedProxy<MyModel, string> _statusPropertyChangedProxy; private MyModel _model; public MyViewModel(MyModel model) { _model = model; _statusPropertyChangedProxy = new PropertyChangedProxy<MyModel, string>( _model, myModel => myModel.Status, s => OnPropertyChanged("Status") ); } // Default INotifyPropertyChanged public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { var handler = PropertyChanged; if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); } }
这是class级本身:
/// <summary> /// Proxy class to easily take actions when a specific property in the "source" changed /// </summary> /// Last updated: 20.01.2015 /// <typeparam name="TSource">Type of the source</typeparam> /// <typeparam name="TPropType">Type of the property</typeparam> public class PropertyChangedProxy<TSource, TPropType> where TSource : INotifyPropertyChanged { private readonly Func<TSource, TPropType> _getValueFunc; private readonly TSource _source; private readonly Action<TPropType> _onPropertyChanged; private readonly string _modelPropertyname; /// <summary> /// Constructor for a property changed proxy /// </summary> /// <param name="source">The source object to listen for property changes</param> /// <param name="selectorExpression">Expression to the property of the source</param> /// <param name="onPropertyChanged">Action to take when a property changed was fired</param> public PropertyChangedProxy(TSource source, Expression<Func<TSource, TPropType>> selectorExpression, Action<TPropType> onPropertyChanged) { _source = source; _onPropertyChanged = onPropertyChanged; // Property "getter" to get the value _getValueFunc = selectorExpression.Compile(); // Name of the property var body = (MemberExpression)selectorExpression.Body; _modelPropertyname = body.Member.Name; // Changed event _source.PropertyChanged += SourcePropertyChanged; } private void SourcePropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == _modelPropertyname) { _onPropertyChanged(_getValueFunc(_source)); } } }
基于INotifyPropertyChanged和INotifyCollectionChanged的通知正是你所需要的。 为了简化您的生活,订阅属性更改,编译时validation属性名称,避免内存泄漏,我build议您使用Josh Smith的MVVM基金会的 PropertyObserver 。 由于这个项目是开源的,你可以从源代码将这个类添加到你的项目中。
要理解,如何使用PropertyObserver阅读这篇文章 。
另外,请看Reactive Extensions(Rx) 。 您可以从模型中公开IObserver <T>并在视图模型中订阅它。
您的select:
- 实现INotifyPropertyChanged
- 活动
- 带代理操作器的POCO
就我所见, INotifyPropertyChanged
是.Net的基础部分。 即它在System.dll
。 在你的“模型”中实现它类似于实现一个事件结构。
如果你想要纯粹的POCO,那么你就必须通过代理/服务来操作你的对象,然后通过监听代理来告诉ViewModel的变化。
就个人而言,我只是松散地实施INotifyPropertyChanged,然后使用FODY来为我做这个肮脏的工作。 它看起来和感觉POCO。
一个例子(使用FODY到IL编译PropertyChanged提高者):
public class NearlyPOCO: INotifyPropertyChanged { public string ValueA {get;set;} public string ValueB {get;set;} public event PropertyChangedEventHandler PropertyChanged; }
那么你可以让你的ViewModel监听PropertyChanged的任何改变; 或财产特定的变化。
INotifyPropertyChanged路由的美妙之处在于,它是通过一个Extended ObservableCollection链接起来的。 所以你把你附近的poco对象转换成一个集合,然后听集合…如果有什么变化的话,你可以在任何地方了解它。
老实说,这可能会join“为什么不是由编译器自动处理INotifyPropertyChanged”的讨论,这个讨论决定了:c#中的每个对象都应该有设施来通知它的任何部分是否被改变; 即默认实现INotifyPropertyChanged。 但是这并不是最好的路线,也就是使用IL Weaving(特别是FODY )。
我一直在倡导定向模型 – >视图模型 – >查看stream的变化很长一段时间,正如你可以在我的MVVM文章从2008年的stream变化部分看到的。这需要在模型上实现INotifyPropertyChanged
。 据我所知,这是成为惯例。
因为你提到乔什·史密斯,请看看他的PropertyChanged类 。 它是订阅模型的INotifyPropertyChanged.PropertyChanged
事件的帮助类。
实际上你可以采取这种方法更进一步,因为我最近创build了我的PropertiesUpdater类 。 视图模型上的属性被计算为在模型上包含一个或多个属性的复杂expression式。
您可以从视图模型需要订阅的模型中引发事件。
例如,我最近在一个项目上工作,我不得不生成一个树视图(自然,该模型具有层次性)。 在模型中,我有一个名为ChildElements
的可观察ChildElements
。
在viewmodel中,我已经存储了对模型中对象的引用,并订阅了observablecollection的CollectionChanged
事件,如下所示: ModelObject.ChildElements.CollectionChanged += new CollectionChangedEventHandler(insert function reference here)
…
然后,一旦模型发生变化,您的视图模型就会自动得到通知。 您可以使用PropertyChanged
遵循相同的概念,但是您需要明确地从您的模型中引发属性更改事件才能工作。
这些人做了一个了不起的工作回答这个,但在这样的情况下,我真的觉得,MVVM模式是一个痛苦,所以我会去使用监督控制器或被动视图方法,并放弃绑定系统至less对模型对象是自己产生的变化。
我发现这篇文章很有帮助: http : //social.msdn.microsoft.com/Forums/vstudio/en-US/3eb70678-c216-414f-a4a5-e1e3e557bb95/mvvm-businesslogic-is-part-of-the-?forum = WPF
我的总结:
MVVM组织背后的思想是允许更容易重用视图和模型,并允许分离testing。 您的视图模型是表示视图实体的模型,您的模型表示业务实体。
如果你以后想做一个扑克游戏怎么办? 大部分的UI应该是可重用的。 如果你的游戏逻辑被束缚在你的视图模型中,那么重新使用这些元素而不必重新编程视图模型是非常困难的。 如果你想改变你的用户界面呢? 如果您的游戏逻辑与您的视图模型逻辑耦合,则需要重新检查您的游戏是否仍然有效。 如果你想创build一个桌面和一个Web应用程序呢? 如果你的视图模型包含游戏逻辑,试图并行维护这两个应用程序将变得复杂,因为应用程序逻辑将不可避免地与视图模型中的业务逻辑绑定在一起。
数据更改通知和数据validation发生在每个层(视图,视图模型和模型)中。
该模型包含您的数据表示(实体)和特定于这些实体的业务逻辑。 一副牌是具有固有属性的逻辑“东西”。 一个好的牌组不能有重复的牌放进去。 它需要揭露一种方式来获得顶级卡。 它需要知道不要发放更多的卡片。 这样的套牌行为是模型的一部分,因为它们是一副牌的固有特征。 还会有经销商模型,玩家模型,手模型等。这些模型可以和将会相互作用。
视图模型将由演示文稿和应用程序逻辑组成。 所有与显示游戏有关的工作都与游戏的逻辑是分开的。 这可以包括将手显示为图像,向经销商模型请求卡,用户显示设置等。
文章的胆量:
基本上,我喜欢解释这一点的方式是您的业务逻辑和实体组成模型。 这是您的特定应用程序正在使用,但可以在许多应用程序共享。
视图是表示层 – 任何与用户直接接口相关的东西。
ViewModel基本上是你的应用程序特有的“粘合剂”,它将两者联系在一起。
我在这里有一个很好的图表,显示他们如何界面:
在你的情况 – 让我们解决一些具体问题…
validation:通常有两种forms。 与用户input有关的validation将发生在ViewModel(主要)和视图(即:“数字”文本框防止input的文本在视图中为您处理等)。 因此,来自用户的input的validation通常是VM关心的问题。 话虽如此,通常还有第二层validation – 这是validation所使用的数据是否符合业务规则。 这通常是模型本身的一部分 – 将数据推送到模型时,可能会导致validation错误。 虚拟机将不得不将这些信息重新映射回查看。
操作“在幕后,没有任何意见,如写入数据库,发送电子邮件等”:这实际上是我图中“特定领域操作”的一部分,实际上完全是模型的一部分。 这就是你要通过应用程序公开的内容。 ViewModel作为揭示这些信息的桥梁,但操作是纯粹的模型。
ViewModel的操作:ViewModel需要的不仅仅是INPC – 它还需要特定于您的应用程序(而不是您的业务逻辑)的任何操作,比如保存偏好和用户状态等。这将会改变应用程序。 甚至在连接相同的“模型”时也是如此。
一个好的方法来思考 – 假设你想制作你订购系统的两个版本。 第一个是WPF,第二个是Web界面。
处理订单本身的共享逻辑(发送电子邮件,进入数据库等)是Model。 您的应用程序将这些操作和数据公开给用户,但是以两种方式进行。
在WPF应用程序中,用户界面(查看器与之交互)是“视图” – 在Web应用程序中,这基本上是(至less最终)在客户端上转换为javascript + html + css的代码。
ViewModel是调整模型所需的“粘合剂”的其余部分(这些操作与订购相关),以使其与您正在使用的特定视图技术/图层协同工作。
在我看来,这是一个非常重要的问题,即使没有压力。 我正在做一个testing项目,涉及到一个TreeView。 有菜单项,这些都映射到命令,例如删除。 目前,我正在从视图模型中更新模型和视图模型。
例如,
public void DeleteItemExecute () { DesignObjectViewModel node = this.SelectedNode; // Action is on selected item DocStructureManagement.DeleteNode(node.DesignObject); // Remove from application node.Remove(); // Remove from view model Controller.UpdateDocument(); // Signal document has changed }
这很简单,但似乎有一个非常基本的缺陷。 典型的unit testing会执行命令,然后检查视图模型中的结果。 但是这并不能certificate模型更新是正确的,因为两者是同时更新的。
所以也许最好使用像PropertyObserver这样的技术让模型更新触发视图模型更新。 同样的unit testing现在只有在这两个动作都成功的情况下才能起作用
我意识到这不是一个可能的答案,但似乎值得在那里提出。
在Model内部实现INotifyPropertyChanged并在ViewModel中侦听它没有任何错误。 事实上,你甚至可以在XAML中直接进入模型的属性:{Binding Model.ModelProperty}
至于从属/计算只读属性,到目前为止,我还没有看到比这更好,更简单: https : //github.com/StephenCleary/CalculatedProperties 。 这非常简单,但却非常有用,它确实是“MVVM的Excel公式” – 就像Excel将更改传播到公式单元格一样,而不需要额外的工作。