MVVM - 为模型视图实现“Is Dirty”功能以保存数据

Posted

技术标签:

【中文标题】MVVM - 为模型视图实现“Is Dirty”功能以保存数据【英文标题】:MVVM - implementing 'IsDirty' functionality to a ModelView in order to save data 【发布时间】:2011-06-01 04:19:42 【问题描述】:

作为 WPF 和 MVVM 的新手,我在一些基本功能上苦苦挣扎。

我先解释一下我的目的,然后附上一些示例代码……

我有一个显示用户列表的屏幕,我在右侧使用可编辑的文本框显示所选用户的详细信息。然后我有一个保存按钮,它是 DataBound,但我只想在数据实际更改时显示这个按钮。即 - 我需要检查“脏数据”。

我有一个完整的 MVVM 示例,其中我有一个名为 User 的模型:

namespace Test.Model

    class User
    
        public string UserName  get; set; 
        public string Surname  get; set; 
        public string Firstname  get; set; 
    

然后,ViewModel 看起来像这样:

using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Windows.Input;
using Test.Model;

namespace Test.ViewModel

    class UserViewModel : ViewModelBase
    
        //Private variables
        private ObservableCollection<User> _users;
        RelayCommand _userSave;

        //Properties
        public ObservableCollection<User> User
        
            get
            
                if (_users == null)
                
                    _users = new ObservableCollection<User>();
                    //I assume I need this Handler, but I am stuggling to implement it successfully
                    //_users.CollectionChanged += HandleChange;

                    //Populate with users
                    _users.Add(new User UserName = "Bob", Firstname="Bob", Surname="Smith");
                    _users.Add(new User UserName = "Smob", Firstname="John", Surname="Davy");
                
                return _users;
            
        

        //Not sure what to do with this?!?!

        //private void HandleChange(object sender, NotifyCollectionChangedEventArgs e)
        //
        //    if (e.Action == NotifyCollectionChangedAction.Remove)
        //    
        //        foreach (TestViewModel item in e.NewItems)
        //        
        //            //Removed items
        //        
        //    
        //    else if (e.Action == NotifyCollectionChangedAction.Add)
        //    
        //        foreach (TestViewModel item in e.NewItems)
        //        
        //            //Added items
        //        
        //     
        //

        //Commands
        public ICommand UserSave
        
            get
            
                if (_userSave == null)
                
                    _userSave = new RelayCommand(param => this.UserSaveExecute(), param => this.UserSaveCanExecute);
                
                return _userSave;
            
        

        void UserSaveExecute()
        
            //Here I will call my DataAccess to actually save the data
        

        bool UserSaveCanExecute
        
            get
            
                //This is where I would like to know whether the currently selected item has been edited and is thus "dirty"
                return false;
            
        

        //constructor
        public UserViewModel()
        

        

    

“RelayCommand”只是一个简单的包装类,“ViewModelBase”也是如此。 (为了清楚起见,我会附上后者)

using System;
using System.ComponentModel;

namespace Test.ViewModel

    public abstract class ViewModelBase : INotifyPropertyChanged, IDisposable
    
        protected ViewModelBase()
         
        

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName)
        
            PropertyChangedEventHandler handler = this.PropertyChanged;
            if (handler != null)
            
                var e = new PropertyChangedEventArgs(propertyName);
                handler(this, e);
            
        

        public void Dispose()
        
            this.OnDispose();
        

        protected virtual void OnDispose()
        
        
    

最后 - XAML

