WPF中的MVVM-如何向ViewModel发出模型更改的警报…还是应该?


112

我正在阅读一些MVVM文章,主要是thisthis

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

在Josh的文章中,我看不到他这样做。ViewModel总是向模型询问属性。在Rachel的示例中,她确实具有模型实现INotifyPropertyChanged,并从模型中引发事件,但是这些事件仅供视图本身使用(有关执行此操作的详细信息,请参阅她的文章/代码)。

我在哪里都看不到模型向ViewModel发出模型属性更改警报的示例。这让我担心也许由于某些原因未完成。 有没有一种模式可以提醒ViewModel模型中的更改? 似乎有必要,因为(1)可以想象每个模型有多个ViewModel,并且(2)即使只有一个ViewModel,对模型的某些操作也可能导致其他属性被更改。

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

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

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

是好还是坏,我把该模型应该不仅包含类,如态度PlayingCardDeck但也BlackJackGame认为保持整场比赛的状态类的,知道什么时候该玩家已经破产,经销商有画卡,玩家和发牌人当前的得分是多少(小于21、21,半身等)。

BlackJackGame我开始,我公开了“ DrawCard”之类的方法,然后想到在绘制卡片时CardScoreIsBust应该更新诸如和的属性,并将这些新值传递给ViewModel。也许那是错误的想法?

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

在我的ViewModel中,我有逻辑来获取纸牌的实际图像(基于西服,等级)并将其用于视图。该模型不必为此担心(也许其他ViewModel只会使用数字而不是纸牌图像)。当然,也许有人会告诉我,该模型甚至不应该具有BlackJack游戏的概念,而应该在ViewModel中进行处理?


3
您所描述的交互听起来像是一种标准的事件机制,仅需您所需要的。该模型可以公开一个名为的事件OnBust,VM可以订阅该事件。我想您也可以使用IEA方法。
code4life 2013年

老实说,如果我在哪里制作一个真正的二十一点“应用程序”,我的数据将隐藏在几层服务/代理以及像A + B = C这样学究的单元测试级别之后。它将是代理/ service通知更改。
Meirion Hughes

1
谢谢大家!不幸的是,我只能选择一个答案。由于额外的体系结构建议,我选择了Rachel,并清理了原始问题。但是有很多很棒的答案,我感谢他们。戴夫
戴夫


2
FWIW:在为维护每个域的VM和M的复杂性而苦苦挣扎了几年之后,我现在认为两者都失败了。通过在单个对象上具有两个接口-“域接口”和“ ViewModel接口”,可以更轻松地完成所需的关注点分离。该对象可以传递给业务逻辑和视图逻辑,而不会造成混乱或缺乏同步。该对象是“身份对象”-它唯一表示实体。因此,要保持域代码与视图代码的分离,就需要在类中使用更好的工具。
制造商史蒂夫,

Answers:


61

如果您希望模型向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");
}

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

如果您遇到的情况是实际上没有引用Model属性将PropertyChanged事件附加到该属性,则可以使用消息系统,例如Prism EventAggregator或MVVM Light Messenger

我在博客上对消息传递系统进行了简要概述,但是总而言之,任何对象都可以广播消息,并且任何对象都可以订阅以侦听特定消息。因此,您可以PlayerScoreHasChangedMessage从一个对象广播一个对象,而另一个对象可以订阅以侦听这些类型的消息,并PlayerScore在听到一个消息时更新其属性。

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

在理想的MVVM世界中,您的应用程序由ViewModel组成,而Models只是用于构建应用程序的模块。它们通常仅包含数据,因此不会包含诸如DrawCard()(在ViewModel中)的方法。

因此,您可能会拥有简单的Model数据对象,如下所示:

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张),可以拆分卡片,等等。我认为所有这些都属于模型类,因此我觉得我需要模型中的BlacJackGame控制器类。我仍在尝试理解这一点,并希望您能喜欢示例/参考。例如,二十一点的想法来自iOS编程上的iTunes类,其中业务逻辑/规则无疑是MVC模式的模型类。
戴夫

3
@Dave是的,该DrawCard()方法将与其他游戏逻辑一起放在ViewModel中。在理想的MVVM应用程序中,只需创建ViewModels并运行其方法(例如通过测试脚本或命令提示符窗口),就应该能够完全在没有UI的情况下运行应用程序。这些模型通常仅是包含原始数据和基本数据验证的数据模型。
雷切尔

6
感谢Rachel的所有帮助。我将不得不对此进行更多研究或编写另一个问题。我仍然对游戏逻辑的位置感到困惑。您(和其他人)主张将其放入ViewModel中,其他人则说“业务逻辑”,在我的情况下,我假设游戏规则和游戏状态属于模型(请参见例如:msdn.microsoft.com/en-us /library/gg405484%28v=pandp.40%29.aspx)和stackoverflow.com/questions/10964003/…)。我认识到在这个简单的游戏中,它可能并不重要。但是很高兴知道。谢谢!
戴夫

1
@戴夫,我可能没有正确使用术语“业务逻辑”,并将其与应用程序逻辑混为一谈。引用您链接的MSDN文章“为最大限度地提高重用机会,模型不应包含任何特定于用例或特定于用户任务的行为或应用程序逻辑”“通常,视图模型将定义可以表示的命令或动作在用户界面中,用户可以调用”。因此,诸如a之类的东西DrawCardCommand()将会出现在ViewModel中,但是我想您可能有一个BlackjackGameModel对象,其中包含一个DrawCard()命令,如果您愿意的话,可以调用该方法
Rachel

2
避免内存泄漏。使用WeakEvent模式。joshsmithonwpf.wordpress.com/2009/07/11/…–
JJS

