A simple implementation of the Undo/Redo Command pattern
Below is a minimal example that demonstrates creating a simple Command, executing it with CommandManager, then undoing and redoing the command.
using System;
using Skc.BestPractices.CommandManager;
// A tiny mutable object we operate on
class Counter
{
public int Value;
}
// A simple command that increments the counter and can undo the increment
class IncrementCommand : Command
{
private readonly Counter _counter;
private readonly int _amount;
public IncrementCommand(Counter counter, int amount)
{
_counter = counter;
_amount = amount;
Description = $"Increment by {_amount}";
}
protected internal override object Execute()
{
_counter.Value += _amount;
return null;
}
protected internal override object Undo()
{
_counter.Value -= _amount;
return null;
}
}
class Program
{
static void Main()
{
var manager = new CommandManager();
var counter = new Counter();
var cmd = new IncrementCommand(counter, 5);
manager.Execute(cmd);
Console.WriteLine(counter.Value); // 5
manager.Undo();
Console.WriteLine(counter.Value); // 0
manager.Redo();
Console.WriteLine(counter.Value); // 5
}
}- Thread-safety: CommandManager is not thread-safe (it uses no synchronization). If you call Execute/Undo/Redo from multiple threads, guard access with your own synchronization (e.g., a lock) or ensure all calls are marshaled to a single thread (UI thread or a dedicated worker).
- Events: subscribe to CommandManager events to update UI or react to changes: CommandHistoryChanged, CommandFutureChanged, Executing, Executed, Discarded. Event handlers are invoked synchronously on the thread that performed the operation — if you update UI from an event handler, marshal to the UI thread.
- Long-running work: keep Execute/Undo implementations fast. If a command needs to perform I/O or long-running work, run that work on a background thread and record only the minimal state needed for undo, or consider splitting the command into a quick state-change command and a separate background task.
- Groups & markers: use BeginGroup/EndGroup for multi-step actions (macros). Use SetMarker/IsAtMarker to track unsaved changes.
# restore & build
dotnet build Skc.BestPractices.CommandManagement.sln
# run tests (tests target net48; run on Windows or with appropriate runtimes)
dotnet test Skc.BestPractices.CommandManager.Tests/Skc.BestPractices.CommandManager.Tests.csproj