WPF中的MVVM - 如何提醒ViewModel模型中的更改...还是应该?

时间:2013-03-15 18:38:24

标签: c# .net wpf mvvm

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

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

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

我在任何地方都没有看到模型警告ViewModel模型属性更改的示例。这让我担心,也许是因为某些原因没有做到的。 是否有模式用于警告ViewModel模型中的更改?这似乎是必要的,因为(1)可以想象每个模型有超过1个ViewModel,(2)即使有只有一个ViewModel,模型上的某些操作可能会导致其他属性被更改。

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

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

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

无论好坏,我采取的态度是模型不仅应包含诸如PlayingCardDeck之类的类,还应包含保持整个游戏状态的BlackJackGame类并且知道当玩家破产时,经销商必须抽牌,以及玩家和经销商当前得分是什么(少于21,21,胸围等)。

BlackJackGame我公开了像“DrawCard”这样的方法,我发现在绘制卡片时,应更新CardScoreIsBust等属性并更新这些新值传达给ViewModel。也许这是错误的思考?

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

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

11 个答案:

答案 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,这意味着你将得到这样的工作流程:

  1. 创建了Viewmodel并包装了模型
  2. Viewmodel订阅了模型的PropertyChanged事件
  3. Viewmodel设置为视图DataContext,属性绑定等
  4. 查看触发器对viewmodel的操作
  5. Viewmodel在模型上调用方法
  6. 模型自行更新
  7. Viewmodel处理模型PropertyChanged并提升自己的PropertyChanged作为回应
  8. 视图反映了其绑定的变化,关闭了反馈循环
  9. 另一方面,如果您的模型包含很少(或没有)业务逻辑,或者由于某些其他原因(例如获得事务性能),您决定让每个视图模型“拥有”其包装模型,然后对模型进行所有修改将通过视图模型,因此不需要这样的安排。

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

答案 2 :(得分:3)

您的选择:

  • 实施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)。

答案 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)

我发现这篇文章很有帮助: 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中发生   (即:处理阻止输入文本的“数字”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等技术让模型更新触发视图模型更新。相同的单元测试现在只有在两个操作都成功的情况下才能工作。

我意识到,这不是一个可能的答案,但它似乎值得推出。