Compilation






Compilation

As mentioned earlier, it is not necessary to compile all WF programs. Only some kinds of WF programs require packaging as a type in a CLR assembly. To be specific, WF programs that are specified using code, as well as WF programs whose XAML uses the x:Class attribute (these are overlapping, but distinct, categories), must be compiled using the WF program compiler. The constructor of the type resulting from compilation can then be used, on demand, to realize the tree of activities that constitutes the WF program. The WF program compiler is available programmatically as the WorkflowCompiler class, which is the central type in the System.Workflow.ComponentModel.Compiler namespace. Listing 7.17 shows that WorkflowCompiler is a simple class, containing just a single Compile method.

Listing 7.17. WorkflowCompiler

namespace System.Workflow.ComponentModel.Compiler
{
  public sealed class WorkflowCompiler
  {
    public WorkflowCompiler();

    public WorkflowCompilerResults Compile(
      WorkflowCompilerParameters parameters,
      params string[] files);
  }
}

Included with the WF SDK is an executable, wfc.exe, which provides the same functionality as WorkflowCompiler only at the command line. The choice of when to use WorkflowCompiler and when to use wfc.exe is entirely up to you.

The WF program compiler is a simple class. Its purpose is to accept one or more source files as input, ensure the validity of the WF programs that are defined in these files, and produce CLR types that package these program definitions. There are two kinds of files passed as parameters to the WF program compilercode files and XAML files. There can be any number of either kind of file. Code files must contain either C# or Visual Basic code. Other languages may be supported in future versions of WF.

The root element of all XAML files presented to the WF program compiler must map to a CLR type that derives from Activity. Additionally, each XAML document must use the x:Class attribute in order to specify the namespace and name of the type that will be produced by compilation. There is no analogous restriction on code files; you are free to define types in code that are not WF programs and they will become a part of the assembly or code compile unit (CCU) that results from WF program compilation.

The process of WF program compilation includes the following steps:

1.
XAML deserialization and code generation

2.
Activity validation

3.
Activity code generation

4.
Code compilation

