Getting started

edit

Install the package

Add the Meziantou.Framework.UndoRedo package to a project:

dotnet package add Meziantou.Framework.UndoRedo

The examples of this documentation omit the using directives: they need using Meziantou.Framework.UndoRedo;.

Record, undo and redo

Create one UndoRedoManager for the document the user edits, and record each change with the code that applies it and the code that reverts it:

var manager = new UndoRedoManager();
var document = new List<string>();

// Each recorded action runs immediately and becomes one undo step
await manager.RecordActionAsync(
    execute: () => document.Add("Hello"),
    unexecute: () => document.RemoveAt(document.Count - 1));
await manager.RecordActionAsync(
    execute: () => document.Add("World"),
    unexecute: () => document.RemoveAt(document.Count - 1));
Assert.Equal(["Hello", "World"], document);

await manager.UndoAsync();
Assert.Equal(["Hello"], document);
Assert.True(manager.CanUndo);
Assert.True(manager.CanRedo);

// Recording a new action clears the redo history
await manager.RecordActionAsync(
    execute: () => document.Add("Everyone"),
    unexecute: () => document.RemoveAt(document.Count - 1));
Assert.Equal(["Hello", "Everyone"], document);
Assert.False(manager.CanRedo);
  • RecordActionAsync applies the change by calling execute, then adds it to the undo history. When execute throws, nothing is recorded.
  • UndoAsync reverts the most recent change by calling unexecute, and moves it to the redo history. RedoAsync applies it again. Both do nothing when the history is empty.
  • CanUndo and CanRedo tell whether UndoAsync and RedoAsync would do something.
  • Recording a change after an undo clears the redo history, as in most editors.
  • Clear empties both histories, for instance after the document is saved or closed.

Asynchronous changes

execute and unexecute can be asynchronous: they receive the CancellationToken passed to RecordActionAsync, UndoAsync or RedoAsync. A synchronous and an asynchronous delegate can be mixed:

var manager = new UndoRedoManager();
var items = new List<string>();

// Asynchronous execute, synchronous unexecute
var action = new UndoRedoDelegateAction(
    execute: async cancellationToken =>
    {
        await Task.Delay(TimeSpan.FromMilliseconds(1), cancellationToken);
        items.Add("item");
    },
    unexecute: () => items.Remove("item"));

await manager.RecordActionAsync(action);
Assert.Equal(["item"], items);

UndoRedoDelegateAction is the action the delegate overloads of RecordActionAsync create. Create it yourself to set its Description or AllowToMergeWithPrevious.

A token that is already canceled makes RecordActionAsync, UndoAsync and RedoAsync throw an OperationCanceledException before anything runs.

Next steps