[英]C# WPF MVVM undo system with INotifyPropertyChanged
我正在尝试创建一个撤消系统,当实现 INotifyPropertyChanged 的 object 上的属性发生更改时,属性名称及其旧值通过 KeyValuePair 推送到堆栈中。 当用户单击“撤消”时,它会从堆栈中弹出并使用反射将属性的值设置为其旧值。 问题在于它再次调用 OnPropertyChanged,因此该属性及其恢复的值再次添加到撤消堆栈中。 另一方面,我仍然希望它调用 OnPropertyChanged,因为我希望视图更新其绑定。 我的设计方式显然有问题,但我似乎无法找到另一种解决方法。
这是我的 model
internal class MyModel : INotifyPropertyChangedExtended
{
private string testProperty1 = "";
public string TestProperty1
{
get { return testProperty1; }
set {
var oldValue = testProperty1;
testProperty1 = value;
OnPropertyChanged(nameof(TestProperty1), oldValue);
}
}
private string testProperty2 = "";
public string TestProperty2
{
get { return testProperty2; }
set {
var oldValue = testProperty2;
testProperty2 = value;
OnPropertyChanged(nameof(TestProperty2), oldValue);
}
}
public event PropertyChangedEventHandler? PropertyChanged;
public void OnPropertyChanged(string propertyName, object oldValue)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgsExtended(propertyName, oldValue));
}
}
}
这是我的 INotifyPropertyChangedExtended 接口
public class PropertyChangedEventArgsExtended : PropertyChangedEventArgs
{
public virtual object OldValue { get; private set; }
public PropertyChangedEventArgsExtended(string propertyName, object oldValue)
: base(propertyName)
{
OldValue = oldValue;
}
}
public class INotifyPropertyChangedExtended : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string propertyName, object oldValue)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgsExtended(propertyName, oldValue));
}
}
这是我的观点 model
internal class MyViewModel
{
public MyModel MyModel { get; set; } = new();
public Stack<KeyValuePair<string, object>> PropertyStateStack = new();
public RelayCommand Undo { get; set; }
public MyViewModel()
{
SetupCommands();
MyModel.PropertyChanged += MyModel_PropertyChanged;
}
private void MyModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var args = e as PropertyChangedEventArgsExtended;
if (args.OldValue != null)
{
PropertyStateStack.Push(new KeyValuePair<string, object>(args.PropertyName, args.OldValue));
}
}
private void SetupCommands()
{
Undo = new RelayCommand(o =>
{
KeyValuePair<string, object> propertyState = PropertyStateStack.Pop();
PropertyInfo? property = MyModel.GetType().GetProperty(propertyState.Key);
if (property != null)
{
property.SetValue(MyModel, Convert.ChangeType(propertyState.Value, property.PropertyType), null);
}
});
}
}
编辑:我确实研究了“纪念品模式”,但我无法让它与 INotifyPropertyChanged 一起使用,因为一旦我将 MyModel 设置为它的备份,与视图的绑定就会停止工作。
实施 Memento 或变体是正确的方法。 与存储特定的修改撤消操作相反,例如Action<T>
(另一个好的解决方案),Memento 具有更高的 memory 占用空间(因为它存储了完整的 object 状态),但允许随机访问存储的状态。
关键是,当正确实现 Memento 时,你不必依赖反射,这只会让你的代码变得又慢又笨重。
下面的示例使用IEditableObject
接口来实现 Memento 模式(变体)。 TextBox
使用相同的接口以类似的方式实现撤消/重做。 优点是您可以完全控制何时记录 object state。 您甚至可以取消正在进行的修改。
此示例克隆完整的 object 以备份 state。 请参阅上面IEditableObject
链接提供的示例,了解如何引入包含对象数据的不可变数据 model。
因为对象可能非常昂贵,例如当它们分配资源时,引入一个实际存储公共可编辑属性值的不可变数据 model 可能是有意义的。 这可以提高关键场景中的性能。
实际的撤消/重做逻辑封装在示例抽象StateTracker<TData>
class 中。 StateTracker<TData>
实现了前面提到的IEditableObject
和ICloneable
接口。 为了增加便利StateTracker<TData>
还实现了一个自定义的IUndoable
接口(以启用匿名使用)。
每个需要支持 state 跟踪(撤消/重做)的 class 都必须扩展抽象StateTracker<TData>
以提供ICloneable.Clone
和StateTracker.UpdateState
实现。
下面的例子是非常基本的。 它允许撤消和重做,但不支持对撤消/重做状态的随机访问。 您必须使用像List<T>
这样的基于索引的后备存储来实现这样的功能。
IUndoable.cs
启用对撤消/重做 API 的匿名访问。
public interface IUndoable
{
bool TryUndo();
bool TryRedo();
}
状态跟踪器.cs
封装实际的撤消/重做逻辑以避免重复实现
对于应该支持撤消/重做的每种类型。
您可以考虑在此 class 中添加公共UndoCommand
和RedoCommand
,并让命令分别调用TryUndo
和TryRedo
。
public abstract class StateTracker<TData> : IEditableObject, IUndoable, ICloneable
{
public bool IsInEditMode { get; private set; }
private Stack<TData> UndoMemory { get; }
private Stack<TData> RedoMemory { get; }
private TData CurrentState { get; set; }
private bool IsUpdatingState { get; set; }
protected StateTracker()
{
this.UndoMemory = new Stack<TData>();
this.RedoMemory = new Stack<TData>();
}
public abstract TData Clone();
protected abstract void UpdateState(TData state);
object ICloneable.Clone() => Clone();
public bool TryUndo()
{
if (this.UndoMemory.TryPop(out TData previousState))
{
this.IsUpdatingState = true;
this.CurrentState = Clone();
this.RedoMemory.Push(this.CurrentState);
UpdateState(previousState);
this.IsUpdatingState = false;
return true;
}
return false;
}
public bool TryRedo()
{
if (this.RedoMemory.TryPop(out TData nextState))
{
this.IsUpdatingState = true;
this.CurrentState = Clone();
this.UndoMemory.Push(this.CurrentState);
UpdateState(nextState);
this.IsUpdatingState = false;
return true;
}
return false;
}
public void BeginEdit()
{
if (this.IsInEditMode || this.IsUpdatingState)
{
return;
}
this.IsInEditMode = true;
this.CurrentState = Clone();
}
public void CancelEdit()
{
if (this.IsInEditMode)
{
UpdateState(this.CurrentState);
this.IsInEditMode = false;
}
}
public void EndEdit()
{
if (this.IsUpdatingState)
{
return;
}
this.UndoMemory.Push(this.CurrentState);
this.IsInEditMode = false;
}
}
我的模型.cs
public class MyModel : StateTracker<MyModel>, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public MyModel()
{
}
// Copy constructor
private MyModel(MyModel originalInstance)
{
// Don't raise PropertyChanged to avoid the loop of death
this.testProperty1 = originalInstance.TestProperty1;
this.testProperty2 = originalInstance.TestProperty2;
}
// Create a deep copy using the copy constructor
public override MyModel Clone()
{
var copyOfInstance = new MyModel(this);
return copyOfInstance;
}
protected override void UpdateState(MyModel state)
{
this.TestProperty1 = state.TestProperty1;
this.TestProperty2 = state.TestProperty2;
}
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
private string testProperty1;
public string TestProperty1
{
get => this.testProperty1;
set
{
this.testProperty1 = value;
OnPropertyChanged();
}
}
private string testProperty2;
public string TestProperty2
{
get => this.testProperty2;
set
{
this.testProperty2 = value;
OnPropertyChanged();
}
}
}
以下示例存储绑定到MyModel
实例的TextBox
的 state。 当TextBox
获得焦点时,调用MyModel.BeginEdit
方法开始记录输入。 当TextBox
失去焦点时,通过调用MyModel.EndEdit
方法将记录的 state 推入撤消堆栈。
主窗口.xaml
<Window>
<Window.DataContext>
<local:MyModel />
</Window.DataContext>
<StackPanel>
<Button Content="Undo"
Click="OnUndoButtonClick" />
<Button Content="Redo"
Click="OnRedoButtonClick" />
<TextBox Text="{Binding TestProperty1, UpdateSourceTrigger=PropertyChanged}"
GotFocus="OnTextBoxGotFocus"
LostFocus="OnTextBoxLostFocus" />
</StackPanel>
</Window>
主窗口.xaml.cs
由于定义了接口,我们可以在不知道实际数据类型的情况下处理撤消/重做。
private void OnTextBoxGotFocus(object sender, RoutedEventArgs e)
=> ((sender as FrameworkElement).DataContext as IEditableObject).BeginEdit();
private void OnTextBoxLostFocus(object sender, RoutedEventArgs e)
=> ((sender as FrameworkElement).DataContext as IEditableObject).EndEdit();
private void OnUndoButtonClick(object sender, RoutedEventArgs e)
=> _ = ((sender as FrameworkElement).DataContext as IUndoable).TryUndo();
private void OnRedoButtonClick(object sender, RoutedEventArgs e)
=> _ = ((sender as FrameworkElement).DataContext as IUndoable).TryRedo();
另一种流程可能是MyModel
EndEdit
在相关属性设置器内部调用BeginEdit
和 EndEdit(在接受新值之前和接受新值之后)。
在这种情况下,不需要先前在TextBox
上定义的GotFocus
和LostFocus
事件处理程序(上面的示例):
我的模型.cs
public class MyModel : StateTracker<MyModel>, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public MyModel()
{
}
// Copy constructor
private MyModel(MyModel originalInstance)
{
// Don't raise PropertyChanged to avoid the loop of death
this.testProperty1 = originalInstance.TestProperty1;
this.testProperty2 = originalInstance.TestProperty2;
}
// Create a deep copy using the copy constructor
public override MyModel Clone()
{
var copyOfInstance = new MyModel(this);
return copyOfInstance;
}
protected override void UpdateState(MyModel state)
{
// UpdateState() is called by the StateTracker
// which internally guards against the infinite loop
this.TestProperty1 = state.TestProperty1;
this.TestProperty2 = state.TestProperty2;
}
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
private string testProperty1;
public string TestProperty1
{
get => this.testProperty1;
set
{
BeginEdit();
this.testProperty1 = value;
EndEdit();
OnPropertyChanged();
}
}
private string testProperty2;
public string TestProperty2
{
get => this.testProperty2;
set
{
BeginEdit();
this.testProperty2 = value;
EndEdit();
OnPropertyChanged();
}
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.