- 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
{
public interface IUndoRedo
{
string Name { get; }
void Undo();
void Redo();
}
}
Now, we need to create a class that implements this interface
.
using System;
namespace UndoRedoSample
{
public class UndoableProperty<T> : IUndoRedo
{
#region Member
private object _oldValue;
private object _newValue;
private string _property;
private T _instance;
#endregion
public UndoableProperty(string property, T instance,
object oldValue, object newValue)
: this(property, instance, oldValue, newValue, property)
{
}
public UndoableProperty(string property, T instance,
object oldValue, object newValue, string name)
: base()
{
_instance = instance;
_property = property;
_oldValue = oldValue;
_newValue = newValue;
Name = name;
UndoManager.Add(this);
}
public string Name { get; private set; }
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 ViewModel
s. This is where the UndoManager
comes in:
using System;
using System.Collections.Generic;
using System.Linq;
namespace UndoRedoSample
{
public static class UndoManager
{
#region Members
private static RangeObservableCollection<IUndoRedo> _undoList;
private static RangeObservableCollection<IUndoRedo> _redoList;
private static int? _maxLimit;
#endregion
public static void Add<T>(T instance) where T : IUndoRedo
{
if (instance == null)
throw new ArgumentNullException("instance");
UndoList.Add(instance);
RedoList.Clear();
TrimUndoList();
}
public static int? MaximumUndoLimit
{
get
{
return _maxLimit;
}
set
{
if (value.HasValue && value.Value < 0)
{
throw new ArgumentOutOfRangeException("value");
}
_maxLimit = value;
TrimUndoList();
}
}
private static void TrimUndoList()
{
if (_maxLimit.HasValue && _maxLimit.Value > 0)
{
while (_maxLimit.Value < UndoList.Count)
{
UndoList.RemoveAt(0);
}
}
}
public static bool CanUndo
{
get
{
return UndoList.Count > 0;
}
}
public static bool CanRedo
{
get
{
return RedoList.Count > 0;
}
}
public static void ClearAll()
{
UndoList.Clear();
RedoList.Clear();
}
public static void Undo()
{
if (UndoList.Count > 0)
{
IUndoRedo item = UndoList.Last();
UndoList.RemoveAt(UndoList.Count - 1);
List<IUndoRedo> copyRedoList = RedoList.ToList();
copyRedoList.Add(item);
List<IUndoRedo> copyUndoList = UndoList.ToList();
item.Undo();
UpdateRedoList(copyRedoList);
UndoList.Clear();
UndoList.AddRange(copyUndoList);
}
}
public static void Redo()
{
if (RedoList.Count > 0)
{
IUndoRedo item = RedoList.Last();
RedoList.RemoveAt(RedoList.Count - 1);
List<IUndoRedo> redoList = RedoList.ToList();
item.Redo();
UpdateRedoList(redoList);
}
}
private static void UpdateRedoList(List<IUndoRedo> redoList)
{
RedoList.Clear();
RedoList.AddRange(redoList);
}
public static RangeObservableCollection<IUndoRedo> UndoList
{
get
{
if (_undoList == null)
_undoList = new RangeObservableCollection<IUndoRedo>();
return _undoList;
}
private set
{
_undoList = value;
}
}
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: