Click here to Skip to main content
Click here to Skip to main content

Undoing MVVM

, 1 Feb 2010
Rate this:
Please Sign up or sign in to vote.
Providing Undo/Redo across VMs (part 1 - simple properties)
  • Download undoredosamplezip
    The usual rules apply, the download needs to be renamed from .doc to .zip.

I apologise that it’s been a while since I last blogged, but I've been busy working on an MVVM framework and it’s been eating up a lot of time – it’s good eat, but it is time consuming. One of the things I've been adding into the code is the ability to handle undo/redo functionality in a ViewModel; and more importantly, coordinating undo/redo across multiple View Models. In this blog post, I'd like to demonstrate how easy it is to add this functionality to properties that support change notification. In a future blog, I'll be demonstrating how to extend this to supporting ObservableCollections as well.

The first thing that we're going to do is define a simple undo/redo interface. Here it is, in all its glory:

using System;
namespace UndoRedoSample
{
    /// <span class="code-SummaryComment"><summary>
</span>    /// The interface describing the Undo/Redo operation.
    /// <span class="code-SummaryComment"></summary>
</span>    public interface IUndoRedo
    {
        /// <span class="code-SummaryComment"><summary>
</span>        /// The optional name for the Undo/Redo property.
        /// <span class="code-SummaryComment"></summary>
</span>        string Name { get; }
        /// <span class="code-SummaryComment"><summary>
</span>        /// Code to perform the Undo operation.
        /// <span class="code-SummaryComment"></summary>
</span>        void Undo();
        /// <span class="code-SummaryComment"><summary>
</span>        /// Code to perform the Redo operation.
        /// <span class="code-SummaryComment"></summary>
</span>        void Redo();
    }
}

Now, we need to create a class that implements this interface.

using System;
namespace UndoRedoSample
{
    /// <span class="code-SummaryComment"><summary>
</span>    /// This class encapsulates a single undoable property.
    /// <span class="code-SummaryComment"></summary>
</span>    /// <span class="code-SummaryComment"><typeparam name="T"></typeparam>
</span>    public class UndoableProperty<T> : IUndoRedo
    {
        #region Member
        private object _oldValue;
        private object _newValue;
        private string _property;
        private T _instance;
        #endregion

        /// <span class="code-SummaryComment"><summary>
</span>        /// Initialize a new instance of <span class="code-SummaryComment"><see cref="UndoableProperty"/>.
</span>        /// <span class="code-SummaryComment"></summary>
</span>        /// <span class="code-SummaryComment"><param name="property">The name of the property.</param>
</span>        /// <span class="code-SummaryComment"><param name="instance">The instance of the property.</param>
</span>        /// <span class="code-SummaryComment"><param name="oldValue">The pre-change property.</param>
</span>        /// <span class="code-SummaryComment"><param name="newValue">The post-change property.</param>
</span>        public UndoableProperty(string property, T instance, 
				object oldValue, object newValue)
            : this(property, instance, oldValue, newValue, property)
        {
        }

        /// <span class="code-SummaryComment"><summary>
</span>        /// Initialize a new instance of <span class="code-SummaryComment"><see cref="UndoableProperty"/>.
</span>        /// <span class="code-SummaryComment"></summary>
</span>        /// <span class="code-SummaryComment"><param name="property">The name of the property.</param>
</span>        /// <span class="code-SummaryComment"><param name="instance">The instance of the property.</param>
</span>        /// <span class="code-SummaryComment"><param name="oldValue">The pre-change property.</param>
</span>        /// <span class="code-SummaryComment"><param name="newValue">The post-change property.</param>
</span>        /// <span class="code-SummaryComment"><param name="name">The name of the undo operation.</param>
</span>        public UndoableProperty(string property, T instance, 
			object oldValue, object newValue, string name)
            : base()
        {
            _instance = instance;
            _property = property;
            _oldValue = oldValue;
            _newValue = newValue;

            Name = name;

            // Notify the calling application that this should be added to the undo list.
            UndoManager.Add(this);
        }

        /// <span class="code-SummaryComment"><summary>
</span>        /// The property name.
        /// <span class="code-SummaryComment"></summary>
</span>        public string Name { get; private set; }

        /// <span class="code-SummaryComment"><summary>
</span>        /// Undo the property change.
        /// <span class="code-SummaryComment"></summary>
</span>        public void Undo()
        {
            _instance.GetType().GetProperty(_property).SetValue
					(_instance, _oldValue, null);
        }

        public void Redo()
        {
            _instance.GetType().GetProperty(_property).SetValue
					(_instance, _newValue, null);
        }
    }
}

