WPF Diagramming. Undo/Redo service and simple commands.


Undo/Redo functionality can be regarded as one of the most important parts of application and not because of it’s complexity. This feature can influence the architecture of your application very much (of course if you didn’t implement the commands pattern from the beginning) and can make you spend a lot of time for refactoring.

Each action in the application can be regarded as some single command. The command knows how to execute an action and so it should know how to undo or redo it easily. In order to provide the list of user-friendly names of commands to be undone the it can also contain some title to be distinguished in the UI. So the basic interface for it can be as following

public interface IDiagramCommand
  {
    /// <summary>
    /// Executes the actual action
    /// </summary>
    void Execute();

    /// <summary>
    /// Executes an action corresponding to an undo
    /// </summary>
    void Undo();

    /// <summary>
    /// Executes an action corresponding to redo in case it has been undone.
    /// </summary>    
    void Redo();

    /// <summary>
    /// Title of the command.
    /// </summary>
    /// <remarks>Typically to be used in undo list or undo stack description.</remarks>
    string Title { get; set; }
  }

Assume you are adding a shape to the canvas and this action should be undone on demand. The action command should be common for all your elements based on UIElement and know the surface it is operating. In my case extended canvas will be the command surface. This is the most common information needed the first simple command.

I’ve implemented some additional helper methods for the canvas and pass my common interface over the commands.

public interface IDrawingSurface: IInputElement
  {
    /// <summary>
    /// Get/Set the value indicating whether elements dragging is allowed for the canvas.
    /// </summary>
    bool IsDragEnabled { get; set; }

    /// <summary>
    /// Add generic element to the surface
    /// </summary>
    /// <typeparam name="T">UIElement type</typeparam>
    /// <param name="element">Element to be added to the surface</param>
    void AddElement<T>(T element);

    /// <summary>
    /// Insert generic element to the surface at a specified index position.
    /// </summary>
    /// <typeparam name="T">UIElement type</typeparam>
    /// <param name="index">The position of element to be placed</param>
    /// <param name="element">Element to be inserted to the surface</param>
    void InsertElement<T>(int index, T element);

    /// <summary>
    /// Remove generic element from the surface
    /// </summary>
    /// <typeparam name="T">UIElement type</typeparam>
    /// <param name="element">Element to be removed from the surface</param>
    void RemoveElement<T>(T element);
  }

 

Using generics in this case help me very much eliminating the type casting and the stuff like that. As canvas deals with UIElement based elements it becomes rather difficult maintaining the collection because I definitely sure you won’t deal with UIElement objects all over you application. It becomes more convenient to wrap the basic child processing logic into some sort of generic helpers.

The simple pieces of code for the canvas exposing such an interface can be as following

/// <summary>
/// Add generic element to the surface
/// </summary>
/// <typeparam name="T">UIElement type</typeparam>
/// <param name="element">Element to be added to the surface</param>
public void AddElement<T>(T element)
{
  UIElement uiElement = element as UIElement;
  if (uiElement != null && !Children.Contains(uiElement))
    Children.Add(uiElement);
}

 

The remove or insert methods are too obvious to be mentioned here. You can implement you own subset of functionality extending the canvas using your own demands.

As I’ve mentioned earlier the simple add element command that can satisfy the undo/redo service should have the possibility of adding some kind element to the drawing surface (command execution), remove the element from the surface (undo operation) and rollback the undo operation so add the element again to the surface (redo operation).

public class AddElementCommand : IDiagramCommand
  {
    public const string DefaultCommandTitle = "Add Element";

    #region Properties
    /// <summary>
    /// Gets or sets the surface.
    /// </summary>
    /// <value>The surface.</value>
    public IDrawingSurface Surface { get; set; }

    /// <summary>
    /// Gets or sets the element.
    /// </summary>
    /// <value>The element.</value>
    public UIElement Element { get; set; }

    /// <summary>
    /// Title of the command.
    /// </summary>
    /// <value></value>
    /// <remarks>Typically to be used in undo list or undo stack description.</remarks>
    public string Title { get; set; }
    #endregion

    #region ctor
    /// <summary>
    /// Initializes a new instance of the <see cref="AddElementCommand"/> class.
    /// </summary>
    /// <param name="surface">The surface.</param>
    /// <param name="element">The element.</param>
    public AddElementCommand(IDrawingSurface surface, UIElement element) : this(surface, element, null) { }

    /// <summary>
    /// Initializes a new instance of the <see cref="AddElementCommand"/> class.
    /// </summary>
    /// <param name="surface">The surface.</param>
    /// <param name="element">The element.</param>
    /// <param name="title">The title.</param>
    public AddElementCommand(IDrawingSurface surface, UIElement element, string title)
    {
      if (surface == null) throw new ArgumentNullException("surface");
      if (element == null) throw new ArgumentNullException("element");
      
      Surface = surface;
      Element = element;
      Title = (!string.IsNullOrEmpty(title)) ? title : DefaultCommandTitle;
    }
    #endregion

    #region IDiagramCommand Members
    /// <summary>
    /// Executes the actual action
    /// </summary>
    public void Execute()
    {
      Surface.AddElement(Element);
    }

    /// <summary>
    /// Executes an action corresponding to an undo
    /// </summary>
    public void Undo()
    {
      Surface.RemoveElement(Element);
    }

    /// <summary>
    /// Executes an action corresponding to redo in case it has been undone.
    /// </summary>
    public void Redo()
    {
      Surface.AddElement(Element);
    }
    #endregion
  }

