다음을 통해 공유


Spawned Contexts - Replicator, While, State, EventHandlers, and CAG

Ever wonder why this.delayActivity1.TimeoutDuration sometimes doesn't change the timeout duration? How come this.callExternalMethodActivity1.ParameterBindings["(ReturnValue)"] isn't giving you the value you expect in some scenarios? How is it possible that sometimes this.GetActivityByName("foo") does not equal my sender for one of foo's events? 

The answer to all of these questions is: spawned contexts.

One of the most powerful and most easily misunderstood concepts in Windows Workflow Foundation (WF) is that of new contexts executing cloned activities. Before we even define a spawned context, let's go back to the beginning ...

The Beginning

One of the binding qualities of the activity state transition diagram is that there are no transitions from Closed back to Executing. You can only get to Executing through an Initialized activity and you can only get to Initialized from a brand new instance. 

How do we handle looping activities then? Replicator, While, and CAG all give the impression of executing the same activity (or set of activities) multiple times. The answer is by creating a new context and cloning the template activity (explained later).

ActivityExecutionContext Interlude

First, let's fully understand the ActivityExecutionContext. This object is passed to all scheduled calls either as a specific parameter (Execute, Cancel, HandleFault) or as the sender (QueueItemAvailable handler, StatusChanged handler). The ActivityExecutionContext provides the activity writer with an interface for controling activity execution (hence the first two words in the name) while giving the runtime enough control to enforce the rules of the WF engine.

But what about the "Context" part of the name? A context in WF is a sphere of execution. There is a root activity for each context and only activities which exist in that context can be executed in that context. In short, a context is a mechanism used by the runtime to determine on which set of activities to enforce rules.

One important note is that the ActivityExecutionContext is merely a short-lived expression of the underlying context - every scheduled call to an activity method gets a new instance of the ActivityExecutionContext object which has been configured specifically for that activity. You'll notice, however, that the Guid associated with the context does not change.

Cloning Activities

Whenever a single activity needs to be executed multiple times it must be cloned. Contexts are the mechanism provided to the activity writer for making this happen. The code looks like this:

ActivityExecutionContext childContext = currentContext.ExecutionContextManager.CreateExecutionContext(childActivity);

This code will cause a new context, childContext, to be created with a root activity which is a clone of childActivity. Note that this is a deep cloning so if childActivity is a composite activity then its entire tree is cloned as well. Consider that we have a custom activity called WorkflowRoot which clones its only child activity using the above code. Visually, we now have the following tree of contexts:

RootContext
| WorkflowRoot (1)
| childActivity (1)
| grandChildActivity (1)
- childContext
childActivity (2)
grandChildActivity (2)

What?

Looking at the above diagram there are several questions which come up. Let's try to deal with a couple of easy ones first:

  • Are changes to childActivity(1) or childActivity(2) reflected in the other instance?
    No. Once cloned these instances have no connection. Changes to the template will affect future clones and changes to the clone will affect its own execution, but changes to one will not affect the other.
  • What is the return value of childActivity(2).Parent?
    WorkflowRoot(1). The activities inside a new context do not know that they are not part of the rest of the tree. The Parent property of the context's root activity still points to the original parent. It is only when walking down the tree that the context's are noticeable. For example, WorkflowRoot(1).Activities[0] will always return childActivity(1) and never childActivity(2). Said another way, childActivity(2).Parent.Activities[0] == childActivity(1). This is strange at first glance, but this soon becomes natural.

Scenarios

While Loop with Delay

Consider the following workflow:

WhileActivity
DelayActivity

Not very useful, I'll admit, but it is handy for this demonstration. Now, if we have implemented this as a code only workflow, we'll probably have some field defined on our root called delayActivity1. Let's say that we want to change the delay amount each time through the loop, so we subscribe to the InitializeTimeoutDuration event with the following code:

// WRONG CODE
this.delayActivity1.TimeoutDuration = TimeSpan.FromSeconds(iterationCount);

Assuming iterationCount is a variable that is incremented each time through the loop, we expect to see: delay 1 second, delay 2 seconds, delay 3 seconds, etc. This, however, is not what we see. Instead we get: delay 0 seconds, delay 1 seconds, delay 2 seconds, etc. 

The reason is that the WhileActivity is spawning a new context when it executes the child. So, each iteration looks like this:

RootContext
| WhileActivity(1)
| Delay(1)
- childContext
Delay(1 + iterationCount)

this.delayActivity1 ALWAYS refers to Delay(1) and therefore we are updating the template every time InitializeTimeoutDuration is called. That means we are always one timeout amount behind ... Delay(2) is about to execute with TimeoutDuration set to 0 seconds and we update the template to 1 seconds. Delay(3) is created with a 1 second timeout because it is just a clone of the template at that point in time.

Some new code for InitializeTimeoutDuration:

// RIGHT CODE
((DelayActivity)sender).TimeoutDuration = TimeSpan.FromSeconds(iterationCount);

This time we will see the following: delay 1 seconds, delay 2 seconds, delay 3 seconds, etc. Here we have updated the cloned value instead of the template. Note that for ALL events subscribed to in code beside the sender will be the actual instance of the activity which is currently running. 

Therefore, the sender above will always be the right one even if the delay is not in a context spawning activity. If you want to avoid issues, learn to access activity properties in a context safe way (like using the sender objects) so that you do it right when it counts. If the delay weren't in a context spawning activity then the WRONG CODE and the RIGHT CODE would be equivalent, but if the delay is in a context spawning activity then the WRONG CODE will never work.

Replicator and GetActivityByName

Replicator
Sequence
CallExternalMethodActivity
HandleExternalEventActivity

The above workflow is a common pattern for replicated user tasks. The CallExternalMethodActivity notifies the user of the task and the HandleExternalEventActivity gets an event when the task is complete. Let's say that we're going to assign 3 tasks for UserA, UserB, and UserC so our replicator will initailize itself with the collection {"UserA", "UserB", "UserC"}. Assuming that the user name is the correlation parameter, our ChildInitialized handler might look like:

// WRONG CODE
CallExternalMethodActiivty act = this.GetActivityByName("createTask1") as CallExternalMethodActivity;
act.ParameterBindings["userName"].Value = e.InstanceData;

The code above will not work as expected. Let's look at why by examining the contexts created:

RootContext
| Replicator (1)
| Sequence(1)
| CallExternalMethodActivity(1)
| HandleExternalEventActivity(1)
- childContext1 (e.InstanceData = "UserA")
| Sequence(2)
| CallExternalMethodActivity(2)
| HandleExternalEventActivity(2)
- childContext2 (e.InstanceData = "UserB")
| Sequence(3)
| CallExternalMethodActivity(3)
| HandleExternalEventActivity(3)
- childContext3 (e.InstanceData = "UserC")
Sequence(4)
CallExternalMethodActivity(4)
HandleExternalEventActivity(4)

"this" in our code snippet refers to the root workflow which exists in the RootContext. When we call GetActivityByName and pass the CallExternalMethodActivity's name we will get the instance that is in the root context - CallExternalMethodActivity(1). What we want is the one in the current context so the code should look like:

// RIGHT CODE
CallExternalMethodActiivty act = e.Activity.GetActivityByName("createTask1", true) as CallExternalMethodActivity;
act.ParameterBindings["userName"].Value = e.InstanceData;

Note the two changes - first we use e.Activity instead of this. e.Activity is the clone of the replicator's template (Sequence(2-4)). Second, we have passed the parameter true to GetActivityByName. This tells the method to look only in the context of the activity on which it was called. This keeps the method from walking into other parts of the tree and returning the RootContext instance.

Conclusion

Hopefully this post eases some confusion around contexts and doesn't make it worse. Please post comments if you want clarifications on anything written above or if you want more information about one topic or another. I will write a separate entry at some point to discuss how to manage contexts you create in custom activities.

