Search code examples
c#.netwpfundo-redocommunity-toolkit-mvvm

WPF + CommunityToolkit.Mvvm how to implement undo/redo commands


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.


Solution

  • 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();
        }
    }