Home > Articles > Home & Office Computing > Microsoft Windows Desktop

  • Print
  • + Share This
This chapter is from the book

Bookmarks Revisited

The simplest activities (in terms of execution logic) are like the WriteLine activity; they complete their work entirely within their Execute method. If all activities did this, you would not be able to build very interesting WF programs. Don't get us wrong; simple activities are useful, and indeed are essential to the definition of most WF programs. Typical duties for such activities include obtaining services and exchanging data with those services, and manipulating the state of the WF program instance.

Most real-world processes, however, reach points in time at which further computational progress cannot be made without stimulus (input) from an external entity. It may be that a WF program waits for a person to make a decision about which branch of execution logic should be taken. Or it may be that an activity delegates some computation to an external entity and then waits for the result of that computation to be returned asynchronously.

In order to understand the mechanics of how this kind of activity executes, we will begin by looking at a contrived example: an activity that delegates work to...itself. Consider the version of WriteLine that is shown in Listing 3.6.

Listing 3.6. WriteLine Activity That Uses a Bookmark

using System;
using System.Workflow.ComponentModel;

namespace EssentialWF.Activities
{
  public class WriteLine : Activity
  {
    // Text property elided for clarity...

    protected override ActivityExecutionStatus Execute(
      ActivityExecutionContext context)
    {
      base.Invoke(this.ContinueAt, EventArgs.Empty);
      return ActivityExecutionStatus.Executing;
    }

    void ContinueAt(object sender, EventArgs e)
    {
      ActivityExecutionContext context =
        sender as ActivityExecutionContext;

      WriterService writer = context.GetService<WriterService>();
      writer.Write(Text);

      context.CloseActivity();
    }
  }
}

Although the example is contrived, there are several things worth looking at here.

By calling Invoke<T> (a protected method defined by Activity), the WriteLine activity creates a bookmark and immediately resumes that bookmark. The bookmark's resumption point is the WriteLine.ContinueAt method, and the payload for the resumed bookmark is EventArgs.Empty.

The bookmark created by the call to Invoke<T> is managed internally by the WF runtime, and because the Invoke<T> method also resumes this bookmark, an item is enqueued in the scheduler work queue (corresponding to the ContinueAt method).

Because it creates a bookmark (and is awaiting resumption of that bookmark), the WriteLine activity can no longer report its completion at the end of the Execute method. Instead it returns a value of ActivityExecutionStatus.Executing, indicating that although WriteLine is yielding the CLR thread by returning from Execute, its work is not complete since there is a pending bookmark. The WriteLine activity remains in the Executing state and does not transition (yet) to Closed.

When the scheduler dispatches the work item corresponding to the ContinueAt method, it passes an ActivityExecutionContext as the sender parameter. This allows the WriteLine to have access to its current execution context.

The ContinueAt method conforms to a standard .NET Framework event handler signature and therefore has a return type of void. Because of this, the WF runtime cannot use the return value of ContinueAt as the way of determining whether or not the activity should remain in the Executing state or transition to the Closed state. The CloseActivity method provided by ActivityExecutionContext can be used instead. If this method is called, the currently executing activity moves to the Closed state; if the method is not called, there is no change in the state of the activity. Because ContinueAt calls CloseActivity, the WriteLine activity moves to the Closed state.

The version of the WriteLine activity that uses Invoke<T>, though contrived, is still illustrative of the general pattern that you will need to use in many of the activities you develop. Although it is possible for an activity to complete its work within the Execute method (as with the version of WriteLine that returns ActivityExecutionStatus.Closed from its Execute method), this is a special case. Just as subroutines are a special, simple case accommodated by the richer concept of a coroutine, activities whose execution logic is embodied in a single Execute method are a special, simple form of episodic computation, in which there is always exactly one episode.

WF Program Execution

Now that we understand the basics of how to write activity execution logic, we can take a closer look at the execution mechanics of a WF program. We will start with a WF program that contains just one activity:

<WriteLine Text="hello, world" xmlns="http://EssentialWF/Activities" />

Running this program results in the expected output:

hello, world

In Chapter 2, "WF Programs," we briefly looked at the code that is required to host the WF runtime and run WF programs. We will return to the host-facing side of the WF runtime in Chapter 5, "Applications." For now, it is enough to know the basics: First, the WorkflowRuntime.CreateWorkflow method returns a WorkflowInstance representing a newly created instance of a WF program; second, the WorkflowInstance.Start method tells the WF runtime to begin the execution of that WF program instance.

