C#/.NET 简单撤销/重做库
C#/.NET 简单撤销/重做库。
介绍
本文提供了一个不使用 Command 或 Momento 模式的撤销/重做功能示例框架。该框架提供了撤销/重做堆栈并支持合并撤销/重做。
注意:本文无意替代完整的 Command 或 Memento 模式实现,仅演示了执行撤销/重做操作的简单方法。这对于为现有应用程序添加撤销/重做支持尤其有用。
背景
最近,一位朋友向我寻求帮助,希望在一个现有应用程序中实现撤销/重做功能。对于这种方法,显而易见的选择是 Command 或 Memento 模式,但这需要对应用程序进行重大更改。
本文提供的解决方案演示了一种引入撤销/重做功能的简单方法,可以在选定的粒度级别(属性/操作/多个操作)进行。
简单的撤销操作示例
使用此库的关键在于在相关操作点引入撤销行为。举一个非常简单的例子,如果你的某个对象有一个属性如下:
public int Age
{
get { return _age; }
set
{
_age = value;
NotifyPropertyChanged("Age");
}
}
如果您想在此属性级别(而不是 UI 级别)实现撤销行为,则应添加下面显示的行。
public int Age
{
get { return _age; }
set
{
UndoRedoManager.Instance().Push(a => this.Age=a, _age, "Change age");
_age= value;
NotifyPropertyChanged("Age");
}
}
可以按以下方式分解 `UndoRedoManager.Instance().Push(a=> Age=a, _age, "Change age")` 这行代码:
UndoRedoManager.Instance()
- 获取 `UndoRedoManager` 单例对象的实例。Push
- 使用以下数据将撤销记录推送到撤销堆栈。a=> Age=a
- 执行撤销时要调用的方法。 在这种情况下,我们只是声明了一个就地 Lambda 表达式,它会调用 `Age` 的设置属性访问器。_age
- 要传递给方法的数据。 在这种情况下,此成员变量包含在更改之前 `Age` 的当前值。"Change age"
- 撤销操作的描述(可选)。
因此,在设置 `Age` 属性后调用撤销操作时,将调用上述 Lambda 表达式,从而有效地将 `Age` 重置为原始值。 基本上,您是在需要撤销的地方创建撤销记录。 无需必须使用 Lambda 表达式 - 您可以将您的撤销方法创建为非匿名方法。
请注意,`UndoRedoManager` 会处理在正在进行的撤销操作的上下文中调用此 Lambda 表达式的条件,在这种情况下,新的 Lambda 表达式将被添加到重做堆栈。您永远不会显式地将重做操作添加到堆栈。
`UndoRedoManager.Push` 操作的签名是:
public void Push<t>(UndoRedoOperation<t> undoOperation, T undoData, string description = "")
正如您所见,数据类型是一个模板参数,可以是任何类型。
根本上,您将在任何希望用户能够执行撤销的地方推送撤销记录(状态)到堆栈。这类似于在 Command 模式中维护要执行的命令列表,然后在需要执行撤销时调用 `Command.Undo`。
稍微复杂的示例
为了测试 `UndoRedoManager` 类,我从 Code Project 下载并修改了 DrawTools 代码,并向提供的代码添加了撤销-重做功能。 下面是一个示例,演示了我如何为 `DrawLine.cs` 类的移动操作添加撤销功能。
public override void Move(int deltaX, int deltaY)
{
UndoRedoManager.Instance().Push((dummy) => Move(-deltaX, -deltaY), this);
startPoint.X += deltaX;
startPoint.Y += deltaY;
endPoint.X += deltaX;
endPoint.Y += deltaY;
Invalidate();
}
这是另一个例子:
public override void Normalize()
{
UndoRedoManager.Instance().Push(r => DrawRectangle.GetNormalizedRectangle(r), rectangle);
rectangle = DrawRectangle.GetNormalizedRectangle(rectangle);
}
合并多个撤销操作
设想您在下面所示的两个不同调用中设置人的姓名和年龄:
Person p = new Person();
p.Name = "new name";
p.Age = p.Age+1;
假设 Name 和 Age 的 setter 都创建了撤销记录,那么上面的代码将在撤销堆栈中产生两个撤销记录。如果您想将它们合并成一个撤销记录,您可以将它们包装在一个事务中。
using (new UndoTransaction("optional description))
{
p.Name = "new name";
p.Age = p.Age+1;
}
这将导致这两个撤销记录被 算作一个撤销记录。另一个例子是在某个可撤销操作可能会调用另一组可撤销操作的情况下,例如:
private Person AddPerson(Person person)
{
//Do not add if the person is already in the list
Person personInList = _personList.Find(p => p.ID == person.ID);
if (personInList != null)
{
return personInList;
}
UndoRedoManager.Instance().Push(p => RemovePerson(p), person,"Add Person");
personListBindingSource.Position = personListBindingSource.Add(person);
return person;
}
private void btnAddTran_Click(object sender, EventArgs e)
{
using (new UndoTransaction("Add Person"))
{
Person p = new Person() {};
AddPerson(p);
p.Name = "<Change Name>";
p.Age = 0;
}
}
在这种情况下,如果在 `btnAddTran_Click` 函数中未使用 `UndoTransaction`,则撤销堆栈将包含 3 个撤销记录(一个用于 AddPerson,一个用于 Name 更改,一个用于 Age 更改),而不是仅 1 条记录。
UndoRedoManager 操作
除了上面描述的 Push 操作之外,`UndoRedoManager` 还提供了以下操作和事件:
Undo()
- 调用此方法以执行撤销操作。在调用此方法之前,应该检查撤销堆栈中是否有撤销操作。Redo()
- 用于执行重做操作。
在调用此方法之前,应该检查重做堆栈中是否有重做操作。HasUndoOperations
/HasRedoOperations
- 可以调用这些方法来确定撤销/重做堆栈中是否有任何撤销/重做记录。MaxItems
- 设置/获取要存储在堆栈中的最大项目数。UndoStackStatusChanged
/RedoStackStatusChanged
- 在向撤销堆栈添加/删除项目时会触发这些事件。 例如,这些事件可用于设置撤销/重做菜单项的状态。
重做
重做由 `UndoRedoManager` 类自动处理。用户只需将撤销操作推送到 `UndoRedoManager` 堆栈即可。
附件解决方案内容
附件解决方案包含以下项目:
- UndoMethods - 此类库包含 `UndoManager` 类和其他支持类。
- UndoPatternSample - 此示例演示了 `UndoManager` 类的各种用法。
- DrawToolkit项目 - 这是 CodeProject 中 DrawTools 的修改版本,具有撤销/重做功能。