An Activity Designer for InvokeAction

[Update! There are some bugs with the attached code, see the sequel post for discussion of bug fixes.]

My workmate Ramraj mentioned that a long way back (during earlier milestones of WF 4.0) something was invented for XAML called a property reference,which was a feature that in the workflow designer would enable creating custom XAML activities 'with holes’ for plugging in actions. More specifically, it enables authoring an InvokeAction<T> activity designer, so that an InvokeAction<T> activity in your workflow can invoke a certain ActivityAction<T> property which was set on your workflow.

This feature is kind of strange because it is hard to imagine how it could work using real CLR properties. Scenario:

- You have CLR property 'Action' on an InvokeAction activity.
- You want it to be exactly the same value as CLR property 'Action' on your custom Xaml Activity (which wraps the InvokeAction activity)
- There is no value of Action yet - so you can't just literally set the two properties to be the same value.
- You need one CLR to 'refer' to another one, as if overriding its getter to do

Action { get { return otherGuy.Action } }

But this seems impossible, because of course with normal CLR objects you can't selectively override properties in this way. Where a little magic is able to enter, and save the day, is the fact that we aren't restricted to dealing with actual CLR objects. The properties only need to be set to equal the same value when the workflow XAML definition for our custom activity is invoked - which, with a few ifs and buts, might be when the needed property value is already known (hint: there are some ways this can fall down).

So back to the original story - I heard this construct existed in the past as a prototype, but more recently (thanks to forum post here) I found out the functionality is actually alive and well in the 4.0 framework, and is even highlighted by one of the XAML samples!

It looks like this:

<Activity mc:Ignorable="sap" x:Class="Microsoft.Samples.Activities.ShowDateTimeAsAction" [namespaces omitted]>
  <x:Members>
    <x:Property Name="CustomAction" Type="ActivityAction(x:String)" />
  </x:Members>

  <InvokeAction x:TypeArguments="x:String" Argument="[str]" sap:VirtualizedContainerService.HintSize="200,82">
      <PropertyReference x:TypeArguments="ActivityAction(x:String)" PropertyName="CustomAction" />
  </InvokeAction>

</Activity>

The PropertyReference is tying the Action property (of type InvokeAction<string>), to the CustomAction property of the ShowDateTimeAsAction class.

So… the framework has support so that you can do all this through XAML. Which is cool. But nothing in XAML is cool enough without a designer experience...

If you want to dig deeper, there’s also a couple mysterious aspects to the above XAML.

-What’s this <PropertyReference> class?
-Why does the value of Action still appear to be null, after you deserialize it using WorkflowDesigner.Load()?
-Even though Action appears as null, the <PropertyReference> data survives loading, editing, and saving in the designer, and getting saved back to XAML?

So definitely something unusual is going on. It works by certain classes which do some custom XAML serialization/deserialization: ActivityPropertyReference, ActivityBuilder,and System.Xaml.AttachablePropertyServices.

Now as I really just want to know how we could use this in designer, the important point for me is that we can find the PropertyReference which has been loaded from XAML, and do something with it.

What we get when we load the above sample XAML (which had some bits cut out) is an object tree something like

-ActivityBuilder
+—Properties
+—Implementation
     +—Sequence
           +—Body
                +—…
                +—ActivityAction(with an attached property)
                     +—Action(=null)

where the attached property is
-ActivityPropertyReference
  +--TargetProperty=”Action”
  +--SourceProperty=”CustomAction”

We get/set the attached property using these APIs:

ActivityBuilder.GetPropertyReference(object target)
ActivityBuilder.SetPropertyReference(object target, ActivityPropertyReference value)

Now that we have this cool information, we should be able to make a designer for InvokeAction<T>, right? Right. Really. This is what it looks like in action:

image 

Download the attached code to play with it yourself. (P.S. I only tested it with InvokeAction<string> so far, let me know in the comments if you have issues....)

InvokeActionDesigner.zip

Comments

  • Anonymous
    April 29, 2010
    Hi Tim Really good!  However, if I change the PropertyReference to a VariableReference, I get an error: <?xml version="1.0" encoding="utf-8"?> <!-- Copyright (c) Microsoft Corporation.  All rights reserved. --> <Activity x:Class="Microsoft.Samples.Activities.ShowDateTimeAsAction"  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  xmlns:sample="clr-namespace:Microsoft.Samples.Activities;assembly=Activities"  xmlns:s="clr-namespace:System;assembly=mscorlib"  xmlns:swm="clr-namespace:System.Activities;assembly=System.Activities"  xmlns="http://schemas.microsoft.com/netfx/2009/xaml/activities">  <x:Members>    <x:Property Name="CustomAction" Type="ActivityAction(s:String)"/>  </x:Members>  <Sequence>    <Sequence.Variables>      <Variable Name="str" x:TypeArguments="s:String" /> <Variable x:TypeArguments="swm:ActivityAction(s:String)" Name="action"  x:Name="__action"/>    </Sequence.Variables>    <sample:GetDateTime Date="[str]" />    <InvokeAction x:TypeArguments="s:String" Argument="[str]"> <InvokeAction.Action> <!-- <PropertyReference x:TypeArguments="swm:ActivityAction(s:String)" PropertyName="CustomAction" /> --> <VariableReference x:TypeArguments="swm:ActivityAction(s:String)" Variable="{x:Reference __action}"> </VariableReference> </InvokeAction.Action>    </InvokeAction>  </Sequence> </Activity> Gives: Error 1 Unexpected object of type 'System.Activities.Expressions.VariableReference(ActivityAction(String))'. Member 'System.Activities.Statements.InvokeAction(String).Action' expects item of type 'System.Activities.ActivityAction(String)'. D:CodeWF_WCF_SamplesWFBasicCustomActivitiesCode-BodiedActivityActionCSSimpleActivityShowDateTimeAsAction.xaml 22 5 Where am I going wrong?  The PropertyReference and VariableReference both inherit from System.Activities.CodeActivity<Location<TResult>>, so it should be a straight swap. Thanks Nick

  • Anonymous
    April 30, 2010
    Hi Nick, I probably wasn't clear enough about this in the article. This ActivityAction designer relies on a special XAML feature for attached properties. When you look at the XAML it looks like <PropertyReference> is just another XAML tag. In which case you can also logically think that it is an instance of the PropertyReference class in System.Activities.Expressions. However, although the explanation gets rather technical that is not actually the case. The PropertyReference<> you are looking at is System.Activities.Expressions.PropertyReference. But magically, the <PropertyReference> XAML tag is actually being converted into an ActivityPropertyReference object. The magic is from System.Xaml.AttachablePropertyServices and custom XAML handling in DynamicActivityXamlReader. So how can <PropertyReference> deserialize as <ActivityPropertyReference>? In fact XAML lets the workflow runtime (or you if you want) intercept any XML nodes and attributes, and convert them to attached property references, or indeed interpret them however you like (transform them to refer to another type etc). Back to your question, of how you can use a VariableReference, the question is just a little bit off base. The reason is that on one hand, the activity expects an ActivityAction<String>, which is like a delegate function to run with a string input. On the other hand, you are passing a VariableReference - which is actually an Activity<ActivityAction<String>>. So I think something or other needs to change in your design... (PropertyReference. on the other hand. is really giving back the type of the referenced property, which is ActivityAction<String>.)

  • Anonymous
    April 30, 2010
    The comment has been removed

  • Anonymous
    May 11, 2010
    The comment has been removed

  • Anonymous
    May 12, 2010
    Added a sequel post. Tim