WPF 中的 MVVM - 如何提醒 ViewModel 模型中的变化......或者我应该吗?

Posted

技术标签:

【中文标题】WPF 中的 MVVM - 如何提醒 ViewModel 模型中的变化......或者我应该吗?【英文标题】:MVVM in WPF - How to alert ViewModel of changes in Model... or should I? 【发布时间】:2013-03-04 14:33:17 【问题描述】:

我正在浏览一些 MVVM 文章,主要是 this 和 this。

我的具体问题是:如何将模型更改从模型传递到视图模型?

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

我没有看到模型提醒 ViewModel 模型属性更改的示例。这让我担心也许由于某种原因它没有完成。 是否有一种模式可以提醒 ViewModel 模型发生变化?这似乎是必要的,因为 (1) 每个模型可能有多个 ViewModel,并且 (2) 即使有只有一个 ViewModel,对模型的某些操作可能会导致其他属性被更改。

我怀疑可能会有“你为什么要这样做?”形式的答案/cmets。 cmets,所以这是我的程序的描述。我是 MVVM 的新手,所以我的整个设计可能有问题。我将简要描述一下。

我正在编写比“客户”或“产品”类更有趣的东西(至少对我来说!)。我正在编程二十一点。

我有一个没有任何代码的视图,它仅依赖于绑定到视图模型中的属性和命令(请参阅 Josh Smith 的文章)。

无论好坏,我的态度是模型不仅应该包含PlayingCardDeck等类,还应该包含保持整个游戏状态的BlackJackGame类,并且知道玩家何时已经破产,庄家必须抽牌,玩家和庄家当前的得分是多少(小于 21、21、破产等)。

BlackJackGame 我公开了诸如“DrawCard”之类的方法,我突然想到,当抽出一张卡片时,应该更新诸如CardScoreIsBust 之类的属性,并将这些新值传达给 ViewModel。也许这是错误的想法?

可以采取 ViewModel 调用DrawCard() 方法的态度,因此他应该知道要求更新分数并找出他是否破产。意见?

在我的 ViewModel 中,我有逻辑来获取一张扑克牌的实际图像(基于花色、等级)并使其可用于视图。模型不应该关心这个(也许其他 ViewModel 只会使用数字而不是扑克牌图像)。当然,也许有人会告诉我,Model 甚至不应该有 BlackJack 游戏的概念,而应该在 ViewModel 中处理?

【问题讨论】:

您所描述的交互听起来像标准事件机制,这就是您所需要的。该模型可以公开一个名为OnBust 的事件,VM 可以订阅它。我想您也可以使用 IEA 方法。 老实说,如果我在哪里制作真正的二十一点“应用程序”,我的数据将隐藏在几层服务/代理和类似于 A+B 的迂腐级别的单元测试之后= C. 通知更改的是代理/服务。 感谢大家!不幸的是,我只能选择一个答案。由于额外的架构建议和清理原始问题,我选择了 Rachel。但是有很多很棒的答案,我很感激。 -戴夫 另见Josh G's answer with link to Robert McCarter's Design Patterns - Problems and Solutions with Model-View-ViewModel FWIW:在为每个域概念维护 VM 和 M 的复杂性苦苦挣扎了几年之后,我现在认为两者都失败了 DRY;通过在一个对象上设置两个 INTERFACES(一个“域接口”和一个“ViewModel 接口”),可以更轻松地完成所需的关注点分离。这个对象既可以传递给业务逻辑,也可以传递给视图逻辑,不会混淆或缺乏同步。该对象是一个“身份对象”——它唯一地代表实体。保持域代码与视图代码的分离需要更好的工具来在类中执行此操作。 【参考方案1】:

如果您希望您的模型提醒 ViewModels 发生变化,它们应该实现 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");

但通常只有在多个对象将对模型数据进行更改时才需要这样做,但通常情况并非如此。

如果您实际上没有引用您的模型属性来将 PropertyChanged 事件附加到它,那么您可以使用消息传递系统,例如 Prism 的 EventAggregator 或 MVVM Light 的 Messenger

我的博客上有一个brief overview of messaging systems,但总而言之,任何对象都可以广播消息,任何对象都可以订阅以收听特定消息。因此,您可以从一个对象广播PlayerScoreHasChangedMessage,而另一个对象可以订阅以侦听这些类型的消息,并在听到消息时更新它的PlayerScore 属性。

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

在理想的 MVVM 世界中,您的应用程序由您的 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;
        
    

