Making Swiss Cheese Look Good, or Designers for ActivityAction in WF4

In my last post, I covered using ActivityAction in order to provide a schematized callback, or hole, for the consumers of your activity to supply.  What I didn’t cover, and what I intend to here, is how to create a designer for that.

If you’ve been following along, or have written a few designers using WorkflowItemPresenter, you may have a good idea how we might go about solving this.  There are a few gotcha’s along the way that we’ll cover as we go through this.

First, let’s familiarize ourselves with the Timer example in the previous post:

 using System;
 using System.Activities;
 using System.Diagnostics;
  
 namespace WorkflowActivitiesAndHost
 {
     public sealed class TimerWithAction : NativeActivity<TimeSpan>
     {
         public Activity Body { get; set; }
         public Variable<Stopwatch> Stopwatch { get; set; }
         public ActivityAction<TimeSpan> OnCompletion { get; set; }
  
         public TimerWithAction()
         {
             Stopwatch = new Variable<Stopwatch>();
         }
  
         protected override void CacheMetadata(NativeActivityMetadata metadata)
         {
             metadata.AddImplementationVariable(Stopwatch);
             metadata.AddChild(Body);
             metadata.AddDelegate(OnCompletion);
         }
  
         protected override void Execute(NativeActivityContext context)
         {
             Stopwatch sw = new Stopwatch();
             Stopwatch.Set(context, sw);
             sw.Start();
             // schedule body and completion callback
             context.ScheduleActivity(Body, Completed);
  
         }
  
         private void Completed(NativeActivityContext context, ActivityInstance instance)
         {
             if (!context.IsCancellationRequested)
             {
                 Stopwatch sw = Stopwatch.Get(context);
                 sw.Stop();
                 Result.Set(context, sw.Elapsed);
                 if (OnCompletion != null)
                 {
                     context.ScheduleAction<TimeSpan>(OnCompletion, Result.Get(context));
                 }
             }
         }
  
         protected override void Cancel(NativeActivityContext context)
         {
             context.CancelChildren();
             if (OnCompletion != null)
             {
                 context.ScheduleAction<TimeSpan>(OnCompletion, TimeSpan.MinValue);
             }
         }
     }
 }
  

 

So, let’s build a designer for this.  First we have to provide a WorkflowItemPresenter bound to the .Body property.  This is pretty simple.  Let’s show the “simple” XAML that will let us easily drop something on the Body property

 <sap:ActivityDesigner x:Class="actionDesigners.ActivityDesigner1"
    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"
    xmlns:sapv="clr-namespace:System.Activities.Presentation.View;assembly=System.Activities.Presentation">
    <StackPanel>
        <sap:WorkflowItemPresenter 
                 HintText="Drop the body here" 
                 BorderBrush="Black" 
                 BorderThickness="2" 
                 Item="{Binding Path=ModelItem.Body, Mode=TwoWay}"/>
        <Rectangle Width="80" Height="6" Fill="Black" Margin="10"/>
    </StackPanel>
</sap:ActivityDesigner>

Not a whole lot of magic here yet.  What we want to do is add another WorkflowItemPresenter, but what do I bind it to? Well, let’s look at how ActivityDelegate is defined [the root class for ActivityAction and ActivityFunc (which I’ll get to in my next post).:

image

hmmm, Handler is an Activity, that looks kind of useful.    Let’s try that:

[warning, this XAML won’t work, you will get an exception, this is by design :-) ]

 <sap:ActivityDesigner x:Class="actionDesigners.ActivityDesigner1"
    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"
    xmlns:sapv="clr-namespace:System.Activities.Presentation.View;assembly=System.Activities.Presentation">
    <StackPanel>
        <sap:WorkflowItemPresenter HintText="Drop the body here" BorderBrush="Black" BorderThickness="2" Item="{Binding Path=ModelItem.Body, Mode=TwoWay}"/>
        <Rectangle Width="80" Height="6" Fill="Black" Margin="10"/>
<!-- this next line will not work like you think it might --> 
        <sap:WorkflowItemPresenter HintText="Drop the completion here" BorderBrush="Black" BorderThickness="2" Item="{Binding Path=ModelItem.OnCompletion.Handler, Mode=TwoWay}"/>

    </StackPanel>
</sap:ActivityDesigner>

While this gives us what we want visually, there is a problem with the second WorkflowItemPresenter (just try dropping something on it):

image

Now, if you look at the XAML after dropping, the activity you dropped is not present.  What’s happened here:

  • The OnCompletion property is null, so binding to OnCompletion.Handler will fail
  • We (and WPF) are generally very forgiving of binding errors, so things appear to have succeeded. 
  • The instance was created fine, the ModelItem was created fine, and the it was put in the right place in the ModelItem tree, but there is no link in the underlying object graph, basically, the activity that you dropped is not connected
  • Thus, on serialization, there is no reference to the new activity in the actual object, and so it does not get serialized.

How can we fix this?

Well, we need to patch things up in the designer, so we will need to write a little bit of code, using the OnModelItemChanged event.  This code is pretty simple, it just means that if something is assigned to ModelItem, if the value of “OnCompletion” is null, initialize it.  If it is already set, we don’t need to do anything (for instance, if you used an IActivityTemplateFactory to initialize).  One important thing here (putting on the bold glasses) YOU MUST GIVE THE DELEGATEINARGUMENT A NAME.  VB expressions require a named token to reference, so, please put a name in there (or bind it, more on that below).

 using System;
using System.Activities;

namespace actionDesigners
{
    // Interaction logic for ActivityDesigner1.xaml
    public partial class ActivityDesigner1
    {
        public ActivityDesigner1()
        {
            InitializeComponent();
        }

        protected override void OnModelItemChanged(object newItem)
        {
            if (this.ModelItem.Properties["OnCompletion"].Value == null)
            {
                this.ModelItem.Properties["OnCompletion"].SetValue(
                    new ActivityAction<TimeSpan>
                    {
                        Argument = new DelegateInArgument<TimeSpan>
                        {
                            Name = "duration"
                        }
                    });
            }
            base.OnModelItemChanged(newItem);

        }
    }
}

Well, this works :-)  Note that you can see the duration DelegateInArgument that was added.

image

Now, you might say something like the following “Gosh, I’d really like to not give it a name and have someone type that in” (this is what we do in our ForEach designer, for instance).  In that case, you would need to create a text box bound to OnCompletion.Argument.Name, which is left as an exercise for the reader.

Alright, now you can get out there and build activities with ActivityActions, and have design time support for them!

One question brought up in the comments on the last post was “what if I want to not let everyone see this” which is sort of the “I want an expert mode” view.  You have two options.  Either build two different designers and have the right one associated via metadata (useful in rehosting), or you could build one activity designer that switches between basic and expert mode and only surfaces these in expert mode.