After an activity tree is realized in memory from the source files of a WF program, the WF program compiler uses the WF validation infrastructure, discussed in the previous section, to perform validation of the program. The WF program compiler relies upon language compilers (the C# and Visual Basic compilers) to perform the final step in the compilation process, which produces the assembly or code compile unit.

Compiler Parameters

The WorkflowCompilerParameters class, shown in Listing 7.18, carries properties that can be set in order to influence the WF program compilation process. The Compile method of WorkflowCompiler accepts a parameter of type WorkflowCompilerParameters.

Listing 7.18. WorkflowCompilerParameters

namespace System.Workflow.ComponentModel.Compiler
{
  public sealed class WorkflowCompilerParameters : CompilerParameters
  {
    public WorkflowCompilerParameters();
    public WorkflowCompilerParameters(string[] assemblyNames);
    public WorkflowCompilerParameters(string[] assemblyNames,
      string outputName);
    public WorkflowCompilerParameters(string[] assemblyNames,
      string outputName, bool includeDebugInformation);

    public string CompilerOptions { get; set; }
    public bool GenerateCodeCompileUnitOnly { get; set; }
    public string LanguageToUse { get; set; }
    public StringCollection LibraryPaths { get; }
    public IList<CodeCompileUnit> UserCodeCompileUnits { get; }
  }
}

WorkflowCompilerParameters derives from the CompilerParameters class in the System.CodeDom.Compiler namespace. All properties that are inherited from CompilerParameters are available to help you manage the compilation of WF programs.

Among the more frequently used properties inherited from CompilerParameters are OutputAssembly, which defines the location and name of the assembly that results from successful compilation, and ReferencedAssemblies, which is a collection of strings specifying the locations of assemblies referenced by the WF program code being compiled.

Compilation Base Classes

Please consult .NET Framework documentation for descriptions of the CompilerParameters, CompilerResults, and CompilerError classes, which are part of the System.CodeDom.Compiler namespace.


The LanguageToUse property specifies the language used in any code files being compiled. The value must be either "VB" or "CSharp" in the current version of WF; it is taken as "CSharp" by default. The LibraryPaths property specifies the set of paths on the file system that should be searched for assemblies that are referenced only by name.

The UserCodeCompileUnits property specifies additional compilation inputs, in the form of System.CodeDom.CodeCompileUnit objects. The GenerateCodeCompileUnitOnly property indicates whether the WF program compiler should suppress the production of an assembly and only produce a CodeCompileUnit as the result of WF program compilation.

Compiler Results

The results of WF program compilation are returned as a WorkflowCompilerResults object. Not surprisingly, WorkflowCompilerResults (shown in Listing 7.19) inherits from the CompilerResults class that is defined in the System.CodeDom.Compiler namespace.

Listing 7.19. WorkflowCompilerResults

namespace System.Workflow.ComponentModel.Compiler
{
  public sealed class WorkflowCompilerResults : CompilerResults
  {
    CodeCompileUnit CompiledUnit { get; }
  }
}

The CompiledUnit property will contain the result of WF program compilation (as a CodeCompileUnit). If the GenerateCodeCompileUnitOnly property is set to true in the WorkflowCompilerParameters object used for compilation, an assembly is not produced and the code compile unit is the only tangible result of a successful compilation.

The Errors property, inherited from CompilerResults, will contain a set of zero or more WorkflowCompilerError objects that represent errors or warnings that resulted from WF program compilation. WorkflowCompilerError is a simple class. Its most useful properties are inherited from CompilerError. Inherited properties include IsWarning, FileName, Line, Column, ErrorNumber, and ErrorText. Collectively, they provide information about a specific warning or error that has occurred during compilation.

Validation and Compilation

Now we can try to compile an invalid WF program to confirm that activity validation occurs automatically during the compilation process. The following WF program is invalid because it uses the activity name "write1" twice:


<Sequence x:Class="Chapter7.Workflow1" x:Name="Workflow1" xmlns="http://EssentialWF
/Activities" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <WriteLine x:Name="write1" Text="hello" />
  <WriteLine x:Name="write1" Text="goodbye" />
</Sequence>

A simple console application, shown in Listing 7.20, can be used to programmatically compile our WF program.

Programmatic WF Program Compilation

using System;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Compiler;

namespace Ch7
{
  class Program
  {
    static void Main()
    {
      WorkflowCompiler compiler = new WorkflowCompiler();
      WorkflowCompilerParameters parameters =
        new WorkflowCompilerParameters(
          new string[] { "EssentialWF.dll" },
          "test.dll"
        );

      WorkflowCompilerResults results = compiler.Compile(
        parameters, "test.xoml");

      for (int i = 0; i < results.Errors.Count; i++)
        Console.WriteLine(results.Errors[i].ToString());
    }
  }
}

The output of running the console application of Listing 7.20 reports a validation error due to the use of a duplicate activity name:

test.xoml : error WF1538: Activity 'write1' validation failed: There is already an
activity named 'write1'. Activity names must be unique.

Let's return now to our While composite activity, which validates that exactly one child activity is present. To simplify things, let's assume now that we've changed the definition of While so that its Expression property is not required (and is presumed by While to take a value of true if not present). We can try to compile the While source code using wfc.exe:

>wfc.exe While.cs
Microsoft (R) Windows Workflow Compiler version 3.0.0.0
Copyright (C) Microsoft Corporation 2005. All rights reserved.

The compiler generated the following message(s):

error WF7002: Activity 'While' validation failed: While must have exactly one
child activity

Compilation finished with 0 warning(s), 1 error(s).

As we can see from the output, the compilation fails. This must mean that the validator component associated with While has been invoked. Validation, of course, fails because the Activities collection of a newly instantiated While object is empty. For this reason, composite activity types such as While can be compiled using the language compilerin this case, the C# compiler.

Now, all we are really saying with the previous experiment is that While is not a valid WF program on its own. Nor was it ever intended to be. In fact, if we try to load a WF program (expressed in XAML) that consists entirely of an empty While activity, we will see exactly the same validation failure because the default loader service invokes validation as part of its loading of a WF program.

Activity Code Generation

Now that we understand WF program compilation, we can discuss how activities can participate in the compilation process (beyond validation). Activities can generate code that will become part of the assembly or code compile unit that results from compilation. The use cases for code generation are much narrower than those for validation, so it is fair to consider this an advanced feature that can be safely ignored by the majority of activity developers.

As with validation, custom code generation is an opt-in model in which you first need to associate a code generator component with an activity by decorating the activity class with the ActivityCodeGeneratorAttribute, as shown here:

[ActivityCodeGenerator(typeof(MyActivityCodeGenerator))]
public class MyActivity : Activity
{
  ...
}

public class MyActivityCodeGenerator : ActivityCodeGenerator
{
  public override void GenerateCode(CodeGenerationManager manager,
    object obj)
  {
    ...
  }
}

An activity code generator component will always derive from ActivityCodeGenerator or, in the case of a composite activity, CompositeActivityCode-Generator. The ActivityCodeGenerator class contains a single public method, and is shown in Listing 7.21. CompositeActivityCodeGenerator simply derives from ActivityCodeGenerator and overrides the GenerateCode method in order to recursively involve all enabled child activities in the code generation process.

Listing 7.21. ActivityCodeGenerator

namespace System.Workflow.ComponentModel.Compiler
{
  public class ActivityCodeGenerator
  {
    public ActivityCodeGenerator();

    public virtual void GenerateCode(CodeGenerationManager manager,
      object obj);
  }
}

ActivityCodeGenerator defines a single public virtual method called GenerateCode. It is in this method that you will be allowed to generate code for an activity. If you are unfamiliar with the types in the System.CodeDom namespace, you will need to consult .NET Framework documentation in order to proceed without puzzlement.

Essentially, the code generator component of an activity can obtain a reference to the CodeTypeDeclaration for the WF program being compiled, and add new CodeTypeMembers to it. Let's dig into this with an example. Suppose we wanted the ability to declare local variables in markup, much as we can do in code like this:

 private string s;

This will give us an avenue for pursuing "markup only" WF programs (though it will force us to compile these programs in order for code generation to occur). So, let us begin by creating a variant of the Sequence activity (call it SequenceWithVars) that supports declarative variables. We will then define a code generator component for SequenceWithVars that generates fields or properties within the compiled type representing a WF program.

First let's quickly define a type that can act as a variable declaration within markup:

public class VariableDeclaration : DependencyObject
{
  // Implementations use metadata dependency properties
  public string Name { ... }
  public Type Type { ... }
  public bool IsProperty { ...}

  public VariableDeclaration() { }
  public VariableDeclaration(string name, Type type, bool isProperty)
    :base()
  {
    this.Name = name;
    this.Type = type;
    this.IsProperty = isProperty;
  }
}

VariableDeclaration conceptually allows us to define the equivalent of a CLR field using markup.

 <VariableDeclaration Name="s" Type="{x:Type System.String}" />

Listing 7.22 shows how we can allow the SequenceWithVars activity to support a list of declarative variables, modeled as a property.

Sequence that Supports Declarative Variables


using System;
using System.CodeDom;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Compiler;

namespace EssentialWF.Activities
{
  [ActivityCodeGenerator(typeof(SequenceWithVarsCodeGenerator))]
  public class SequenceWithVars : CompositeActivity
  {
    public static readonly DependencyProperty VariableDeclsProperty
      = DependencyProperty.Register(
        "VariableDecls",
        typeof(List<VariableDeclaration>),
        typeof(SequenceWithVars),
        new PropertyMetadata(
          DependencyPropertyOptions.Metadata |
          DependencyPropertyOptions.ReadOnly,
          new Attribute[] { new DesignerSerializationVisibilityAttribute
(DesignerSerializationVisibility.Content)}
        }
      );

    public SequenceWithVars()
      : base()
    {
      base.SetReadOnlyPropertyValue
          (SequenceWithVars.VariableDeclsProperty,
           new List<VariableDeclaration>());
    }

    [DesignerSerializationVisibility
       (DesignerSerializationVisibility.Content)]
    public List<VariableDeclaration> VariableDecls
    {
      get
      {

      return base.GetValue(
        SequenceWithVars.VariableDeclsProperty)
          as List<VariableDeclaration>;
      }
    }
  }
}

Now we can turn our attention to the code generator component for SequenceWithVars. We override the GenerateCode method and, based upon the collection of variable declarations associated with the SequenceWithVars being compiled, we generate the appropriate private fields and public variables. This is shown in Listing 7.23.

Listing 7.23. Code Generator Component for Sequence


public class SequenceWithVarsCodeGenerator :
CompositeActivityCodeGenerator
  {
    public override void GenerateCode(
      CodeGenerationManager manager, object obj)
    {
      base.GenerateCode(manager, obj);

      SequenceWithVars s = obj as SequenceWithVars;
      CodeTypeDeclaration codeTypeDecl =
        this.GetCodeTypeDeclaration(manager,
          s.GetType().FullName);

      foreach (VariableDeclaration decl in s.VariableDecls)
      {
        if (decl.IsProperty)
        {
          CodeMemberField field =
           new CodeMemberField(decl.Type, "_" + decl.Name);
          field.Attributes = MemberAttributes.Private;

          CodeMemberProperty prop = new CodeMemberProperty();
          prop.Name = decl.Name;
          prop.Type = new CodeTypeReference(decl.Type);
          prop.Attributes = MemberAttributes.Public | MemberAttributes.Final;

          prop.GetStatements.Add(new CodeMethodReturnStatement(new
 CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "_"+ decl.Name)));
          prop.SetStatements.Add(new CodeAssignStatement(new CodeFieldReferenceExpression
(new CodeThisReferenceExpression(), "_"+ decl.Name), new CodeProperty

SetValueReferenceExpression()));

          codeTypeDecl.Members.Add(field);
          codeTypeDecl.Members.Add(prop);
        }
        else // field only
        {
        CodeMemberField field = new CodeMemberField(
          decl.Type, decl.Name);
        field.Attributes = MemberAttributes.Private;

        codeTypeDecl.Members.Add(field);
      }
    }
  }
}

Now we can run a simple example. Here is some markup that is valid based upon the types we have defined:

<SequenceWithVars x:Class="Workflow2" xmlns="http://EssentialWF/Activities"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <SequenceWithVars.VariableDecls>
    <VariableDeclaration Name="StringProp" Type="{x:Type System.String}"
IsProperty="True" />
    <VariableDeclaration Name="n" Type="{x:Type System.Int32}" IsProperty="False" />
  </SequenceWithVars.VariableDecls>
</SequenceWithVars>

Compilation of this markup yields a CLR type with one field and one property, the code for which was created by the code generator component of SequenceWithVars:

public partial class Workflow2 : SequenceWithVars
{
  private int n;
  private string stringProp;

  public string StringProp
  {
    get
    {
      return this.stringProp;
    }
    set
    {
      this.stringProp = value;
    }
  }

  public Workflow2() { ... }

  ...
}

Perhaps the strongest use case for activity code generators is a situation where compilation-time code generation can avoid reflection at runtime (during the execution of the activity). You won't come across this circumstance every day, but when you do you'll benefit from the code generation capability of activities.



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