你会有一个像这样的 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,但为了简单起见,我省略了)

【讨论】:

更一般地说,所有业务逻辑/规则都进入模型吗?说你可以拿一张牌最多 21 张(但庄家保持在 17 张),你可以分牌等等的所有逻辑都在哪里。我认为它都属于模型类,因此我觉得我需要模型中的 BlacJackGame 控制器类。我仍在努力理解这一点,并希望得到示例/参考。举个例子,二十一点的想法是从一个关于 ios 编程的 iTunes 类中提炼出来的,其中业务逻辑/规则绝对是在 MVC 模式的模型类中。 @Dave 是的,DrawCard() 方法将与您的其他游戏逻辑一起在 ViewModel 中。在一个理想的 MVVM 应用程序中,您应该能够在完全没有 UI 的情况下运行您的应用程序,只需创建 ViewModel 并运行它们的方法,例如通过测试脚本或命令提示符窗口。模型通常只是包含原始数据和基本数据验证的数据模型。 感谢瑞秋的所有帮助。我将不得不对此进行更多研究或写另一个问题;我仍然对游戏逻辑的位置感到困惑。您(和其他人)主张将其放在 ViewModel 中,其他人说“业务逻辑”,在我的情况下,我认为是游戏规则和游戏状态属于模型(例如:msdn.microsoft.com/en-us/library/gg405484%28v=pandp.40%29.aspx)和***.com/questions/10964003/…) .我认识到,在这个简单的游戏中,它可能并不重要。但很高兴知道。谢谢! @Dave 我可能错误地使用了术语“业务逻辑”并将其与应用程序逻辑混为一谈。引用您链接的 MSDN 文章 “为了最大限度地提高重用机会,模型不应包含任何特定于用例或特定于用户任务的行为或应用程序逻辑”“通常,视图模型将定义可以在 UI 中表示并且用户可以调用的命令或操作”。所以像 DrawCardCommand() 这样的东西会在 ViewModel 中,但我想你可以有一个 BlackjackGameModel 对象,其中包含一个 DrawCard() 方法,如果你愿意,该命令会调用该方法 避免内存泄漏。使用 Wea​​kEvent 模式。 joshsmithonwpf.wordpress.com/2009/07/11/…【参考方案2】:

简短回答:这取决于具体情况。

在您的示例中,模型正在“自行”更新,这些更改当然需要以某种方式传播到视图。由于视图只能直接访问视图模型,这意味着模型必须将这些更改传达给相应的视图模型。这样做的既定机制当然是INotifyPropertyChanged,这意味着你会得到这样的工作流程:

    Viewmodel 已创建并包装模型 Viewmodel 订阅模型的PropertyChanged 事件 Viewmodel 设置为视图的DataContext,属性绑定等 视图触发视图模型上的操作 Viewmodel 在模型上调用方法 模型自行更新 Viewmodel 处理模型的 PropertyChanged 并引发自己的 PropertyChanged 作为响应 视图反映其绑定的变化,关闭反馈循环

另一方面,如果您的模型包含很少(或没有)业务逻辑,或者由于某些其他原因(例如获得事务能力)您决定让每个视图模型“拥有”其包装模型,那么对模型的所有修改将通过视图模型,因此不需要这样的安排。

我在另一个 MVVM 问题 here 中描述了这样的设计。

【讨论】:

您好,您制作的清单非常棒。但是,我对 7. 和 8 有疑问。特别是:我有一个 ViewModel,它没有实现 INotifyPropertyChanged。它包含一个子级列表,其中包含一个子级列表本身(它用作 WPF Treeview 控件的 ViewModel)。如何使 UserControl DataContext ViewModel“监听”任何子项(TreeviewItems)中的属性更改?我究竟如何订阅所有实现 INotifyPropertyChanged 的​​子元素?还是我应该提出一个单独的问题?【参考方案3】:

您的选择:

实施 INotifyPropertyChanged 活动 带有代理操纵器的 POCO

在我看来,INotifyPropertyChanged 是 .Net 的基本组成部分。即它在System.dll。在“模型”中实现它类似于实现事件结构。

如果您想要纯 POCO,那么您必须通过代理/服务有效地操作您的对象,然后您的 ViewModel 通过侦听代理收到更改通知。

就我个人而言,我只是松散地实现 INotifyPropertyChanged,然后使用 FODY 为我做脏活。它的外观和感觉 POCO。

