i'm wondering what is the best way to implement custom Undo/Redo commands?
In my case, I am implementing a diagram editor and I want to create Undo/Redo commands to undo manipulations on canvas elements, for example to undo rotation, move or resize an element.
Ideas I'm not sure of: -Write my own implementation of ICommand or a derived class from RelayCommand and store commands in the main View Model in stacks; -Write a repository that will store command stacks in session memory and a service that will implement Register, Execute and Undo methods.
If someone interested, this is how i implemented it for my case:
/// <summary>
/// An interface that defines base methods to manage undoable commands.
/// </summary>
public interface IUndoableCommandManager
{
/// <summary>
/// Executes command.
/// </summary>
/// <param name="command">Target command with execution logic.</param>
public void Execute(IUndoableCommand command);
/// <summary>
/// Cancels the effects of an command execution.
/// </summary>
public void Undo();
/// <summary>
/// Performs command execution again.
/// </summary>
public void Redo();
/// <summary>
/// Clears history of all commands inside manager.
/// </summary>
public void Clear();
}
/// <summary>
/// A class that implements <see cref="IUndoableCommandManager"/>. Manages undoable commands.
/// </summary>
public sealed class UndoableCommandManager : IUndoableCommandManager
{
private readonly Stack<IUndoableCommand> _undoStack = [];
private readonly Stack<IUndoableCommand> _redoStack = [];
/// <summary>
/// Gets a value indicating whether an undo operation can be performed.
/// </summary>
private bool CanUndo => _undoStack.Count > 0;
/// <summary>
/// Gets a value indicating whether a redo operation can be performed.
/// </summary>
private bool CanRedo => _redoStack.Count > 0;
/// <inheritdoc/>
public void Execute(IUndoableCommand command)
{
command.Execute(null);
_undoStack.Push(command);
_redoStack.Clear();
}
/// <inheritdoc/>
public void Undo()
{
if (CanUndo)
{
var command = _undoStack.Pop();
command.Undo();
_redoStack.Push(command);
}
}
/// <inheritdoc/>
public void Redo()
{
if (CanRedo)
{
var command = _redoStack.Pop();
command.Execute(null);
_undoStack.Push(command);
}
}
/// <inheritdoc/>
public void Clear()
{
_undoStack.Clear();
_redoStack.Clear();
}
}
/// <summary>
/// An interface expanding <see cref="ICommand"/> with undo behavior.
/// </summary>
public interface IUndoableCommand : ICommand
{
/// <summary>
/// Undoes previous execution.
/// </summary>
void Undo();
}
/// <summary>
/// Initializes a new instance of the <see cref="UndoableCommand"/> class that can undo previous execution.
/// </summary>
public sealed class UndoableCommand : IUndoableCommand
{
private readonly Action _execute;
private readonly Action _undo;
/// <inheritdoc/>
public event EventHandler? CanExecuteChanged;
/// <summary>
/// Initializes a new instance of the <see cref="UndoableCommand"/> class.
/// </summary>
/// <param name="execute">The execution logic.</param>
/// <param name="undo">The undo behavior logic.</param>
public UndoableCommand(Action execute, Action undo)
{
ArgumentNullException.ThrowIfNull(execute);
ArgumentNullException.ThrowIfNull(undo);
_execute = execute;
_undo = undo;
}
/// <inheritdoc/>
public bool CanExecute(object? parameter = null)
{
return true;
}
/// <inheritdoc/>
public void Execute(object? parameter = null)
{
_execute();
}
/// <inheritdoc/>
public void Undo()
{
_undo();
}
}