Freigeben über


Freeform Custom Activity Designers using ICompositeView (Part 2)

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

Last time we found out that ICompositeView.OnItemsDelete() will never get called - unless we tell our contained activities exactly which composite view they belong to. But we didn’t get to implement OnItemsDelete(). Our entire article today is filled with attempts to implement OnItemsDelete(). Unfortunately the first several implementations I try won’t work. If you are here just looking for the right approach, and not interested in the learning curve, you might want to jump to Part 3.

Implementing ICompositeView.OnItemsDelete

Attempt #1:

    void ICompositeView.OnItemsDelete(List<ModelItem> itemsToDelete)

    {

        ModelItem canvasActivity = this.ModelItem;

        foreach (var i in itemsToDelete)

        {

            var view = (UIElement)Context.Services.GetService<ViewService>().Getiew(i);

            ContentCanvas.Children.Remove(view);

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

        }

    }

What’s wrong with this code?

When you press [DEL] to delete your activity you will see (by looking at generated XAML) that the model tree item removal works, so the function is half right. What goes wrong? The visual tree update doesn’t occur, i.e. the activity’s visual does not get deleted from our designer's canvas. Because I am a WPF newbie I assumed this was somehow due to me being a stupid user: "Probably there is some good reason Canvas doesn't want me removing its children. Blah. Stupid me." We will see later this is wrong.

Attempt #2:

    void ICompositeView.OnItemsDelete(List<ModelItem> itemsToDelete)

    {

        ModelItem canvasActivity = this.ModelItem;

     foreach (var i in itemsToDelete)

        {

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

        }

        this.OnModelItemChanged(this.ModelItem); //repopulate canvas (see below for details)

     }

I was going to refactor that OnModelItemsChanged() call into something nicer, but I had another problem. This code didn't work either! What is going on?

To see what was going on, I adding Debug.WriteLine() all through my code and attaching a debugger:

    protected override void OnModelItemChanged(object newValue)
    {
        Debug.WriteLine("OnModelItemChanged");
        ModelItem canvasActivity = (ModelItem)newValue;
        this.ContentCanvas.Children.Clear();
        foreach (ModelItem modelItem in canvasActivity.Properties["Children"].Collection)
        {
            Debug.WriteLine("modelItem: {0}", GetID(modelItem));
            var view = (UIElement)Context.Services.GetService<ViewService>().GetView(modelItem);
            Debug.WriteLine("view: {0}", GetID(view));

         this.ContentCanvas.Children.Add((UIElement)view);
            DragDropHelper.SetCompositeView((WorkflowViewElement)view, this);
        }
    }

The results of this exercise:

(*I Open the XAML file*)
OnModelItemChanged
modelItem: 0
view: 0

(*I Press Delete*)
OnModelItemChanged
modelItem: 0
view: 0

Wait, there's still a model item in the Children collection? That’s weird. Look at the XAML we get when we save! No children!

    <local:CanvasActivitysap:VirtualizedContainerService.HintSize="514,536" />

How can this be possible?

Meet the ModelEditingScope

What is a ModelEditingScope? Let's look at an example (sans pretty colors).

    private void CombinedEdit(ModelItem canvasActivity)
    {
1:      using (ModelEditingScope edit = canvasActivity.BeginEdit())
2:      {
3:          canvasActivity.Properties["Children"].Collection.Add(new If());
4:          canvasActivity.Properties["Children"].Collection.Add(new If());
5:          edit.Complete();
        }
    }

This code does adds two If activities as children of a CanvasActivity. What's special about this code is that the two add actions happen as a single (atomic) action. It is atomic both from the point of view of undo/redo, and error recovery if an exception is thrown. Time for a comparison table:

 

With Editing Scope

Without Editing Scope

Undo Stack

1 Undo Item

2 Undo Items

Single Commitof side effects

No

Yes

When we add the using (ModelEditingScope clause as above, then the Workflow Designer's undo/redo stack only increases in size by one. So when we press Ctrl+Z both of the 'adds' get undone at one instant.

The other important benefit of ModelEditingScope is error recovery. Suppose we use (using) a ModelEditingScope and half-way through the using statement an exception is thrown. Then we never reach line 5 of the above code: edit.Complete(); which is the line that commits the editing scope and all of its changes. When the exception exits the using block, ModelEditingScope.Dispose() is automatically called, and all the uncommitted changes to the model tree are thrown away.

Look at our problem scenario and the debugging output above, and now we start to understand what is going on. OnItemsDelete() is actually being called from inside of a ModelEditingScope created by the command-handling code in System.Activities.Presentation. Even though we called Remove() on the model item collection, our debugging code still shows us that the item is in the collection because the editing scope hasn’t been committed yet. After clearing the canvas, oops! We add a child view corresponding to a ModelItem that is in a state of limbo – not yet dead, not yet alive. Mystery solved.

Solving the mystery is good news, but I still don't have a working OnItemsDelete() function and it seems like an attempt to write code working around these heisen-ModelItems that may or may not be alive would be messy. Does Canvas really not support Children.Remove()? Can we instead fix implementation attempt #1?

Getting Delete Working

A simple plain WPF experiment proved to me that Canvas does support Children.Remove(). (This restored my faith in WPF a little!) So the real reason our first implementation of OnItemsDelete() doesn't work was actually something else, let's look at it again:

   void ICompositeView.OnItemsDelete(List<ModelItem> itemsToDelete)
   {
1:     ModelItem canvasActivity = this.ModelItem;
2:     foreach (var i in itemsToDelete)
3:     {
4:         var view = (UIElement)Context.Services.GetService<ViewService>().Getiew(i);
5:         ContentCanvas.Children.Remove(view);
6:         canvasActivity.Properties["Children"].Collection.Remove(i);
7:      }
    }

If we add Debug.WriteLine() statements everywhere, we can find out that the number of items in ContentCanvas.Children doesn't actually change when we execute line 5 . We tried to remove something from the collection that isn't in the collection. Why?

The reason is simply that ViewService.GetView() doesn't do what I thought it did. It actually seems to be creating a new view every time I call it. (I was expecting it would be smarter, caching views, and return the existing one where possible.)

So what is the way to get a view for our ModelItem that already exists? Hmm, ModelItem has a property View which returns a DependencyObject… That seems like questionable architecture. But convenient. Finally, we fix our implementation of OnItemsDelete.

Attempt #3:

    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);

            //not sure yet if we should try this:

            //DragDropHelper.SetCompositeView((WorkflowViewElement)view, null);

        }

    }

I’m still going to call this one just an attempt, because while we can now successfully delete an Activity, it turns out there are other issues with the implementation. Read on to Part 3.

[Last Updated 01/06/2010]