RError.com

RError.com Logo RError.com Logo

RError.com Navigation

  • 主页

Mobile menu

Close
  • 主页
  • 系统&网络
    • 热门问题
    • 最新问题
    • 标签
  • Ubuntu
    • 热门问题
    • 最新问题
    • 标签
  • 帮助
主页 / 问题 / 705396
Accepted
trydex
trydex
Asked:2020-08-13 04:03:54 +0000 UTC2020-08-13 04:03:54 +0000 UTC 2020-08-13 04:03:54 +0000 UTC

ViewModel 属性的撤消/重做实现

  • 772

有一个类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为所有人订阅事件并接收发生更改的属性的名称、当前值和新值。

但是在这种情况下,有必要通过反射回滚更改 - 按名称搜索所需的属性并进行更改。这不是一个非常快速的方法。

有可能做一些不同的事情吗?

c#
  • 2 2 个回答
  • 10 Views

2 个回答

  • Voted
  1. Best Answer
    Андрей NOP
    2020-08-15T13:46:45Z2020-08-15T13:46:45Z

    我扩展了基类ViewModel以保留更改的历史记录。我有一个测试示例,所以像这样,你很可能需要两个类——有历史和没有历史,这样你就可以从一个或另一个继承。
    最好有一个包含类及其属性列表的列表,应该保留其更改历史,甚至可以组成一个自定义属性。

    我有这样的课:

    abstract class Vm : INotifyPropertyChanged
    {
        protected bool Set<T>(ref T field, T value, [CallerMemberName]string propertyName = null)
        {
            if (EqualityComparer<T>.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());
    

    现在让我们将 save 添加到方法中Set<T>:

        protected bool Set<T>(ref T field, T value, [CallerMemberName]string propertyName = null)
        {
            if (EqualityComparer<T>.Default.Equals(field, value))
                return false;
            // Сюда
            SaveHistory(this, propertyName, field);
            field = value;
            NotifyPropertyChanged(propertyName);
            return true;
        }
    

    请注意,该解决方案不是线程安全的,但不是必需的,因为 您应该只在 GUI 线程上更新 VM 属性

    此处使用以下命令实现:

    class DelegateCommand : ICommand
    {
        protected readonly Predicate<object> _canExecute;
        protected readonly Action<object> _execute;
    
        public event EventHandler CanExecuteChanged;
    
        public DelegateCommand(Action<object> execute) : this(execute, _ => true) { }
    
        public DelegateCommand(Action<object> execute, Predicate<object> 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<PeopleVm> Peoples { get; }
            = new ObservableCollection<PeopleVm>();
    
        public DelegateCommand AddPeopleCommand { get; }
    
        public MainVm()
        {
            Peoples.Add(new PeopleVm { FirstName = "Иван", SecondName = "Петрович", LastName = "Сидоров" });
            AddPeopleCommand = new DelegateCommand(_ => Peoples.Add(new PeopleVm()));
        }
    }
    

    表现:

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
    
        <ToolBar>
            <Button Content="Undo" Command="{Binding UndoCommand}"/>
            <Button Content="Redo" Command="{Binding RedoCommand}"/>
            <Button Content="Clear" Command="{Binding ClearHistoryCommand}"/>
            <Separator/>
            <Button Content="Add" Command="{Binding AddPeopleCommand}"/>
        </ToolBar>
    
        <ScrollViewer Grid.Row="1">
            <ItemsControl ItemsSource="{Binding Peoples}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Border BorderBrush="DarkGreen"
                                BorderThickness="1"
                                Margin="2" CornerRadius="5">
                            <Grid>
                                <Grid.Resources>
                                    <Style TargetType="TextBlock">
                                        <Setter Property="Margin" Value="3,3,0,3"/>
                                        <Setter Property="Padding" Value="3"/>
                                        <Setter Property="VerticalAlignment" Value="Center"/>
                                        <Setter Property="HorizontalAlignment" Value="Right"/>
                                    </Style>
                                    <Style TargetType="TextBox">
                                        <Setter Property="Margin" Value="0,3,3,3"/>
                                        <Setter Property="Padding" Value="3"/>
                                        <Setter Property="VerticalAlignment" Value="Center"/>
                                    </Style>
                                </Grid.Resources>
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="Auto"/>
                                    <RowDefinition Height="Auto"/>
                                    <RowDefinition Height="Auto"/>
                                </Grid.RowDefinitions>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="Auto"/>
                                    <ColumnDefinition/>
                                </Grid.ColumnDefinitions>
    
                                <TextBlock Grid.Row="0" Text="Имя:"/>
                                <TextBox Grid.Row="0" Grid.Column="1"
                                         Text="{Binding FirstName}"/>
    
                                <TextBlock Grid.Row="1" Text="Отчество:"/>
                                <TextBox Grid.Row="1" Grid.Column="1"
                                         Text="{Binding SecondName}"/>
    
                                <TextBlock Grid.Row="2" Text="Фамилия:"/>
                                <TextBox Grid.Row="2" Grid.Column="1"
                                         Text="{Binding LastName}"/>
                            </Grid>
                        </Border>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </ScrollViewer>
    </Grid>
    

    撤消重做测试


    添加属性!

    创建一个属性类:

    [AttributeUsage(AttributeTargets.Property)]
    class UndoRedoAttribute : Attribute { }
    

    在方法的开头,Vm.SaveHistory添加一个属性是否存在的检查:

            if (obj.GetType()
                   .GetProperty(propertyName)
                   .GetCustomAttributes(typeof(UndoRedoAttribute), true)
                   .Length == 0) return;
    

    一切!现在将这个属性附加到所需的属性就足够了:

    class PeopleVm : Vm
    {
        ...
    
        [UndoRedo]
        public string FirstName
        { ... }
    
        public string SecondName
        { ... }
    
        [UndoRedo]
        public string LastName
        { ... }
    }
    

    而且,瞧,故事只是根据指定的属性!

    • 6
  2. sp7
    2020-08-14T04:38:54Z2020-08-14T04:38:54Z

    由于您自己执行更改的“回滚”非常繁琐。使用现成的解决方案更容易。开箱即用提供此功能的一个 WPF 框架是Catel。

    以下是此类应用程序的一个简单示例:

    1)创建一个新项目:File - New - Project... - WpfApplication.

    2)安装卡特。

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

    3) 在项目中,我们从以下文件夹创建标准结构:Models、ViewModels、Views。

    在此处输入图像描述

    4) 在Models文件夹中,创建一个类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文件夹中,为我们的MainView创建一个ViewModel。我们称之为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) 原则上,就是这样,在解决方案资源管理器中,我们的项目现在看起来像这样:

    在此处输入图像描述

    10) 现在如果您运行应用程序,在 DataGrid 中选择一个用户并开始对其进行编辑,然后单击“ 取消”按钮,所有更改都将被取消。


    PS 如果您不想使用 Catel 的 DP 使用这种繁琐的语法怎么办?

    选项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

    使用代码片段

    • 3

