Freeform Custom Activity Designers using ICompositeView

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

Motivation: Building an activity like Flowchart

Creating composite activities and designers in Workflow Foundation 4.0 is really easy when you use WorkflowItemPresenter and WorkflowItemsPresenter. (Seriously. If you haven’t tried it yet, try it.) While these controls are easy to develop with, there are limits to how they can be used or customized. What are you supposed to do if you want to break out into a full free-form layout, like Flowchart?

In this series we're going to implement ICompositeView and start building an activity designer with some of the features of Flowchart, particularly free-form layout. We will start by inheriting ICompositeView and gradually we will learn how to provide a real implementation for the methods we inherit. We will encounter a lot of educational bugs and set backs along the way, and I think reading about them and how to fix them is fun. On the other hand if you just want downloadable code, please just jump to the last article in the series, which will be as complete and bug free as I have time to make it.

Setting up the project

To start, I set up an ‘Activity Library’ project in Visual Studio. You could do it in a rehosted designer project instead if you prefer and have one handy, and it can be easier to debug that way.

We are coding a designer but we also need an Activity class so we can test it. Here is a skeleton activity class called CanvasActivity. It uses a few attributes to help make the XAML look nice when we save our workflow. (Note: it is going to save us some stupid bugs later that the collection property always returns a non-null value, even for a new CanvasActivity() .)

CanvasActivity.cs:

    [Designer(typeof(CanvasDesigner))]

    [ContentProperty("Children")]

    public sealed class CanvasActivity : NativeActivity

    {

        [DefaultValue(null)]

        private Collection<Activity> _children;

        public Collection<Activity> Children {

            get

            {

                return (_children = _children ?? new Collection<Activity>());

            }

        }

 

        protected override void Execute(NativeActivityContext context) {} //TODO

    }

 

Next, we begin writing WPF XAML for the custom designer. A WPF Canvas object will be useful for doing the free-form layout. And actually that’s about all we will need.

CanvasDesigner.xaml:

<sap:ActivityDesigner x:Class="ActivityLibrary1.CanvasDesigner"

   xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

   xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

   xmlns:sap="clr-namespace:System.Activities.Presentation;assembly=System.Activities.Presentation">

    <Canvas Name="ContentCanvas" Width="500" Height="500" />

</sap:ActivityDesigner>

The XAML is simple. The guts of the implementation will be in the C# code-beside file, CanvasDesigner.xaml.cs. There we should declare that our CanvasDesigner class implements ICompositeView. Then right-click the ICompositeView declaration and choose ->'Implement Interface Explicitly'. Everything gets implemented as 'throw new NotImplementedException()'. If we compile our project and we can already test the activity we created.

Debugging the activity designer

Aside - We don’t need to be using a rehosted application to debug our designer, but if you have one it helps avoids symbol file access conflicts at build time. I don’t originally use a rehosted application for testing. Instead I fire up a second instance of VS, and ‘attached process’ to the first instance of VS. Mostly I am debugging using output from Debug.WriteLine or set breakpoints. But I can also load the source file in the second instead of VS and set breakpoints. Use any similar technique to see when the interface methods get called.

As soon as we start trying out our code, we start racking up the bugs.

Bug #1: We can’t paste

If we set breakpoints or use debug traces, no matter how we try to interact with our new activity, we can see that most methods of ICompositeView aren’t getting called at all. The one method we can easily ‘hit’ on is ICompositeView.CanPasteItems() (by pasting an activity we cut from somewhere else in the workflow).

Since our code still throws NotImplementedException paste is disabled, and we can't actually paste anything. What if we instead implement the function to return true?

    bool ICompositeView.CanPasteItems(List<object> itemsToPaste)

    {

        return true;

    }

And rebuild...

