- Scheduling
- Activity Automaton
- Bookmarks Revisited
- Activity Initialization and Uninitialization
- Composite Activity Execution
- WF Threads
- Where are We?
Composite Activity Execution
Enough with WF programs that are only a single activity! It's time to develop some composite activities and then declare and run some more interesting WF programs.
We will begin with the Sequence activity of Chapter 2, shown again here:
public class Sequence : CompositeActivity { protected override ActivityExecutionStatus Execute( ActivityExecutionContext context) { if (this.EnabledActivities.Count == 0) return ActivityExecutionStatus.Closed; Activity child = this.EnabledActivities[0]; child.Closed += this.ContinueAt; context.ExecuteActivity(child); return ActivityExecutionStatus.Executing; } void ContinueAt(object sender, ActivityExecutionStatusChangedEventArgs e) { ActivityExecutionContext context = sender as ActivityExecutionContext; e.Activity.Closed -= this.ContinueAt; int index = this.EnabledActivities.IndexOf(e.Activity); if ((index + 1) == this.EnabledActivities.Count) context.CloseActivity(); else { Activity child = this.EnabledActivities[index + 1]; child.Closed += this.ContinueAt; context.ExecuteActivity(child); } }
The job of the Sequence activity is to emulate a C# {} statement block, and execute its child activities one by one. Only when the final child activity of a Sequence finishes can the Sequence report that it is complete.
The Execute method of Sequence first checks to see if there are any child activities at all. If none are present, the method returns ActivityExecutionStatus.Closed. The Sequence is done because it has nothing to do. It is like an empty C# statement block. If one or more child activities are present, though, the first child activity needs to be scheduled for execution. In order to do this, two lines of code are necessary:
child.Closed += ContinueAt; context.ExecuteActivity(child);
These two statements constitute a very simple bookmarking pattern that you will encounter repeatedly in composite activity implementations. The subscripton to the Closed event of the child activity sets up a bookmark that is managed internally by the WF runtime. The Activity.Closed event is merely syntactic sugar on top of the bookmark management infrastructure. The += results in the creation of a bookmark, and the dispatch of the Closed event (the resumption of the bookmark), is brokered via the scheduler.
The invocation of ActivityExecutionContext.ExecuteActivity requests that the indicated child activity be scheduled for execution. Specifically, the Execute method of the child activity is added as a work item in the scheduler work queue.
In order to enforce the activity automaton, the WF runtime will throw an exception from within ExecuteActivity if the child activity is not in the Initialized state. If the call to ExecuteActivity succeeds, an item is added to the scheduler work queue, representing the invocation of the child activity's Execute method. A successful call to ExecuteActivity also immediately places the child activity in the Executing state.
The Sequence activity's code that schedules the execution of its first child activity and subscribes for this child activity's Closed event is analogous to the ReadLine activity's logic that creates a WF program queue and subscribes to that queue's QueueItemAvailable event. In both cases, the activity is dependent upon some work, outside of its control, and can proceed no further until it is notified that this work has been completed. The code is somewhat different, but the bookmarking pattern is exactly the same.
Of course, for a composite activity like Sequence, the pattern must be repeated until all child activities have completed their execution. This is achieved in the ContinueAt method, which is scheduled for execution when the currently executing child activity moves to the Closed state. When it receives notification that a child activity has completed its execution, Sequence first removes its subscription for that child activity's Closed event. If the child activity that just completed is the last child activity in the Sequence, the Sequence reports its own completion. Otherwise, the bookmarking pattern is repeated for the next child activity.
There are a couple of crucial aspects to the WF runtime's role as the enforcer of state transitions. If the Sequence activity tries to report its completion while a child activity is in the Executing state, this transition will not be allowed. This fact is the cornerstone of the WF runtime's composition-related enforcement (and is not implied by the activity automaton).
The corollary to this rule is that only an activity's parent is allowed to request that activity's execution. A call to ActivityExecutionContext.ExecuteActivity by its parent is the only stimulus that will cause an activity to move to the Executing state.
These simple enforcements play a huge role in establishing the meaning and ensuring the integrity of composite activities and, by extension, WF programs.
Of course, there must be one exception to the rule that only the parent of an activity can schedule its execution, and that is for the root activity, whose Parent property is null. As we have already seen, it is the application hosting the WF runtime that makes a request to the WF runtime to schedule the execution of the root activity's Execute method.
Effectively, as part of the creation of a WF program instance, the WF runtime creates an implicit bookmark whose resumption point is the Execute method of the root activity. The invocation of WorkflowInstance.Start resumes this bookmark, and begins the execution of the program instance.
It will be instructive to trace the execution of a simple WF program that uses Sequence, noting the changes that occur at each step to the scheduler work queue. The XAML in Listing 3.15 is a Sequence with a set of WriteLine child activities.
Listing 3.15. A WF Program That Uses Sequence
<Sequence x:Name="s1" xmlns="http://EssentialWF/Activities" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <WriteLine x:Name="w1" Text="One" /> <WriteLine x:Name="w2" Text="Two" /> <WriteLine x:Name="w3" Text="Three" /> <WriteLine x:Name="w4" Text="Four" /> </Sequence>
Running an instance of this program will result in the expected output.
One Two Three Four
When the application hosting the WF runtime calls WorkflowInstance.Start, it is telling the WF runtime to resume the initial, implicit bookmark. The result of the call to Start is that the scheduler work queue for this instance contains a single item—a work item for the Execute method of the root activity.
The root activity—in our example, the Sequence—is now in the Executing state, even though its Execute method has not actually been called. Figure 3.10 shows the scheduler work queue, along with the state of the WF program instance (with Executing activities shown in boldface).
Figure 3.10 WF program instance after WorkflowInstance.Start
At this point, the WF runtime's dispatcher logic enters the picture, and invokes the Sequence activity's Execute method, removing the corresponding item from the scheduler work queue. From this point forward, it is the activities in the WF program that drive the program forward; the WF runtime plays a passive role as the scheduler of work and the enforcer of activity state transitions.
The Execute method of Sequence will, as we know, schedule its first child activity for execution. When the Execute method returns, the scheduler work queue looks as it is shown in Figure 3.11. The first WriteLine activity is now in the Executing state (again, even though its Execute method has not been called). The Sequence too is in the Executing state.
Figure 3.11 WF program instance after Sequence.Execute
As we know from the basic pattern used for child activity execution, Sequence has, at this point, also subscribed to the Closed event of its first child activity. Even though Closed (and the other events defined on the Activity class) looks like a normal event, under the covers it is an internally managed bookmark.
When the Execute method of WriteLine returns, the WriteLine activity moves to the Closed state. Because the Sequence has subscribed to the event corresponding to this transition, an appropriate work item will be placed in the scheduler work queue. The current state of the program instance is as shown in Figure 3.12; the first WriteLine is underlined to indicate that it has completed its execution and is in the Closed state.
Figure 3.12 WF program instance after first child activity completes
Now the work item for the ContinueAt method of Sequence is dispatched. As we know, ContinueAt will follow the standard pattern for requesting the execution of the second child activity. When ContinueAt method returns, the program state is as shown in Figure 3.13, with the second WriteLine activity now in the Executing state.
Figure 3.13 WF program instance after first callback to Sequence.ContinueAt
This pattern will continue as the Sequence marches through the list of its child activities. When the last child activity reports its completion, the ContinueAt method will report the completion of the Sequence. The WF runtime will observe this (you can think of the WF runtime as a subscriber to the root activity's Closed event), and will do the necessary bookkeeping to complete this WF program instance.
Figure 3.14 summarizes the execution of our WF program as an interaction diagram.
Figure 3.14 Interaction diagram of the execution of Listing 3.15
One crucial point about the Sequence activity is that it implemented sequential execution of its child activities using the general-purpose methods and events available on AEC and Activity. The WF runtime contains no knowledge of sequential activity execution; it only pays attention to the activity automaton and the containment relationships between activities in its role as enforcer of state transitions.
To see how easy it is to define other forms of control flow as composite activities, let's write a composite activity that executes its child activities in an interleaved manner.
The Interleave activity shown in Listing 3.16 implements an AND join by first scheduling the execution of all child activities in a single burst and waiting for them all to complete before reporting its own completion.
Listing 3.16. Interleave Activity
using System; using System.Collections; using System.Workflow.ComponentModel; namespace EssentialWF.Activities { public class Interleave : CompositeActivity { protected override ActivityExecutionStatus Execute( ActivityExecutionContext context) { if (this.EnabledActivities.Count == 0) return ActivityExecutionStatus.Closed; IList<Activity> shuffled = ShuffleList(EnabledActivities); foreach (Activity child in shuffled) { child.Closed += ContinueAt; context.ExecuteActivity(child); } return ActivityExecutionStatus.Executing; } void ContinueAt(object sender, ActivityExecutionStatusChangedEventArgs e) { e.Activity.Closed -= ContinueAt; ActivityExecutionContext context = sender as ActivityExecutionContext; foreach (Activity child in this.EnabledActivities) { if ((child.ExecutionStatus != ActivityExecutionStatus.Initialized) && (child.ExecutionStatus != ActivityExecutionStatus.Closed)) return; } context.CloseActivity(); } // ShuffleList method elided for clarity } }
We will discuss the finer points of the Interleave activity's execution (which induces a form of pseudo-concurrency) a bit later in this chapter. First, though, let's look at the mechanics of how Interleave executes, just as we did for Sequence.
You can see right away that the code for Interleave is quite similar to that of Sequence. In the Execute method, all of the child activities are scheduled for execution, not merely the first one as with Sequence. In the implementation of ContinueAt, the Interleave reports itself as completed only if all child activities are in the Closed state.
There is one other interesting line of code:
IList<Activity> shuffled = ShuffleList(EnabledActivities);
ShuffleList is presumed to be a private helper method that simply shuffles the list of child activities into some random order. The Interleave activity will work just fine without ShuffleList, but we have added it so that users of Interleave cannot predict or rely upon the order in which child activities are scheduled for execution.
The XAML in Listing 3.17 is an Interleave activity that contains a set of WriteLine child activities.
Listing 3.17. Interleaved Execution of WriteLine Activities
<Interleave x:Name="i1" xmlns="http://EssentialWF/Activities" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <WriteLine x:Name="w1" Text="One" /> <WriteLine x:Name="w2" Text="Two" /> <WriteLine x:Name="w3" Text="Three" /> <WriteLine x:Name="w4" Text="Four" /> </Interleave>
Running the program in Listing 3.17 may result in the following output:
Four Two Three One
Or the following:
Three One Four Two
Or the following:
One Two Three Four
Or any of the other possible orderings of the four strings printed by the four WriteLine activities.
Let's trace through the execution of an instance of this program, showing the scheduler work queue and program state. The program is started exactly like the one we developed earlier with Sequence; an item is placed in the scheduler work queue representing a call to the Execute method of the root activity, and the root activity is placed in the Executing state (see Figure 3.15).
Figure 3.15 WF program instance in Listing 3.17 after WorkflowInstance.Start
Let's assume that the call to ShuffleList results in the following ordering of child activities: w2, w4, w1, w3. When the Execute method of Interleave returns, the program state is as shown in Figure 3.16.
Figure 3.16 WF program instance in Listing 3.17 after Interleave.Execute
Now all four child activities are queued for execution and all four child activities of the Interleave are in the Executing state. The dispatcher will pick the item from the front of the queue (Execute "w2"). This will cause the Execute method of WriteLine named "w2" to be invoked. When this method returns, "Two" will have been printed to the console and the state of the program is as shown in Figure 3.17.
Figure 3.17 WF program instance in Listing 3.17 after WriteLine "w2" completes
As expected, because Interleave has subscribed to the Closed event of each child activity, there is a callback to the ContinueAt method present in the scheduler work queue. This item, however, sits behind three other items—the execution handlers for the Execute methods of w4, w1, and w3. The process outlined for w2 will therefore repeat three more times, resulting in the state shown in Figure 3.18.
Figure 3.18 WF program instance in Listing 3.17 after WriteLine "w4" completes
At this point, all four WriteLine activities have completed. The Interleave activity, though, has not actually received any notifications because its work items are still in the scheduler work queue. The four work items in the scheduler work queue are all resumptions of the same bookmark. The resumption point is the ContinueAt method of Interleave; the four work items differ only in the EventArgs data that is the payload of each resumed bookmark.
When the first work item is delivered to Interleave, the logic of the ContinueAt method will determine that all child activities are in the Closed state, so the Interleave itself is reported as complete. When the other three callbacks are subsequently dispatched, the WF runtime observes that the Interleave is already in the Closed state, so the callbacks are not delivered (they are simply discarded); delivery of these callbacks would violate the activity automaton because the Interleave cannot resume execution once it is in the Closed state.
Now, what we have seen in the execution of this WF program is quite a bit different than what we saw for the WF program that used Sequence. Things get even more interesting, though, if each child activity of the Interleave is not a simple activity like WriteLine, but a Sequence (which might contain other Interleave activities). Furthermore, it's clearly not very interesting or useful to simply execute WriteLine activities in an interleaved manner. It is much more realistic for each branch to be performing work that depends upon external input. In this way, the ordering of the execution of activities is determined, in part, by the timing of EnqueueItem operations performed by external code on WF program queues. By modeling these interactions in an Interleave, no branch is blocked by any other (because activities use bookmarks when their execution awaits external stimulus) and the execution of the activities within the branches can interleave.
As we know, the Interleave activity uses an explicit shuffling technique to decide the ordering in which its child activities are scheduled for execution. The influence of Interleave, however, ends there. If a Sequence activity is added as a child activity of an Interleave, the Interleave controls when the Sequence executes, but only the Sequence controls when its child activities are executed.
The XAML in Listing 3.18 is an Interleave with a set of Sequence child activities that contain child activities. The name of the WF program queue created by ReadLine in its Initialize method is the name of the activity. So, four WF program queues will be created during the initialization of a WF program instance, and these WF program queues are named r1, r2, r3, and r4.
Listing 3.18. Interleaved Execution of Sequence Activities
<Interleave x:Name="i1" xmlns="http://EssentialWF/Activities" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:wf="http://schemas.microsoft.com/winfx/2006/xaml/workflow"> <Sequence x:Name="s1"> <ReadLine x:Name="r1" /> <WriteLine x:Name="w1" Text="{wf:ActivityBind r1,Path=Text}" /> <ReadLine x:Name="r2" /> <WriteLine x:Name="w2" Text="{wf:ActivityBind r2,Path=Text}" /> </Sequence> <Sequence x:Name="s2"> <ReadLine x:Name="r3" /> <WriteLine x:Name="w3" Text="{wf:ActivityBind r3,Path=Text}" /> <ReadLine x:Name="r4" /> <WriteLine x:Name="w4" Text="{wf:ActivityBind r4,Path=Text}" /> </Sequence> </Interleave>
We are not going to go through the execution of an instance of this program step by step—it would take a few pages of diagrams—but we know enough about the execution logic of the Sequence and Interleave activities to predict what will happen. Assuming that no items are enqueued into any of the WF program queues, the program will reach the state shown in Figure 3.19.
Figure 3.19 WF program instance in Listing 3.18 after reaching ReadLine activities
At this point, the program is idle. Both Sequence activities have started executing, and each has, in turn, requested the execution of their first child activity (which happens to be a ReadLine activity in both cases). Each ReadLine activity is stuck waiting for an item to appear in its WF program queue. If the Interleave had a third child activity that was a Sequence of any number of WriteLine activities, then this Sequence would run to completion.
If we enqueue the string "hello" into WF program queue "r3", there will be an episode of action. The ContinueAt method of the ReadLine activity with name "r3" will be scheduled (the name of the WF program queue created by ReadLine is the same as its Name property). This will cause the ReadLine activity to complete, which will schedule notification of its Closed event to the enclosing Sequence "s2". That Sequence will schedule the execution of the WriteLine "w3" that follows the just-completed ReadLine. The WriteLine will get the string received by the ReadLine activity (via activity databinding) and write it to the console. The WriteLine will complete, again causing a notification to the enclosing Sequence. The Sequence will then move on to its third child activity, another ReadLine, which will now wait until an item is enqueued into its WF program queue.
The series of steps just described will result in the state of the program shown in Figure 3.20.
Figure 3.20 WF program instance in Listing 3.18 again in an idle state
This example is typical of the episodic execution we described at the outset of the chapter. As a result of stimulus from the external world, the WF program instance moves forward. And it is truly the composite activities that are driving the program's execution by providing the control flow; the WF runtime is passively dispatching whatever items appear in the scheduler work queue while enforcing adherence to the activity automaton.
Once you understand the activity automaton and the execution-related rules of activity composition, it is easy to model other control flow patterns beyond simple sequential and interleaved execution. This allows your programs to mirror more precisely whatever processes they are trying to coordinate. In the next chapter, we will look at several additional aspects of composite activity development that aid in building different kinds of control flow.
It may be helpful to pause here and consolidate what you've learned from this chapter so far. As an exercise, we suggest writing a custom composite activity. An appropriate choice on which to test your skills is PrioritizedInterleave. The PrioritizedInterleave activity executes its child activities in priority order. Each child activity has a property, named Priority, of type int.
When PrioritizedInterleave executes, first all child activities with a priority of 1 are executed in an interleaved manner; when those are completed, all child activities with a priority of 2 are executed (also in an interleaved manner). This continues until all child activities have been executed. As you might guess, the execution logic of PrioritizedInterleave is something of a combination of the logic we developed for Sequence and the logic we developed for Interleave.
Listing 3.19 shows an example WF program containing a Prioritized-Interleave. The seven child activities of the PrioritizedInterleave are grouped into three different sets according to the values of their Priority property. The best way to implement the Priority property is as an attached property, which supports the XAML syntax shown in Listing 3.19. Attached properties are covered in Chapter 7, "Advanced Authoring." You can take a simpler approach and add a Priority property to WriteLine and then test your PrioritizedParallel activity using the modified WriteLine.
Listing 3.19. WF Program that Is a PrioritizedInterleave
<PrioritizedInterleave xmlns="http://EssentialWF/Activities"> <B PrioritizedInterleave.Priority="1" /> <C PrioritizedInterleave.Priority="2" /> <A PrioritizedInterleave.Priority="1" /> <E PrioritizedInterleave.Priority="2" /> <F PrioritizedInterleave.Priority="3" /> <G PrioritizedInterleave.Priority="3" /> <D PrioritizedInterleave.Priority="2" /> </PrioritizedInterleave>
This WF program is depicted in a more readable form in Figure 3.21, which conveys the interleaved execution that occurs within the groupings of child activities.
Figure 3.21 PrioritizedInterleave activity containing three groupings
You might conclude from Figure 3.21 that this WF program could just as easily be built using the Sequence and Interleave activities we developed previously. True, but there is another way of looking at things. Both Sequence and Interleave are nothing but degenerate cases of our PrioritizedInterleave, in which the priorities of the child activities are either all different or all the same, respectively. This is a simple but instructive example of the control flow flexibility afforded by the composition model of WF.