The call to WorkflowRuntime.CreateWorkflow prepares a scheduler (and the accompanying scheduler work queue) for the new WF program instance. When this method returns, all activities in the WF program are in the Initialized state.

The call to WorkflowInstance.Start enqueues one item in the the scheduler work queue—a delegate corresponding to the Execute method of the root activity of the WF program. The root activity—in our example, the WriteLine—is now in the Executing state, even though the Execute method has not actually been called (the work item has not yet been dispatched). The scheduler work queue is shown in Figure 3.6.

Figure 3.6

Figure 3.6 Scheduler work queue after WorkflowInstance.Start

Let's assume that we are using the version of WriteLine that doesn't call Invoke<T>.

When the Execute method returns a value of ActivityExecutionStatus. Closed, the WriteLine activity moves to the Closed state. In this case, the WF runtime recognizes that the program instance is complete since the root activity in the program instance is complete.

The asynchronous version of WriteLine is only slightly more complex. The call to Invoke<T> within Execute will enqueue a work item in the scheduler work queue (corresponding to the resumption of the internally created bookmark).

Thus, when the Execute method (of the version of WriteLine that does call Invoke<T>) returns, the activity remains in the Executing state and the scheduler work queue looks as it is shown in Figure 3.7.

Figure 3.7

Figure 3.7 Scheduler work queue after WriteLine.Execute

When the WriteLine.ContinueAt method returns, the WriteLine activity moves to the Closed state and the program instance completes.

WF Program Queues

Any activity that requires input from an external entity must figure out a way to (a) let that external entity know that it requires input, and (b) receive notification when the input is available. This simple pattern is at the heart of episodic computation, and it is supported in a first-class way by the WF runtime. The plain requirement is that an activity must be able to receive input even if the WF program instance in which it exists is idle and sitting in persistent storage like a SQL Server database table. When the input arrives, the WF program instance must be reactivated and its execution resumed (at the appropriate bookmark).

In Chapter 2, we developed an activity called ReadLine (shown again in Listing 3.7), which waits for a string to arrive from an external entity. If you understand how this activity is built and how it executes, you will have the right basis for understanding and creating higher level communication patterns that are used in WF programs. All such patterns are built on top of the same notion of bookmarks.1

Listing 3.7. ReadLine Activity

using System;
using System.Workflow.ComponentModel;
using System.Workflow.Runtime;

namespace EssentialWF.Activities
{
  public class ReadLine : Activity
  {
    private string text;
    public string Text
    {
      get { return text; }
    }

    protected override ActivityExecutionStatus Execute(
      ActivityExecutionContext context)
    {
      WorkflowQueuingService qService =
        context.GetService<WorkflowQueuingService>();

      WorkflowQueue queue =
        qService.CreateWorkflowQueue(this.Name, true);
      queue.QueueItemAvailable += this.ContinueAt;

      return ActivityExecutionStatus.Executing;
    }

    void ContinueAt(object sender, QueueEventArgs e)
    {
      ActivityExecutionContext context =
        sender as ActivityExecutionContext;

      WorkflowQueuingService qService =
        context.GetService<WorkflowQueuingService>();

      WorkflowQueue queue = qService.GetWorkflowQueue(this.Name);
      text = (string) queue.Dequeue();
      qService.DeleteWorkflowQueue(this.Name);

      context.CloseActivity();
    }
  }
}

The execution logic of the ReadLine activity uses a WF program queue. A WF program queue is essentially a named location (a bookmark) where an activity can receive data, even if the WF program instance in which the activity exists is not in memory. A WF program queue is not the same as the WF program instance's scheduler queue, which is managed by the WF runtime. Think of a WF program queue as the data structure in which an explicitly created bookmark holds its payload (to be delivered upon the resumption of the bookmark). It is an addressable location where external entities can deliver data.

The Execute method of ReadLine obtains the WorkflowQueuingService from its ActivityExecutionContext. The WorkflowQueuingService is asked to create a WF program queue with a name that is the same as that of the activity (this.Name). The name of a WF program queue can be any IComparable object; usually a string will suffice. We are choosing a simple queue naming convention here, but other schemes are possible. Regardless, the external code that provides input to a WF program instance must know the name of the appropriate WF program queue.

