Freeform Custom Activity Designers using ICompositeView (Part 3)

[This post is Part 3 of a series on writing custom activity designers. Part 1Part 2 - Part 3 - Part 4]

In Part 1 we started implementing a freeform layout designer using ICompositeView. So far we have implemented ICompositeView.OnItemsPasted() and ICompositeView.OnItemsDelete(). But the bad news is that our implementations aren’t keepers. The reason is that they don’t support Undo and Redo.

The problem scenario
  1. Add an If activity to the workflow
  2. 'Cut' the activity (Ctrl+X)
  3. Add a CanvasActivity to the workflow
  4. 'Paste' the If activity inside the CanvasActivity (Ctrl+V)
  5. 'Delete' the If activity ([DEL])
  6. Undo (Ctrl+Z)

Expected: The visual of the If activity should reappear. (Also, but less importantly, the CanvasActivity should show a validation error: 'One or more children have validation errors or warnings'.)

Observed: The visual for the If activity does not appear. But the validation error does appear.

The code again

Here is how we implemented ICompositeView.OnItemsDelete() last time.

    void ICompositeView.OnItemsDelete(List<ModelItem> itemsToDelete)

    {

        ModelItem canvasActivity = ModelItem;

        foreach (var i in itemsToDelete)

        {

            UIElement view = (UIElement)i.View;

            canvasActivity.Properties["Children"].Collection.Remove(i);

            this.ContentCanvas.Children.Remove(view);

        }

    }

Understanding what went wrong

Because we never coded any explicit support for undo, so it’s no surprise that it doesn’t quite work in our scenario. What might be a surprise is that we have undo half-working already! How?

The code in OnItemsDelete() creates the correct state of the View tree and the Model tree. And when we press Ctrl+Z we can see that the Model tree change is being undone (we can also verify by saving to XAML). But again, how?

Well, how does the Workflow Designer implement undo/redo? Last time we learned about how ModelEditingScope is used to control the grouping of actions on the Undo stack. What we sort of brushed over is that for each call to ModelProperty.SetValue() or ModelItemCollection.Add(), an item is being created on the undo stack for us automatically. (Gory implementation details to be in a later post).

Why doesn’t the Workflow Designer also know how to undo our changes to the View? Why should we expect it to? Good reasons not to expect it are that it’s not performant nor what the user wants. Imagine these scenarios:

1) Zoom in to view a single activity. If the activity was collapsed, we need to populate the views of its children. We also need to hide or delete all the views which are no longer ‘in view’.

2) How many times do I press undo? Should undo revert through every single view you used? It would become impossible to remember how many times you need to press Ctrl+Z to undo the last actual edit that you made.

Now I’ve got the good argument out of the way here’s a bad argument I find much more appealing: It’s not the WPF way! The WPF way is to use binding (and change notifications) to update the view automatically in response to changes in the model (or view model - MVVM). Think about the humble check box. It doesn’t need to know about a global undo/redo stack if we just bind it to the appropriate model data which is itself covered with an undo/redo stack.

Model Change Notifications

Actually the main point of the Workflow Designer’s model tree is to provide these change notifications and enable the binding of view to model. Unfortunately, Canvas.Children isn't bindable, and even if it were, I don’t see how it would handle layout, so we are just going to have to solve our problem in code. First, we’re going to modify the OnModelItemChanged override we wrote in Part1.

protected override void OnModelItemChanged(object newValue)
{
    ModelItem canvasActivity = (ModelItem)newValue;
Update(canvasActivity);
canvasActivity.Properties["Children"].Collection.CollectionChanged +=
        new System.Collections.Specialized.NotifyCollectionChangedEventHandler
           ((senders, args) => Update(this.ModelItem));
}

The important part is that we are going to listen for notifications to changes to CanvasActivity.Children, and respond to them by calling Update(ModelItem):

void Update(ModelItem canvasActivity)
{
    this.ContentCanvas.Children.Clear();
    foreach (ModelItem modelItem in canvasActivity.Properties["Children"].Collection)
{
        var view = Context.Services.GetService<ViewService>().GetView(modelItem);
        this.ContentCanvas.Children.Add((UIElement)view);
        DragDropHelper.SetCompositeView((WorkflowViewElement)view, this);
}
}

Once we have the change notification/handling in place, we should rewrite OnItemsDelete() and OnItemsPaste() :

void ICompositeView.OnItemsDelete(List<ModelItem> itemsToDelete)
{
ModelItem canvasActivity = ModelItem;
foreach (var i in itemsToDelete)
{
canvasActivity.Properties["Children"].Collection.Remove(i);
}
}

void ICompositeView.OnItemsPasted(List<object> itemsToPaste,

    List<object> metadata, Point pastePoint, WorkflowViewElement pastePointReference)

{

    System.Activities.Presentation.Model.ModelItem canvasActivity = this.ModelItem;

    foreach (var i in itemsToPaste)

    {

        canvasActivity.Properties["Children"].Collection.Add(i);

    }

}

 

In these functions all we should do is update the model. And then we can undo delete, hurray!

[Last Revision: 01/20/2010]