Undo / Redo functionality

In this tutorial I’ll show you, how to implement an undo/redo-functionality for the WPF Diagram Designer. The purpose is not to show the full implementation, but give you the complete idea how to do it.

Pre-Requisites for this tutorial
Generally undo/redo-functionality is implemented based on the memento design pattern, which is a pattern of the famous GOF (gang-of-four). It saves the state of objects, when a change occures. You can find a great generic implementation of this pattern on http://www.blackwasp.co.uk/UndoRedo.aspx. Btw, it’s a great page anyway! Download the source-code on this page, as our implementation will base on it.

Setup the undo/redo-functionality

If you open the source-code mentioned in the pre-requisites, you’ll see four classes providing the undo/redo-functionality. Copy the following four files to you WPF Diagram Designer project:

  • IUndoable.cs
  • IUndoState.cs
  • Undoable.cs
  • UndoState.cs

If you want to have an explanation in short how this works, have a look at the Program.cs, which comes along the solution. You’ll see that it’s quit simple.

Refactor the Diagram Designer

Before we begin with the undo-functionality, we modify the Save- and Open-method. We need to separate the serialization- and the file save- and loadmechanism of the diagram-designer.

  1. Replace the existing Save_Executed-method with the following code. As you can see we can call the StoreDiagram-method to get the serialized diagram. We will use this method for the undo-functionality as well!
    private void Save_Executed(object sender, ExecutedRoutedEventArgs e)
    {
    	XElement root = StoreDiagram();
    	SaveFile(root);
    }
    
    public XElement StoreDiagram()
    {
    	IEnumerable<DesignerItem> designerItems = this.Children.OfType<DesignerItem>();
    	IEnumerable<Connection> connections = this.Children.OfType<Connection>();
    
    	XElement designerItemsXML = SerializeDesignerItems(designerItems);
    	XElement connectionsXML = SerializeConnections(connections);
    
    	XElement root = new XElement("Root");
    	root.Add(designerItemsXML);
    	root.Add(connectionsXML);
    
    	return root;
    }
    
  2. Replace the existing Open_Executed-method with the following code. As you can see, we separate here the fileload of the diagram and the deserialization. We will use the new RestoreDiagram-method to undo an action of the user.
    private void Open_Executed(object sender, ExecutedRoutedEventArgs e)
    {
    	XElement root = LoadSerializedDataFromFile();
    	RestoreDiagram(root);
    }
    
    public void RestoreDiagram(XElement root)
    {
    	if (root == null)
    		return;
    
    	this.Children.Clear();
    	this.SelectionService.ClearSelection();
    
    	IEnumerable<XElement> itemsXML = root.Elements("DesignerItems").Elements("DesignerItem");
    	foreach (XElement itemXML in itemsXML)
    	{
    		Guid id = new Guid(itemXML.Element("ID").Value);
    		DesignerItem item = DeserializeDesignerItem(itemXML, id, 0, 0);
    		this.Children.Add(item);
    		SetConnectorDecoratorTemplate(item);
    	}
    
    	this.InvalidateVisual();
    
    	IEnumerable<XElement> connectionsXML = root.Elements("Connections").Elements("Connection");
    	foreach (XElement connectionXML in connectionsXML)
    	{
    	Guid sourceID = new Guid(connectionXML.Element("SourceID").Value);
    	Guid sinkID = new Guid(connectionXML.Element("SinkID").Value);
    
    	String sourceConnectorName = connectionXML.Element("SourceConnectorName").Value;
    	String sinkConnectorName = connectionXML.Element("SinkConnectorName").Value;
    
    	Connector sourceConnector = GetConnector(sourceID, sourceConnectorName);
    	Connector sinkConnector = GetConnector(sinkID, sinkConnectorName);
    
    	Connection connection = new Connection(sourceConnector, sinkConnector);
    	Canvas.SetZIndex(connection, Int32.Parse(connectionXML.Element("zIndex").Value));
    	this.Children.Add(connection);
    	}
    }
    

After this simple refactoring, we can start with the implementation of the undo-functionality!

Implement the undo-functionality in the WPF Diagram Designer

  1. Open the file DesignerCanvas.Commands and add the property diagramState. It will store all the different states, which we want to be able to revert to.
    private IUndoable<string> diagramState = null;
    
  2. Add the following two lines to the DesignerCanvas constructor, to initialize the diagramState with an object of the Undoable-class, which provides the undo-/redofunctionality. As the XElement is not serializable, we store it as string.
    string initialStore = this.StoreDiagram().ToString();
    this.diagramState = new Undoable<string>(initialStore);
    
  3. Now this is the important part. We need to store the state, when the diagram changes. Unfortunatly there’s no nice notify-me-when-the-diagram-changes method. Instead you need to implement it for each and every method where a change happens. In this tutorial I’ll show you how to implement this for the drop- and move of a DesignerItem.
    Open the file DesignerCanvas.cs and add the following code to the OnDrop-method above the line e.Handled = true; to store the state when a drop of a DesignerItem happend.

    this.diagramState.SaveState();
    this.diagramState.Value = this.StoreDiagram().ToString();
    

    Add the following code to the DesignerCanvas-class as well. It detects, if a DesignerItem was moved. If so, the state of the diagram will be stored.

    protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e)
    {
    	base.OnPreviewMouseLeftButtonUp(e);
    
    	string storedDiagram = this.StoreDiagram().ToString();
    	if(storedDiagram != this.diagramState.Value)
    	{
    		this.diagramState.SaveState();
    		this.diagramState.Value = storedDiagram;
    	}
    }
    

Implement the binding for ctrl+z

To bind the ctrl+z keys to the undo-functionality we can use the existing ApplicationCommands.Undo, which implements the commandbinding for us already.

  1. Open the file DesignerCanvas.Commands again and open the following line of code to the constructor.
    this.CommandBindings.Add(new CommandBinding(ApplicationCommands.Undo, Undo_Executed, Undo_Enabled));
    
  2. For now you’ll see errors, because the methods Undo_Executed and Undo_Enabled are still missing. The Undo_Executed-method performs an undo and then restores the diagram. The Undo_Enabled-method is directly bound to the CanUndo of the Undoable-class, preventing an undo is executed, when there’s no undo available. Now let’s add these two methods:
    private void Undo_Executed(object sender, ExecutedRoutedEventArgs e)
    {
    	this.diagramState.Undo();
    	XElement diagram = XElement.Parse(this.diagramState.Value);
    	RestoreDiagram(diagram);
    }
    
    private void Undo_Enabled(object sender, CanExecuteRoutedEventArgs e)
    {
    	e.CanExecute = this.diagramState.CanUndo;
    }
    

If you now start the diagram-designer, the undo-functionality will work for dropped and moved DesignerItems. To fully implement it, you will need to implement if for all other methods affecting the diagram like cut, paste and so on. You can also implement the redo-functionality if needed. I think you got the idea now how you can to achieve this.


One thought on “Undo / Redo functionality

Comments are closed.