<Window x:Class="Test.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:Test.ViewModel"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <vm:UserViewModel/>
    </Window.DataContext>
    <Grid>
        <ListBox Height="238" HorizontalAlignment="Left" Margin="12,12,0,0" Name="listBox1" VerticalAlignment="Top" 
                 Width="197" ItemsSource="Binding Path=User" IsSynchronizedWithCurrentItem="True">
            <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel>
                        <TextBlock Text="Binding Path=Firstname"/>
                        <TextBlock Text="Binding Path=Surname"/>
                </StackPanel>
            </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <Label Content="Username" Height="28" HorizontalAlignment="Left" Margin="232,16,0,0" Name="label1" VerticalAlignment="Top" />
        <TextBox Height="23" HorizontalAlignment="Left" Margin="323,21,0,0" Name="textBox1" VerticalAlignment="Top" Width="120" Text="Binding Path=User/UserName" />
        <Label Content="Surname" Height="28" HorizontalAlignment="Left" Margin="232,50,0,0" Name="label2" VerticalAlignment="Top" />
        <TextBox Height="23" HorizontalAlignment="Left" Margin="323,52,0,0" Name="textBox2" VerticalAlignment="Top" Width="120" Text="Binding Path=User/Surname" />
        <Label Content="Firstname" Height="28" HorizontalAlignment="Left" Margin="232,84,0,0" Name="label3" VerticalAlignment="Top" />
        <TextBox Height="23" HorizontalAlignment="Left" Margin="323,86,0,0" Name="textBox3" VerticalAlignment="Top" Width="120" Text="Binding Path=User/Firstname" />
        <Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="368,159,0,0" Name="button1" VerticalAlignment="Top" Width="75" Command="Binding Path=UserSave" />
    </Grid>
</Window>

所以基本上,当我编辑姓氏时,应该启用保存按钮;如果我撤消我的编辑 - 那么它应该再次禁用,因为没有任何改变。

我在很多例子中都看到了这一点,但还没有弄清楚如何去做。

任何帮助将不胜感激! 布伦丹

【问题讨论】:

【参考方案1】:

根据我的经验,如果您在视图模型中实现IsDirty,您可能还希望视图模型实现IEditableObject

假设您的视图模型是通常的类型,实现PropertyChanged 和一个私有或受保护的OnPropertyChanged 方法来提升它,设置IsDirty 很简单:你只需在OnPropertyChanged 中设置IsDirty,如果它还不是真的。

您的IsDirty 设置器应该,如果属性是假的,现在是真的,调用BeginEdit

您的Save 命令应该调用EndEdit,这会更新数据模型并将IsDirty 设置为false。

您的Cancel 命令应该调用CancelEdit,它会从数据模型中刷新视图模型并将IsDirty 设置为false。

CanSaveCanCancel 属性(假设您对这些命令使用 RelayCommand)只返回 IsDirty 的当前值。

请注意,由于这些功能都不依赖于视图模型的具体实现,您可以将其放在抽象基类中。派生类不必实现任何与命令相关的属性或IsDirty 属性;他们只需要覆盖BeginEditEndEditCancelEdit

【讨论】:

【参考方案2】:

我已经为包装在我的 ViewModel 中的模型实现 IsDirty 做了一些工作。

结果确实简化了我的 ViewModel:

public class PersonViewModel : ViewModelBase

    private readonly ModelDataStore<Person> data;
    public PersonViewModel()
    
        data = new ModelDataStore<Person>(new Person());
    

    public PersonViewModel(Person person)
    
        data = new ModelDataStore<Person>(person);
    

    #region Properties

    #region Name
    public string Name
    
        get  return data.Model.Name; 
        set  data.SetPropertyAndRaisePropertyChanged("Name", value, this); 
    
    #endregion

    #region Age
    public int Age
    
        get  return data.Model.Age; 
        set  data.SetPropertyAndRaisePropertyChanged("Age", value, this); 
    
    #endregion

    #endregion

代码@http://wpfcontrols.codeplex.com/ 查看 Patterns 程序集和 MVVM 文件夹下,您会找到一个 ModelDataStore 类。

附: 我还没有对其进行全面测试,只是进行了非常简单的测试,您会发现 Test 程序集。

【讨论】:

【参考方案3】:

