Some Gory Details of WorkflowDesigner Undo Redo
(I've pulled this out from where it was embedded in one of my posts about implementing ICompositeView because it didn't really fit there and modified it but it’s still pretty badly researched.)
A few gory implementation details for the Workflow Designer’s Undo/Redo features. There are a few classes in System.Activities.Presentation.Model heavily involved in undo and redo:
- ModelEditingScope [msdn] (public abstract class ModelEditingScope, protected constructor)
- EditingScope [msdn] (public class EditingScope, private/internal constructor)
- Change [msdn] (public abstract class Change, protected constructor)
The most atomic class to understand here is abstract class Change, which is shaped like this:
{ (); (); (); { ; }}
|
Blue Text Doubleyou Tee Eff. Ignore it.
Change is the core extensibility mechanism for Undo/Redo. A Change is meant to model an action or side-effect in the designer which knows how to undo and redo itself. Internally to System.Activities.Presentation, there are several subclasses of Change such as PropertyChange which the designer uses to build up its undo-redo stack. Since Change is public and non-sealed, we should also be able to create our own subclasses if we want to introduce any new side-effects which need support for undo and redo in the designer.
The other two classes, ModelEditingScope and EditingScope are rather confusingly named, but they are basically two versions of the same thing - a group of Changes which will be undone and redone all at one time.
What happens when we press Ctrl+Z? Let’s walk through the steps. The Change or EditingScope on top of the Undo/Redo Stack is going to be:
- Invert()ed
- Apply()ed
- Moved from top of the undo stack to top of the redo stack.
Simple enough.
So far everything I’ve described is still abstract, and I haven’t explained how exactly you create a Change and add it to the Undo stack in concrete terms.
The good news, is that you shouldn’t need to. Every edit you make to the designer’s Model Tree automatically becomes a Change and gets added to the current editing scope. Here’s a rather dumb example just to make the point:
void SetChildActivity(ModelItem mi, Activity newChild)
{
//this SetValue() call automatically creates a Change and adds it to the current editing scope
mi.Properties[“Child”].SetValue(newChild);
}
If there is no current editing scope (as created by ModelItem.BeginEdit()), the change executes immediately.
When you implement complex UI in designer you may notice und/redo is not all smooth sailing. Model changes are getting created automatically, but you may find yourself wondering how to fix consistency issues with non-model state: e.g. Change aren’t being created to undo everything you do in the WPF view tree, therefore your Model and View become desynced after undo and redo.
Actually that is the scenario we encounter in the ICompositeView posts, but a recap here:
What are the options we might have for fixing this? One approach might be for us to introduce our own subclasses of Change which know how to undo/redo the action of adding or removing an element from the Visual Tree. If we also add these Changes to the active ModelEditingScope, then the Workflow Designer will magically store all the info it needs to undo the Visual changes on the undo redo stack.
This would be a bad idea. But why? Because we would be mixing our View and our Model. Ramraj spent some time kindly pointing out to me why it would be a really good idea to keep them separate. Firstly, usability - changing View is not considered an ‘edit’ by the user, therefore switching View is not something they care to treat as an actual undoable action. Another issue might be that as Views get created and destroyed a lot, and can be relatively heavy objects compared to Model objects. If we keep Views on our undo stack, it might get expensive!
What do we normally choose to do instead? Use the idea of binding. ( Again, see the article linked at top for details.)
When doesn’t binding work? When you’re really trying to extend the model, or work outside of the model. Here we branch off into new territory.
Extending the model: Suppose it’s not the View we want to add undo/redo support for because we’ve instead figured out how to do it by binding and handling change events. Suppose we actually want to extend the capabilities of the model, and what is editable by the user, say for instance by adding something like the ViewStateService. How should we go about it?
Here ViewStateService should be a pretty good model – it basically managed to add undo/redo support by having one method – StoreViewStateWithUndo(). What does it do?
1) Create a ViewStateChange (custom subclass of Change). The Change is created in the forward direction.
2) Call ‘AddToCurrentEditingScope()’
Uh oh! Step 2 is calling an internal API. That isn’t going to work for us. Is Workflow Undo/Redo actually that unextensible? (Also what about that work outside of the model idea?)
Well, I’m not sure yet! There are actually two parts to the Workflow Designer Undo/Redo API, and so far we have been overfocused on the part for implementing Undo for the ModelTree. But there is also a more general looking Undo/Redo API, based around a class called System.Activities.Presentation.UndoUnit.
[OK. This is better than what I had before I started tidying up loose ends, I just realized I don’t know anything about UndoEngine, it’s late, and this is getting long so I think I will have to go multipart again, to cover UndoUnit and UndoEngine late. Until next time!]
Comments
Anonymous
January 27, 2010
Hi Tim, If I use IActivityTemplateFactory, to do stuff like add child activities, variable, etc, to preconfigure my activity, what will happen in terms of undo/redo behaviour, should a user undo adding my activity to the designer? Is it part of one transaction or multiple? Thanks, NotreAnonymous
January 27, 2010
That depends... :-) ...on what you do inside IActivityTemplateFactory.Create(), that is. If no ModelItems are involved then the only thing visible to the undo/redo stack would be the 'Add' it does of the item returned from create, so it's 'atomic'. TimAnonymous
January 27, 2010
Thanks Tim. I think ultimately, everything eventually becomes a model item in the designer, so by saying "ModelItems" are involved do, you mean explicitly doing something to update the model item tree? If ModelItems are involved, what should be done to ensure an atomic transaction? Wrap the operations on the model items in a ModelEditingScope (by using activity BeginEdit and Complete calls)? Thanks, NotreAnonymous
January 27, 2010
Understand the expected timing. Things can turn into model items in the designer, but it's not supposed to be a model item until after it is returned from IActivityTemplateFactory.Create(). Tim