The WorkflowQueuingService type is shown in Listing 3.8.

Listing 3.8. WorkflowQueuingService

namespace System.Workflow.Runtime
{
  public class WorkflowQueuingService
  {
    public WorkflowQueue CreateWorkflowQueue(IComparable queueName,
      bool transactional);
    public bool Exists(IComparable queueName);
    public WorkflowQueue GetWorkflowQueue(IComparable queueName);
    public void DeleteWorkflowQueue(IComparable queueName);

    /* *** other members *** */
  }
}

The same WF program queue name may be used in more than one WF program instance. This just means that if we write a WF program containing a ReadLine activity named "r1", we can execute any number of instances of this WF program without any problems. Each instance will create a separate WF program queue with the name "r1". Because data is always enqueued to a specific WF program instance (via WorkflowInstance.EnqueueItem), there is no conflict or ambiguity. Another way of stating this is that WF program queues are not shared across WF program instances. This allows us to think of the logical address of a WF program queue as the WorkflowInstance.InstanceId identifying the WF program instance that owns the WF program queue, plus the WF program queue name.

A WF program queue acts as a conduit for communication between external entities and an activity in a WF program instance. Code outside of the WF program instance can deposit data into a WF program queue using the EnqueueItem method defined on the WorkflowInstance class. An activity (and, by extension, a WF program) can create as many distinct WF program queues as it requires.

The CreateWorkflowQueue method returns a WorkflowQueue object that represents the WF program queue. The WorkflowQueue type is shown in Listing 3.9.

Listing 3.9. WorkflowQueue

namespace System.Workflow.Runtime
{
  public class WorkflowQueue
  {
    public IComparable QueueName { get; }
    public int Count { get; }

    public object Dequeue();
    public object Peek();

    public event EventHandler<QueueEventArgs>
      QueueItemAvailable;

    /* *** other members *** */
  }
}