The command itself doesn’t create the UI element because it is not responsible for that. It even doesn’t distinguish your objects being passed. It only assumes the objects belong to UIElement type and that’s all. As far as you can understand the undo/redo service is also blind to the content of the commands it deals with. It will process the IDiagramCommand objects and will attach it’s undo/redo functionality to each command.

Here’s the what could be the simple interface for basic undo/redo service

public interface IUndoService
  {
    /// <summary>
    /// Get the undo commands history.
    /// </summary>
    Stack<IDiagramCommand> UndoCommands { get; }

    /// <summary>
    /// Get the redo commands history.
    /// </summary>
    Stack<IDiagramCommand> RedoCommands { get; }

    /// <summary>
    /// Get the undo commands titles.
    /// </summary>
    ObservableCollection<string> UndoTitles { get; }

    /// <summary>
    /// Get the redo commands titles.
    /// </summary>
    ObservableCollection<string> RedoTitles { get; }

    /// <summary>
    ///  Gets a value indicating whether there is anything that can be undone.
    /// </summary>
    bool CanUndo { get; }

    /// <summary>
    /// Gets a value indicating whether there is anything that can be rolled forward.
    /// </summary>
    bool CanRedo { get; }

    /// <summary>
    /// This method puts the command to the Undo stack and then executes it.
    /// </summary>
    /// <param name="command">The command to be executed.</param>
    void Execute(IDiagramCommand command);

    /// <summary>
    /// Rollback the last command.
    /// </summary>
    void Undo();

    /// <summary>
    /// Rollback the last undone command.
    /// </summary>
    void Redo();

    /// <summary>
    /// Clear the undo history.
    /// </summary>
    void ClearUndoHistory();

    /// <summary>
    /// Clear the redo history.
    /// </summary>
    void ClearRedoHistory();

    /// <summary>
    /// Clear all the undo and redo history.
    /// </summary>
    void ClearHistory();
  }

Hope the code is self explaining. We introduce two stacks for commands according to undo and redo implementation. Also we have two observable collections giving the titles. Also we provide the obvious Execute/Undo/Redo methods alongside with a set of helper methods and properties as clearing the history or returning a boolean value determining whether this or that functionality as possible at execution time.

Here’s the Undo/Redo service that expose the interface mentioned above

public class UndoService : IUndoService
  {
    #region ctor
    public UndoService()
    {
      UndoCommands = new Stack<IDiagramCommand>();
      UndoTitles = new ObservableCollection<string>();
      RedoCommands = new Stack<IDiagramCommand>();
      RedoTitles = new ObservableCollection<string>();
    } 
    #endregion

    #region Routed events bindng support
    public void OnExecuteUndo(object sender, ExecutedRoutedEventArgs e)
    {
      Undo();
    }

    public void OnCanExecuteUndo(object sender, CanExecuteRoutedEventArgs e)
    {
      e.CanExecute = CanUndo;
    }

    public void OnExecuteRedo(object sender, ExecutedRoutedEventArgs e)
    {
      Redo();
    }

    public void OnCanExecuteRedo(object sender, CanExecuteRoutedEventArgs e)
    {
      e.CanExecute = CanRedo;
    }
    #endregion

    #region IUndoService Members
    /// <summary>
    /// Get the undo commands history.
    /// </summary>
    public Stack<IDiagramCommand> UndoCommands { get; protected set; }

    /// <summary>
    /// Get the redo commands history.
    /// </summary>
    public Stack<IDiagramCommand> RedoCommands { get; protected set; }
    
    /// <summary>
    /// Get the undo commands titles.
    /// </summary>
    public ObservableCollection<string> UndoTitles { get; protected set; }

    /// <summary>
    /// Get the redo commands titles.
    /// </summary>
    public ObservableCollection<string> RedoTitles { get; protected set; }

    /// <summary>
    ///  Gets a value indicating whether there is anything that can be undone.
    /// </summary>
    public bool CanUndo { get { return UndoCommands.Count > 0; } }

    /// <summary>
    /// Gets a value indicating whether there is anything that can be rolled forward.
    /// </summary>
    public bool CanRedo { get { return RedoCommands.Count > 0; } }

    /// <summary>
    /// This method puts the command to the Undo stack and then executes it.
    /// </summary>
    /// <param name="command">The command to be executed.</param>
    public void Execute(IDiagramCommand command)
    {
      if (command == null) return;
      // Execute command
      command.Execute();
      // Push command to undo history
      if (command is IDiagramCommand)
      {
        UndoCommands.Push(command);
        UndoTitles.Insert(0, command.Title);
        // Clear the redo history upon adding new undo entry. This is a typical logic for most applications
        RedoCommands.Clear();
        RedoTitles.Clear();
      }
    }

    /// <summary>
    /// Rollback the last command.
    /// </summary>
    public void Undo()
    {
      if (CanUndo)
      {
        IDiagramCommand command = UndoCommands.Pop();
        command.Undo();
        UndoTitles.RemoveAt(0);
        RedoCommands.Push(command);
        RedoTitles.Insert(0, command.Title);
      }
    }

    /// <summary>
    /// Rollback the last undone command.
    /// </summary>
    public void Redo()
    {
      if (CanRedo)
      {
        IDiagramCommand command = RedoCommands.Pop();
        RedoTitles.RemoveAt(0);
        //Execute(command);
        if (command != null)
        {
          command.Execute();

          if (command is IDiagramCommand)
          {
            UndoCommands.Push(command);
            UndoTitles.Insert(0, command.Title);
          }
        }        
      }
    }

    /// <summary>
    /// Clear the undo history.
    /// </summary>
    public void ClearUndoHistory()
    {
      UndoCommands.Clear();
      UndoTitles.Clear();
    }

    /// <summary>
    /// Clear the redo history.
    /// </summary>
    public void ClearRedoHistory()
    {
      RedoCommands.Clear();
      RedoTitles.Clear();
    }

    /// <summary>
    /// Clear all the undo and redo history.
    /// </summary>
    public void ClearHistory()
    {
      ClearRedoHistory();
      ClearUndoHistory();
    }
    #endregion
  }