24

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

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

  1. 创建视图模型并包装模型
  2. Viewmodel订阅模型的PropertyChanged事件
  3. Viewmodel设置为view DataContext,属性绑定等
  4. 视图触发对视图模型的操作
  5. ViewModel在模型上调用方法
  6. 模型本身会更新
  7. Viewmodel处理模型PropertyChanged并提出自己PropertyChanged的响应
  8. 视图反映了其绑定中的更改,从而关闭了反馈循环

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

我在这里在另一个MVVM问题中描述了这种设计。


您好,您列出的清单很棒。但是,我对7.和8有问题。特别是:我有一个ViewModel,它没有实现INotifyPropertyChanged。它包含一个子级列表,其中包含一个子级本身列表(用作WPF Treeview控件的ViewModel)。如何使UserControl DataContext ViewModel“侦听”任何子级(TreeviewItems)中的属性更改?我如何准确地订阅所有实现INotifyPropertyChanged的子元素?还是我应该另外提出一个问题?
伊戈尔(Igor)2015年

4

您的选择:

  • 实现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对象转储到一个集合中,并聆听该集合...如果任何地方发生任何更改,您都可以了解它。

老实说,这可以加入“为什么编译器不自动处理INotifyPropertyChanged”的讨论,该讨论演变为:c#中的每个对象都应具有通知其任何部分是否更改的功能;即默认实现INotifyPropertyChanged。但这并不需要,需要最少工作量的最佳方法是使用IL Weaving(特别是FODY)。


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模式。joshsmithonwpf.wordpress.com/2009/07/11/…–
JJS

1
@JJS-OTOH,请考虑弱事件模式很危险。就我个人而言,如果我忘记注销(-= my_event_handler),我宁愿冒着内存泄漏的风险,因为与一个罕见的,不可预测的僵尸问题(可能会或可能不会发生)相比,查找起来更容易。
制造商史蒂夫

@ToolmakerSteve感谢您添加一个平衡的论点。我建议开发人员在自己的情况下为他们做最适合自己的事情。不要盲目采用互联网上的源代码。还有其他模式,例如EventAggregator / EventBus通常使用的跨组件消息传递(它们也有其自身的风险)
JJS

2

我发现这篇文章很有帮助: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中进行(即:“ Numeric” TextBox防止在视图中为您输入文本,等等)。这样,来自用户的输入的验证通常是VM关注的问题。话虽这么说,通常还有第二个验证“层”-这是对所使用的数据与业务规则匹配的验证。这通常是模型本身的一部分-将数据推送到模型时,可能会导致验证错误。然后,VM将不得不将此信息重新映射回View。

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

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

考虑这一点的好方法-假设您要订购两个版本的订购系统。第一个是在WPF中,第二个是Web界面。

处理订单本身(发送电子邮件,进入数据库等)的共享逻辑是模型。您的应用程序向用户公开了这些操作和数据,但是有两种方式。

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

ViewModel是调整模型(与订购有关的这些操作)所必需的“胶水”的其余部分,以使其与您正在使用的特定视图技术/层一起使用。


也许一个简单的例子就是音乐播放器。您的模型将包含库和活动的声音文件,编解码器以及播放器逻辑和数字信号处理代码。视图模型将包含您的控件和可视化效果以及库浏览器。显示所有这些信息需要很多UI逻辑,允许一个程序员专注于音乐播放,而允许另一个程序员专注于使UI直观有趣则很好。视图模型和模型应允许这两个程序员在一组接口上达成一致并分别工作。
VoteCoffee 2014年

另一个很好的例子是网页。服务器端逻辑通常等效于模型。客户端逻辑通常等效于视图模型。我很容易想到游戏逻辑将属于服务器,而不是委托给客户端。
VoteCoffee 2014年

2

正是您需要基于INotifyPropertyChangedINotifyCollectionChanged的通知。与订阅属性发生变化,属性名的编译时验证简化你的生活,避免内存泄漏,我会建议你使用PropertyObserver约什-史密斯的MVVM基金会。由于该项目是开源的,因此您可以从源代码中仅将该类添加到项目中。

要了解,如何使用PropertyObserver阅读本文

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


非常感谢您引用Josh Smiths的出色文章并介绍了弱事件!
JJS

1

伙计们在回答这个问题上做得非常出色,但是在这样的情况下,我真的觉得MVVM模式很痛苦,所以我会去使用Supervising Controller或Passive View方法,至少对于那些模型对象放开绑定系统自己产生变化。


1

我很久以来一直在提倡定向模型->视图模型->查看更改流,正如您在2008年我的MVVM文章的“更改流”部分中所看到的那样。这需要在模型上实现。据我所知,这已成为惯例。INotifyPropertyChanged

因为您提到了乔什·史密斯(Josh Smith),所以请看一下他的PropertyChanged类。订阅模型的辅助类INotifyPropertyChanged.PropertyChanged事件。

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


1

实现INotifyPropertyChanged没有错在Model内部并在ViewModel内部进行侦听。实际上,您甚至可以直接在XAML中插入模型的属性:{Binding Model.ModelProperty}

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


0

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

例如,我最近在一个项目中工作,我必须为其生成树状视图(自然地,模型具有层次结构性质)。在模型中,我有一个称为ChildElements

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

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


如果处理的分层数据,你会想看看演示2我MVVM文章
HappyNomad

0

在我看来,这是一个非常重要的问题-即使没有压力也可以这样做。我正在一个测试项目中,其中涉及一个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之类的技术来让模型更新触发视图模型更新。现在,只有两个动作都成功时,相同的单元测试才有效。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.