Creating a WF Program Instance






Creating a WF Program Instance

The first step in running an instance of a WF program is actually creating the instance. This is the job of the CreateWorkflow method of WorkflowRuntime. As you can see from Listing 5.2, CreateWorkflow has several overloads, each of which returns a WorkflowInstance. The creation of an instance of a WF program involves a series of steps called activation.

In application code, activation simply amounts to calling CreateWorkflow. But inside the implementation of this method, a few interesting things happen.

First of all, a new WF program instance (an actual tree of Activity objects) needs to be manufactured from some representation of a WF program. To make things more concrete, consider the following Echo program:

<Sequence x:Name="Echo" xmlns="http://EssentialWF/Activities"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wf="http://schemas.microsoft.com/winfx/2006/xaml/workflow">
  <ReadLine x:Name="r1" />
  <WriteLine x:Name="w1" Text="{wf:ActivityBind r1,Path=Text}" />
</Sequence>

Clearly, this is just a textual representation of a WF program, which happens to use XAML as the format of that text. Although it is common practice to call this XAML snippet a WF program (as we have done throughout this book), we can label it more precisely by calling it a WF program blueprint. A WF program blueprint is an artifact (or a set of artifacts) that represents a tree of activity declarations. It is something that can become an in-memory tree of activity objects.

A WF program blueprint is not something natively understood by the core of the WF runtime. A blueprint must be translated into an in-memory tree of activity objects by a runtime service called the WF program loader in order to be executed by the WF runtime. The job of a WF program loader is to accept a WF program blueprint as input and produce an activity tree as output. The activity tree produced by a WF program loader is called a WF program prototype, sometimes called a program definition. The program prototype consists of metadatathe values of activity properties that are immutable at runtime. This metadata includes the structure of the WF program (the Activities property of all composite activities in the program) and also the values of any properties that are registered as metadata properties (such as the Name property of Activity). Metadata properties were discussed briefly in Chapter 2, "WF Programs," and will be explored in more detail in Chapter 7, "Advanced Authoring."

A WF program prototype is held in memory by the WF runtime as a template, from which multiple instances can be manufactured (each resulting from a call to CreateWorkflow). A WF program instance is the entity (represented in memory by CLR objects and in durable storage as a continuation) that is actually executed by the WF runtime.

To run our Echo program, the default WF program loader translates the XAML into an in-memory tree of activity objectsa WF program prototype. If we run the program once, a single WF program instance is manufactured from the WF program prototype (and is eventually disposed when the instance completes its execution). If we run the program six times, six WF program instances come and go independently; all of these WF program instances share (a reference to) the same WF program prototype.

The relationship between a WF program prototype and the set of WF program instances manufactured from that prototype is depicted in Figure. All instances manufactured from the same prototype share the same metadata.

2. A WF program prototype and three WF program instances manufactured from that prototype


The situation depicted in Figure is analogous to that which exists in the CLR between a type and objects of that type (see Figure). A typical CLR type acts as a template from which multiple objects can be instantiated. These objects share metadata (for example, CLR attributes that are applied to the type or inherited by it) but also hold unique instance-specific state (the values of nonstatic fields and properties defined by the type).

3. A CLR type and three objects of that type


The CLR does not care about the programming language used in source files that define a type, so long as the compiler of that language can translate the source code to MSIL. Similarly, the WF runtime does not care about WF program blueprint formats, so long as WF program prototypes can be realized from blueprints that use a particular format. The fact that WF does not have an underlying grammar, but is instead activity oriented, opens the door to programming in domain-specific languages (DSLs). XAML may be fine for general-purpose WF program authoring. However, DSLs offer the potential for more effective programming because the constructions of a DSL match the domain of the problem being solved and are usually more expressive (compared to a general-purpose language) in that domain. A set of activity types can be developed to exactly mirror the semantics of a chosen DSL.

There is a subtle but crucial difference here. What the WF runtime cares about is the in-memory representation of a WF program. Whereas the CLR loader is (for the most part) wedded to the assembly as a unit of packaging for MSIL and metadata, the WF runtime imposes no such restriction. WF program prototypes are not packaged; only the blueprints from which prototypes are loaded are packagable. As we shall see later, a WF program blueprint can be a CLR type that is packaged in an assembly. But as we already know, a WF program blueprint can also be XAML. XAML representing a WF program can be packaged as a file on disk, or as a blob in a database table, or might also be a stream of data (that is created dynamically by application code) without any packaging at all.

Figure depicts the loading of WF program prototypes from several different formats and forms of packaging.

4. Loading of WF program prototypes


