Custom ‘Activity Sequences’ With Private Implementation (Part 1)
[Here’s a post covering some custom activity scenarios that should be pretty easy when you know how, but the problem is learning how. Including scheduling, variables, and implementation children.]
Some popular things to do when writing custom activities seem to be
1) creating a group of activities which should execute together in a fixed pattern
2) creating ‘customized built-in’ activities, for example wrapping a messaging activity, or preconfiguring sendReply and receive to point to each other
3) creating customized sequence wrapper which should do some initial processing and ‘inject’ some specific data to be available to its contained children. In theory it is a lot like having a variable inside the sequence, but in practical terms it should have more restricted behavior. The variable is initialized in a predetermined way, and the variable cannot be deleted via the workflow ‘Variables’ control.
Here is a sample scenario which touches on all of the above. Building a less-editable, more-compact, nearly-as-flexible Receive-Request-Send-Reply pattern.
Idea 1. Use an IActivityTemplateFactory
When we use an IActivityTemplateFactory in the toolbox, then dragging the one item from the toolbox creates the desired group of activities. What you get in designer is exactly like what you would get from a user creating that pattern of activities manually. A user editing the workflow can still delete activities, variables, etc.
On the other hand, sometimes you might be pretty sure that giving so much flexibility to your customer is a bad idea. Here are some example constraints which we could tack on to the simple Send/ReceiveReply pattern:
- you should never be able to do SendReply before the corresponding Receive.
- you should never have a SendReply correspond to the wrong Receive activity (somewhere else in your workflow) or being disconnected from the Receive above it. (Copy/paste sometimes does weird things like this.)
- you should never be able to delete the correlation handle involved in the request response pattern.
- when implementing a well-known service contract, we may wish to make some details of the contract hardcoded, instead of having them user-editable.
Also notice that it’s not really important whether you can insert activities in the Sequence shown before the Receive or after the SendReply. If you want to do this you can just take the whole sequence, and wrap it up inside another sequence.
So, how could we go about this in practice?
Idea 2. Make it a Custom Composite Activity, instead
In this idea we take the above group of activities, put it in its own XAML file (HandleRequest.xaml), and compile it into a new activity class.
Our project looks like the above, in our toolbox we get the below,
and upon dragging it into our workflow, we get an opaque blob.
I think we have definitely constrained the customer from being able to edit the workflow enough to do anything interesting! But I think we have constrained them a little too much, because now they have no way to insert custom logic. Which they would need either in order to do work, or to generate the response data of the request.
Idea 3. Make it a Native Activity, instead
First, let’s sketch of what we want to do. We want to expose a customizable ‘Body‘ on HandleRequest, where the customer can add their own logic, which executes in between the Receive, and the SendReply. We guess we could make that with a property Body like this:
public sealed class NativeHandleRequest : NativeActivity { public Activity Body { get; set; }
protected override void Execute(NativeActivityContext context) { } } |
And once we have that sketch of an activity, we can also quickly generate an ActivityDesigner to apply to the activity, and allow that customization via drag and drop. Here’s an ugly prototype UI:
Where we really can drag in some custom handler logic:
Notice about this solution:
- We don’t actually see the Receive and the SendReply activities exposed on the design surface any more. We are hiding the implementation detail of our NativeHandleRequest. This stops the customer editing implementation detail that we don’t think they should.
- We don’t actually have any implementation detail there in code yet. Nothing works yet. Oops. We’ll get there.
- The request data isn’t actually exposed to our request handler in any way yet. This is probably important to fix!
First let’s address a couple subproblems:
- The custom RequestHandler logic never gets scheduled, since our Execute() method doesn’t schedule anything at all.
- We have no Receive and SendReply activities, and of course they never get scheduled either.
Idea 4: Reimplement Sequence Scheduling
Note: this may turn out to be a bad idea, but it’s still educational. First, this is how NOT to do it (WRONG).
[Designer(typeof(HandleRequestDesigner))] public sealed class NativeHandleRequest : NativeActivity { public Activity Body { get; set; }
private Receive Receive { get; set; }
private SendReply SendReply { get; set; }
protected override void Execute(NativeActivityContext context) { context.ScheduleActivity(Receive); context.ScheduleActivity(Body); context.ScheduleActivity(SendReply); } } |
Why is this wrong?
- We didn’t override CacheMetadata(). And the default implementation won’t pick up our private Receive and SendReply details. Minor detail.
- It schedules the activities in parallel. Which basically means you don’t know what order they will execute in. This is a problem. We need ordered execution.
- Receive and SendReply are neither editable through designer, nor are they initialized here, so they are always going to be null. And when we schedule them, we will get exceptions.
On the other hand, what is probably right?
- Because they are private properties Receive and SendReply will never serialize to XAML. This seems good. If customers could edit those activities in the XAML, it wouldn’t matter that the UI hides them.
How do we get Receive and SendReply to be not null? Initializing them in the default constructor is fine.
How could we get the activities scheduled sequentially instead of in parallel? We would need to use a different overload of ScheduleActivity, like this:
protected override void Execute(NativeActivityContext context) { context.ScheduleActivity(Receive, (CompletionCallback)OnReceiveComplete); }
private void OnReceiveComplete(NativeActivityContext context, ActivityInstance completedInstance) { context.ScheduleActivity(Body, (CompletionCallback)OnBodyComplete); } |
And this works, and gives us infinite scheduling flexibility if we wanted it, but feels a little labor-intensive somehow. It turns out there is another, more compositional way we could do this. Which we will explore in a later part.
Next time, we will take up with the answer to two questions.
1) What does the constructor look like?
2) What exactly does the CacheMetadata() implementation look like?
And see that the answers are tied together.
Comments
Anonymous
February 07, 2011
Hi Tim, I'm new to workflow (like a week ago). Please accept my apologies If I'm sounding bit silly. I've a native activity which has one activityfunc for oncompletion and ActivityAction for onFault. I've tested the code programatically and it works perfectly fine. My next step is to write activitydesigner for this which I've done to some extent. I however am struggling with attaching the above call backs to the designer. Could you please shed some light on this? Would be highly appreciated. Below's the snippet from my code. [Designer(typeof(ProcessDisplayEntityDesigner))] //[ContentProperty("Parameters")] public sealed class ProcessDisplayEntity : NativeActivity<WorkflowResponse> { [Browsable(false)] public ActivityFunc<DisplayEntityData, bool> processEntityData { get; set; } [Browsable(false)] public ActivityAction<Exception> OnFault { get; set; } } In the designer I've something like this. (Trying to attach handler to processentityData Handler) <sap:WorkflowItemPresenter x:Uid="sap:WorkflowItemPresenter_1" Margin="0,10,0,10" HintText="Drop Activities Here" AllowedItemType="{x:Type Activities:Activity}" Item="{Binding Path=ModelItem.processEntityData.Handler, Mode=TwoWay}" Grid.Row="1" Grid.Column="1" /> this is currently not working for me and haven't got much clue. Can you please help me with this? Kind regards, KavyaAnonymous
March 04, 2011
Belated answer: looks like you are running into 'The trouble with System.Activities.ForEach' - and other activities which use ActivityAction and ActivityFunc, which is that your action/func itself is initially null. Tim