实现一个管理器,用于实现Redo和Undo。
UndoRedoManager类
public class UndoRedoManager
{
private readonly Dictionary<AvaloniaObject, ObjectState> objectStates = new();
private bool isRestoring;
public event EventHandler? StateChanged;
// 内部类,封装对象的状态
private sealed class ObjectState
{
public List<(AvaloniaProperty Property, object? OldValue, object? NewValue)> History { get; set; } = new();
public int HistoryIndex { get; set; }
public List<IDisposable> Subscriptions { get; } = new();
public HashSet<AvaloniaProperty> TrackedProperties { get; } = new();
}
// 缓存类型属性查找结果,避免重复反射
private static readonly Dictionary<Type, Dictionary<string, AvaloniaProperty>> propertyCache = new();
public void Attach<T>(T target, params Expression<Func<T, object>>[] properties) where T : AvaloniaObject
{
if (properties == null || properties.Length == 0)
{
InternAttach(target, Array.Empty<AvaloniaProperty>());
return;
}
var avaloniaProperties = new AvaloniaProperty[properties.Length];
var targetType = target.GetType();
int validCount = 0;
for (int i = 0; i < properties.Length; i++)
{
var expr = properties[i];
var propPath = GetPropertyPath(expr.Body);
if (string.IsNullOrEmpty(propPath))
{
Console.WriteLine($"警告:无法解析属性表达式 '{expr}'");
continue;
}
var propName = propPath.Split('.').Last();
var avaloniaProperty = GetAvaloniaProperty(targetType, propName);
if (avaloniaProperty != null)
{
avaloniaProperties[validCount++] = avaloniaProperty;
}
else
{
Console.WriteLine($"警告:在类型 '{targetType.Name}' 上找不到属性 '{propName}'");
}
}
if (validCount < avaloniaProperties.Length)
{
Array.Resize(ref avaloniaProperties, validCount);
}
InternAttach(target, avaloniaProperties);
}
public void Attach(AvaloniaObject target, params AvaloniaProperty[] properties)
{
InternAttach(target, properties);
}
private void InternAttach(AvaloniaObject target, AvaloniaProperty[] properties)
{
if (objectStates.ContainsKey(target))
{
Console.WriteLine($"提示:目标对象已附加,跳过重复附加");
return;
}
var objectState = new ObjectState();
objectStates[target] = objectState;
if (properties == null || properties.Length == 0)
{
Console.WriteLine($"提示:{target.GetType().Name}未指定属性,将跟踪目标对象的所有 Avalonia 属性");
properties = GetAllAvaloniaProperties(target);
}
objectState.TrackedProperties.UnionWith(properties);
void Handler(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (isRestoring || e.Sender != target || !objectStates.TryGetValue(target, out var state))
return;
if (state.TrackedProperties.Contains(e.Property))
{
Record(target, e.Property, e.OldValue, e.NewValue);
}
}
target.PropertyChanged += Handler;
objectState.Subscriptions.Add(new DisposableAction(() => target.PropertyChanged -= Handler));
if (target is Visual visual)
{
void DetachedHandler(object? s, VisualTreeAttachmentEventArgs a)
{
Detach(target);
visual.DetachedFromVisualTree -= DetachedHandler;
}
visual.DetachedFromVisualTree += DetachedHandler;
objectState.Subscriptions.Add(new DisposableAction(() => visual.DetachedFromVisualTree -= DetachedHandler));
}
}
private static string GetPropertyPath(Expression expr)
{
if (expr == null) return "";
var pathBuilder = new StringBuilder();
var currentExpr = expr;
var stack = new Stack<string>();
while (currentExpr != null)
{
switch (currentExpr)
{
case MemberExpression memberExpr:
stack.Push(memberExpr.Member.Name);
currentExpr = memberExpr.Expression;
break;
case UnaryExpression unaryExpr when unaryExpr.NodeType == ExpressionType.Convert:
currentExpr = unaryExpr.Operand;
break;
case MethodCallExpression methodCallExpr when methodCallExpr.Method.Name == "get_Item":
var indexValue = GetIndexValue(methodCallExpr);
stack.Push($"[{indexValue}]");
currentExpr = methodCallExpr.Object;
break;
case ParameterExpression:
currentExpr = null;
break;
case ConstantExpression constantExpr:
stack.Push(constantExpr.Value?.ToString() ?? "constant");
currentExpr = null;
break;
default:
Console.WriteLine($"警告:不支持的表达式类型 {currentExpr.GetType().Name}");
currentExpr = null;
break;
}
}
while (stack.Count > 0)
{
var part = stack.Pop();
if (pathBuilder.Length > 0)
{
if (!part.StartsWith("[") && pathBuilder[pathBuilder.Length - 1] != '[')
{
pathBuilder.Append(".");
}
}
pathBuilder.Append(part);
}
return pathBuilder.Length > 0 ? pathBuilder.ToString() : "";
}
private static string GetIndexValue(MethodCallExpression methodCallExpr)
{
if (methodCallExpr.Arguments.Count == 0)
return "";
var indexArg = methodCallExpr.Arguments[0];
if (indexArg is ConstantExpression constantExpr)
{
return constantExpr.Value?.ToString() ?? "";
}
if (indexArg is MemberExpression memberExpr)
{
return GetPropertyPath(memberExpr);
}
return "index";
}
private static AvaloniaProperty? GetAvaloniaProperty(Type type, string propertyName)
{
if (!propertyCache.TryGetValue(type, out var propertyMap))
{
propertyMap = new Dictionary<string, AvaloniaProperty>(StringComparer.OrdinalIgnoreCase);
var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)
.Where(f => typeof(AvaloniaProperty).IsAssignableFrom(f.FieldType));
foreach (var field in fields)
{
if (field.GetValue(null) is AvaloniaProperty property)
{
propertyMap[property.Name] = property;
}
}
propertyCache[type] = propertyMap;
}
propertyMap.TryGetValue(propertyName, out var result);
return result;
}
private static AvaloniaProperty[] GetAllAvaloniaProperties(AvaloniaObject target)
{
var type = target.GetType();
if (!propertyCache.TryGetValue(type, out var propertyMap))
{
propertyMap = new Dictionary<string, AvaloniaProperty>(StringComparer.OrdinalIgnoreCase);
var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)
.Where(f => typeof(AvaloniaProperty).IsAssignableFrom(f.FieldType));
foreach (var field in fields)
{
if (field.GetValue(null) is AvaloniaProperty property)
{
propertyMap[property.Name] = property;
}
}
propertyCache[type] = propertyMap;
}
Console.WriteLine($"提示:找到 {propertyMap.Count} 个属性");
var result = new AvaloniaProperty[propertyMap.Count];
propertyMap.Values.CopyTo(result, 0);
return result;
}
public void Detach(AvaloniaObject target)
{
if (!objectStates.TryGetValue(target, out var state))
{
Console.WriteLine($"提示:目标对象未附加,无需分离");
return;
}
for (int i = state.Subscriptions.Count - 1; i >= 0; i--)
{
state.Subscriptions[i].Dispose();
}
objectStates.Remove(target);
Console.WriteLine($"提示:已成功分离目标对象");
}
private void Record(AvaloniaObject target, AvaloniaProperty property, object? oldValue, object? newValue)
{
if (Equals(oldValue, newValue)) return;
if (!objectStates.TryGetValue(target, out var state)) return;
var list = state.History;
var index = state.HistoryIndex;
if (index < list.Count)
{
list.RemoveRange(index, list.Count - index);
}
list.Add((property, oldValue, newValue));
state.HistoryIndex = index + 1;
StateChanged?.Invoke(this, EventArgs.Empty);
Console.WriteLine($"记录:{target.GetType().Name}.{property.Name} 从 '{oldValue}' 变更为 '{newValue}'");
}
public void Undo(AvaloniaObject target)
{
if (!CanUndo(target) || !objectStates.TryGetValue(target, out var state))
{
return;
}
var index = state.HistoryIndex - 1;
var entry = state.History[index];
isRestoring = true;
try
{
target.SetCurrentValue(entry.Property, entry.OldValue);
state.HistoryIndex = index;
StateChanged?.Invoke(this, EventArgs.Empty);
Console.WriteLine($"撤销:{target.GetType().Name}.{entry.Property.Name}, '{entry.NewValue}' 恢复为 '{entry.OldValue}'");
}
finally
{
isRestoring = false;
}
}
public void Redo(AvaloniaObject target)
{
if (!CanRedo(target) || !objectStates.TryGetValue(target, out var state))
{
return;
}
var index = state.HistoryIndex;
var entry = state.History[index];
isRestoring = true;
try
{
target.SetCurrentValue(entry.Property, entry.NewValue);
state.HistoryIndex = index + 1;
StateChanged?.Invoke(this, EventArgs.Empty);
Console.WriteLine($"重做:{target.GetType().Name}.{entry.Property.Name} , '{entry.OldValue}' 变更为 '{entry.NewValue}'");
}
finally
{
isRestoring = false;
}
}
public bool CanUndo(AvaloniaObject target)
{
return objectStates.TryGetValue(target, out var state) && state.HistoryIndex > 0;
}
public bool CanRedo(AvaloniaObject target)
{
return objectStates.TryGetValue(target, out var state) && state.HistoryIndex < state.History.Count;
}
public IRelayCommand GetUndoCommand(AvaloniaObject target)
{
var command = new RelayCommand(
() => Undo(target),
() => CanUndo(target)
);
this.StateChanged += (_, _) => command.NotifyCanExecuteChanged();
return command;
}
public IRelayCommand GetRedoCommand(AvaloniaObject target)
{
var command = new RelayCommand(
() => Redo(target),
() => CanRedo(target)
);
this.StateChanged += (_, _) => command.NotifyCanExecuteChanged();
return command;
}
private sealed class DisposableAction : IDisposable
{
private Action? action;
public DisposableAction(Action action) => this.action = action;
public void Dispose()
{
var toExecute = Interlocked.Exchange(ref action, null);
toExecute?.Invoke();
}
}
}
MonitorCommands.axaml代码
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Height="320" Width="339.2" xmlns:local ="clr-namespace:AvaloniaUI;assembly=AvaloniaUI" x:DataType="local:MonitorCommands" x:Class="AvaloniaUI.MonitorCommands" Title="MonitorCommands"> <Grid RowDefinitions="auto,*"> <ToolBarTray Margin="5"> <ToolBar> <Button Command="{Binding UndoCommand}">Undo</Button> <Button Command="{Binding RedoCommand}">Redo</Button> </ToolBar> </ToolBarTray> <TextBox Name="txt" Margin="5" Grid.Row="1" TextWrapping="Wrap" AcceptsReturn="True"> </TextBox> </Grid> </Window>
MonitorCommands.axmal.cs代码
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using CommunityToolkit.Mvvm.Input;
using Shares.Avalonia;
using System;
using System.Windows.Input;
namespace AvaloniaUI;
public partial class MonitorCommands : Window
{
private readonly UndoRedoManager manager = new UndoRedoManager();
public IRelayCommand UndoCommand { get; set; }
public IRelayCommand RedoCommand { get; set; }
public MonitorCommands()
{
InitializeComponent();
manager.Attach(txt, TextBox.TextProperty);
UndoCommand = manager.GetUndoCommand(txt);
RedoCommand = manager.GetRedoCommand(txt);
this.DataContext = this;
/*
var textBox1 = new TextBox();
var textBox2 = new TextBox();
manager.Attach(textBox1, x=>x.Text!);
manager.Attach(textBox2, TextBox.TextProperty);
textBox1.Text = "Hello";
textBox1.Text = "World";
textBox2.Text = "Foo";
textBox2.Text = "Bar";
Console.WriteLine("textBox1 当前 Text: " + textBox1.Text); // World
Console.WriteLine("textBox2 当前 Text: " + textBox2.Text); // Bar
manager.Undo(textBox1);
Console.WriteLine("textBox1 Undo 后: " + textBox1.Text); // Hello
manager.Redo(textBox2);
Console.WriteLine("textBox2 Redo 后: " + textBox2.Text); // Bar(没有变化,因为 redo 栈为空)
manager.Undo(textBox1);
manager.Redo(textBox1);
Console.WriteLine($"textBox1 Undo, Redo 后:{textBox1.Text}");
Console.WriteLine("textBox1 CanUndo: " + manager.CanUndo(textBox1)); // True,初始值null存在
Console.WriteLine("textBox2 CanRedo: " + manager.CanRedo(textBox2)); // False
*/
}
}
运行效果

浙公网安备 33010602011771号