WPF中的MVVM模式 - 如何通知ViewModel Model的更改...还是不应该?

133

我正在阅读一些关于 MVVM 架构的文章,主要是 这篇这篇

我的具体问题是:如何将 Model 的更改通知给 ViewModel?

在 Josh 的文章中,我没有看到他做过这件事。ViewModel 总是向 Model 请求属性。在 Rachel 的示例中,她确实让模型实现了 INotifyPropertyChanged 接口,并从模型触发事件,但它们是供视图本身使用的(有关她为什么这样做的更多细节,请参阅她的文章/代码)。

我没有看到任何例子中, 模型如何向 ViewModel 发出更改模型属性的通知。这让我开始担心,可能由于某种原因不会这样做。 是否有一种模式可以通知 ViewModel 模型的更改? 这似乎是必要的,因为(1)可以想象每个模型可能有不止一个 ViewModel,(2)即使只有一个 ViewModel,对模型的某些操作可能导致其他属性发生变化。

我怀疑可能会有以下回答/评论:"为什么要那样做呢?",因此这里描述一下我的程序。我是 MVVM 的新手,所以也许我的整个设计都有问题。我简要地描述一下它。

我正在编写比 "Customer" 或 "Product" 类更有趣(至少对我来说如此)的东西——21点游戏。

我有一个 View,它没有任何代码,只依赖于 ViewModel 中的属性和命令进行绑定(请参见 Josh Smith 的文章)。

无论是好是坏,我认为模型(Model)不仅应该包含类(如PlayingCard, Deck),而且还应该包括BlackJackGame类,该类维护整个游戏的状态,并知道玩家何时爆牌(bust),庄家必须抽牌,以及玩家和庄家当前的得分情况(小于21,21点,爆牌等)。

我会从BlackJackGame公开一些方法,比如"DrawCard",当抽取一张卡片时,我想更新CardScore和IsBust等属性,并将这些新值传达给ViewModel。或许这种想法是错误的?

有人可能认为ViewModel调用了DrawCard()方法,所以他应该知道要求更新分数并查看自己是否爆牌。你们怎么看?

在我的ViewModel中,我有获取实际扑克牌图像(基于花色和点数)并使其在视图中可用的逻辑。模型不应涉及此事(也许其他ViewModel只使用数字,而不是扑克牌图像)。当然,也许有些人会告诉我,模型甚至不应该有BlackJack游戏的概念,而应该在ViewModel中处理?


3
你所描述的交互听起来只需要标准的事件机制。该模型可以公开一个名为OnBust的事件,虚拟机可以订阅它。我猜你也可以使用IEA方法。 - code4life
说实话,如果我要制作一个真正的二十一点“应用” ,我的数据将会隐藏在几个层次的服务/代理后面,并且会进行一系列严格的单元测试,就像A+B=C一样。它将是代理/服务通知变化的媒介。 - Meirion Hughes
1
谢谢大家!不幸的是,我只能选择一个答案。我选择Rachel的答案,因为她提供了额外的架构建议并清理了原始问题。但是有很多很好的答案,我很感激。- Dave - Dave
3
值得一提的是:经过多年来努力尝试维护域模型和每个域模型相关的视图模型概念之后,我现在认为同时拥有这两者会导致不符合DRY原则;更好的做法是在一个对象上拥有两个接口——“域接口”和“视图模型接口”,通过这样的方式实现关注点的分离可以更加容易,而且不会因为混淆或缺乏同步而出现问题。这个对象是一个“身份对象”——它唯一地代表着实体。为了维护领域代码和视图代码的分离,需要更好的工具在一个类内部实现这种分离。 - ToolmakerSteve
11个回答

74

如果你想让你的模型通知视图模型发生了变化,它们应该实现INotifyPropertyChanged接口,而视图模型应该订阅以接收属性更改通知。

你的代码可能看起来像这样:

// 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");
}

但通常只有在多个对象需要更改模型的数据时(这通常不是情况),才需要使用此方法。

如果您遇到一种情况,即实际上没有引用到您的模型属性来附加 PropertyChanged 事件,则可以使用消息系统,例如 Prism 的 EventAggregator 或 MVVM Light 的 Messenger

我在我的博客上有一个消息系统简介,总结一下,任何对象都可以广播消息,任何对象都可以订阅特定的消息以进行监听。因此,您可能会从一个对象广播出一个 PlayerScoreHasChangedMessage,而另一个对象可以订阅侦听这些类型的消息,当收到消息时更新其 PlayerScore 属性。