我建议您使用GalaSoft MVVM Light Toolkit,因为它比 DIY 方法更容易实现。

对于脏读,你需要保留每个字段的快照,并从UserSaveCanExecute()方法返回true或false,这将相应地启用/禁用命令按钮。

【讨论】:

我也会使用 MVVM Light Toolkit 谢谢,我已经安装了 MVVM-Light 工具包,但我还没有看到任何轻松实现“IsDirty”功能的方法。然而,我设法解决了我的问题(也许不是最好的方法 - 但它有效) - 稍后将详细回答我自己的问题 MVVM 不支持脏读功能。这只是一个建议,以尽量减少实现 MVVM 模式的工作量。很高兴知道您已经解决了脏读问题。现在我建议在轻量级工具包中探索进一步的消息传递和事件命令,这将为您提供更多控制权。玩得开心。 投票赞成建议 another_framework (tm) 但实际上并没有回答这个问题?普里莫!【参考方案4】:

如果您想采用框架方法而不是自己编写基础架构,您可以使用 CSLA (http://www.lhotka.net/cslanet/) - Rocky 用于开发业务对象的框架。在属性更改时为您管理对象状态,并且代码库还包括一个示例 ViewModel 类型,该类型支持基础模型、Save 动词和 CanSave 属性。即使您不想使用框架,您也可以从代码中获得灵感。

【讨论】:

【参考方案5】:

我想出了一个可行的解决方案。这当然可能不是最好的方法,但我相信随着我了解更多,我可以继续努力......

当我运行项目时,如果我取消任何项目,列表框将被禁用,并且保存按钮启用。如果我撤消我的编辑,则列表框会再次启用,而保存按钮会被禁用。

我已经更改了我的用户模型以实现 INotifyPropertyChanged,并且我还创建了一组私有变量来存储“原始值”和一些逻辑来检查“IsDirty”

using System.ComponentModel;
namespace Test.Model

    public class User : INotifyPropertyChanged
    
    //Private variables
    private string _username;
    private string _surname;
    private string _firstname;

    //Private - original holders
    private string _username_Orig;
    private string _surname_Orig;
    private string _firstname_Orig;
    private bool _isDirty;

    //Properties
    public string UserName
    
        get
        
            return _username;
        
        set
        
            if (_username_Orig == null)
            
                _username_Orig = value;
            
            _username = value;
            SetDirty();
        
    
    public string Surname
    
        get  return _surname; 
        set
        
            if (_surname_Orig == null)
            
                _surname_Orig = value;
            
            _surname = value;
            SetDirty();
        
    
    public string Firstname
    
        get  return _firstname; 
        set
        
            if (_firstname_Orig == null)
            
                _firstname_Orig = value;
            
            _firstname = value;
            SetDirty();
        
    

    public bool IsDirty
    
        get
        
            return _isDirty;
        
    

    public void SetToClean()
    
        _username_Orig = _username;
        _surname_Orig = _surname;
        _firstname_Orig = _firstname;
        _isDirty = false;
        OnPropertyChanged("IsDirty");
    

    private void SetDirty()
    
        if (_username == _username_Orig && _surname == _surname_Orig && _firstname == _firstname_Orig)
        
            if (_isDirty)
            
                _isDirty = false;
                OnPropertyChanged("IsDirty");
            
        
        else
        
            if (!_isDirty)
            
                _isDirty = true;
                OnPropertyChanged("IsDirty");
            
        
    

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propertyName)
    
        PropertyChangedEventHandler handler = PropertyChanged;

        if (handler != null)
        
            handler(this, new PropertyChangedEventArgs(propertyName));
        
    

然后,我的 ViewModel 也发生了一些变化......

using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Windows.Input;
using Test.Model;
using System.ComponentModel;

