Using XAML to Define Activity Types


Thus far in our examples, we have drawn a crisp distinction between WF programs and activity types. What happens, though, if we want to reuse the logic of a WF program in a larger WF program?

If we tackle this problem in the context of a C# program, the result might look something like this:

 using System; public class Echo {   public void DoEcho()   {     string s = Console.ReadLine();     Console.WriteLine(s);   } } class Program {   static void Main()   {     Echo echo1 = new Echo();     echo1.DoEcho();     ...     Echo echo2 = new Echo();     echo2.DoEcho();   } } 


The echo functionality is factored out into a separate type, Echo. Now we can instantiate objects of type Echo and invoke their DoEcho method, as shown in the Main method of the preceding simple code snippet.

With what we know so far of the WF programming model, we are stuck. Here is our WF Echo program:

 <Sequence 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> 


As it stands, there is no way for us to reuse this logic as-is in a larger WF program. The problem is that activity declarations in WF programs are typed, and the preceding XAML is not defining a typeit is just an XML document that is a blueprint for an activity tree.

In fact, XAML supports the use case in which we are interested quite elegantly. The use of a single special XML attribute turns the WF Echo program into the definition of an activity type:

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


The Class attribute is defined in the special XML namespace for XAML constructs, which we have mapped (according to XAML convention) to the XML namespace prefix "x". Using the x:Class attribute in our XAML means that we are actually specifying

 namespace EssentialWF.Activities {   public class Echo : Sequence   {     ...   } } 


That's right, the XAML is now equivalent to a C# type definition. The namespace-qualified name of the type is "EssentialWF.Activities.Echo", which is the value of the Class attribute in the root element of the XAML document. The Echo type derives from Sequence, which is the activity type of the root element of the XAML.

There are many interesting details and implications of the underlying mechanics of this approach; we will explore these in just a moment. But first, let's appreciate the fact that this solution gives us precisely the capability we were seeking. We can now write another WF program that uses the Echo activity type, even though Echo was defined in XAML:

 <Sequence x:Name="s1" xmlns="http://EssentialWF/Activities" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">   <Echo x:Name="echo1" />   <Echo x:Name="echo2" /> </Sequence> 


One implication of the preceding XAML snippet is that each of the two Echo activities has exactly the same substructureboth contain a ReadLine followed by a WriteLine. As a consequence, the value of the Activity.Name property for the two ReadLine activities is the same. Because of this use case, activities cannot assume that the value of their Name property is unique within a WF program. Name is only guaranteed to be unique among the immediate child activities of a given composite activity. This is a problem for the ReadLine activity because our current implementation of ReadLine uses the value of its Name property as the name of its WF program queue.

The Activity.QualifiedName property can help us. Activity.QualifiedName uniquely identifies each activity within a WF program. The qualified name of an activity is a computed value (the QualifiedName property has no setter). This value is a concatenation of the Name of the activity and the values of the Name properties of all activity name scopes in the activity's chain of parent activities (with a '.' character separating each name). An activity name scope is a composite activity whose type definition specifies one or more child activities. Echo is an activity name scope, but Sequence is not (because a new Sequence() has no child activities but a new Echo() does). Thus, in the preceding XAML snippet, the two ReadLine activities have the qualified names "echo1.r1" and "echo2.r1". We can now refer to them unambiguously, and we can update the implementation of ReadLine to use the value of QualifiedName as the name of its bookmark (WF program queue).

The first WF Echo program that we wrote, which did not use the XAML x:Class attribute, is an artifact from which a WF program prototype can be directly loaded. When we changed our WF program by adding the x:Class attribute, though, we changed the meaning of the XAML. Rather than simply representing a tree of activities, the updated XAML represents the specification of a new activity type.

A XAML document that defines a new activity type is compiled using the WF program compiler. The WF program compiler is available programmatically as the WorkflowCompiler type (details of which will be discussed later in this chapter) or by using wfc.exe, which is a command-line wrapper around WorkflowCompiler that is available as part of the WF SDK.

The WF program compiler accepts a XAML document, turns it into the equivalent C# code (or VisualBasic.NET code), and then passes this code to the C# (or VisualBasic.NET) compiler. The result of compilation is, of course, an assembly.

Let's go ahead and compile our WF Echo program, which is shown in Listing 7.6, and which we will assume resides in a file "echo.xoml":

  wfc /r:EssentialWF.dll echo.xoml 


