Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Undoing MVVM

0.00/5 (No votes)
1 Feb 2010 1  
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
{
    /// <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 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
{
    /// <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 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 has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here