MVVM - 为模拟视图实现“脏”功能以保存数据

时间:2010-12-29 17:44:49

标签: c# wpf mvvm observablecollection

刚接触WPF& MVVM我在努力学习一些基本功能。

首先让我解释一下我的目标,然后附上一些示例代码......

我有一个显示用户列表的屏幕,我在右侧显示所选用户的详细信息,其中包含可编辑的文本框。然后我有一个Save按钮,它是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>

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

我在很多例子中都看过这个,但还没有发现如何做到这一点。

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

7 个答案:

答案 0 :(得分:8)

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

假设您的视图模型是通常的排序方式,实现PropertyChanged以及提升它的私有或受保护OnPropertyChanged方法,设置IsDirty非常简单:您只需设置{{1如果它不是真的那么在IsDirty中。

如果属性为false且现在为true,则OnPropertyChanged setter应该调用IsDirty

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

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

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

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

答案 1 :(得分:4)

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

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

答案 2 :(得分:4)

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

结果真正简化了我的ViewModels:

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

P.S。 我没有对它进行过全面的测试,只是你会找到测试组件的非常简单的测试。

答案 3 :(得分:3)

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

答案 4 :(得分:2)

我想出了一个有效的解决方案。这当然不是最好的方式,但我相信我可以继续努力,因为我了解更多......

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

我已经更改了我的用户模型以实现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

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

答案 5 :(得分:0)

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

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

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

答案 6 :(得分:0)

这就是我实施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。