Listing 7.6. WF Echo Program in Echo.xoml

 <Sequence x: 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> 


XAML File Extensions

WF uses ".xoml" as the file extension for XAML files that represent WF programs. This allows applications and tools to easily distinguish between XAML files that have meaning in WF, and XAML files that are meant for use in other frameworks such as WPF.


The "/r:EssentialWF.dll" command-line parameter indicates an assembly reference to the assembly that contains the WriteLine, ReadLine, and Sequence activity types.

To see descriptions of all wfc.exe options, you can type the following:

  wfc /help 


The result of compilation is a new assembly, echo.dll. If we examine this assembly, we will see that it contains compiled code for a single type, Echo, that has been defined like this:

 namespace EssentialWF.Activities {   public class Echo : Sequence   {     private ReadLine r1;     private WriteLine w1;     public Echo()     {       InitializeComponent();     }     private void InitializeComponent()     {       this.CanModifyActivities = true;       this.w1 = new WriteLine();       this.w1.Name = "w1";       this.r1 = new ReadLine();       this.r1.Name = "r1";       ActivityBind bind1 = new ActivityBind();       bind1.Name = "r1";       bind1.Path = "Text";       this.w1.SetBinding(WriteLine.TextProperty, bind1);       this.Activities.Add(this.r1);       this.Activities.Add(this.w1);       this.Name = "Echo";       this.CanModifyActivities = false;     }   } } 


As you can see, the constructor logic of Echo builds the same tree of activities that we had represented earlier in XAML. The Echo type, in the assembly echo.dll, is a WF program blueprintit is just a different form of packaging for the representation of our activity tree!

There are a few things to observe about the Echo type:

  • The logic to construct the activity tree is factored into a private method called InitializeComponent, which is called by the WriteLine constructor. This is merely a convention that is also followed in other programming models such as Windows Forms. There are situations in which the Visual Studio Workflow Designer relies upon this convention, so it is good practice to follow it.

  • The CanModifyActivities property controls the window of time in which it is legal to add child activities to a composite activity.

The default WF program loader recognizes both XAML documents (that do not use the x:Class attribute) and compiled types that derive from Activity as valid WF program blueprints. Thus, any activity type, packaged in an assembly, will be recognized by the default WF program loader as a WF program blueprint. Hence, even the WriteLine type is a WF program blueprint; if you point the default WF program loader at this blueprint, it will load a WF program prototype (an activity tree) that has exactly one nodea WriteLine activity.

From the preceding discussion, we can infer that compilation of XAMLat a high levelinvolves conversion of XAML to code, and then compilation of the code using a language compiler. This is indeed the case. A WF program that is expressed in XAML can be deserialized into an in-memory tree of activity objects. This tree of activities can also be serialized back into XAML. The WorkflowMarkupSerializer type, which is shown in Listing 7.7 and is defined in the System.Workflow.ComponentModel.Serialization namespace, allows any object (not just any activity object) to be serialized to and deserialized from XAML.

Listing 7.7. WorkflowMarkupSerializer

 namespace System.Workflow.ComponentModel.Serialization {   public class WorkflowMarkupSerializer   {     public WorkflowMarkupSerializer();     public object Deserialize(XmlReader reader);     public void Serialize(XmlWriter writer, object obj);     /* *** other members *** */   } } 


Using WorkflowMarkupSerializer, it is easy to programmatically create the XAML for the WF Echo program we earlier wrote by hand:

 using System; using System.Xml; using System.Text; using System.Workflow.ComponentModel; using System.Workflow.ComponentModel.Serialization; using EssentialWF.Activities; class Program {   static void Main()   {     ReadLine read = new ReadLine();     read.Name = "r1";     WriteLine write = new WriteLine();     write.Name = "w1";     ActivityBind bind = new ActivityBind();     bind.Name = "r1";     bind.Path = "Text";     write.SetBinding(WriteLine.TextProperty, bind);     Sequence seq = new Sequence();     seq.Activities.Add(read);     seq.Activities.Add(write);     using (XmlWriter writer = new XmlTextWriter("echo.xoml",       System.Text.Encoding.Default))     {       WorkflowMarkupSerializer serializer =         new WorkflowMarkupSerializer();       serializer.Serialize(writer, seq);     }   } } 


Later in this chapter, we will get a much closer look at how activity serialization unfolds, and how serialization to and from custom formats is accommodated by the WF programming model.