namespace Test.ViewModel

    class UserViewModel : ViewModelBase
    
        //Private variables

    private ObservableCollection<User> _users;
    RelayCommand _userSave;
    private User _selectedUser = new User();

    //Properties
    public ObservableCollection<User> User
    
        get
        
            if (_users == null)
            
                _users = new ObservableCollection<User>();
                _users.CollectionChanged += (s, e) =>
                
                    if (e.Action == NotifyCollectionChangedAction.Add)
                    
                        // handle property changing
                        foreach (User item in e.NewItems)
                        
                            ((INotifyPropertyChanged)item).PropertyChanged += (s1, e1) =>
                                
                                    OnPropertyChanged("EnableListBox");
                                ;
                        
                    
                ;
                //Populate with users
                _users.Add(new User UserName = "Bob", Firstname="Bob", Surname="Smith");
                _users.Add(new User UserName = "Smob", Firstname="John", Surname="Davy");
            
            return _users;
        
    

    public User SelectedUser
    
        get  return _selectedUser; 
        set  _selectedUser = value; 
    

    public bool EnableListBox
    
        get  return !_selectedUser.IsDirty; 
    

    //Commands
    public ICommand UserSave
    
        get
        
            if (_userSave == null)
            
                _userSave = new RelayCommand(param => this.UserSaveExecute(), param => this.UserSaveCanExecute);
            
            return _userSave;
        
    

    void UserSaveExecute()
    
        //Here I will call my DataAccess to actually save the data
        //Save code...
        _selectedUser.SetToClean();
        OnPropertyChanged("EnableListBox");
    

    bool UserSaveCanExecute
    
        get
        
            return _selectedUser.IsDirty;
        
    

    //constructor
    public UserViewModel()
    

    


最后,XAML 我更改了用户名、姓氏和名字的绑定以包括 UpdateSourceTrigger=PropertyChanged 然后我绑定了列表框的 SelectedItem 和 IsEnabled

正如我在开始时所说的那样 - 这可能不是最好的解决方案,但它似乎有效......

【讨论】:

【参考方案6】:

由于您的 UserSave 命令位于 ViewModel 中,我会在那里跟踪“脏”状态。我会将数据绑定到 ListBox 中的选定项目,当它发生变化时,存储选定用户属性的当前值的快照。然后您可以与此进行比较以确定是否应启用/禁用该命令。

但是,由于您直接绑定到模型,因此您需要一些方法来确定是否发生了变化。您也可以在模型中实现 INotifyPropertyChanged,或者将属性包装在 ViewModel 中。

请注意,当命令的 CanExecute 发生变化时,您可能需要触发 CommandManager.InvalidateRequerySuggested()。

【讨论】:

【参考方案7】:

这就是我实现 IsDirty 的方式。在 ViewModal 中为 User 类的每个属性创建一个包装器(使用 IPropertyChanged 继承 User 类并在 User 类中实现 onpropertychanged 无济于事)。您需要将绑定从 UserName 更改为 WrapUserName。

public string WrapUserName 
    
        get
        
            return User.UserName          
        
        set
        
            User.UserName = value;
            OnPropertyChanged("WrapUserName");
        
    

现在有一个属性

 public bool isPageDirty
    
        get;
        set;
         

由于您的 viewmodal 继承自 baseviewmodal 并且 baseviewmodal 实现了 onPropertyChanged。

UserViewModel.PropertyChanged += (s, e) =>  isPageDirty = true; ;    

如果任何属性更改,isPageDirty 将为 true,因此在保存时检查 isPageDirty。

【讨论】:

谢谢。这看起来很有趣——今晚会玩。然而,我想出了另一个解决方案,稍后我将在下面解释

以上是关于MVVM - 为模型视图实现“Is Dirty”功能以保存数据的主要内容,如果未能解决你的问题,请参考以下文章

MVPMVC和MVVM框架介绍

MVVM框架

mvvm模式的简单介绍

DevOps is dirty work - What's the deal

使用 Unity 进行 MVVM 依赖注入,用于分层视图模型

Android MVVM:无法实例化视图模型