Страницы

Поиск по вопросам

суббота, 14 декабря 2019 г.

Реализация Undo/Redo для свойств ViewModel

#c_sharp #wpf


Есть класс PersonVm, который представляет информацию о человеке:

public class PersonVm : BaseViewModel
{
    private string _name;
    public string Name
    {
      get {return _name; }
      set 
      {   
          _name = value;
          RaisePropertyChanged();
      }
    }
}


Класс PersonManager представляет собой коллекцию персон и позволяет добавлять/удалять
персоны, а также откатывать эти изменения через UndoRedoService:

public class PersonManager  : BaseViewModel
{
    public ObservableCollection Persons {get;set;}
    public UndoRedoService UndoRedoService {get;set;} = new UndoRedoService();
}


Хотелось бы также откатывать изменения, которые происходят в PersonVm. 
Можно было бы подписаться на событие PropertyChangedу всех персон и получать название
свойства в котором произошло изменение, текущее и новое значения. 

Но в таком случае откатывать изменения пришлось бы через рефлексию — искать по названию
нужное свойство и менять его. А это не слишком быстрый способ.

Возможно сделать как-то иначе?
    


Ответы

Ответ 1



Я расширил базовый класс для ViewModel, чтобы он вёл историю изменений. У меня пример тестовый, поэтому так, вам скорее всего потребуются оба класса - с ведением истории и без нее, чтобы можно было наследоваться либо от того либо от другого. А еще лучше завести список с перечнем классов и их свойств по которым должна вестись история изменений, а может даже и сочинить вместо этого кастомный атрибут. У меня был такой класс: abstract class Vm : INotifyPropertyChanged { protected bool Set(ref T field, T value, [CallerMemberName]string propertyName = null) { if (EqualityComparer.Default.Equals(field, value)) return false; field = value; NotifyPropertyChanged(propertyName); return true; } protected void NotifyPropertyChanged([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); public event PropertyChangedEventHandler PropertyChanged; } Я его дополнил следующим функционалом: // Пришлось добавить флаги для того чтобы отличать обычную // установку свойства от Undo/Redo static bool isUndoProcess = false; static bool isRedoProcess = false; // Пара стеков для хранения истории static Stack<(object Obj, string Prop, object OldValue)> undoHistory = new Stack<(object Obj, string Prop, object OldValue)>(); static Stack<(object Obj, string Prop, object OldValue)> redoHistory = new Stack<(object Obj, string Prop, object OldValue)>(); static void Undo() { if (undoHistory.Count == 0) return; var undo = undoHistory.Pop(); UndoCommand.RaiseCanExecuteChanged(); // Обернуто для того чтобы в случае исключения флаг всё равно снимался try { isUndoProcess = true; undo.Obj.GetType().GetProperty(undo.Prop).SetValue(undo.Obj, undo.OldValue); } finally { isUndoProcess = false; } } static void Redo() { if (redoHistory.Count == 0) return; var redo = redoHistory.Pop(); RedoCommand.RaiseCanExecuteChanged(); try { isRedoProcess = true; redo.Obj.GetType().GetProperty(redo.Prop).SetValue(redo.Obj, redo.OldValue); } finally { isRedoProcess = false; } } static void SaveHistory(object obj, string propertyName, object value) { if (isUndoProcess) { redoHistory.Push((obj, propertyName, value)); RedoCommand.RaiseCanExecuteChanged(); } else if (isRedoProcess) { undoHistory.Push((obj, propertyName, value)); UndoCommand.RaiseCanExecuteChanged(); } else { undoHistory.Push((obj, propertyName, value)); UndoCommand.RaiseCanExecuteChanged(); redoHistory.Clear(); RedoCommand.RaiseCanExecuteChanged(); } } static void ClearHistory() { undoHistory.Clear(); UndoCommand.RaiseCanExecuteChanged(); redoHistory.Clear(); RedoCommand.RaiseCanExecuteChanged(); } // Команды, которые можно выставлять в GUI public static DelegateCommand UndoCommand { get; } = new DelegateCommand(_ => Undo(), _ => undoHistory.Count > 0); public static DelegateCommand RedoCommand { get; } = new DelegateCommand(_ => Redo(), _ => redoHistory.Count > 0); public static DelegateCommand ClearHistoryCommand { get; } = new DelegateCommand(_ => ClearHistory()); Теперь добавим сохранение в метод Set: protected bool Set(ref T field, T value, [CallerMemberName]string propertyName = null) { if (EqualityComparer.Default.Equals(field, value)) return false; // Сюда SaveHistory(this, propertyName, field); field = value; NotifyPropertyChanged(propertyName); return true; } Обратите внимание, решение не безопасно к потокам, но это и не требуется, т.к. вы должны обновлять свойства VM только в потоке GUI Здесь использована следующая реализация команды: class DelegateCommand : ICommand { protected readonly Predicate _canExecute; protected readonly Action _execute; public event EventHandler CanExecuteChanged; public DelegateCommand(Action execute) : this(execute, _ => true) { } public DelegateCommand(Action execute, Predicate canExecute) { _execute = execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute = canExecute ?? throw new ArgumentNullException(nameof(canExecute)); } public bool CanExecute(object parameter) => _canExecute(parameter); public void Execute(object parameter) => _execute(parameter); public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); } Ну и пример использования: class PeopleVm : Vm { string firstName; string secondName; string lastName; public string FirstName { get => firstName; set => Set(ref firstName, value); } public string SecondName { get => secondName; set => Set(ref secondName, value); } public string LastName { get => lastName; set => Set(ref lastName, value); } } class MainVm : Vm { public ObservableCollection Peoples { get; } = new ObservableCollection(); public DelegateCommand AddPeopleCommand { get; } public MainVm() { Peoples.Add(new PeopleVm { FirstName = "Иван", SecondName = "Петрович", LastName = "Сидоров" }); AddPeopleCommand = new DelegateCommand(_ => Peoples.Add(new PeopleVm())); } } Представление: