Undoing MVVM

I apologise that it’s been a while since I last blogged, but I’ve been busy working on a 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 it’s glory:

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

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

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

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

        /// <summary>
        /// Initialize a new instance of <see cref="UndoableProperty"/>.
        /// </summary>
        /// <param name="property">The name of the property.</param>
        /// <param name="instance">The instance of the property.</param>
        /// <param name="oldValue">The pre-change property.</param>
        /// <param name="newValue">The post-change property.</param>
        /// <param name="name">The name of the undo operation.</param>
        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);
        }

        /// <summary>
        /// The property name.
        /// </summary>
        public string Name { get; private set; }

        /// <summary>
        /// Undo the property change.
        /// </summary>
        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 it’s 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
{
    /// <summary>
    /// 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.
    /// </summary>
    public static class UndoManager
    {
        #region Members
        private static RangeObservableCollection<IUndoRedo> _undoList;
        private static RangeObservableCollection<IUndoRedo> _redoList;
        private static int? _maxLimit;
        #endregion

        /// <summary>
        /// Add an undoable instance into the Undo list.
        /// </summary>
        /// <typeparam name="T">The type of instance this is.</typeparam>
        /// <param name="instance">The instance this undo item applies to.</param>
        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();
        }

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

        /// <summary>
        /// Ensure that the undo list does not get too big by
        /// checking the size of the collection against the
        /// <see cref="MaximumUndoLimit"/>
        /// </summary>
        private static void TrimUndoList()
        {
            if (_maxLimit.HasValue && _maxLimit.Value > 0)
            {
                while (_maxLimit.Value < UndoList.Count)
                {
                    UndoList.RemoveAt(0);
                }
            }
        }

        /// <summary>
        /// Actions can only be undone if there are items in the <see cref="UndoList"/>.
        /// </summary>
        public static bool CanUndo
        {
            get
            {
                return UndoList.Count > 0;
            }
        }

        /// <summary>
        /// Actions can only be redone if there are items in the <see cref="RedoList"/>.
        /// </summary>
        public static bool CanRedo
        {
            get
            {
                return RedoList.Count > 0;
            }
        }

        /// <summary>
        /// Clear all items from the list.
        /// </summary>
        public static void ClearAll()
        {
            UndoList.Clear();
            RedoList.Clear();
        }

        /// <summary>
        /// Undo the last VM change.
        /// </summary>
        public static void Undo()
        {
            if (UndoList.Count > 0)
            {
                // Extract the item from the undo list.
                IUndoRedo item = UndoList.Last();
                UndoList.RemoveAt(UndoList.Count - 1);
                List<IUndoRedo> copyRedoList = RedoList.ToList();
                copyRedoList.Add(item);
                // We need to copy the undo list here.
                List<IUndoRedo> copyUndoList = UndoList.ToList();
                item.Undo();
                // Now repopulate the undo and redo lists.
                UpdateRedoList(copyRedoList);
                UndoList.Clear();
                UndoList.AddRange(copyUndoList);
            }
        }

        /// <summary>
        /// Redo the last undone VM change.
        /// </summary>
        /// <remarks>
        /// Unlike the undo operation, we don't need to copy the undo list out
        /// because we want the item we're redoing being added back to the redo
        /// list.
        /// </remarks>
        public static void Redo()
        {
            if (RedoList.Count > 0)
            {
                // Extract the item from the redo list.
                IUndoRedo item = RedoList.Last();
                // Now, remove it from the list.
                RedoList.RemoveAt(RedoList.Count - 1);
                // Here we need to copy the redo list out because
                // we will clear the list when the Add is called and
                // the Redo is cleared there.
                List<IUndoRedo> redoList = RedoList.ToList();
                // Redo the last operation.
                item.Redo();
                // Now reset the redo list.
                UpdateRedoList(redoList);
            }
        }

        private static void UpdateRedoList(List<IUndoRedo> redoList)
        {
            RedoList.Clear();
            RedoList.AddRange(redoList);
        }

        /// <summary>
        /// Get the undo list.
        /// </summary>
        public static RangeObservableCollection<IUndoRedo> UndoList
        {
            get
            {
                if (_undoList == null)
                    _undoList = new RangeObservableCollection<IUndoRedo>();
                return _undoList;
            }
            private set
            {
                _undoList = value;
            }
        }

        /// <summary>
        /// Get the redo list.
        /// </summary>
        public static RangeObservableCollection<IUndoRedo> RedoList
        {
            get
            {
                if (_redoList == null)
                    _redoList = new RangeObservableCollection<IUndoRedo>();
                return _redoList;
            }
            private set
            {
                _redoList = 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 needs 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 mothers 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:

Download: The usual rules apply, the download needs to be renamed from .doc to .zip. undoredosamplezip

About these ads
  1. Michael Chandler
    February 1, 2010 at 10:37 pm

    I just skimmed over it, and it looks kinda cool, but is probably too simplistic for my needs.

    I think keeping a stack of Commands with an Undo method gives much more flexibility, because often a user’s action can mean multiple internal actions.

    I think the UndoManager could probably be enhanced with an UndoAll, and that the ability to Undo/Redo per VM could be a neccessity (I don’t think the interface allows it ATM)?

    • peteohanlon
      February 1, 2010 at 10:52 pm

      Michael. Thanks for the feedback – you’re right, it does seem simplistic – the aim is to provide the ability to put in custom actions that handle Undo/Redo. As I stated, this stage just represents undoing/redoing simple property changes – more complex scenarios will be added in a future post (such as undoing/redoing collection changes). I’m not so sure about implementing Undo per VM – this goes counter to the aim of what I’m trying to achieve.

      In Goldlight, Undo/Redo will be expressed using MEF, so it’s easy to drop in other implementations as things progress.

    • kevin
      February 2, 2011 at 3:06 pm

      I am looking for some command level undo implementation since the requirements on hand is to provide n-level undo with repect to time and the state of data in the store (application scope but not instance scope) and also need to sync undo from other users (I know… it’s insance, not to mention dependencies of the operations and the state of data). so the complexity is much bigger and I am not sure if it’s feasible at the end, but this seems like a start..

  2. February 1, 2010 at 11:50 pm

    There are Queue and Stack classes in Framework library, you know…

    • peteohanlon
      February 2, 2010 at 7:24 am

      Alex, I do know about the Queue and Stack classes – in the final implementation of Undo, for the purposes of what I’m doing though, I need the list to be observable. This gives me the ability to display undo/redo operations without writing any boilerplate code.

  3. Michael Chandler
    February 2, 2010 at 12:10 am

    Okay, I look forward to the next post :)

    Why do you see undoing per VM as counter to your aim?

    Take for example a tabbed MDI application where each tab is the details of a customer. It seems fair to allow the user to undo all changes to just the one customer’s details, does it not?

    • peteohanlon
      February 2, 2010 at 1:17 pm

      I typically have small specialised VMs, where one screen may host multiple Views/ViewModels, so I don’t want to have to worry about where things are in relation to each other. I’ll have a think about this though, there could well be an elegant way to achieve this, possibly hooking into the Goldlight WorkspaceManager. BTW – I keep referring to Goldlight because this is the MVVM framework I’m developing, so this code is part of the core framework.

  4. February 2, 2010 at 9:56 am

    Like it Pete. If its all in one VM I would use IEO, but this is very cool and I can see where you are coming from.

    • peteohanlon
      February 2, 2010 at 10:19 am

      Thanks Sacha – just wait until I’ve posted the collection code. It makes it really easy to use, and the fact that I’m hosting this through MEF in Goldlight means that it’s useful even for those who haven’t embraced the gooey goodness of MVVM.

  5. April 14, 2010 at 7:44 pm

    This is tricky, I would thing that undoing commands would be more useful than undoing specific values. Especially when you can just provide a cancel but on the form. The code to implement this on a larger form could grow immensely depending on the amount of fields.

    • peteohanlon
      April 15, 2010 at 12:47 pm

      Thanks for your comments Owen. This is just one more trick in your bag of code, and has a lot more emphasis when you see what I’m doing with the ObservableCollection.

      The whole point of this code though is to work over changes on multiple VMs; where a command typically affects one VM this works on multiple. One of the features I’m considering adding is the ability to batch undos together so that multiple values can be rolled back simultaneously. Combine this with a T4 template (or the upcoming release of Goldlight where this forms part of the core framework), and it becomes a lot less work.

  6. May 13, 2010 at 4:59 pm

    Looks good… question about your MVVM implementation, I dont see a model class – for us we use that to ecapsulate what is coming from database… is that because you dont persist the data anywhere?

    -Deva

  1. No trackbacks yet.

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

The Canny Coder

Java 8 Functional Programming with Lambda Expressions

pihole.org

Adventures in theoretical computer science, with your host, chaiguy1337

Confessions of a coder

Confessions of a WPF lover

WordPress.com

WordPress.com is the best place for your personal blog or business site.

Follow

Get every new post delivered to your Inbox.

Join 38 other followers

%d bloggers like this: