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

354
14 августа 2017, 08:12

Есть класс 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<Person> Persons {get;set;}
    public UndoRedoService UndoRedoService {get;set;} = new UndoRedoService();
}

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

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

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

Answer 1

Так как реализация "отката" изменений самостоятельно дело довольное утомительное. Проще использовать готовое решение. Одним из WPF-Фреймворков, который предоставляет данную функциональность из коробки является Catel.

Ниже показан простейший пример такого приложения:

1) Создаем новый проект: File - New - Project... - WpfApplication.

2) Устанавливаем Catel.

PM> Install-Package Catel.MVVM
PM> Install-Package Catel.Core -Version 4.5.4 

3) В проекте создаем стандартную структуру из папок: Models, ViewModels, Views.

4) В папке Models cоздаем класс User, представляющий нашу модель, он будет содержать всего пару свойств Name и LastName и наследуем его от ModelBase.

public class UserModel : ModelBase
{
     public string Name
     {
         get { return GetValue<string>(AuthorProperty); }
         set { SetValue(AuthorProperty, value); }
     }
     public static readonly PropertyData AuthorProperty = 
        RegisterProperty(nameof(Name), typeof(string), string.Empty);
    public string LastName
    {
        get { return GetValue<string>(LastNameProperty); }
        set { SetValue(LastNameProperty, value); }
    }
    public static readonly PropertyData LastNameProperty =
        RegisterProperty(nameof(LastName), typeof(string), string.Empty);
}

5) Далее в папке Views создаем нашу View, назовем ее MainView, ее разметка представлена ниже. Обратите внимание, что тип окна catel:Window.

<catel:Window x:Class="WpfApplication1.Views.MainView"
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:catel="http://schemas.catelproject.com"
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
              xmlns:viewModels="clr-namespace:WpfApplication1.ViewModels"
              Title="MainWindow"
              Width="525"
              Height="350"
              d:DataContext="{d:DesignInstance Type=viewModels:MainViewModel, IsDesignTimeCreatable=False}"
              mc:Ignorable="d">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <DataGrid Grid.Row="0"
                  AutoGenerateColumns="False"
                  CanUserAddRows="False"
                  CanUserDeleteRows="False"
                  IsReadOnly="True"
                  ItemsSource="{Binding Users}"
                  SelectedItem="{Binding SelectedUser}">
            <DataGrid.Columns>
                <DataGridTextColumn Width="*"
                                    Binding="{Binding Name}"
                                    Header="Имя" />
                <DataGridTextColumn Width="100"
                                    Binding="{Binding LastName}"
                                    Header="Фамилия" />
            </DataGrid.Columns>
        </DataGrid>
        <Button Grid.Row="1"
                Command="{Binding EditUserCommand}"
                Content="Редактировать" />
    </Grid>
</catel:Window>

6) В папке ViewModels создаем ViewModel для нашей MainView. Назовем ее MainViewModel.

public class MainViewModel : ViewModelBase
{
    // Сервис открытия окон. Поставляется из коробки.
    private readonly IUIVisualizerService _uiVisualizerService;
    public MainViewModel(IUIVisualizerService uiVisualizerService)
    {
        _uiVisualizerService = uiVisualizerService;
        EditUserCommand = new Command(EditUserAsync, () => SelectedUser != null);
    }
    private async void EditUserAsync()
    {
        // Создаем нашу ViewModel.
        var editUserViewModel = new EditUserViewModel(SelectedUser);
        // Передаем объект ViewModel сервису окон, он самостоятельно найдет соответствующую ей View.
        await _uiVisualizerService.ShowDialogAsync(editUserViewModel);
    }
    // Инициализируем коллекцию тестовыми данными.
    protected override Task InitializeAsync()
    {
        Users.Add(new UserModel()
        {
            Name = "Вася",
            LastName = "Иванов"
        });
        Users.Add(new UserModel()
        {
            Name = "Петя",
            LastName = "Петров"
        });
        return base.InitializeAsync();
    }
    public Command EditUserCommand { get; }
    // Catel использует DP для уведомления View об изменении.
    // Выбранный в DataGrid пользователь. 
    public UserModel SelectedUser
    {
        get { return GetValue<UserModel>(SelectedUserProperty); }
        set { SetValue(SelectedUserProperty, value); }
    }
    public static readonly PropertyData SelectedUserProperty =
        RegisterProperty(nameof(SelectedUser),
            typeof(UserModel));
    // Список всех пользователей.
    public ObservableCollection<UserModel> Users
    {
        get { return GetValue<ObservableCollection<UserModel>>(UsersProperty); }
        set { SetValue(UsersProperty, value); }
    }
    public static readonly PropertyData UsersProperty =
        RegisterProperty(nameof(Users), 
            typeof(ObservableCollection<UserModel>), 
            new ObservableCollection<UserModel>());
} 

