Custom Flowchart Runtime Thoughts (for ICompositeView)
Disclaimer: no WF 4.0 runtime experts were consulted for advice in the thinking of this. YMMV.
Things have been busy lately and though I have had a couple requests to continue the ICompositeView series (see earlier posts), I haven’t had much free time. Compounding the free time scarcity, decisions seem needed. Do people want to clone flowchart? Do they want to do something new? Pageflow? Circuit diagrams? State machines?
One of the bets feature requests around flowchart so far is the ability to define custom flowswitches and custom flowdecisions. Basically to be able to create a custom activity that can by its own logic set which activity will run next.
Runtime Constraints
How does an Activity decide which Activity runs next? Normally it would call context.ScheduleActivity(). But in a flowchart will that work? When I thought about this it didn’t seem like that it would work. An activity can only schedule its own children, and no two activities can share a child. (Activity families are single-parent families.)
So that’s a limitation. How can we work around that? One idea is that we can cheat. :-)
Just because you (some object/Activity) are not the parent Activity does not mean you cannot call context.ScheduleActivity(child). The only rule you really need to follow is, the context object must be the context of that Activity’s parent.
We could use this idea to create our own pseudo-Activity class which can schedule Activities in its parent context:
class CustomFlowNodeBase
{
virtual void ExecuteInParentContext(NativeActivityContext parentContext)
{
//in subclass we would e.g. parentContext.ScheduleActivity<int>(someFunc, funcCallback); and in callback do further scheduling as desired…
}
//and something like
virtual List<Activity> GetChildActivities(); //for parent.CacheMetadata to call to know what Children this guy owns that need caching
virtual List<Activity> GetChildActivityActions(); //and similar
//etc.
}
It would be nicest if we can link from a custom flow node to another custom flow node, not just linking from custom flow nodes to activities. But there are some ugly parts to that code sketch: CustomFlowNodeBase isn’t an Activity. This means
- you can’t call parentContext.ScheduleActivity(CustomFlowNodeBase)
- you can’t store CustomFlowNodeBase and Activity in the same Collection<T> (unless you throw away type safety and do type casting using e.g. Collection<Object>)
Some alternatives to that idea:
- Create a union type? (an ‘is either A or B’ type). Without any special support for this in C#, this could mean wrapping CustomFlowNodeBases and Activities in something else before inserting them in the flow diagram – or lots of special casing logic.
- Inherit Activity anyway, but never get schedule as a real activity. This is a gross idea. The gross part is normally you look at a piece of code that says it subclasses Activity or NativeActivity and you correctly assume that code is meant to get executed as an activity by the workflow runtime. Why would we suddenly start breaking that assumption?
- Wrap Activity in CustomFlowNodeBase. This isn’t that different to the first idea with the wrapper implementation
Diversion – One Way for Activities to share children (multi-parent families)
(The day I was writing this, I got a fortune cookie ‘You will learn something new today’.)
While thinking about this scenario I imagined many flow nodes inside of a flow chart diagram. They seemed, somehow like they were just pieces of the flowchart itself. Pieces of its implementation.
What I thought I knew was that no two activities could share a child. But I realized I never had actually tried sharing a public child between two implementation children before. Since the implementation of the activity is invisible to the outside world, to the outside world it should look like there is a single parent-child relationship.
Does the runtime allow this? I had to try this out:
public sealed class Ac1 : NativeActivity
{
private Ac2 Ac2;
private Ac2 Ac3;
private Sequence Seq;
public Ac1 ()
{
Seq = new Sequence { Activities = { new WriteLine() { Text = new Literal<String>("Hello World") } } };
Ac2 = new Ac2(Seq);
Ac3 = new Ac2(Seq);
}
protected override void Execute(NativeActivityContext context)
{
context.ScheduleActivity(Ac2);
context.ScheduleActivity(Ac3);
}
protected override void CacheMetadata(NativeActivityMetadata metadata)
{
metadata.AddChild(Seq);
metadata.AddImplementationChild(Ac2);
metadata.AddImplementationChild(Ac3);
}
}
public sealed class Ac2 : NativeActivity
{
private Sequence Seq;
public Ac2(Sequence Seq)
{
this.Seq = Seq;
}
protected override void Execute(NativeActivityContext context)
{
context.ScheduleActivity(Seq);
}
protected override void CacheMetadata(NativeActivityMetadata metadata)
{
metadata.AddChild(Seq);
}
}
class Program
{
static void Main(string[] args)
{
WorkflowInvoker.Invoke(new Ac1());
}
}
What do you know! It passes validation and runs without error. “Hello World\r\nHello World\r\n”.
Can we apply it in our scenario? If flow nodes were implementation children of our flow diagram, then they could share a public child (of the flowchart), and two different flow nodes could schedule the same guy. But can they schedule each other?
The answer here appears to be No. Implementation children cannot schedule their own implementation siblings. So using implementation children doesn’t give us any better solution for free despite my hopes.
A Design that works at DesignTime too?
There is one more reason we might want our custom flow node to be a wrapper class that does not subclass Activity. By defining
public class CustomFlowNode : Activity {…}
we will have created a CustomFlowNode class that can be drag+dropped inside of a Sequence (because Sequence will accept anything of type Activity). But that doesn’t make sense.
A wrapper class is seeming like the best idea so far.
One more constraint – Runtime behavior
There is one more way that it would be good for Activity and Custom Flow Node to have consistent behavior. ScheduleForExecution(Activity) needs to work the same as ScheduleForExecution(CustomFlowNode). That is, it should place a work item on the WF scheduler in the same way regardless of the thing being scheduled.
For Activity that could be easy – the inner implementation could call through to NativeContext.ScheduleActivity().
For Custom Flow Node it first seems like the solution might be to wrap Custom Flow Node execution inside the execution of another activity, so that there is a real work item. I like the idea of writing a specialized ActivityContext just for use by custom flow nodes – it could look like ActivityContext but also know how to schedule FlowNodes wrapping or not wrapping activities - and basically relaxes all those restrictions about scheduling ‘sibling’ nodes.
New Sketch:
public class CustomFlowchartContext // : ActivityContext??
{
delegate void CompletionCallback(…);
void ScheduleNode(CustomFlowchartNode node);
void ScheduleActivity(Activity activity, CompletionCallback onCompletion);
}
public class CustomFlowchartNode // definitely not inherit Activity
{
abstract void Execute(CustomFlowchartContext context);
}
public class ActivityFlowchartNode
{
void Execute(CustomFlowchartContext context)
{
context.ScheduleActivity(activity);
}
}
Well, all of the above seemed like a fine idea, and I burned through implementation, then tried to run the thing. After the first few bugs were ironed out I hit a real design issue:
Creating an ActivityContext wrapper is hard!
Here’s the basic problem:
I wanted to do composition rather than inheritance, so I created the class CustomFlowchartContext, does not inherit ActivityContext or NativeActivityContext. In order to schedule Activity, and Activity<T> I needed to provide a bunch of functions like this:
public void ScheduleActivity(Activity a, CompletionCallback completionCallback)
{
_flowchartContext.ScheduleActivity(a,
(NativeActivityContext context, ActivityInstance instance) =>
{
completionCallback(new CustomFlowchartContext(_flowchart, context), instance);
});
}
where, to be consistent, the CompletionCallback should receive a CustomFlowchartContext, in order that you can schedule a CustomFlowNode inside of your completion callback.
When I ran the above I got an exception:
'System.Activities.CompletionCallback' is not a valid activity execution callback. The execution callback used by '1.1: NodeExecutor' must be an instance method on '1.1: NodeExecutor'.
Parameter name: onCompleted
Arg! Why does this happen? It happens because of the demon of persistance. The workflow might be persisted and unloaded while the activity we scheduled is running. The workflow infrastructure copes with this by creating a Bookmark, which has to be a serializable callback. This works in practice by saving the class/method details without any object instance data, i.e. you can’t bookmark a delegate, it has to be a static method - or plain instance method on the currently active activity. That is the only way the runtime rehydrates the workflow. Interesting. Yes, the last half of this blog is useless, sigh.