MVVM - реализация функциональности IsDirty в ModelView для сохранения данных - PullRequest
14 голосов
/ 29 декабря 2010

Будучи новичком в 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>

Таким образом, в принципе, когда я редактирую фамилию, кнопка «Сохранить» должна быть включена; и если я отменю свое редактирование - тогда его снова нужно отключить, поскольку ничего не изменилось.

Я видел это во многих примерах, но еще не выяснил, как это сделать.

Любая помощь будет высоко ценится! Брендан

Ответы [ 7 ]

8 голосов
/ 29 декабря 2010

По моему опыту, если вы реализуете IsDirty в своей модели представления, вы, вероятно, также хотите, чтобы модель представления реализовала IEditableObject.

Предполагая, что ваша модель представления является обычной сортировкой, реализует PropertyChanged и частный или защищенный OnPropertyChanged метод, который вызывает его, установка IsDirty достаточно проста: вы просто устанавливаете IsDirty в OnPropertyChanged, если это еще не так.

Ваш IsDirtysetter должен, если свойство имело значение false и теперь имеет значение true, вызвать BeginEdit.

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

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

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

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

4 голосов
/ 07 января 2011

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

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

4 голосов
/ 29 декабря 2010

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

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

3 голосов
/ 29 декабря 2010

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

2 голосов
/ 31 декабря 2010

Я пришел с рабочим решением. Конечно, это может быть не лучшим способом, но я уверен, что смогу поработать над этим, узнав больше ...

Когда я запускаю проект, если я могу изменить какой-либо элемент, окно списка отключается, а кнопка сохранения активируется. Если я отменил свои изменения, окно списка снова будет активировано, а кнопка сохранения отключена.

Я изменил свою модель пользователя для реализации 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

Как я сказал в начале - это может быть не лучшим решением, но, похоже, оно работает ...

0 голосов
/ 30 декабря 2010

Вот как я реализовал IsDirty.Создайте оболочку для каждого свойства класса User (наследование класса User с IPropertyChanged и реализация onpropertychanged в классе User не помогут) в вашем 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.

0 голосов
/ 29 декабря 2010

Поскольку ваша команда UserSave находится во ViewModel, я бы отслеживал состояние «грязного» там. Я бы привязал данные к выбранному элементу в ListBox, и когда он изменится, сохраню моментальный снимок текущих значений свойств выбранного пользователя. Затем вы можете сравнить это, чтобы определить, должна ли команда быть включена / отключена.

Однако, поскольку вы привязываетесь непосредственно к модели, вам нужен какой-то способ выяснить, изменилось ли что-то. Либо вы также реализуете INotifyPropertyChanged в модели, либо переносите свойства в ViewModel.

Обратите внимание, что при изменении CanExecute команды может потребоваться запустить CommandManager.InvalidateRequerySuggested ().

...