7) Создаем представление, для редактирования нашего пользователя, оно будет следующим. Обратите внимание, что тип окна catel:DataWindow.

<catel:DataWindow x:Class="WpfApplication1.Views.EditUserView"
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:catel="http://schemas.catelproject.com"
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
              xmlns:viewModels="clr-namespace:WpfApplication1.ViewModels"
              d:DataContext="{d:DesignInstance Type=viewModels:EditUserViewModel, IsDesignTimeCreatable=False}"
              mc:Ignorable="d">
<Grid Margin="10">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>
    <TextBlock Grid.Row="0"
               Grid.Column="0"
               Text="Имя" />
    <TextBox Grid.Row="0"
             Grid.Column="1"
             Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" />
    <TextBlock Grid.Row="1"
               Grid.Column="0"
               Text="Фамилия" />
    <TextBox Grid.Row="1"
             Grid.Column="1"
             Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged}" />
</Grid>

8) И ViewModel для него:

public class EditUserViewModel : ViewModelBase
{
    public EditUserViewModel(UserModel user)
    {
        User = user;
    }
    // Свойства, которые связаны со View и отражаются на объект редактируемой модели. 
    [ViewModelToModel(nameof(User), nameof(UserModel.Name))]
    public string Name
    {
        get { return GetValue<string>(AuthorProperty); }
        set { SetValue(AuthorProperty, value); }
    }
    public static readonly PropertyData AuthorProperty =
        RegisterProperty(nameof(Name), typeof(string), string.Empty);
    // Свойства, которые связаны со View и отражаются на объект редактируемой модели. 
    [ViewModelToModel(nameof(User), nameof(UserModel.LastName))]
    public string LastName
    {
        get { return GetValue<string>(LastNameProperty); }
        set { SetValue(LastNameProperty, value); }
    }
    public static readonly PropertyData LastNameProperty =
        RegisterProperty(nameof(LastName), typeof(string), string.Empty);
    // Переданный объект модели, который мы редактируем.
    [Model]
    public UserModel User
    {
        get { return GetValue<UserModel>(UserProperty); }
        set { SetValue(UserProperty, value); }
    }
    public static readonly PropertyData UserProperty =
        RegisterProperty(nameof(User), typeof(UserModel));
}

9) В принципе на этом все, в Solution Explorer наш проект теперь выглядит так:

10) Теперь если запустить приложение, выбрать в DataGrid какого-нибудь пользователя и начать редактировать его, а после нажать на кнопку Отмена все изменения будут отменены.

P.S. Что делать, если не охота использовать этот громоздкий синтаксис с использованием DP из Catel?

Вариант 1

Установить Catel.Fody

PM> Install-Package Catel.Fody -Version 2.17.0  

В этом случае нужный код для DP будет сгенерирован автоматически, путем перезаписи IL, после чего во ViewModel достаточно написать так:

public class EditUserViewModel : ViewModelBase
{
    public EditUserViewModel(UserModel user)
    {
        User = user;
    }
    [ViewModelToModel(nameof(User), nameof(UserModel.Name))]
    public string Name { get; set; }
    [ViewModelToModel(nameof(User), nameof(UserModel.LastName))]
    public string LastName { get; set; }
    [Model]
    public UserModel User { get; set; }
}

Вариант 2

Использовать Code Snippets

READ ALSO
Включить и выключить TextBox с помощью кнопки Button c#

Включить и выключить TextBox с помощью кнопки Button c#

Подскажите пожалуйста, как сделать чтобы при нажатии на Button один раз TextBox включился и второй раз нажать на этот же Button TextBox должен выключитсяСпасибо

278
не работает прокрутка в ListBox из-за DataGrid

не работает прокрутка в ListBox из-за DataGrid

Добрый деньОбнаружил такую проблему

280
Как ограничить добавление товара в Modx Revolution имея тв поле остаток?

Как ограничить добавление товара в Modx Revolution имея тв поле остаток?

Ограничение максимального числа ввода добавления в корзину minishop2 из tv availability Чтобы расширить поле availability читаем: //githubcom/bezumkin/miniShop2/blob/master/assets/components/minishop2/js/web/default

461