You can obtain a copy of the prototype for any WF program instance by calling the GetWorkflowDefinition method of WorkflowInstance. In order to preserve program execution robustness, the WF runtime does not provide access to actual prototypes; it only hands out copies thereof. Along the same lines, the WF runtime never provides direct access to the Activity objects that (transiently) represent underlying WF program instances while they are in memory. It is the WorkflowInstance wrapper that allows application code to interact with and manage WF program instances.

We will see in a minute, when we discuss custom loader services, exactly how different program representations are accommodated by the WF runtime. First, though, let's go through the capabilities that come for free with the default loader service.

WorkflowRuntime provides two flavors of CreateWorkflow. The first accepts a System.Type as the packaging for the WF program. The second accepts a System.Xml.XmlReader. There are three overloads of each, for a total of six distinct CreateWorkflow method signatures:

public WorkflowInstance CreateWorkflow(XmlReader reader);
public WorkflowInstance CreateWorkflow(XmlReader reader,
  XmlReader rulesReader,
  Dictionary<string, object> namedArgumentValues);
public WorkflowInstance CreateWorkflow(XmlReader reader,
  XmlReader rulesReader,
  Dictionary<string, object> namedArgumentValues,
  Guid instanceId);
public WorkflowInstance CreateWorkflow(Type workflowType);
public WorkflowInstance CreateWorkflow(Type workflowType,
  Dictionary<string, object> namedArgumentValues);
public WorkflowInstance CreateWorkflow(Type workflowType,
  Dictionary<string, object> namedArgumentValues,
    Guid instanceId);

In the absence of a custom loader service, the overloads of CreateWorkflow that accept a System.Type will realize a WF program prototype by calling the constructor of the type. The type that is passed as a parameter to CreateWorkflow must therefore be a derivative of the Activity class because the constructor of any other kind of type will not yield a valid WF program prototype.

The first overload accepts only a System.Type.

A second overload accepts, in addition to a System.Type, a collection of name-value pairs that are used in the initialization of the WF program instance. Specifically, each name in the collection corresponds to the name of a settable public property of the root activity in the WF program. The value provided for that name is supplied as the value to that property's setter, as shown here:

using (WorkflowRuntime runtime = new WorkflowRuntime())
{
  runtime.StartRuntime();

  Dictionary<string, object> inputs =
    new Dictionary<string, object>();

  // The root activity must have public properties named
  // Assignee and DueDate, of the appropriate types
  inputs.Add("Assignee", "David");
  inputs.Add("DueDate", DateTime.Now.Add(new TimeSpan(1, 0, 0)));

  Type type = ...
  WorkflowInstance instance = runtime.CreateWorkflow(type, inputs);

  ...
}

This initialization of root activity properties, which may fairly be characterized as a way of providing "inputs" to the WF program instance, is entirely optional (hence the overload). As we will see later, when a WF program instance completes its execution, the values of the gettable public properties of the root activity will be readable by the application, and so may be considered the "outputs" of the instance.

The third overload of CreateWorkflow is exactly like the second except that you also provide a value for the globally unique identifier that will be used to identify the WF program instance being created. This overload can be useful when you need to map a WF program instance identifier to data that exists elsewhere in your application; sometimes it is helpful to represent the creation of a WF program instance before the call to CreateWorkflow is actually made. For the other overloads, the identifier will be generated by the WF runtime. In either case, the identifier is available via the InstanceId property of WorkflowInstance.

The second set of CreateWorkflow overloads accepts an XmlReader instead of a Type. The simplest of these overloads accepts a single XmlReader that (in the absence of a custom loader service) must contain a XAML representation of the WF program. We will see soon, when we discuss the details of the loader service, how formats other than XAML can be accommodated in a first-class manner. XAML, however, is what you get for free with the default loader.

The other two overloads accept, in addition to the first XmlReader representing the program, a second XmlReader expected to contain the definition of declarative rules (essentially, CodeDOM expressions serialized in XAML) that are part of the WF program. Rules will be discussed in detail in Chapter 8, "Miscellanea." And, just as with the Type-based overloads, there are XmlReader-based overloads that allow you to provide a collection of name-value pairs for root activity property initialization as well as a globally unique identifier for the WF program instance being created.

When the CreateWorkflow method returns, a brand new WF program instance exists in memory but it is not yet running. To run the instance, you must call the Start method of WorkflowInstance. It is the Start method that will enqueue (into the WF scheduler work queue) an item corresponding to the invocation of the Execute method of the root activity. Before examining the rest of the lifecycle of the WF program instance, though, let's summarize the instance creation process and discuss a few details we have missed so far.

Figure depicts the internals of WF program instance creation.

