Udostępnij za pośrednictwem


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.

Comments

  • Anonymous
    January 15, 2010
    Nice one, thanks.  WF4 is looking pretty good so far.  Posts like this are definitely helping fill the documentation holes for us early adopters.  BTW, this seems kind of hacky; I like to use an activity factory to set up my custom activities rather than sniff that event in the codebehind (*shivers at the word "codebehind".

  • Anonymous
    January 15, 2010
    @WillSullivan, In part, I agree that anytime we have to drop to the code behind leaves something to be desired.  In looking at things, the general "initialization" story that we have could use some improvement in subsequent releases (and we'll get there), I seem to see a lot more people who have the need for ActivityTemplateFactory for things like dropping templated items into the workflow.

  • Anonymous
    January 15, 2010
    Another thing that we could have done (and that someone else could do) is likely to write a value converter that attempts to fill in any nulls in the underlying instance.  This would be tough to handle the general case, but you could probably make it so that it could nicely handle some of these scenarios.  

  • Anonymous
    January 15, 2010
    Thanks for this post Matt.  Just a FYI that Tim has a similar post on the problem with the designer for ActivityAction (http://blogs.msdn.com/tilovell/archive/2009/12/29/the-trouble-with-system-activities-foreach-and-parallelforeach.aspx). Kushal has a couple of blog posts which seem to adddress the details of how to switch designers to support the "export mode" view (starting with http://blogs.msdn.com/kushals/archive/2010/01/14/dev-ba-collaboration-part-1.aspx) You may want to point other readers to these posts.  I just recently discovered Tim's blog posts (though a forum post), so maybe you can advertise his blog to others as well?  I know an earlier blog post of yours mentions Kushal and Cathy, but it's nice that Tim has joined the group of bloggers - the more information the better! Thanks!