但我认为您所描述的系统不需要这样做。

在理想的 MVVM 环境中,您的应用程序由您的 ViewModels 组成,而您的 Models 只是用于构建应用程序的块。它们通常只包含数据,因此不会有像 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,但为简单起见我将其省略)


3
一般来说,所有的业务逻辑/规则都放在模型中吗?抽卡上限为21(但荷官必须停留在17),可以分牌等逻辑放在哪里?我认为它们都应该放在模型类中,因此我觉得我需要一个BlackJackGame控制器类在模型中。我仍在努力理解这个问题,并希望有例子/参考资料。这个例子关于二十一点游戏取自一份iOS编程的iTunes课程,MVC模式中业务逻辑/规则明确地放在了模型类中。 - Dave
3
是的,DrawCard()方法应该在ViewModel中,以及其他游戏逻辑。在理想的MVVM应用程序中,您应该能够完全在没有UI的情况下运行应用程序,只需创建ViewModel并运行它们的方法,例如通过测试脚本或命令提示窗口。Models通常只是包含原始数据和基本数据验证的数据模型。 - Rachel
6
感谢 Rachel 提供的帮助。我需要进一步研究这个问题或者写另一个问题;我仍然对游戏逻辑的位置感到困惑。你(和其他人)主张将其放在 ViewModel 中,而其他人则说“业务逻辑”,在我的情况下,我认为是游戏规则和游戏状态应该归属于模型(例如:http://msdn.microsoft.com/en-us/library/gg405484%28v=pandp.40%29.aspx 和 https://dev59.com/TWgu5IYBdhLWcg3w8bds)。我认识到在这个简单的游戏中,这可能并不重要。但知道这点会很好。谢谢! - Dave
4
避免内存泄漏。 使用弱事件模式(WeakEvent pattern)。 https://joshsmithonwpf.wordpress.com/2009/07/11/one-way-to-avoid-messy-propertychanged-event-handling/ - JJS
6
这怎么成了得票最高且被接受的回答呢?不,不,不!业务逻辑应该放在模型里,视图模型处理应用程序逻辑。视图模型不应该知道游戏的实际规则! - keelerjr12
显示剩余2条评论

28
简短回答:具体情况而定。
在您的示例中,模型是“自主”更新的,这些更改当然需要以某种方式传播到视图中。由于视图只能直接访问视图模型,这意味着模型必须将这些更改通知给相应的视图模型。为此建立的机制当然是 INotifyPropertyChanged,这意味着你会得到以下工作流程:
  1. 创建视图模型并包装模型
  2. 视图模型订阅模型的 PropertyChanged 事件
  3. 将视图模型设置为视图的 DataContext,绑定属性等
  4. 视图触发视图模型上的操作
  5. 视图模型调用模型上的方法
  6. 模型更新自身
  7. 视图模型处理模型的 PropertyChanged 并相应地引发自己的 PropertyChanged
  8. 视图反映其绑定中的更改,关闭反馈循环
另一方面,如果您的模型包含少量(或没有)业务逻辑,或者出于其他原因(例如获得事务能力),您决定让每个视图模型“拥有”其封装的模型,则对模型进行的所有修改都将通过视图模型传递,因此不需要这样的安排。
我在另一个 MVVM 问题中描述了这样的设计。 这里.

你好,你制作的列表非常棒。但是我在7和8上遇到了问题。具体来说:我有一个ViewModel,它没有实现INotifyPropertyChanged。它包含一个子列表,该子列表本身又包含一个子列表(它用作WPF Treeview控件的ViewModel)。如何使UserControl DataContext ViewModel“监听”任何子项(TreeviewItems)中的属性更改?我应该如何订阅所有实现INotifyPropertyChanged的子元素?还是我应该提出一个单独的问题? - Igor

6

你的选择:

  • 实现INotifyPropertyChanged
  • 事件
  • 带有代理操作器的POCO

在我看来,INotifyPropertyChanged是.Net的一个基本部分,即它在System.dll中。在您的“Model”中实现它类似于实现事件结构。

如果您想要纯粹的POCO,则必须通过代理/服务来操作您的对象,然后通过监听代理来通知您的ViewModel更改。

个人而言,我只是松散地实现了INotifyPropertyChanged,然后使用FODY来为我完成脏活。它看起来和感觉像POCO。

以下是一个示例(使用FODY将PropertyChanged raisers IL Weave):

public class NearlyPOCO: INotifyPropertyChanged
{
     public string ValueA {get;set;}
     public string ValueB {get;set;}

     public event PropertyChangedEventHandler PropertyChanged;
}

然后您可以让您的 ViewModel 监听 PropertyChanged 以检测任何更改;或者特定于属性的更改。

使用 INotifyPropertyChanged 的美妙之处在于,您可以将其与 扩展 ObservableCollection 链接起来。因此,您可以将近 POCO 对象转储到集合中,并监听该集合...如果任何地方发生更改,您都可以了解到。

说实话,这可能会加入“为什么编译器没有自动处理 INotifyPropertyChanged”讨论,这归结为:C# 中的每个对象都应具备通知其任何部分已更改的功能;即默认实现 INotifyPropertyChanged。但它并没有,而需要最少的努力的最佳路线是使用 IL 编织(特别是FODY)。


5
我发现这篇文章很有用: http://social.msdn.microsoft.com/Forums/vstudio/en-US/3eb70678-c216-414f-a4a5-e1e3e557bb95/mvvm-businesslogic-is-part-of-the-?forum=wpf 我的总结:
MVVM架构的核心理念在于,使视图(View)和数据模型(Model)更容易复用,并实现松耦合的测试。你的视图模型(View-Model)是代表视图实体的模型,而你的数据模型(Model)则代表业务实体。
如果你想创建一个扑克游戏,大部分UI都可以复用。但是,如果游戏逻辑与视图模型紧密耦合,则需要重新编写视图模型来实现复用。同时,如果你想改变用户界面,同样需要检查游戏是否仍能正常运行。如果你想创建桌面应用程序和Web应用程序,则需要维护两个应用程序并行开展,若视图模型包含游戏逻辑,则会变得非常复杂,因为应用程序逻辑必然与视图模型中的业务逻辑相结合。
数据更改通知和数据验证在每个层(视图(View),视图模型(View-Model)和模型(Model))中发生。
模型(Model)包含数据表示(实体)以及与这些实体相关的业务逻辑。一副牌是一个逻辑"事物",拥有固有属性。一张好的牌不能重复放入。它需要提供一个获取顶部卡片的方法,并知道不要发出超过其余牌的数量。这些牌堆行为是模型(Model)的一部分,因为它们是牌堆的内在特征。还会有经销商模型、玩家模型、手牌模型等。这些模型可以相互作用。
视图模型包括展示和应用逻辑。所有与显示游戏相关的工作都与游戏的逻辑分开。这可能包括以图像形式显示手牌、向经销商模型请求卡片、用户显示设置等。
文章核心:
基本上,我喜欢这样解释:您的业务逻辑和实体组成了模型。 这是您特定应用程序使用的内容,但可以在许多应用程序之间共享。 视图是表示层 - 与直接与用户交互相关的任何内容。 ViewModel 基本上是将两者联系起来的特定于您的应用程序的“粘合剂”。 我在这里有一个很好的图表,显示它们如何进行接口:http://reedcopsey.com/2010/01/06/better-user-and-developer-experiences-from-windows-forms-to-wpf-with-mvvm-part-7-mvvm/ 在您的情况下 - 让我们解决一些具体问题... 验证:这通常有两种形式。 与用户输入相关的验证通常会在 ViewModel 中发生(主要),而 View(例如:“数字”文本框防止输入的文本在视图中处理,等等)。 因此,来自用户的输入验证通常是 VM 的关注点。 话虽如此,通常还有第二个“层次”的验证 - 这是使用的数据与业务规则相匹配的验证。 这通常是模型本身的一部分 - 当您将数据推送到您的模型时,可能会导致验证错误。 VM 然后必须将此信息重新映射回 View。 在幕后进行操作,没有视图,例如写入 DB,发送电子邮件等:这实际上是我图表中“特定领域操作”的一部分,实际上纯粹是模型的一部分。 这就是您试图通过应用程序公开的内容。 ViewModel 作为桥梁来公开此信息,但操作是纯模型。 ViewModel 的操作:ViewModel 需要更多的 INPC,还需要任何特定于应用程序(而不是业务逻辑)的操作,例如保存首选项和用户状态等。 即使在接口相同的情况下,这也将因应用程序而异。 一个很好的思考方式 - 假设您想制作两个版本的订购系统。 第一个是在 WPF 中,第二个是 Web 接口。 处理订单本身(发送电子邮件,输入到 DB 等)的共享逻辑是模型。 您的应用程序正在向用户公开这些操作和数据,但以两种方式进行。 在 WPF 应用程序中,用户界面(观看者与之交互的内容)是“视图” - 在 Web 应用程序中,这基本上是在客户端转换为 JavaScript + HTML + CSS 的代码。 ViewModel 是其余的“粘合剂”,它需要适应您正在使用的特定视图技术/层的模型(这些与订单相关的操作)。

也许一个简单的例子是音乐播放器。你的模型将包含库、活动声音文件和编解码器、播放器逻辑和数字信号处理代码。视图模型将包含控件、可视化和库浏览器。需要大量的UI逻辑来显示所有这些信息,让一个程序员专注于使音乐播放,同时允许另一个程序员专注于使UI直观和有趣,这将是很好的。视图模型和模型应该允许这两个程序员达成一致的接口集并分别工作。 - VoteCoffee
1
另一个很好的例子是网页。服务器端逻辑通常相当于模型。客户端逻辑通常相当于视图模型。我很容易想象游戏逻辑应该放在服务器上,而不是委托给客户端。 - VoteCoffee

4

虽然这是一个比较老的帖子,但在经过大量搜索后,我想出了自己的解决方案:使用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));
        }
    }
}