Now CanPasteItems returns true, we can select ‘Paste’ from the context menu, and if then ICompositeView.OnItemsPasted() gets called. Of course this will throw, but using the debugger we can inspect the parameters coming in. With this parameters we need to do two things:

  1. Update the model tree: add the pasted activity to CanvasActivity's Children collection,
  2. Update the visual tree: create an activity designer visual, and add them to the visual tree

Of course we should be able to handle pasting multiple items not just one. OK, here’s some code for a first attempt. Read it and try to guess where the bugs are:

    void ICompositeView.OnItemsPasted(List<object> itemsToPaste, List<object> metadata,

                              Point pastePoint, WorkflowViewElement pastePointReference)

    {

    //itemsToPaste - some ModelItems, or maybe just raw CLR objects we need to paste.

    //metadata - this could hold anything or nothing. Let’s leave it alone.

        ModelItem canvasActivity = this.ModelItem;

        foreach(var i in itemsToPaste)

  {

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

        var view = Context.Services.GetService<ViewService>().GetView(createdModelItem);

        this.ContentCanvas.Children.Add((UIElement)view);

        }

    }

 

Notes:

  • The object we add to the model tree might not actually be a ModelItem yet. But it will automatically get wrapped up in a ModelItem if we add it. In this case we get the wrapper ModelItem from the return value of Add(). [Aside: What’s this model tree thing? Read about it here.]
  • We don’t ever new an activity designer. Instead we should request ViewService to create it for us. (ViewService is a factory service that figures out which designer class to instantiate automatically, based on DesignerAttribute.)

Testing our new implementation, we celebrate – we have pasted an activity!

 

Bug #2: Reloading a pasted activity from XAML doesn’t work

 

Let’s look at the XAML. Our pasted activity was saved out to XAML properly, which is important. But when we reload the XAML file, we can’t see the activity we created. What if we save the file again? The pasted activity is still saved in XAML. Weird… oh. It’s just the view which is missing.

 

We need to populate the Canvas in case our designer is created and initialized with the activities loaded from XAML. We can’t do that by overriding OnInitialized(), because even then this.ModelItem may still be null. The best place I can think of to do it is by overriding WorkflowViewElement.OnModelItemChanged().

 

    protected override void OnModelItemChanged(object newValue)

    {

    this.ContentCanvas.Children.Clear();

    ModelItem canvasActivity = (ModelItem)newValue;

    foreach (ModelItem modelItem in canvasActivity.Properties["Children"].Collection)

    {

    var view = Context.Services.GetService<ViewService>().GetView(modelItem);

    this.ContentCanvas.Children.Add((UIElement)view);

    }

    }

The code is similar to what we just did above.

Next bug?

Bug #3: We can’t delete the activity we added

We can add items now, using Paste. Delete should be really, really easy, right?

Let's select something in our activity, and press [DEL].

Uh oh, ICompositeView.OnItemsDelete() isn’t getting called. Why not?

It's easy to see in a debugger why you are getting called, hard seeing why you are not. In retrospect, the easiest and best way to think about it is from the caller’s point of view.

Suppose I’m an event handler for [DEL]. I have to call ICompositeView.OnItemsDelete(). But… the only information I have to work on is the current selection i.e. the activity being deleted, not the container activity. But how would I know what the containing activity is? Maybe someone could make it easy for me by adding a link pointing from the contained activity to its container and make my life easier?

OK, I wouldn’t ever have figured that out without checking the WorkflowItemsPresenter implementation. But yes, the framework is expecting that when we add a WorkflowViewElement as a containee of our CanvasActivity, we also inform the framework of that fact by calling a static method called DragDropHelper.SetCompositeView(). This creates the missing link.

        this.ContentCanvas.Children.Add((UIElement)view);

        DragDropHelper.SetCompositeView((WorkflowViewElement)view, this);

 

Adding the crucial line of code is needed in two places since we have two functions that add children to our CanvasActivity.

OK. Now we know how to get OnItemsDelete() called, we'll discuss how to implement OnItemsDelete() in Part 2 and Part 3.

[Last revision 08/30/2010]