Navigating the Workflow Tree (WF 4.0 Workflow Designer)

People who who start interesting things in rehosted designer applications or just VS, will often find that they will want to navigate through the workflow tree, looking for activities, arguments, variables, and so on. There are a few facts it can be helpful to understand before you start your coding just to make sure you go down the right path.

This is pretty quick and off-the-cuff, almost a FAQ but not in FAQ format. Feel free to ask questions in the comments for clarifications (faqification!?).

The WF4 runtime provides an API for introspecting on activities

Here the main (maybe only?) class of interest is System.Activities.WorkflowInspectionServices. It gives you a way to find all the child activities of an activity. The way that works is basically by calling CacheMetadata() on all the activities to find out what their children are.

It seems like it might be useful if you could also use some API to find out what the variables and arguments of an activity are. Unfortunately I don’t know any API specifically designed to do this. It does seem like there could be some hacks to call CacheMetadata() directly… I’m not going to recommend that.

A workflow XAML is (mostly) a CLR type. A workflow XAML loaded in designer is sort of a type. Neither is actually an Activity

Create a new activity in designer. The root element of the XAML is <Activity x:Class>. When it is compiled, it defines a workflow type which can be instantiated in C# like any regular CLR type. But only when you instantiate that type do you get an activity.

So far that’s only talking about the XAML. What happens when you load it in designer? We can’t build a type at runtime… (safely), so instead we build a representation of a type currently called an ActivityBuilder (in Beta1 it was known as ActivitySchemaType). For more lengthy info on this, there’s Matt’s post from last June.

Arguments, Variable Collections, and child Activities are going to be public, anyway…

Otherwise they wouldn’t be showing up in the workflow designer. Or the Model Tree, which is what the activity designer is using to figure out what to show. So we have no problem with, for instance, calling into activities directly and asking if they have a Variable or an Argument yet.

Also, of course we would have no problem reflecting over the activity object, asking for all its children of type Argument or Collection<Variable>, as the case may be. It’s something of an assumption that that variable will be declared in CacheMetadata or used at runtime… but actually you will find that the Workflow Designer makes that assumption most of the time already.

Special Case, Variable Collections

The designer doesn’t show any Variables or Variable Collections as editable in the Variables window except for properties that look like:

public Collection<Variable> Variables { get; }

i.e. they have to actually be named ‘Variables’. I don’t this this is unreasonable - what would be the point of having two publically editable Variables collections for instance?

Why Reflect when the Model Tree does it for you?

True, you could reflect over all the properties of an activity looking for Variables and Arguments. But if you have already loaded your activity in the designer, why not just iterate through ModelItem.Properties looking for properties of type variable or argument?

The Model Tree is lazily created

This one is pretty unexpected and hence trips everybody up. It’s lazily created for reasons of performance, of course. Here’s a really good forum thread about navigating to a specific activity in the model tree.

The key API for ensuring the object you want to find is really instantiated as a model item? ModelService.Find. example:

        IEnumerable<ModelItem> activityCollection = modelService.Find(modelService.Root, typeof(Activity));

(ModelService modelService is got from EditingContext.Services)

Of course you can find anything in your Model Tree this way, not just activities.

Getting/Setting expressions on Variables and Arguments

Variables and Argument values, which are editable in the designer as Visual Basic expressions, are implemented using VisualBasicValue and VisualBasicReference. Can’t find a really good article to link to on this – yet... :-)

Here’s a couple snippets stolen from myself about it though: (sources - one two)

“You can't directly ask the ETB what the text of the expression is, but an alternative is to get/set the Expression property instead, which is still public. You will find that an Expression created in designer is usually a Literal<>, a VisualBasicValue<>, or a VisualBasicReference<>, and you should be able to set the property to instances you create of the same types.”

“the ETB only displays actual VB expressions built up as strings like this:
  .CorrelatesWith = new InArgument(new VisualBasicValue<CorrelationHandle>("variable1"));”

(ETB = workflow designer’s expression text box)

I can find the variables OK but where are the arguments hiding?

They live attached to the ActivityBuilder, but where? Well, all the InArguments and OutArguments are actually stored at design time as objects of type DynamicActivityProperty - along side any non-argument properties. The object tree looks something like this:

    new ActivityBuilder {
       /* ... */,
       Properties = {
           new DynamicActivityProperty { Name = "SampleInArgInt", Type = typeof(InArgument<int>) /* optionally Value = some expression */ },
           new DynamicActivityProperty { Name = "SampleOutArgBool" Type = typeof(OutArgument<bool>) /* Value = null */ },
           new DynamicActivityProperty { Name = "SamplePropertyTimespan", Type = typeof(TimeSpan), /* optionally Value = null, Value = some constant default value */ }

The type of the argument, which can be configured in the designer, is actually just a value of a property which is itself a value in the 'Properties' collection. OK that one was just to confuse you. :p

So you can hunt for all the DynamicActivityProperty objects attached to the root ActivityBuilder, either with ModelService or risk lazy loading issues and take the direct iteration approach although it might usually be safe. (Not too sure. :-))


[Last Updated 2/23/2010]