Comments

  • Anonymous
    May 04, 2006
    The comment has been removed
  • Anonymous
    May 19, 2006
    The comment has been removed
  • Anonymous
    August 17, 2006
    Brian Noyes' post "Understanding Windows Workflow and its complexities" has me thinking.
    I know a few...
  • Anonymous
    September 24, 2006
    The comment has been removed
  • Anonymous
    September 25, 2006
    The comment has been removed
  • Anonymous
    June 26, 2007
    The comment has been removed
  • Anonymous
    June 28, 2007
    The comment has been removed
  • Anonymous
    July 03, 2007
    The comment has been removed
  • Anonymous
    July 29, 2007
    PingBack from http://www.kcdholdings.com/blog/?p=74
  • Anonymous
    September 04, 2007
    Hello,how can i access the right activity from within a declarative condition? And how inside the Excecute-Event of an activity.I would like to make the conditions react directly on activity properties, because my workflow ist just a xml definition and its base class should only have a really limited set of properties.It would be awesome if you still watch the comments on this blog.Tanks,Lars (originally from Germany, but now in Mexico)
  • Anonymous
    November 06, 2007
    Hello Nate,I was wondering if you've got any pointers on how to use the replicator activity in a state machine workflow. I've tried using the WssTaskActivity but that keeps throw some exceptions about the number of activities contained that implement the IEventActivity.Any ideas will be welcomed.Regards,Raphael.
  • Anonymous
    November 06, 2007
    Lars, unfortunately I can't be much help for your scenario as I'm wholly unversed in declarative workflows and pretty unfamiliar with the syntax for our rules.  That said, the WF forums are a great place for these types of questions.  The forums are very active these days: http://forums.microsoft.com/MSDN/ShowForum.aspx?ForumID=122&SiteID=1Sorry I couldn't be more help directly ... and sorry about the late reply.
  • Anonymous
    November 06, 2007
    Raphael, the short version is that you cannot use replicator in a state machine in the manner that you are trying to use it.  State machine is very strict about being composed only of States.  Each state, in turn, either contains more states or events.  Events MUST start with IEventActivities and must not contain IEventActivities anywhere else in their flow.The benefit of these restrictions is that the state machine can guarantee that all events in a state are atomic (you will never get part of the event's work done while another event is running), it can guarantee mutual exclusion between the events in a state (only one will fire at a time), and it can guarantee that events are largely CPU bound.Replicator is designed to be used with sequential style workflows and allows for easy management of multiple, similar, tasks such as the WssTaskActivity.  If you want to do the same thing in StateMachine then you need to approach the problem from a completely different standpoint.  Instead of replicating the task that everyone should do, you need to view it as a state where you create all of the tasks, a state where you wait for all of the tasks to complete (or be updated, etc), and finally a state where you do your final set of work.
  • Anonymous
    November 07, 2007
    You say that we cannot use the Replicator in a state machine workflow.  Instead, you say we have to program in a completely different manner.  Please provide a link to an example to back up what you say.  I'll give $500 dollars to the first person who provides a real example of a state machine workflow that creates an unknown-until-run-time set of parallel tasks based upon InfoPath task forms, all of which have to finish before the state of the workflow can move forward to the next state.  I have a feeling that my money is pretty safe.
  • Anonymous
    November 07, 2007
    Frederick, I'm not terribly familiar with the WssTaskActivity (and the other Sharepoint activities) and I was definitely speaking from a theoretical point of view - when you want to replicate things which have some sort of receive in them inside of a state machine you have to separate the receive into its own event handler.For questions of how to make use of WssTaskActivity in scenarios where the list of users is dynamic I'll redirect you to the Sharepoint Workflow forums: http://forums.microsoft.com/MSDN/ShowForum.aspx?ForumID=1207&SiteID=1
  • Anonymous
    April 29, 2008
    We are about to start a great day showing technologies like .NET 3.5, Visual Studio 2008, and SharePoint
  • Anonymous
    July 13, 2008
    PingBack from http://sdesmedt.wordpress.com/2008/07/13/exploring-workflow-foundation-part-1-custom-activities/
  • Anonymous
    September 09, 2008
    PingBack from http://www.winterdom.com/weblog/2006/05/04/SpawnedContextsAndActivityBinding.aspx
  • Anonymous
    October 05, 2008
    PingBack from http://sdesmedt.wordpress.com/2008/10/05/exploring-workflow-foundation-part-4-custom-composite-activities/
  • Anonymous
    December 16, 2008
    PingBack from http://www.baby-parenting.com/family/child_activity.html
  • Anonymous
    January 18, 2009
    Hi,We are developing an application using Windows workflow and InformixV7.x version. I have got by batch service and Persistence servivce working perfectly. But we would like to have a feature where in if a workflow instance and the application objects associated with that workflow is in process by a user then the other users should not be able to modify it.We are using ASP.NET 2.0 for implementing this application and the Runtime is created as an application level object and the persistence service is created and initialized at application on start itself.Also we would like to have the locking based on Application IDs rather than arbitrary GUID.What do you suggest ?1) Do some modification (if so please tell us the modification) to persistence service and get this done2) Handle the application as part of application objects retrieval and persistence.Please help us in this regard
  • Anonymous
    January 20, 2009
    heman, that type of integration scenario is a bit out of scope of this blog.  I'd suggest you post the same information on the MSDN WF forum (http://social.msdn.microsoft.com/forums/en-US/windowsworkflowfoundation/threads/).  These forums are actively monitored by the product team and should be the quickest route to a solution.
  • Anonymous
    June 08, 2009
    PingBack from http://insomniacuresite.info/story.php?id=4108
  • Anonymous
    June 09, 2009
    PingBack from http://cellulitecreamsite.info/story.php?id=398
  • Anonymous
    June 09, 2009
    PingBack from http://quickdietsite.info/story.php?id=751
  • Anonymous
    June 19, 2009
    PingBack from http://debtsolutionsnow.info/story.php?id=3912
  • Anonymous
    April 14, 2010
    That was an awesome explanation - thanks!I'm impressed you were doing this 4 years ago!