相关问题

Sidebar

Stats

  • 问题 10021
  • Answers 30001
  • 最佳答案 8000
  • 用户 6900
  • 常问
  • 回答
  • Marko Smith

    Python 3.6 - 安装 MySQL (Windows)

    • 1 个回答
  • Marko Smith

    C++ 编写程序“计算单个岛屿”。填充一个二维数组 12x12 0 和 1

    • 2 个回答
  • Marko Smith

    返回指针的函数

    • 1 个回答
  • Marko Smith

    我使用 django 管理面板添加图像,但它没有显示

    • 1 个回答
  • Marko Smith

    这些条目是什么意思,它们的完整等效项是什么样的

    • 2 个回答
  • Marko Smith

    浏览器仍然缓存文件数据

    • 1 个回答
  • Marko Smith

    在 Excel VBA 中激活工作表的问题

    • 3 个回答
  • Marko Smith

    为什么内置类型中包含复数而小数不包含?

    • 2 个回答
  • Marko Smith

    获得唯一途径

    • 3 个回答
  • Marko Smith

    告诉我一个像幻灯片一样创建滚动的库

    • 1 个回答
  • Martin Hope
    Air 究竟是什么标识了网站访问者? 2020-11-03 15:49:20 +0000 UTC
  • Martin Hope
    Алексей Шиманский 如何以及通过什么方式来查找 Javascript 代码中的错误? 2020-08-03 00:21:37 +0000 UTC
  • Martin Hope
    Qwertiy 号码显示 9223372036854775807 2020-07-11 18:16:49 +0000 UTC
  • Martin Hope
    user216109 如何为黑客设下陷阱,或充分击退攻击? 2020-05-10 02:22:52 +0000 UTC
  • Martin Hope
    Qwertiy 并变成3个无穷大 2020-11-06 07:15:57 +0000 UTC
  • Martin Hope
    koks_rs 什么是样板代码? 2020-10-27 15:43:19 +0000 UTC
  • Martin Hope
    user207618 Codegolf——组合选择算法的实现 2020-10-23 18:46:29 +0000 UTC
  • Martin Hope
    Sirop4ik 向 git 提交发布的正确方法是什么? 2020-10-05 00:02:00 +0000 UTC
  • Martin Hope
    faoxis 为什么在这么多示例中函数都称为 foo? 2020-08-15 04:42:49 +0000 UTC
  • Martin Hope
    Pavel Mayorov 如何从事件或回调函数中返回值?或者至少等他们完成。 2020-08-11 16:49:28 +0000 UTC

热门标签

javascript python java php c# c++ html android jquery mysql

Explore

  • 主页
  • 问题
    • 热门问题
    • 最新问题
  • 标签
  • 帮助

Footer

RError.com

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

帮助

© 2023 RError.com All Rights Reserve   沪ICP备12040472号-5