5. WF program instance activation


As you can see, there are a few steps in Figure that we have not yet mentioned. The first is that the WF runtime maintains a cache of WF program prototypes. When a call to CreateWorkflow is made, the WF runtime first checks to see whether the relevant prototype is available. If a prototype is found, it is used to manufacture the instance. If no prototype is available, the loader service is invoked in order to obtain the prototype, and the resulting prototype is cached for future use.

After it has a new instance, the WF runtime prepares the instance for execution by creating its scheduler and the corresponding work queue, and providing the WF program instance with a default ActivityExecutionContext. The collection of name-value pairs (if provided to CreateWorkflow) is next used to set the values of properties of the root activity of the instance. The Initialize method of the root activity in the instance is called after the properties are set. Finally, a WorkflowInstance object that acts as a handle to the actual program instance is constructed and returned to the caller of CreateWorkflow.

The Program Loader Service

The important extensibility point that exists in the process of creating new WF program instances is the ability to provide the WF runtime with a custom loader service.

The WF runtime delegates the actual loading of a program to a runtime service called the program loader service (or simply the loader). The loader service is responsible for materializing an Activity object from a WF program blueprint.

The WorkflowLoaderService is the abstract base class for all loader service implementations. It defines two methods, as shown in Listing 5.4.

Listing 5.4. WorkflowLoaderService

namespace System.Workflow.Runtime.Hosting
{
  public abstract class WorkflowLoaderService
    : WorkflowRuntimeService
  {
    protected internal abstract Activity CreateInstance(
      XmlReader workflowDefinitionReader, XmlReader rulesReader);

    protected internal abstract Activity CreateInstance(
      Type workflowType);
  }
}

As indicated earlier in Figure, the WF runtime will use a default loader if a custom implementation of WorkflowLoaderService is not provided.

The default loader implementation is named DefaultWorkflowLoaderService and is shown, with selected code snippets illustrating roughly how it works, in Listing 5.5. You can adapt this code as needed when writing a custom loader service.

Listing 5.5. DefaultWorkflowLoaderService

namespace System.Workflow.Runtime.Hosting
{
  public class DefaultWorkflowLoaderService : WorkflowLoaderService
  {
    // Error handling elided for clarity

    protected override Activity CreateInstance(
      Type workflowType)
    {
       return (Activity) Activator.CreateInstance(workflowType);
     }

     protected override Activity CreateInstance(
      XmlReader workflowDefinitionReader, XmlReader rulesReader)
    {
      Activity root = null;
      ServiceContainer serviceContainer = new ServiceContainer();
      ITypeProvider typeProvider =
        this.Runtime.GetService<ITypeProvider>();
      if (typeProvider != null)
        serviceContainer.AddService(typeof(ITypeProvider),
          typeProvider);

      DesignerSerializationManager manager = new
          DesignerSerializationManager(serviceContainer);
      using (manager.CreateSession())
      {
        WorkflowMarkupSerializationManager xamlSerializationManager =
          new WorkflowMarkupSerializationManager(manager);
        root = new WorkflowMarkupSerializer().Deserialize(
          xamlSerializationManager, workflowDefinitionReader)
          as Activity;

        if (root != null && rulesReader != null)
        {
          object rules = new WorkflowMarkupSerializer().Deserialize(
            xamlSerializationManager, rulesReader);
          root.SetValue(RuleDefinitions.RuleDefinitionsProperty,
            rules);
        }
      }

      return root;
    }
  }
}

The overload of CreateInstance that accepts a System.Type simply uses the System.Activator class to create a new instance of the specified type. The overload of CreateInstance that accepts two XmlReader objects deserializes the WF program and its associated rules using WorkflowMarkupSerializer.

The application that hosts the WF runtime can provide a custom loader implementation that materializes an Activity from any DSL. For example, we can write a loader that converts a simple Microsoft Visio file into a tree of activity objects. We start by creating a simple Visio file, say WriteLines.vdx, which is visualized in Figure. The content of the file is based on a custom domain-specific Visio stencil. Each WriteLine shape in WriteLines.vdx has a Name property and a Value property. The Name is the text on the shape, and will become the Name property of our WriteLine activity. The Value property of the WriteLine shape will become the Text property of the WriteLine activity. Let's assume that our WriteLines.vdx file specified four WriteLine shapes, with their values set to "one," "two," "three," and "four," respectively.

6. WriteLines.vdx


Next, we host the WF runtime in a console application that we've called DiagramsCanExecuteToo. You can pass a .vdx file to this application as an argument, like so:

> DiagramsCanExecuteToo.exe WriteLines.vdx