一个例子(使用 FODY 来 IL 编织 PropertyChanged raisers):

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)。

【讨论】:

【参考方案4】:

相当老的线程,但经过大量搜索后,我想出了自己的解决方案:A 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));
        
    

【讨论】:

避免内存泄漏。使用 Wea​​kEvent 模式。 joshsmithonwpf.wordpress.com/2009/07/11/… @JJS - OTOH,考虑The Weak Event Pattern Is Dangerous。就我个人而言,如果我忘记取消注册 (-= my_event_handler),我宁愿冒内存泄漏的风险,因为这比可能发生或可能不会发生的罕见且不可预测的僵尸问题更容易追踪。 @ToolmakerSteve 感谢您添加平衡的论点。我建议开发人员根据自己的情况做最适合他们的事情。不要盲目采用网上的源代码。还有其他模式,如 EventAggregator/EventBus 常用的跨组件消息传递(它们也有自己的危险)【参考方案5】:

我发现这篇文章很有帮助: http://social.msdn.microsoft.com/Forums/vstudio/en-US/3eb70678-c216-414f-a4a5-e1e3e557bb95/mvvm-businesslogic-is-part-of-the-?forum=wpf

我的总结:

MVVM 组织背后的理念是允许更轻松地重用视图和模型,并允许解耦测试。您的视图模型是代表视图实体的模型,您的模型代表业务实体。

如果您以后想制作扑克游戏怎么办?大部分 UI 应该是可重用的。如果您的游戏逻辑绑定在您的视图模型中,那么在不重新编程视图模型的情况下重用这些元素将非常困难。如果你想改变你的用户界面怎么办?如果您的游戏逻辑与您的视图模型逻辑耦合,您将需要重新检查您的游戏是否仍然有效。如果您想创建桌面和 Web 应用程序怎么办?如果您的视图模型包含游戏逻辑,那么尝试并排维护这两个应用程序会变得很复杂,因为应用程序逻辑将不可避免地与视图模型中的业务逻辑绑定。

数据更改通知和数据验证发生在每一层(视图、视图模型和模型)。

模型包含您的数据表示(实体)和特定于这些实体的业务逻辑。一副纸牌是具有内在属性的逻辑“事物”。一个好的套牌不能放重复的牌。它需要公开一种获得***卡片的方法。它需要知道不要发出比它剩下的更多的牌。这种牌组行为是模型的一部分,因为它们是牌组固有的。还会有庄家模型、玩家模型、手模型等。这些模型可以而且将会互动。

视图模型将由表示和应用程序逻辑组成。所有与显示游戏相关的工作都与游戏逻辑分开。这可能包括将手牌显示为图像、向庄家模型请求牌、用户显示设置等。

文章内容:

基本上,我想解释的方式是您的业务 逻辑和实体构成模型。这是你的具体 应用程序正在使用,但可以在多个应用程序之间共享。

视图是表示层——任何与实际相关的东西 直接与用户交互。

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 将不得不重新映射此信息 返回视图。

操作“在没有视图的幕后,例如写入数据库, 发送电子邮件等”:这实际上是“特定领域”的一部分 操作”在我的图表中,并且实际上纯粹是模型的一部分。 这就是您试图通过应用程序公开的内容。这 ViewModel 充当了公开此信息的桥梁,但 操作是纯模型。

ViewModel 的操作:ViewModel 需要的不仅仅是 INPC - 它还需要特定于您的应用程序(不是您的业务逻辑)的任何操作,例如保存首选项和用户状态, 等。这将改变应用程序。通过应用程序,即使在连接 相同的“模型”。

考虑它的好方法 - 假设您想要制作 2 个版本的 订购系统。第一个是WPF,第二个是Web 界面。

处理订单本身的共享逻辑(发送 电子邮件,进入数据库等)是模型。你的申请是 将这些操作和数据暴露给用户,但在 2 方式。

在 WPF 应用程序中,用户界面(查看器交互的内容) with) 是“视图”——在 Web 应用程序中,这基本上是 (至少最终)变成 javascript + html + css 的代码 在客户端上。

ViewModel 是调整您的 模型(这些操作与订购有关)以使其工作 使用您正在使用的特定视图技术/层。

【讨论】:

