我的具体问题是:如何将模型更改从模型传递到ViewModel?
在乔希的文章中,我没有看到他这样做。 ViewModel始终向Model询问属性。在Rachel的例子中,她确实拥有模型实现INotifyPropertyChanged
,并从模型中引发事件,但它们是供视图本身使用的(有关她为什么这样做的详细信息,请参阅她的文章/代码)。
我在任何地方都没有看到模型警告ViewModel模型属性更改的示例。这让我担心,也许是因为某些原因没有做到的。 是否有模式用于警告ViewModel模型中的更改?这似乎是必要的,因为(1)可以想象每个模型有超过1个ViewModel,(2)即使有只有一个ViewModel,模型上的某些操作可能会导致其他属性被更改。
我怀疑可能会有“你为什么要这样做?”的答案/评论。评论,所以这里是我的程序的描述。我是MVVM的新手,所以也许我的整个设计都有问题。我将简要介绍一下。
我编写的东西比“客户”或“产品”类更有趣(至少对我而言!)。我在编程BlackJack。
我有一个View,后面没有任何代码,只依赖于绑定到ViewModel中的属性和命令(参见Josh Smith的文章)。
无论好坏,我采取的态度是模型不仅应包含诸如PlayingCard
,Deck
之类的类,还应包含保持整个游戏状态的BlackJackGame
类并且知道当玩家破产时,经销商必须抽牌,以及玩家和经销商当前得分是什么(少于21,21,胸围等)。
从BlackJackGame
我公开了像“DrawCard”这样的方法,我发现在绘制卡片时,应更新CardScore
和IsBust
等属性并更新这些新值传达给ViewModel。也许这是错误的思考?
人们可以采取ViewModel调用DrawCard()
方法的态度,这样他就应该知道要求更新得分并找出他是否破产。意见?
在我的ViewModel中,我有逻辑来获取扑克牌的实际图像(基于套装,等级)并使其可用于视图。该模型不应该与此有关(也许其他ViewModel只使用数字而不是扑克牌图像)。当然,也许有些人会告诉我模型甚至不应该有BlackJack游戏的概念,而且应该在ViewModel中处理?
答案 0 :(得分:56)
如果您希望模型提醒ViewModel更改,则应实现INotifyPropertyChanged,并且ViewModel应订阅以接收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事件附加到它的情况,那么您可以使用Messaging系统,例如Prism的EventAggregator
或MVVM Light {{ 1}}。
我的博客上有brief overview of messaging systems,但总结一下,任何对象都可以广播消息,任何对象都可以订阅侦听特定消息。因此,您可以从一个对象广播Messenger
,另一个对象可以订阅以侦听这些类型的消息,并在听到一个消息时更新它的PlayerScoreHasChangedMessage
属性。
但我认为您所描述的系统不需要这样做。
在理想的MVVM世界中,您的应用程序由ViewModel组成,而您的模型只是用于构建应用程序的块。它们通常只包含数据,因此不会有PlayerScore
等方法(将在ViewModel中)
所以你可能会有这样的普通模型数据对象:
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;
}
}
}
(上面的对象都应该实现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);
}
}
,但为了简单起见,我把它留了出来)
答案 1 :(得分:21)
简短回答:这取决于细节。
在您的示例中,模型正在“自行更新”,并且这些更改当然需要以某种方式传播到视图。由于视图只能直接访问视图模型,因此表示模型必须将这些更改传递给相应的视图模型。这样做的既定机制当然是INotifyPropertyChanged
,这意味着你将得到这样的工作流程:
PropertyChanged
事件DataContext
,属性绑定等PropertyChanged
并提升自己的PropertyChanged
作为回应另一方面,如果您的模型包含很少(或没有)业务逻辑,或者由于某些其他原因(例如获得事务性能),您决定让每个视图模型“拥有”其包装模型,然后对模型进行所有修改将通过视图模型,因此不需要这样的安排。
我在另一个MVVM问题here中描述了这样的设计。
答案 2 :(得分:3)
您的选择:
在我看来,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对象转移到一个集合中,并听取集合......如果有任何变化,你可以在任何地方了解它。
我会说实话,这可以加入“为什么不是由编译器自动处理的INotifyPropertyChanged”讨论,该讨论转移到:c#中的每个对象都应该有通知它是否有任何部分被更改;即默认情况下实现INotifyPropertyChanged。但它没有,而且需要最少努力的最佳途径是使用IL Weaving(特别是FODY)。
答案 3 :(得分:3)
相当老的线程,但经过大量搜索,我想出了自己的解决方案:一个PropertyChangedProxy
使用此类,您可以轻松注册到其他人的NotifyPropertyChanged,并在注册属性被触发时采取适当的操作。
下面是一个示例,当你有一个模型属性“Status”可以改变它自己然后应该自动通知ViewModel在它的“Status”属性上触发它自己的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));
}
}
这是班级本身:
/// <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));
}
}
}
答案 4 :(得分:2)
基于 INotifyPropertyChanged 和 INotifyCollectionChanged 的通知正是您所需要的。为了通过订阅属性更改来简化您的生活,对属性名称进行编译时验证,避免内存泄漏,我建议您使用Josh Smith's MVVM Foundation中的 PropertyObserver 。由于此项目是开源的,因此您可以从源代码中将该类添加到项目中。
要了解,如何使用PropertyObserver阅读this article。
另外,请深入了解Reactive Extensions (Rx)。您可以在模型中公开 IObserver&lt; T&gt; ,并在视图模型中订阅它。
答案 5 :(得分:2)
我的总结:
MVVM组织背后的想法是允许更容易地重用视图和模型,并允许解耦测试。您的视图模型是表示视图实体的模型,您的模型表示业务实体。
如果您想稍后制作扑克游戏怎么办? UI的大部分应该是可重用的。如果您的视图模型中绑定了游戏逻辑,则无需重新编程视图模型就很难重用这些元素。如果您想更改用户界面怎么办?如果您的游戏逻辑与视图模型逻辑耦合,则需要重新检查您的游戏是否仍然有效。如果您想创建桌面和Web应用程序,该怎么办?如果您的视图模型包含游戏逻辑,那么尝试并排维护这两个应用程序会变得很复杂,因为应用程序逻辑将不可避免地与视图模型中的业务逻辑绑定。
数据更改通知和数据验证发生在每一层(视图,视图模型和模型)中。
模型包含您的数据表示(实体)和特定于这些实体的业务逻辑。一副牌是具有固有属性的逻辑“东西”。好的套牌不能放入重复的卡片。它需要暴露一种获得顶级卡的方式。它需要知道不要提供比它剩下的更多的卡。这种甲板行为是模型的一部分,因为它们是一副牌固有的。还有经销商模型,玩家模型,手模型等。这些模型可以并且将会互动。
视图模型将包含表示和应用程序逻辑。与显示游戏相关的所有工作都与游戏的逻辑分开。这可能包括将手显示为图像,向经销商模型请求卡,用户显示设置等。
文章的内容:
基本上,我想解释的方式就是你的业务 逻辑和实体构成模型。这是你具体的 应用程序正在使用,但可以在许多应用程序之间共享。
视图是表示层 - 与实际有关的任何内容 直接与用户交流。
ViewModel基本上是特定于您的“胶水” 将两者联系在一起的应用程序。
我在这里有一个很好的图表,显示了它们的界面:
在你的情况下 - 让我们解决一些具体细节......
验证:这通常有两种形式。验证相关 用户输入将在ViewModel(主要)和View中发生 (即:处理阻止输入文本的“数字”TextBox 对于你在视图中等)。因此,输入的验证 用户通常是VM关注的问题。话虽如此,经常有一个 第二个“层”验证 - 这是验证数据 使用符合业务规则。这通常是其中的一部分 模型本身 - 当您将数据推送到模型时,它可能会导致 验证错误。然后,VM必须重新映射此信息 回到视图。
操作“幕后没有视图,比如写入DB, 发送电子邮件等“:这实际上是”特定领域“的一部分 操作“在我的图表中,并且纯粹是模型的一部分。 这是您尝试通过应用程序公开的内容。该 ViewModel充当公开此信息的桥梁,但是 操作是纯粹的模型。
ViewModel的操作:ViewModel需要的不仅仅是INPC - 它还需要任何特定于您的应用程序的操作(而不是您的业务逻辑),例如保存首选项和用户状态, 这将改变应用程序。通过app。,甚至在连接时 相同的“模型”。
考虑它的一个好方法 - 假设您想制作2个版本的 订购系统。第一个是WPF,第二个是Web 接口
处理订单本身的共享逻辑(发送 电子邮件,进入数据库等)是模型。你的申请是 将这些操作和数据暴露给用户,但是在2中进行 方式。
在WPF应用程序中,用户界面(查看者与之交互的内容) with)是“视图” - 在Web应用程序中,这基本上是 代码(至少最终)变成了javascript + html + css 在客户端。
ViewModel是适应你的“胶水”的其余部分 model(这些操作与排序有关),以使其工作 使用您正在使用的特定视图技术/图层。
答案 6 :(得分:1)
我一直在倡导定向模型 - &gt;查看模型 - &gt;现在可以查看更改的流程,如您在2008年MVVM article的更改流程部分所示。这需要在模型上实施INotifyPropertyChanged
。据我所知,这是常见的做法。
因为你提到约什史密斯,请看看his PropertyChanged class。这是订阅模型的INotifyPropertyChanged.PropertyChanged
事件的辅助类。
您实际上可以更进一步地采用这种方法,因为我最近创建了my PropertiesUpdater class。视图模型上的属性计算为包含模型上一个或多个属性的复杂表达式。
答案 7 :(得分:1)
这些家伙做了一个惊人的工作来回答这个问题,但在这样的情况下,我真的觉得MVVM模式是一种痛苦所以我会去使用监督控制器或被动视图方法,并放弃绑定系统至少模型对象,它们自己生成变化。
答案 8 :(得分:1)
在Model中实现 INotifyPropertyChanged 并在ViewModel中监听它并没有错。 实际上你甚至可以在XAML中找到模型的属性:{Binding Model.ModelProperty}
至于依赖/计算的只读属性,到目前为止,我还没有看到比这更好,更简单的东西: https://github.com/StephenCleary/CalculatedProperties。它非常简单但非常有用,它真的是&#34; MVVM的Excel公式&#34; - 就像Excel传播对公式单元格的更改一样,无需额外的努力。
答案 9 :(得分:0)
您可以从模型中引发事件,视图模型需要订阅这些事件。
例如,我最近参与了一个项目,我必须为其生成一个树视图(当然,模型具有层次结构性质)。在模型中,我有一个名为ChildElements
的可观察集合。
在viewmodel中,我已经存储了对模型中对象的引用,并订阅了observablecollection的CollectionChanged
事件,如下所示:ModelObject.ChildElements.CollectionChanged += new CollectionChangedEventHandler(insert function reference here)
...
然后,一旦模型发生变化,您的viewmodel就会自动得到通知。您可以使用PropertyChanged
遵循相同的概念,但是您需要从模型中明确提出属性更改事件才能生效。
答案 10 :(得分:0)
在我看来,这似乎是一个非常重要的问题 - 即使没有压力要做。我正在研究一个涉及TreeView的测试项目。有一些菜单项和映射到命令的项目,例如Delete。目前,我正在视图模型中更新模型和视图模型。
例如,
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
}
这很简单,但似乎有一个非常基本的缺陷。典型的单元测试将执行命令,然后检查视图模型中的结果。但这并不能测试模型更新是否正确,因为两者同时更新。
因此,最好使用PropertyObserver等技术让模型更新触发视图模型更新。相同的单元测试现在只有在两个操作都成功的情况下才能工作。
我意识到,这不是一个可能的答案,但它似乎值得推出。