The code above mostly doesn’t require long supplementary information. On each command execution the redo stack is cleared to reflect the most common behavior in all applications. The undo and redo methods mainly do the manipulations with theirs stacks and corresponding title collections. The main entry point for the undo service is Execute method. It executes the command and adds it to the undo stack (great thanks to François for that idea)

The only part that can be a point of detailed interest is the region "Routed events binding support". These four methods provide an easy and convenient possibility of binding the undo/redo commands to the UI. Usually you’ll have two buttons and maybe each button will contain the list of operation titles for undoing of redoing.

In my sample projects I usually implement two buttons bound to the standard application commands.

<Button Margin="0,3" Name="cmdUndo" Command="ApplicationCommands.Undo">
    <StackPanel>
        <Image Source="Images/undo.png" Width="32" Height="32"/>
        <TextBlock>Undo</TextBlock>
    </StackPanel>
</Button>
<Button Margin="0,3" Name="cmdRedo" Command="ApplicationCommands.Redo">
    <StackPanel>
        <Image Source="Images/redo.png" Width="32" Height="32"/>
        <TextBlock>Redo</TextBlock>
    </StackPanel>
</Button>

Take a look at the "Command" property of each button. "ApplicationCommands" is a standard class so if interested you look through MSDN to get more information and use cases.

Upon the window loading I attach my undo/redo logic to the list of command bindings for the window so that buttons accessing "ApplicationCommands.Undo" and "ApplicationCommands.Redo" automatically go to my UndoService.

Something like the following

#region Configure command bindings
UndoService undoService = designer.GetService<IUndoService>() as UndoService;
if (undoService != null)
{
  this.CommandBindings.Add(new CommandBinding(ApplicationCommands.Undo, undoService.OnExecuteUndo, undoService.OnCanExecuteUndo));
  this.CommandBindings.Add(new CommandBinding(ApplicationCommands.Redo, undoService.OnExecuteRedo, undoService.OnCanExecuteRedo));
}
#endregion

OnCanExecuteUndo and OnCanExecuteRedo methods are returning the CanUndo and CanRedo properties of the underlying Undo/Redo service. The last two mentioned are bound to the corresponding stacks and return true if the stack contains any command.

This gives you a very interesting feature. Your buttons will themselves control their state and will become enabled/disabled according to the contents of undo/redo stacks.

This is a very basic pattern to get the idea of implementing undo/redo functionality and commands. It’s up to you what commands will be executed and what will they do. The undo/redo service is usually implemented in the way it doesn’t take care of that. It deals with interface part of commands and that’s all.

Thanks for your attention. The sources can be found at my skydrive under "Diagramming" folder or in the previous article.

Advertisements

2 thoughts on “WPF Diagramming. Undo/Redo service and simple commands.

  1. Hey Denis, you posted a great article,  but the only problem I found is that in UndoService you have to keep track of two collections and the first one contains the information from the second one. One possible solution is using Select() Extension method to get the Titles directly from the stacks, but not in observable collection…
    Good day.
    Miroslav

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s