[英]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.