Undo and Redo: Take 2

Please excuse the rhyming in the title…sometimes I just can’t help myself.  It’s a problem.

A few weeks ago I started working on a  super-duper-secret project (to be revealed soon), a big part of which was a new editor.   Since I’m the kind of guy who gets all worked up about having proper undo and redo support, I took the opportunity to make it an up-front part of my design rather than just shoving it in afterwords.

One of the things I’d thought about for map editor was having a well-defined boundary between the user’s input and actions that could be performed on the document.  For the map editor it was too late for that, but this time I could put it in from the start.  What I came up with was the ActionManager (yeah I know, bad name.  Sue me.).  It provides as public methods a variety of actions that can be performed on the document: adding a new item, removing an item, setting a property on an item, etc.   When one of these methods gets called it creates an IEditAction derivative, configures it, has the IEditAction “do” the action, and then pushes it onto the Undo stack.  So similar to what I had previously in my map editor, except that the EditActions actually perform the action the first time around and all the Undo/Redo stuff is wrapped up in a nice class.  It’s also less error prone, because you go through the ActionManager layer rather than going directly to the document (this helps ensure that everything the user does goes through the proper Undo/Redo jazz).

I also managed to get it down to just three EditAction’s: AddRemoveItemAction, PropertyEditAction, and CompoundAction.  The first is for adding and removing items to the document, the second is for whenever an item’s property is modified (this is the majority of actions), and the third just represents multiple AddRemoveItemAction’s and/or PropertyEditAction’s that are peformed as the result of a single user action.   It still doesn’t necessarily deal with the problem of having the number of EditAction’s explode as the app grows, but it helps that Reflection in .NET is awesome enough to let me use PropertyEditAction for just about everything.

The one problem I still had to deal with was the stupid PropertyGrid.  The PropertyGrid is fantastic, but it’s not realy set up for Undo and Redo.  Well that’s a lie, it sorta is.  See it raises a PropertyValueChanged event whenever a property value changes, and the EventArgs conveniently has an OldValue property that tells you what the previous value was.  Great, right?  Right…except  for the fact that this is null when you have multple objects selected on the PropertyGrid.  Not so great.

This led me to approach #1:  each time an item is supposed to be set onto the PropertyGrid, create a “proxy” item byt cloning the original and set that onto the PropertyGrid.  Then whenever a property value is changed, I can look up the “real item, query it for the old property value, and then actually set the property value via the ActionManager.  And this worked…at first.  Where I ran into problems was where setting properties on an item affected the state of another item.  For instance items have an “Index” property that controls the index within a parent item’s children collection.  So setting that property causes the item to send a request to the parent item for a reorder of the children, and that might fail based on the state of parent.  This means that if I leave my references hooked up properly in my clone I end up with a situation where actions like that get performed twice (and usually failing the second time), or if I  “detatch” a clone from all outside references I lose my error verification (not to mention the fact that I have to be very very careful in how I clone something).

This brought me to attempt #2, which I consider uglier but has actually worked out: every time the user selects a GridItem in the PropertyGrid, save the current state of the Property for all selected items so that I have an OldValue.

private GridItem GetRootReferenceGridItem(GridItem gridItem)
{
    GridItem rootItem = gridItem;
    if (!rootItem.PropertyDescriptor.ComponentType.IsValueType)
        return rootItem;

    while (gridItem.Parent.Parent != null)
    {
        gridItem = gridItem.Parent;
        if (gridItem.PropertyDescriptor != null 
            && !gridItem.PropertyDescriptor.ComponentType.IsValueType)
        {
            rootItem = gridItem;
            break;
        }
    }

    return rootItem;
}

private void SetOldValues()
{
    GridItem gridItem = propertyGrid.SelectedGridItem;

    if (gridItem != null && gridItem.GridItemType == GridItemType.Property)
    {
        gridItem = GetRootReferenceGridItem(gridItem);

        oldValues = new object[selectedItems.Count];
        for (int i = 0; i < oldValues.Length; i++)
            oldValues[i] = gridItem.Value;
    }
    else
        oldValues = null;
}

void propertyGrid_SelectedGridItemChanged(object sender, SelectedGridItemChangedEventArgs e)
{
    if (e.NewSelection.PropertyDescriptor == null 
        || e.OldSelection == null
        || e.OldSelection.PropertyDescriptor == null 
        || e.NewSelection.PropertyDescriptor.Name != e.OldSelection.PropertyDescriptor.Name)
        SetOldValues();
}

void propertyGrid_PropertyValueChanged(object s, PropertyValueChangedEventArgs e)
{
    object[] items = new object[selectedItems.Count];

    // Trace backwards through the chain of properties until we find
    // the first property
    List<string> propertyChain = new List<string>();
    GridItem gridItem = GetRootReferenceGridItem(e.ChangedItem);
    string propertyName = gridItem.PropertyDescriptor.Name;

    while (gridItem.Parent.Parent != null)
    {
        gridItem = gridItem.Parent;
        if (gridItem.PropertyDescriptor != null)
            propertyChain.Add(gridItem.PropertyDescriptor.Name);                              
    }

    // Now walk the chain and find the owner of the property or field that was modified
    for (int i = 0; i < selectedItems.Count; i++)
    {                
        items[i] = selectedItems[i];
        object nextItem = items[i];
        for (int j = propertyChain.Count - 1; j >= 0; j--)                
            items[i] = ActionManager.GetPropertyOrFieldValue(items[i], propertyChain[j]);                
    }

    actionManager.PropertyValueChanged(items, propertyName, oldValues);

    SetOldValues();
}

The main problem with this is that the PropertyGrid is now editing a “live” object: changes it makes to items actually affect their state.  This unfortunately broke my “everything must go through the ActionManager” philosophy, but I couldn’t think of any better alternatives.  So I added a new method to the ActionManager that allows me to “register” that a property value was changes after the fact.  It basically works the same as the old ChangePropertyValue method, except that it doesn’t call “Do” on the PropertyEditAction after it creates it.   So yeah kinda ugly…but it works.  Good enough, I guess.

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