1
避免内存泄漏。使用WeakEvent模式。https://joshsmithonwpf.wordpress.com/2009/07/11/one-way-to-avoid-messy-propertychanged-event-handling/ - JJS
1
@JJS - 另一方面,考虑一下弱事件模式是危险的。就个人而言,如果我忘记注销(-= my_event_handler),我宁愿冒着内存泄漏的风险,因为这比可能发生或可能不发生的罕见且不可预测的僵尸问题更容易追踪。 - ToolmakerSteve
@ToolmakerSteve 感谢您提出平衡的论点。我建议开发人员根据自己的情况选择最适合自己的方式。不要盲目地采用来自互联网的源代码。还有其他模式,如EventAggregator/EventBus,常用于跨组件消息传递(但也存在其自身的风险)。 - JJS

2
您可以从模型中引发事件,而视图模型需要订阅这些事件。
例如,我最近在一个项目中工作,需要生成一个树形视图(自然地,该模型具有分层结构)。在模型中,我有一个名为ChildElements的可观察集合。
在视图模型中,我已经存储了对模型对象的引用,并订阅了可观察集合的CollectionChanged事件,如下所示:ModelObject.ChildElements.CollectionChanged += new CollectionChangedEventHandler(insert function reference here)...
然后,一旦模型发生变化,您的视图模型将自动收到通知。您可以使用PropertyChanged遵循相同的概念,但是您需要明确从模型中引发属性更改事件才能使其起作用。

如果处理分层数据,您需要查看我的MVVM文章演示2 - HappyNomad

2
我长期以来一直在倡导使用 Model -> View Model -> View 的变更方向流程,正如你可以在我于2008年撰写的 MVVM文章 的“变更流程”部分中看到的那样。这需要在模型上实现 INotifyPropertyChanged 接口。据我所知,这已成为常见做法。
由于你提到了Josh Smith,请看看他的 PropertyChanged类 。它是一个订阅模型的 INotifyPropertyChanged.PropertyChanged 事件的辅助类。
实际上,你可以采用更深入的方法,就像我最近创建的 PropertiesUpdater类 一样。视图模型上的属性被计算为包括模型上一个或多个属性的复杂表达式。

2
基于INotifyPropertyChanged和INotifyCollectionChanged的通知正是您所需的。为了简化您对属性更改的订阅、编译时属性名称验证、避免内存泄漏,我建议您使用Josh Smith's MVVM Foundation中的PropertyObserver。由于这个项目是开源的,您可以从源代码中将该类添加到您的项目中。
要了解如何使用PropertyObserver,请阅读本文
此外,更深入地查看反应式扩展(Rx)。您可以在模型中公开IObserver<T>并在视图模型中订阅它。

非常感谢您参考Josh Smith的精彩文章并涉及弱事件! - JJS

1
这些人回答得非常好,但在这种情况下,我真的觉得MVVM模式很麻烦,所以我会使用Supervising Controller或Passive View方法,并放弃绑定系统,至少对于那些自己生成更改的模型对象。

1

在 Model 中实现 INotifyPropertyChanged 并在 ViewModel 中监听它并没有问题。事实上,你甚至可以直接在 XAML 中访问模型的属性:{Binding Model.ModelProperty}。

至于依赖/计算只读属性,迄今为止我还没有看到比这更好、更简单的东西了:https://github.com/StephenCleary/CalculatedProperties。它非常简单但非常有用,就像 MVVM 的 "Excel 公式" - 与 Excel 类似,它会自动传播更改到公式单元格,而不需要额外的努力。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接