You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C# WPF MVVM基于INotifyPropertyChanged的撤销系统问题求助

解决方案

问题1:撤销操作重复触发入栈的解决方法

核心思路是在执行撤销操作时,临时跳过将变更记录压入栈的逻辑,但保留PropertyChanged事件触发以更新视图。可以在ViewModel中添加一个标记变量控制这个行为。

修改后的ViewModel代码:

internal class MyViewModel
{
    public MyModel MyModel { get; set; } = new();
    
    public Stack<KeyValuePair<string, object>> PropertyStateStack = new();
    public RelayCommand Undo { get; set; }
    // 添加撤销状态标记
    private bool isUndoing;

    public MyViewModel()
    {
        SetupCommands();
        MyModel.PropertyChanged += MyModel_PropertyChanged;
    }

    private void MyModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        // 撤销过程中不压栈
        if (isUndoing) return;
        
        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 =>
        {
            if (PropertyStateStack.Count == 0) return;
            
            isUndoing = true;
            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);
            }
            // 撤销完成后恢复标记
            isUndoing = false;
        });
    }
}

问题2:备忘录模式绑定失效的优化方案

之前备忘录模式失效是因为替换了整个MyModel实例,视图绑定的是原实例引用,所以会失效。正确的做法是备份实例的属性状态而非替换实例,恢复时直接给现有实例的属性赋值,保持引用不变。

实现备忘录类

// 备忘录类,保存模型的完整属性状态
public class ModelMemento
{
    private Dictionary<string, object> _propertyStates = new();

    // 从模型创建备忘录
    public static ModelMemento CreateFromModel(MyModel model)
    {
        var memento = new ModelMemento();
        var properties = typeof(MyModel).GetProperties(BindingFlags.Public | BindingFlags.Instance);
        foreach (var prop in properties)
        {
            if (prop.CanRead && prop.CanWrite)
            {
                memento._propertyStates[prop.Name] = prop.GetValue(model);
            }
        }
        return memento;
    }

    // 将备忘录状态恢复到模型
    public void RestoreToModel(MyModel model)
    {
        foreach (var kvp in _propertyStates)
        {
            var prop = model.GetType().GetProperty(kvp.Key);
            if (prop != null && prop.CanWrite)
            {
                prop.SetValue(model, Convert.ChangeType(kvp.Value, prop.PropertyType));
            }
        }
    }
}

修改ViewModel使用备忘录

internal class MyViewModel
{
    public MyModel MyModel { get; set; } = new();
    // 改用备忘录栈
    public Stack<ModelMemento> MementoStack = new();
    public RelayCommand Undo { get; set; }
    private bool isUndoing;

    public MyViewModel()
    {
        SetupCommands();
        // 初始状态入栈
        MementoStack.Push(ModelMemento.CreateFromModel(MyModel));
        MyModel.PropertyChanged += MyModel_PropertyChanged;
    }

    private void MyModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        if (isUndoing) return;
        // 每次属性变更后保存当前状态到栈
        MementoStack.Push(ModelMemento.CreateFromModel(MyModel));
    }
    
    private void SetupCommands()
    {
        Undo = new RelayCommand(o =>
        {
            if (MementoStack.Count <= 1) return; // 保留初始状态
            
            isUndoing = true;
            MementoStack.Pop(); // 弹出当前状态
            var previousState = MementoStack.Peek();
            previousState.RestoreToModel(MyModel);
            isUndoing = false;
        });
    }
}

额外优化:修正INotifyPropertyChangedExtended的定义

当前把INotifyPropertyChangedExtended定义为类不符合接口设计意图,改为接口可避免继承限制:

public interface INotifyPropertyChangedExtended : INotifyPropertyChanged
{
    void OnPropertyChanged(string propertyName, object oldValue);
}

public class PropertyChangedEventArgsExtended : PropertyChangedEventArgs
{
    public object OldValue { get; }

    public PropertyChangedEventArgsExtended(string propertyName, object oldValue)
           : base(propertyName)
    {
        OldValue = oldValue;
    }
}

修改MyModel实现该接口:

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)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgsExtended(propertyName, oldValue));
    }
}

内容的提问来源于stack exchange,提问作者92carmnad

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 06:25:19