This class simply wraps a property. Whenever Undo is called, reflection is used to set the property back to its prechanged value. Calling Redo reverses this change. Now, that’s all well and good, but we need to keep track of these changes and apply them – and more importantly, we need to apply them across ViewModels. This is where the UndoManager comes in:

using System;
using System.Collections.Generic;
using System.Linq;

namespace UndoRedoSample
{
    /// <span class="code-SummaryComment"><summary>
</span>    /// This class is responsible for coordinating the 
    /// undo/redo messages from the various view models
    /// in the application. By having a central repository, 
    /// the undo/redo state is managed without the
    /// need for the VMs having to subscribe to any complex hierarchy.
    /// <span class="code-SummaryComment"></summary>
</span>    public static class UndoManager
    {
        #region Members
        private static RangeObservableCollection<IUndoRedo> _undoList;
        private static RangeObservableCollection<IUndoRedo> _redoList;
        private static int? _maxLimit;
        #endregion

        /// <span class="code-SummaryComment"><summary>
</span>        /// Add an undoable instance into the Undo list.
        /// <span class="code-SummaryComment"></summary>
</span>        /// <span class="code-SummaryComment"><typeparam name="T">The type of instance this is.</typeparam>
</span>        /// <span class="code-SummaryComment"><param name="instance">The instance this undo item applies to.</param>
</span>        public static void Add<T>(T instance) where T : IUndoRedo
        {
            if (instance == null)
                throw new ArgumentNullException("instance");

            UndoList.Add(instance);
            RedoList.Clear();

            // Ensure that the undo list does not exceed the maximum size.
            TrimUndoList();
        }

        /// <span class="code-SummaryComment"><summary>
</span>        /// Get or set the maximum size of the undo list.
        /// <span class="code-SummaryComment"></summary>
</span>        public static int? MaximumUndoLimit
        {
            get
            {
                return _maxLimit;
            }
            set
            {
                if (value.HasValue && value.Value < 0)
                {
                    throw new ArgumentOutOfRangeException("value");
                }
                _maxLimit = value;
                TrimUndoList();
            }
        }

        /// <span class="code-SummaryComment"><summary>
</span>        /// Ensure that the undo list does not get too big by
        /// checking the size of the collection against the
        /// <span class="code-SummaryComment"><see cref="MaximumUndoLimit"/>
</span>        /// <span class="code-SummaryComment"></summary>
</span>        private static void TrimUndoList()
        {
            if (_maxLimit.HasValue && _maxLimit.Value >

In this code, we have two lists – the undoable list and the redoable list. These lists wrap up the IUndoRedo interface we defined earlier and will actually handle calling Undo or Redo as appropriate. There’s a little wrinkle when you call Undo – we need to copy the Redo list out to a temporary copy so that we can add it back later on. The way that the undo works, is to extract the last item from the undo stack – which then gets removed. This item is put onto the redo stack so that we can redo it later if need be. If you notice, in the Add method, we clear the Redo stack so that we can’t perform a Redo after a new operation. As the property gets updated and triggers the Add method, we have to copy the Redo out, and add it back in after the Add has been performed.

All you need to do now, is wire your ViewModel up to the UndoManager and Robert’s your mother's brother. I’ve attached a sample application which demonstrates this in action – this application isn’t finished yet as we're leaving room for the next installment, where we hook into an undoable observable collection. Here’s a screenshot of the application in action:

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Pete O'Hanlon
CEO
United Kingdom United Kingdom
A developer for over 30 years, I've been lucky enough to write articles and applications for Code Project as well as the Intel Ultimate Coder - Going Perceptual challenge. I live in the North East of England with 2 wonderful daughters and a wonderful wife.
 
I am not the Stig, but I do wish I had Lotus Tuned Suspension.
Follow on   Twitter   Google+

Comments and Discussions

 
GeneralHave a 5 Pinmemberzagitta9-Feb-10 14:07 
GeneralRe: Have a 5 PinmvpPete O'Hanlon9-Feb-10 21:47 
GeneralA few comments Pinmemberkornman004-Feb-10 3:27 
GeneralRe: A few comments PinmvpPete O'Hanlon4-Feb-10 3:49 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140814.1 | Last Updated 1 Feb 2010
Article Copyright 2010 by Pete O'Hanlon
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid