MVVM - реализация функциональности IsDirty для ModelView для сохранения данных

Будучи новым для 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 достаточно проста: вы просто устанавливаете IsDirty в OnPropertyChanged, если это еще не верно.

Ваш установщик IsDirty должен, если свойство было ложным и теперь истинно, вызывает BeginEdit.

Ваша команда Save должна вызвать EndEdit, которая обновляет модель данных и устанавливает IsDirty на false.

Ваша команда Cancel должна вызвать CancelEdit, которая обновляет модель представления из модели данных и устанавливает IsDirty в false.

Свойства CanSave и CanCancel (если вы используете RelayCommand для этих команд), просто верните текущее значение IsDirty.

Обратите внимание: поскольку ни одна из этих функций не зависит от конкретной реализации модели представления, вы можете поместить ее в абстрактный базовый класс. Производные классы не должны реализовывать какие-либо свойства, связанные с командой, или свойство IsDirty; они просто должны переопределить BeginEdit, EndEdit и CancelEdit.

Ответ 2

Я предлагаю вам использовать GalaSoft MVVM Light Toolkit, поскольку его гораздо проще реализовать, чем подход DIY.

Для грязных чтений вам необходимо сохранить снимок каждого поля и вернуть true или false из метода UserSaveCanExecute(), который соответственно включит/отключит командную кнопку.

Ответ 3

Если вы хотите использовать каркасный подход, а не писать инфраструктуру самостоятельно, вы можете использовать CSLA (http://www.lhotka.net/cslanet/) - Rocky framework для разработка бизнес-объектов. Состояние объекта управляется для вас при изменениях свойств, а база кода также включает пример типа ViewModel, который поддерживает базовую модель, Сохранить глагол и свойство CanSave. Вы можете получить вдохновение от кода, даже если вы не хотите использовать фреймворк.

Ответ 4

Я проделал определенную работу по внедрению IsDirty для моделей, которые завернуты в мою ViewModel.

Результат действительно упростил мои 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/ Проверьте под сборку шаблонов и папку MVVM, вы найдете класс ModelDataStore.

P.S. Я не проводил полномасштабный тест, просто очень простой тест, который вы найдете в тестовой сборке.

Ответ 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.InvalidateRequerySposed().

Ответ 7

Вот как я реализовал IsDirty. Создайте оболочку для каждого свойства класса User (наследуйте класс User с помощью IPropertyChanged и реализуйте onpropertychanged в классе User wont help) в вашем ViewModal. Вам нужно изменить привязку с 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 будет истинным, поэтому, сохраняя чан, проверьте isPageDirty.