The result of executing WriteLines.vdx is

One
Two
Three
four

The implementation of the DiagramsCanExecuteToo application is shown in Listing 5.6.

Listing 5.6. The DiagramsCanExecuteToo Application

using System;
using System.Threading;
using System.Workflow.Runtime;
using System.Workflow.Runtime.Hosting;
using System.Xml;

namespace DiagramsCanExecuteToo
{
  class Program
    {
      static void Main(string[] args)
      {
        using (WorkflowRuntime runtime = new WorkflowRuntime())

        AutoResetEvent waitHandle = new AutoResetEvent(false);
        runtime.WorkflowCompleted += delegate(object sender,
           WorkflowCompletedEventArgs e)
        {
          waitHandle.Set();
        };

        if (args.Length != 1)
          throw new ArgumentException("expecting one vdx file");

        if (args[0].Contains(".vdx"))
          runtime.AddService(new VisioLoader());
        else
          throw new ArgumentException("we only support vdx files!");
        using (XmlReader reader = new XmlTextReader(args[0]))
        {
          WorkflowInstance instance = runtime.CreateWorkflow(reader);
          instance.Start();
          waitHandle.WaitOne();
        }
      }
    }
  }
}

The custom Visio loader service upon which our solution depends is shown in Listing 5.7. There is no XAML here, and no compilation of the WF program to a CLR type either. The program is loaded directly from the Visio file.

Custom Loader that Reads Visio Files

using System;
using System.IO;
using System.Workflow.ComponentModel;
using System.Workflow.Runtime.Hosting;
using System.Xml;

using Microsoft.Office.Interop.Visio;
using EssentialWF.Activities;

public class VisioLoader : WorkflowLoaderService
{
  protected override Activity CreateInstance(XmlReader
    workflowReader, XmlReader rulesReader)
  {
    // Visio OM requires a file on disk
    // We create a temp file from the XmlReader

    workflowReader.MoveToContent();
    while (!workflowReader.EOF && !workflowReader.IsStartElement())
      workflowReader.Read();

    string tempPath = Environment.CurrentDirectory + @"\temp.vdx";
    string text = workflowReader.ReadOuterXml();

    using (StreamWriter sw = new StreamWriter(tempPath))
    {
      sw.Write(text);
    }

    // Create Visio Application Object
    Application app = new Application();

    // Open the Visio document that we just created
    Document doc = app.Documents.OpenEx(tempPath,
      (short)VisOpenSaveArgs.visOpenRO |
      (short)VisOpenSaveArgs.visOpenHidden |
      (short)VisOpenSaveArgs.visOpenMinimized |
      (short)VisOpenSaveArgs.visOpenNoWorkspace
    );

    // Translate Visio shapes to activities
    Sequence seq = new Sequence();
    foreach (Shape shape in doc.Pages[1].Shapes)
    {
      if (shape.Master.Name == "WriteLine")
      {
        WriteLine wl = new WriteLine();

        Cell cell1 = shape.get_Cells("Prop.Row_1");
        wl.Text = cell1.get_ResultStr(null);

        Cell cell2 = shape.get_Cells("Prop.Row_2");
        wl.Name = cell2.get_ResultStr(null);

        seq.Activities.Add(wl);
      }
    }

    // Close the document, quit the Visio app
    doc.Close();
    app.Quit();

    // Delete the temp file
    File.Delete(tempPath);

    // Return the activity tree to the WF runtime
    return seq;
  }

  protected override Activity CreateInstance(Type workflowType)
  {
    throw new NotSupportedException("Packaging a visio file in a .NET
     assembly is not supported");
  }
}

Using the Visio automation model (API) works but is a bit heavyweight. Ideally, because Visio documents can be expressed as XML, one can envision writing a serializer (a class named VisioSerializer) that operates directly on this XML format, bypassing the automation API. With this improved design, the custom Visio loader service can simply delegate the job of materializing an Activity object from markup to the Visio serializer (just as the DefaultWorkflowLoaderService delegates work to WorkflowMarkupSerializer, which knows how to parse XAML). In Chapter 7, we will discuss custom serializers for activities.

Even our simplistic Visio loader demonstrates that the WF runtime is truly format agnostic. The currency of the WF runtime is in-memory trees of activitiesactual CLR objects. What this means is that the WF runtime does not care how you choose to author a WF program so long as a custom loader implementation can translate your representations of WF programs to activities. This opens up avenues for directly executing programs of all kinds that are written in domain-specific languages.



 Python   SQL   Java   php   Perl 
 game development   web development   internet   *nix   graphics   hardware 
 telecommunications   C++ 
 Flash   Active Directory   Windows