Swiss Cheese and WF4, or, An Introduction to ActivityAction
One common scenario that was often requested by customers of WF 3 was the ability to have templated or “grey box” or “activities with holes” in them (hence the Swiss cheese photo above). In WF4 we’ve done this in a way that way we call ActivityAction
Motivation
First I’d like to do a little bit more to motivate the scenario.
Consider an activity that you have created for your ERP system called CheckInventory. You’ve gone ahead and encapsulated all of the logic of your inventory system, maybe you have some different paths of logic, maybe you have interactions with some third party systems, but you want your customers to use this activity in their workflows when they need to get the level of inventory for an item.
Consider more generally an activity where you have a bunch of work you want to get done, but at various, and specific, points throughout that work, you want to allow the consumer of that activity to receive a callback and provide their own logic to handle that. The mental model here is one of delegates.
Finally, consider providing the ability for a user to specify the work that they want to have happen, but also make sure that you can strongly type the data that is passed to it. In the first case above, you want to make sure that the Item in question is passed to the action that the consumer supplies.
In wf3, we had a lot of folks want to be able to do something like this. It’s a very natural extension to wanting to model things as activities and composing into higher level activities. We like being able to string together 10 items as a black box for reuse, but we really want the user to specify exactly what should happen between steps 7 and 8.
A slide that I showed at PDC showed it this way (the Approval and Payment boxes represent the places I want a consumer to supply additional logic):
Introducing ActivityAction
Very early on the release, we knew this was one of the problems that we needed to tackle. The mental model that we are most aligned with is that of a delegate/ callback in C#. If you think about a delegate, what are you doing, you are giving an object the implementation of some bit of logic that the object will subsequently call. That’s the same thing that’s going on with an ActivityAction. there are three important parts to an ActivityAction
- The Handler (this is the logic of the ActivityAction)
- The Shape (this determines the data that will be passed to the handler)
- The way that we invoke it from our activity
Let’s start with some simple code (this is from a demo that I showed in my PDC talk). This is a timer activity which allows us to time the execution for the contained activity and then uses an activity action to act on the result.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;
using System.Diagnostics;
namespace CustomActivities.ActivityTypes
{
public sealed class Timer : NativeActivity<TimeSpan>
{
public Activity Body { get; set; }
public Variable<Stopwatch> Stopwatch { get; set; }
public ActivityAction<TimeSpan> OnCompletion { get; set; }
public Timer()
{
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);
}
}
}
}
A few things to note about this code sample:
The declaration of an ActivityAction<TimeSpan> as a member of the activity. You’ll note we use the OnXXX convention often for activity actions.
The usage of the ActivityAction<T> with on type argument. The way to read this, or any of the 15 other types is that the T is the type of the data that will be passed to the activity action’s handler.
- Think about this like an Activity<Foo> corresponding to a void DoSomething(Foo argument1) method
The call to NativeActivityMetadata.AddDelegate() which lets the runtime know that it needs to worry about the delegate
The code in the Completed( ) method which checks to see if OnCompletion is set and then schedules it using ScheduleAction. I want to call out that line of code.
if (OnCompletion != null)
{
context.ScheduleAction<TimeSpan>(OnCompletion, Result.Get(context));
}
It is important to note that I use the second parameter (and the third through 16th if that version is provided) in order to provide the data. This way, the activity determines what data will be passed to the handler, allowing the activity to determine what data is visible where. This is a much better way than allowing an invoked child to access any and all data from the parent. This lets us be very specific about what data goes to the ActivityAction. Also, you could make it so that OnCompletion must be provided, that is, the only way to use the activity is to supply an implementation. If you have something like “ProcessPayment” you likely want that to be a required thing. You can use the CacheMetadata method in order to check and validate this.
Now, let’s look at the code required to consume this time activity:
DelegateInArgument<TimeSpan> time = new DelegateInArgument<TimeSpan>();
a = new Timer
{
Body = new HttpGet { Url = "https://www.microsoft.com" },
OnCompletion = new ActivityAction<TimeSpan>
{
Argument = time,
Handler = new WriteLine {
Text = new InArgument<string>(
ctx =>
"Time input from timer " + time.Get(ctx).TotalMilliseconds)
}
}
};
There are a couple of interesting things here:
- Creation of DelegateInArgument<TimeSpan> : This is used to represent the data passed by the ActivityAction to the handler
- Creation of the ActivityAction to pass in. You’ll note that the Argument property is set to the DelegateInArgument, which we can then use in the handler
- The Handler is the “implementation” that we want to invoke. here’s it’s pretty simple, it’s a WriteLine and when we construct the argument we construct if from a lambda that uses the passed in context to resolve the DelegateInArgument when that executes.
At runtime, when we get to the point in the execution of the Timer activity, the WriteLine that the hosting app provided will be scheduled when the ScheduleAction is called. This means we will output the timing information that the Timer observed. A different implementation could have an IfThen activity and use that to determine if an SLA was enforced or not, and if not, send a nasty email to the WF author. The possibilities are endless, and they open up scenarios for you to provide specific extension points for your activities.
That wraps up a very brief tour of ActivityAction. ActivityAction provides a easy way to create an empty place in activity that the consumer can use to supply the logic that they want executed. In the second part of this post, we’ll dive into how to create a designer for one of these, how to represent this in XAML, and a few other interesting topics.
It’s that time of year that I’ll be taking a little bit of time off for the holidays, so I will see y’all in 2010!
Photo Credit
https://www.flickr.com/photos/myklroventine/ / CC BY 2.0
Comments
Anonymous
December 27, 2009
Again, nice one. We who are currently using WF4 appreciate the blog posts!Anonymous
January 11, 2010
This is awesome - fits our current requirements exactly. Keep it coming (i.e. "In the second part of this post, we’ll dive into how to create a designer for one of these..."). thanks a million!Anonymous
January 12, 2010
The specific scenario we are looking into is to allow client developers or power-users the ability to setup the "holes" in the workflows we ship with the product. So from your example above I would like to ship the entire workflow, but when using the WorkflowDesigner (either hosted or VS) the user would only be allowed to drop activities into the "holes" - basically configuring the ActivityActions. So they can't delete the shipped activities, or change order, etc. Is this possible - to effectively disable edit on "our" stuff, but allow for the assignment of an activity to an ActivityAction handler? (I realize that it's all XML and so they could really do whatever they want... but I'm mainly concerned with their experience using the designer)Anonymous
January 14, 2010
I echo the others appreciation for this and your other blog posts. One comment you made in this post: "Also, you could make it so that OnCompletion must be provided, that is, the only way to use the activity is to supply an implementation. If you have something like “ProcessPayment” you likely want that to be a required thing. You can use the CacheMetadata method in order to check and validate this" The use case makes sense, I'm just not clear on how to used the CacheMetadata method to do this. Can you please elaborate or point to another post if you've covered this already somewhere? Thank you!Anonymous
January 14, 2010
@Jamie, Yes you could easily do that by providing two different activity designers and associate the correct one via the metadata store, or make your designer clever enough to detect something about which "face" it should display and render that. I have a post up and coming on building the designer. mattAnonymous
January 14, 2010
The comment has been removedAnonymous
January 14, 2010
The comment has been removedAnonymous
January 14, 2010
The comment has been removed