首页 文章

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

提问于
浏览
95

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

我的具体问题是: How do I communicate Model changes from the Model to the ViewModel?

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

我在任何地方都看不到模型警告ViewModel模型属性更改的示例 . 这让我担心,也许是因为某些原因没有做到的 . Is there a pattern for alerting the ViewModel of changes in the Model? 似乎有必要,因为(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 回答

  • 1

    我一直在提倡定向模型 - >查看模型 - >查看更改流程很长一段时间,正如您在2008年MVVM article的更改流程部分中所看到的那样 . 这需要在模型上实现 INotifyPropertyChanged . 据我所知,它已成为惯例 .

    因为你提到约什史密斯,看看his PropertyChanged class . 它's a helper class for subscribing to the model' s INotifyPropertyChanged.PropertyChanged 事件 .

    实际上你可以通过创建my PropertiesUpdater class来进一步采用这种方法 . 视图模型上的属性计算为包含模型上一个或多个属性的复杂表达式 .

  • 20

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

    要了解,如何使用PropertyObserver读取this article .

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

  • 3

    在我看来,这似乎是一个非常重要的问题 - 即使没有压力要做 . 我正在开发一个涉及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等技术让模型更新触发视图模型更新 . 现在,只有两个操作都成功,相同的单元测试才会起作用 .

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

  • 0

    在Model内部实现 INotifyPropertyChanged 并在ViewModel中监听它没有错 . 实际上你甚至可以在XAML中点入模型的属性:

    至于依赖/计算的只读属性,到目前为止我还没有看到比这更好,更简单的东西:https://github.com/StephenCleary/CalculatedProperties . 它's very simple but incredibly useful, it'真的"Excel formulas for MVVM" - 就像Excel传播对公式单元格的更改一样没有你额外的努力 .

  • 0

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

    这是 class 本身:

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

    您可以从模型中引发事件,viewmodel需要订阅该事件 .

    例如,我最近在一个项目上工作,我必须生成一个树视图(当然,模型具有层次结构性质) . 在模型中,我有一个名为 ChildElements 的observablecollection .

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

    然后,一旦模型中发生更改,您的viewmodel就会自动得到通知 . 您可以使用 PropertyChanged 遵循相同的概念,但是您需要从模型中明确地引发属性更改事件才能生效 .

  • 55

    你的选择:

    • 实施INotifyPropertyChanged

    • 活动

    • POCO与代理操纵器

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

    如果你想要纯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的任何变化;或 property 特定的变化 .

    INotifyPropertyChanged路线的美妙之处在于你用Extended ObservableCollection链接它 . 所以你将你附近的poco对象转移到一个集合中,并听取集合......如果有任何变化,无论在哪里,你都会了解它 .

    我需要最少努力的最佳途径是使用IL Weaving(特别是FODY) .

  • 0

    这些家伙做了一个惊人的工作回答这个,但在这样的情况下,我真的觉得MVVM模式是一个痛苦所以我会去使用监督控制器或被动视图方法,并放弃绑定系统至少为模型对象,是自己产生变化 .

  • 1

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

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

    如果您遇到过't actually have a reference to your Model property to attach the PropertyChanged event to it, then you can use a Messaging system such as Prism' s 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 ,但为了简单起见,我把它留下了)

  • 2

    简短回答:这取决于细节 .

    在您的示例中,模型正在更新"on their own",这些更改当然需要以某种方式传播到视图 . 由于视图只能直接访问视图模型,因此表示模型必须将这些更改传递给相应的视图模型 . 这样做的既定机制当然是 INotifyPropertyChanged ,这意味着你将获得这样的工作流程:

    • 创建了Viewmodel并包装了模型

    • Viewmodel订阅了模型的 PropertyChanged 事件

    • Viewmodel设置为视图 DataContext ,属性绑定等

    • View触发viewmodel上的操作

    • Viewmodel在模型上调用方法

    • 模型更新自身

    • Viewmodel处理模型的 PropertyChanged 并在响应中引发自己的 PropertyChanged

    • 视图反映了其绑定的变化,关闭了反馈循环

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

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

  • 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基本上是特定于您的应用程序的“ Binders ”,将两者链接在一起 . 我在这里有一个很好的图表,显示了它们如何界面: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必须将此信息重新映射回View . “没有视图的幕后操作,如写入数据库,发送电子邮件等”:这实际上是我的图表中“域特定操作”的一部分,并且纯粹是模型的一部分 . 这是您尝试通过应用程序公开的内容 . ViewModel充当公开此信息的桥梁,但操作是纯模型 . ViewModel的操作:ViewModel需要的不仅仅是INPC - 它还需要特定于您的应用程序(而不是您的业务逻辑)的任何操作,例如保存首选项和用户状态等 . 这将改变应用程序 . 通过app . ,甚至在连接相同的“模型”时 . 考虑它的好方法 - 假设您想要制作2个版本的订购系统 . 第一个是WPF,第二个是Web界面 . 处理订单本身(发送电子邮件,进入数据库等)的共享逻辑是模型 . 您的应用程序将这些操作和数据暴露给用户,但是以两种方式执行 . 在WPF应用程序中,用户界面(查看者与之交互)是“视图” - 在Web应用程序中,这基本上是在客户端上(至少最终)变成javascript html css的代码 . ViewModel是调整模型所需的“胶水”的其余部分(这些操作与排序有关),以使其适用于您正在使用的特定视图技术/图层 .

相关问题