Those of you who are already familiar with WF may have been wondering when we would get around to explaining what is sometimes known as code-beside. We left this topic aside (so to speak) because the association of code with WF programs is not central to the WF programming model, but is merelyas you probably now realizea WF program-authoring convenience.

Because a WF program can be defined as a type (textually as C# or VisualBasic.NET code or as XAML that uses x:Class), there is nothing stopping us from adding additional custom code to this type. From there it is a simple enhancement to have activities (within a running WF program instance) raise events that are handled by this custom code.

We don't want the presence of such custom code to prevent us from authoring WF programs in XAML. To meet this requirement, we will rely upon a feature of the C# and VisualBasic.NET compilers called partial types. Partial types allow the definition of a type (such as a WF program) to be split across multiple physical files, such as two different C# source files or a C# source file and a XAML file. In the case of WF, the sweet spot is to allow an activity tree to be specified as XAML (utilizing the x:Class attribute) while allowing custom code, which accompanies that XAML, to reside in a separate C# or VisualBasic.NET source file.

Let's enhance our WriteLine activity so that it raises an event, by adding the following event declaration:

  public event System.EventHandler BeforeWrite; 


We express the subscription to this event in the XAML of our WF Echo program like so:

  <WriteLine BeforeWrite="OnBeforeWrite" /> 


We handle the BeforeWrite event in code, where we assign the value of the WriteLine.Text property:

 using System; using System.Workflow.ComponentModel; namespace EssentialWF.Activities {   public partial class Echo : Sequence   {     void OnBeforeWrite(object sender, EventArgs e)     {       WriteLine write = sender as WriteLine;       CompositeActivity parent = write.Parent;       ReadLine read = parent.GetActivityByName("r1") as ReadLine;       write.Text = read.Text;     }   } } 


There are a few things to observe about the preceding XAML and code snippets:

  • The Echo class is declared using the partial type modifier.

  • The sender parameter to the OnBeforeWrite method is the activity objectin this case, the WriteLine activity instancethat is raising the event. This is a convention that should be adhered to by the execution logic of all activity types that raise events.

  • The WriteLine activity declaration (in XAML) no longer specifies an activity databinding expression for the Text property. The logic of OnBeforeWrite takes its place, and achieves exactly the same result.

Because our WF program is now represented in two different files, we must provide both of these files to the WF program compiler:

  wfc /r:EssentialWF.dll echo.xoml echo.cs 


Figure 7.3 depicts the WF compilation process. First, the XAML plus either a C# or VisualBasic.NET partial class definition are merged into a single type within a temporary assembly managed by the WF program compiler. This temporary type is validated (a topic discussed later in this chapter) and then there is a special WF-specific code generation step (also a topic discussed later in this chapter). Finally, the C# or VisualBasic.NET language compiler is asked to produce an assembly.

Figure 7.3. WF program compilation


As a final demonstration of the equivalence of XAML and code as specifications of a WF program, we can represent our new version of the WF Echo program entirely as code:

 using System; using System.Workflow.ComponentModel; namespace EssentialWF.Activities {   public class Echo : Sequence   {     public Echo()     {       InitializeComponent();     }     private void InitializeComponent()     {       this.CanModifyActivities = true;       WriteLine write = new WriteLine();       write.Name = "w1";       ReadLine read = new ReadLine();       read.Name = "r1";       this.Activities.Add(read);       this.Activities.Add(write);       write.BeforeWrite += this.OnBeforeWrite;       this.Name = "Echo";       this.CanModifyActivities = false;     }     void OnBeforeWrite(object sender, EventArgs e)     {       WriteLine w = sender as WriteLine;       CompositeActivity parent = w.Parent;       ReadLine r = parent.GetActivityByName("r1") as ReadLine;       w.Text = r.Text;     }   } } 


Our WF program is now represented in a single C# source file. We still compile it using the WF program compiler in order to ensure that proper validation of the WF program takes place before the handoff to the C# language compiler:

 wfc /r:EssentialWF.dll echo.cs 


As a WF program author, you can decide when to use just XAML, when to combine XAML with code, and when to develop WF programs entirely in code. Of course, as we learned in Chapter 5, we can also ignore all of this and declare WF programs using a custom domain-specific language of our choosing.




Essential Windows Workflow Foundation
Essential Windows Workflow Foundation
ISBN: 0321399838
EAN: 2147483647
Year: 2006
Pages: 97

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net