也许一个简单的例子是音乐播放器。您的模型将包含库和活动声音文件和编解码器以及播放器逻辑和数字信号处理代码。视图模型将包含您的控件和可视化以及库浏览器。显示所有这些信息需要大量的 UI 逻辑,让一个程序员专注于制作音乐,同时让另一个程序员专注于使 UI 直观和有趣,这将是很好的。视图模型和模型应该允许这两个程序员就一组接口达成一致并单独工作。 另一个很好的例子是网页。服务器端逻辑一般相当于一个模型。客户端逻辑通常等同于视图模型。我很容易想象游戏逻辑将属于服务器,而不是委托给客户端。【参考方案6】:

基于 INotifyPropertyChangedINotifyCollectionChanged 的通知正是您所需要的。为了通过订阅属性更改、属性名称的编译时验证、避免内存泄漏来简化您的生活,我建议您使用来自Josh Smith's MVVM Foundation 的 PropertyObserver。由于这个项目是开源的,您可以从源代码中只将该类添加到您的项目中。

要了解,如何使用 PropertyObserver 阅读this article。

另外,请深入了解Reactive Extensions (Rx)。您可以从模型中公开 IObserver 并在视图模型中订阅它。

【讨论】:

非常感谢您参考 Josh Smith 的优秀文章并涵盖弱事件!【参考方案7】:

这些家伙在回答这个问题时做得非常出色,但在这种情况下,我真的觉得 MVVM 模式很痛苦,所以我会去使用监督控制器或被动视图方法,并至少在一段时间内放弃绑定系统自行生成更改的模型对象。

【讨论】:

【参考方案8】:

我一直在提倡定向模型 -> 视图模型 -> 视图更改流程,您可以在我 2008 年的MVVM article 的 Flow of Changes 部分看到. 这需要在模型上实现INotifyPropertyChanged。据我所知,这已经成为惯例。

因为您提到了 Josh Smith,请查看 his PropertyChanged class。它是一个用于订阅模型的INotifyPropertyChanged.PropertyChanged 事件的助手类。

您实际上可以更进一步地采用这种方法,就像我最近通过创建 my PropertiesUpdater class 所做的那样。视图模型上的属性被计算为包含模型上的一个或多个属性的复杂表达式。

【讨论】:

【参考方案9】:

在Model里面实现INotifyPropertyChanged并在ViewModel里面监听是没有错的。 事实上,您甚至可以在 XAML 中添加模型的属性:Binding Model.ModelProperty

至于依赖/计算的只读属性,到目前为止,我还没有看到比这更好和更简单的东西: https://github.com/StephenCleary/CalculatedProperties。它非常简单但非常有用,它确实是“MVVM 的 Excel 公式”——其工作方式与 Excel 将更改传播到公式单元格的方式相同,无需您付出额外的努力。

【讨论】:

【参考方案10】:

您可以从模型中引发事件,视图模型需要订阅这些事件。

例如,我最近在一个项目中工作,我必须为其生成一个树形视图(当然,该模型具有分层性质)。在模型中,我有一个名为 ChildElements 的 observablecollection。

在视图模型中,我已经存储了对模型中对象的引用,并订阅了 observablecollection 的 CollectionChanged 事件,如下所示:ModelObject.ChildElements.CollectionChanged += new CollectionChangedEventHandler(insert function reference here)...

然后,一旦模型发生更改,您的视图模型就会自动收到通知。您可以使用 PropertyChanged 遵循相同的概念,但您需要从模型中显式引发属性更改事件才能使其正常工作。

【讨论】:

如果处理分层数据,你会想看看my MVVM article的Demo 2【参考方案11】:

在我看来,这是一个非常重要的问题——即使没有压力去做。我正在开发一个涉及 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

这很简单,但似乎有一个非常基本的缺陷。典型的单元测试会执行命令,然后在视图模型中检查结果。但这并不能测试模型更新是否正确,因为两者是同时更新的。

所以也许最好使用像 PropertyObserver 这样的技术来让模型更新触发视图模型更新。相同的单元测试现在只有在两个操作都成功的情况下才能工作。

我意识到这不是一个潜在的答案,但似乎值得提出。

【讨论】:

以上是关于WPF 中的 MVVM - 如何提醒 ViewModel 模型中的变化......或者我应该吗?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 MVVM 自动隐藏 WPF 中的 DataGrid 列? [复制]

如何将搜索应用于 MVVM (WPF) 中的列表,而无需每次都调用列表值?

如何在 MVVM 模式中从页面导航到 WPF 中的页面?没有棱镜的概念[重复]

我去年码了个表(WPF MvvM)

如何在 MVVM 中的 UserControl 之间进行通信 - WPF 应用程序

MVVM