The QueueItemAvailable event is raised when an item is enqueued into the WF program queue. Under the covers, this is just a bookmark (disguised using C# event syntax).

The QueueItemAvailable event is also raised if, when an activity subscribes to this event, there are already (previously enqueued) items present in the WF program queue. This permits a decoupling of the delivery of data to a bookmark and the resumption of that bookmark.

Here is a simple WF program that contains only a single ReadLine activity:

<ReadLine x:Name="r1" xmlns="http://EssentialWF/Activities"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" />

If we save this WF program as a file called "Read.xoml", we can execute it using the console application of Listing 3.10, which hosts the WF runtime and delivers data to the ReadLine activity via the WF program queue.

Listing 3.10. A Console Application That Delivers Data to a ReadLine Activity

using System;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.Runtime;
using System.Xml;

class Program
{
  static void Main()
  {
    using (WorkflowRuntime runtime = new WorkflowRuntime())
    {
      TypeProvider tp = new TypeProvider(null);
      tp.AddAssemblyReference("EssentialWF.dll");
      runtime.AddService(tp);

      runtime.StartRuntime();

      runtime.WorkflowIdled += delegate(object sender,
        WorkflowEventArgs e)
      {
        Console.WriteLine("WF program instance " +
          e.WorkflowInstance.InstanceId + " is idle");
      };

      runtime.WorkflowCompleted += delegate(object sender,
        WorkflowCompletedEventArgs e)
      {
        Console.WriteLine("WF program instance " +
          e.WorkflowInstance.InstanceId + " completed");
      };

      WorkflowInstance instance = null;
      using (XmlTextReader reader = new XmlTextReader("Read.xoml"))
      {
        instance = runtime.CreateWorkflow(reader);
        instance.Start();
      }

      string text = Console.ReadLine();
      instance.EnqueueItem("r1", text, null, null);

      // Prevent Main from exiting before
      // the WF program instance completes
      Console.ReadLine();

      runtime.StopRuntime();
    }
  }
}

The console application calls WorkflowRuntime.CreateWorkflow, which loads the WF program from XAML. It then calls WorkflowInstance.Start, which causes the Execute method of ReadLine—the root activity in the WF program—to be scheduled.

The console application then waits for the user to enter text at the console. Meanwhile, the WF runtime begins the execution of the WF program instance on a thread that is different than the thread on which Main is running. The ReadLine activity has its Execute method invoked. The ReadLine activity creates its WF program queue and then waits for data to arrive there. Because there are no other items in the scheduler work queue, the WF program instance is idle.

The console application subscribes for the WorkflowRuntime.WorkflowIdled event and, when this event is raised by the WF runtime, writes the InstanceId of the WF program instance to the console:

WF program instance 631855e5-1958-4ce7-a29a-dc6f8e2a9238 is idle

When a line of text is read, the console application calls EnqueueItem, passing the text it received from the console as payload associated with the resumption of the bookmark.

The implementation of WorkflowInstance.EnqueueItem enqueues (in the scheduler work queue) work items for all activities that are subscribed to this WF program queue's QueueItemAvailable event. This is depicted in Figure 3.8.

Figure 3.8

Figure 3.8 Enqueuing data to a WF program queue

In our example, the ReadLine activity's callback is called ContinueAt. This delegate will be scheduled as a work item and dispatched by the scheduler; if the idle WF program instance had been passivated (not shown in this example), the WF runtime would automatically bring it back into memory.

The ReadLine activity will set its Text property with the string obtained from the Dequeue operation on its WF program queue. In the example, we are doing no error checking to ensure that the object is indeed of type string. The ContinueAt method informs the WF runtime that it is complete by calling CloseActivity. The WF program instance, because it only contains the ReadLine, also completes. The console application, which subscribed to the WorkflowRuntime.WorkflowCompleted event, prints this fact to the console.

WF Program instance 631855e5-1958-4ce7-a29a-dc6f8e2a9238 completed

If the console application tries to enqueue data to a WF program queue that does not exist, the EnqueueItem method will throw an InvalidOperationException indicating that the WF program queue could not be found. In our implementation of ReadLine, the WF program queue is not created until the ReadLine activity begins executing. Thus, the following lines of code are problematic:

WorkflowInstance instance = runtime.CreateWorkflow(...);
instance.EnqueueItem("r1", "hello", null, null);

The preceding code omits the call to WorkflowInstance.Start, and because of this the WF program queue named "r1" does not yet exist. In other words, the implementation of ReadLine requires that the application doesn't enqueue the data until after the ReadLine activity starts to execute. Even the code in the console application of Listing 3.9 presents a race condition because the execution of the WF program instance occurs on a different thread than the execution of the console application. We may be able to work around this race condition quite easily in our contrived example where the WF program is just a single ReadLine activity. But in a larger WF program, with many activities managing WF program queues, and executing at different times, this is a lot trickier.

One of the ways to mitigate this problem is to allow activities to create WF program queues during the creation of a WF program instance. This will ensure that, after the call to WorkflowRuntime.CreateWorkflow, the WF program instance can immediately receive data (even if it cannot yet process it, which will only begin once WorkflowInstance.Start is called). In a later section, we will change ReadLine to do exactly this.

Timers

Another example of an activity that cannot complete its execution logic entirely within the Execute method is a Wait activity that simply waits for a specified amount of time to elapse before completing. The Wait activity is shown in Listing 3.11.

Listing 3.11. Wait Activity

using System;
using System.Workflow.ComponentModel;
using System.Workflow.Runtime;

namespace EssentialWF.Activities
{
  public class Wait : Activity
  {
    private Guid timerId;

    public static readonly DependencyProperty DurationProperty
      = DependencyProperty.Register("Duration",
        typeof(TimeSpan), typeof(Wait));

    public TimeSpan Duration
    {
      get { return (TimeSpan) GetValue(DurationProperty); }
      set { SetValue(DurationProperty, value); }
    }

    protected override ActivityExecutionStatus Execute(
      ActivityExecutionContext context)
    {
      WorkflowQueuingService qService =
        context.GetService<WorkflowQueuingService>();

      timerId = Guid.NewGuid();

      WorkflowQueue queue = qService.CreateWorkflowQueue(
        timerId, true);
      queue.QueueItemAvailable += this.ContinueAt;

      TimerService timerService = context.GetService<TimerService>();
      timerService.SetTimer(timerId, Duration);

      return ActivityExecutionStatus.Executing;
    }

    void ContinueAt(object sender, QueueEventArgs e)
    {
      ActivityExecutionContext context =
        sender as ActivityExecutionContext;

      WorkflowQueuingService qService =
        context.GetService<WorkflowQueuingService>();

      WorkflowQueue queue = qService.GetWorkflowQueue(timerId);
      qService.DeleteWorkflowQueue(timerId);

      context.CloseActivity();
    }
  }
}

Listing 3.11 shows the basic implementation of a Wait activity that depends upon an implementation of a TimerService (see Listing 3.12) for the actual management of the timer. The Wait activity, in its Execute method, creates a WF program queue providing the bookmark resumption point (ContinueAt) and calls TimerService.SetTimer, passing a unique identifier representing the timer. The TimerService is responsible for managing the actual timers. When the timer is triggered, the timer service resumes the bookmark by enqueuing data in the WF program queue created by the Wait activity. When the ContinueAt method is invoked by the scheduler (with the AEC as the sender argument), the Wait activity deletes the WF program queue and transitions to the Closed state.

The TimerService defines a SetTimer method that allows the activity to specify the duration of the timer as a TimeSpan, along with the name of the WF program queue that the TimerService will use to deliver a notification using WorkflowInstance.EnqueueItem (with a null payload) when the specified amount of time has elapsed.

Listing 3.12. TimerService Used by the Wait Activity

using System;
using System.Workflow.ComponentModel;
using System.Workflow.Runtime;

namespace EssentialWF.Activities
{
  public abstract class TimerService
  {
   public abstract void SetTimer(Guid timerId, TimeSpan duration);
   public abstract void CancelTimer(Guid timerId);
  }
}

A simple implementation of the timer service is shown in Listing 3.13.

Listing 3.13. Implementation of a TimerService

using System;
using System.Collections.Generic;
using System.Threading;
using System.Workflow.ComponentModel;
using System.Workflow.Runtime;
using EssentialWF.Activities;

namespace EssentialWF.Services
{
  public class SimpleTimerService : TimerService
  {
    WorkflowRuntime runtime;
    Dictionary<Guid, Timer> timers = new Dictionary<Guid, Timer>();

    public SimpleTimerService(WorkflowRuntime runtime)
    {
      this.runtime = runtime;
    }

    public override void SetTimer(Guid timerId, TimeSpan duration)
    {
      Guid instanceId = WorkflowEnvironment.WorkflowInstanceId;
      Timer timer = new Timer(delegate(object o)
        {
          WorkflowInstance instance = runtime.GetWorkflow(instanceId);
          instance.EnqueueItem(timerId, null, null, null);
        }, timerId, duration, new TimeSpan(Timeout.Infinite));

      timers.Add(timerId, timer);
    }

    public override void CancelTimer(Guid timerId)
    {
      ((IDisposable)timers[timerId]).Dispose();
      timers.Remove(timerId);
    }
  }
}

The SimpleTimerService mantains a set of System.Threading.Timer objects. The timerId that is passed as a parameter to the SetTimer method serves as the name of the WF program queue created by the Wait activity. When a timer fires, the callback (written as an anonymous method) enqueues a (null) item into the appropriate WF program queue, and the Wait activity resumes its execution.

In Chapter 6 we will discuss transactions, and we will see how transactional services (such as a durable timer service) can be implemented. Because we have followed the practice of making the Wait activity dependent only on the abstract definition of the timer service, we can change the implementation of the timer service without affecting our activities and WF programs.

As mentioned earlier, the WF runtime is a container of services. Custom services that are added to the WF runtime can be obtained by executing activities. An implementation of a TimerService can be added to the WF runtime like so:

using (WorkflowRuntime runtime = new WorkflowRuntime())
{
  runtime.AddService(new SimpleTimerService(runtime));
  ...
}

Executing the Wait activity within a simple WF program will cause the program to pause (and potentially passivate) and later, when the timeout occurs, resume the execution. The following program will start and then pause for 5 seconds, and finally resume its execution and complete:

<Wait Duration="00:00:05" xmlns="http://EssentialWF/Activities" />

Our reason for introducing the Wait activity is to illustrate a general pattern.

There is nothing at all special about timers. The Wait activity makes a request to a service on which it depends, and indicates to the service where (to which WF program queue) the result of the requested work should be delivered. The service takes some amount of time to complete the requested work. When the work is done, the service returns the result of the work to the activity via the WF program queue.

This bookmarking pattern is the basis for developing WF programs that are "coordinators of work" that is performed outside their boundaries.

  • + Share This
  • 🔖 Save To Your Account