C H A P T E R  11

Image

Windows Workflow Foundation

Windows Workflow Foundation (WF) was one of the key features of the .NET 3.0 release along with WPF and WCF. Unlike WPF and WCF, Workflow Foundation never seemed to quite get wholehearted support from developers; however, WF has remained viable. A major overhaul in .NET 4.0 saw considerable improvements to the designer and overall performance as well as integration with WCF. The .NET 4.5 release builds on the 4.0 release by adding further functionality to the designer as well as the addition of new features, which stem from developers’ feedback.

In this chapter, we will focus on the new features in WF rather than provide a how-to guide. If you haven’t worked with WF before or haven’t looked at it for a while, a comprehensive set of resources can be found at http://msdn.microsoft.com/en-us/netframework/dd980559.

New Designer Features and Changes

I suspect that among developers, there has always been a small amount of resistance to the use of visual designers to code. Over the years, we have seen tools that provide variations on UML to code round-tripping, such as the Class Diagram in Visual Studio, but they never seem to achieve a high level of adoption among developers. Even when developing UI, the desire and tendency to hack the HTML or XAML directly is a strong one. There are probably many reasons for this but, in part, I suspect there is a sense that designer gets in the way of what we are trying to achieve. On the other hand, visualization is a great way to handle complexity, especially when developing workflows. To this end, a number of new features have been added to workflow’s visual designer to reduce the disconnect for developers.

C# Expressions

One of the frustrations that has existed for developers with WF has been the fact that no matter whether you created your workflow project in C# or VB.NET, expressions within the workflow could only be entered using VB. This use of VB for entering expressions was based on the idea that business users would be expected to modify workflows and that they would be coming from a Word/Excel background where the macro language is Visual Basic for Applications. Being limited to using VB has been a point of contention for developers, so with this release the WF team has now added C# expressions. In this version, a fully functional C# expression editor is provided including syntax highlighting, fully functional intellisense, and design-time validation. As a result, if you create a workflow project in VB, you will use VB expressions. On the other hand, if you create a C# project, you will use C# expressions. However, it is important to note that any C# workflow project created in Visual 2010 and opened in Visual Studio 2012 will still use VB expressions.

Something that is not obvious about the introduction of C# expressions, unless you look at the underlying XAML, is the introduction of two new classes: CSharpReference<TRresult> and CSharpValue<TResult>. These two classes can be found under the new namespace Microsoft.CSharp.Activities in System.Activities. When using VB expressions in the XAML, an expression would be delineated using []. For example, an Assign object would look like this:

<Assign sap:VirtualizedContainerService.HintSize="242,60">
  <Assign.To>
   <OutArgument x:TypeArguments="x:Int32">[r]</OutArgument>
  </Assign.To>
  <Assign.Value>
    <InArgument x:TypeArguments="x:Int32">[a + b + c]</InArgument>
  </Assign.Value>
</Assign>

Now if we implement the same Assign object using C# expressions, this is the resulting XAML:

<Assign>
  <Assign.To>
    <OutArgument x:TypeArguments="x:Int32">
      <mca:CSharpReference x:TypeArguments="x:Int32">r</mca:CSharpReference>
    </OutArgument>
  </Assign.To>
  <Assign.Value>
    <InArgument x:TypeArguments="x:Int32">
      <mca:CSharpValue x:TypeArguments="x:Int32">a + b + c</mca:CSharpValue>
    </InArgument>
  </Assign.Value>
  <sap2010:WorkflowViewState.IdRef>Assign_1</sap2010:WorkflowViewState.IdRef>
</Assign>

Given the introduction of these new classes, it would seem logical that if you are using imperative code to author activities and workflows, you could use the classes instead of the Visual Basic expression evaluators. For instance, in the code sample below, we have replaced all the instances of VisualBasicValue and VisualBasicReference with their C# equivalents. Unfortunately, if you run this code, it will fail.

static void Main(string[] args)
{
    Activity wf = new Sequence
{
    Variables =
    {
        new Variable<int> { Name = "a", Default= 1 },
        new Variable<int> { Name = "b", Default = 2 },
        new Variable<int> { Name = "c", Default = 3 },
        new Variable<int> { Name = "r"}
    },
    Activities =
    {
      new Assign<int>
      {
        //Previously you would need to use the VB evaluators
        //To = new VisualBasicReference<int>("r"),
        //Value = new VisualBasicValue<int>(" a + b + c")
        To = new CSharpReference<int>("r"),
        Value = new CSharpValue<int>(" a + b + c")
      },
      new WriteLine
      {
       //Text = new VisualBasicValue<string>(""The number is "  + r.ToString()")
         Text = new CSharpValue<string>(""The number is "  + r.ToString()")
      }
    }
  };

  WorkflowInvoker.Invoke(wf);
  Console.ReadKey();
}

This code would fail because, unlike the Visual Basic expression activities, the C# versions require compilation to run and, therefore, you need to compile your workflow.

At this point, we have two options—revert to using VisualBasicValue and VisualBasicReference or compile the Sequence into a DynamicActivity and pass that to the WorkFlowInvoker instead.

If you are interested in using the second option to get the previous code example to run, first add the following method.

ImageNote  You will also need to add using statements for System.Activities.XamlIntegration, System.IO, and System.Xaml.

static Activity CompileToDynamicActivity(Activity activity, string builderName)
{
  var builder = new ActivityBuilder();
  builder.Name = builderName;

  builder.Implementation = activity;
  Activity compiledActivity;
  using(var stream = new MemoryStream())
  {
    XamlWriter xw = ActivityXamlServices.CreateBuilderWriter(new XamlXmlWriter(stream, new XamlSchemaContext()));
    XamlServices.Save(xw, builder);
    var settings = new ActivityXamlServicesSettings { CompileExpressions = true };
    stream.Position = 0;
    compiledActivity = ActivityXamlServices.Load(stream, settings);
  }
  return compiledActivity;
}

Once you have added this method, modify the line that invokes the workflow:

WorkflowInvoker.Invoke(CompileToDynamicActivity(wf, "CompiledActivity"));

All things being equal, the workflow should now successfully run. Whether there is any benefit to be gained from doing this or just sticking with Visual Basic implementation of the classes is really up to you.

Designer Search

Designer search is a small thing that makes working with complex workflows just that little bit easier. Both Quick Find (Ctrl +F or Edit Image Find and Replace Image Quick Find) and Find in Files (Ctrl + Shift + F or Edit Image Find and Replace Image Find in Files) will now search for keywords within the workflow designer. Search expressions will be matched against Activity, FlowNode, State and Transition objects, as well as custom flow-control items, variables, arguments, and expressions. With Find in Files, the search expression will match the content of the workflow files and if you double-click a result, you will be taken to the activity that contains the match in the workflow designer.

It should be noted that this functionality is only available within Visual Studio and not in a rehosted designer, nor is Replace supported.

Designer Annotations

To help you keep track of your thinking when working on a workflow, it is now possible to add annotations to activities, states, flowchart nodes, arguments, and variables. By right-clicking on any of these items and selecting Annotations in the context menu, you will be given a selection of annotation actions as shown in Figure 11-1.

Image

Figure 11-1. Annotations context menu

Clicking on Add Annotation will display an input window where you can type your annotation (see Figure 11-2). Once you have completed the annotation, an icon will be displayed in the right-hand side of the object's title bar indicating that there is an annotation associated with it (see Figure 11-3). Hovering the mouse over this icon will display the annotation.

Image

Figure 11-2. Adding an annotation

Image

Figure 11-3. Assign object with Annotation icon

The same process applies for adding an annotation to a variable or argument with the icon being displayed to the far right of the name once the annotation is added (see Figure 11-4).

Image

Figure 11-4. Caption added to an argument

When you add an annotation by default, it is undocked. If you dock it, the annotation text will be displayed as greyed text within the object (see Figure 11-5). If you select Show All Annotations from the context menu, all annotations will be docked; Hide All Annotations does the reverse.

Image

Figure 11-5. Docked annotation

Validation Changes

In previous versions, workflow validation was done as a foreground process, which could lead to the UI hanging during complex or time-consuming validations. With this version, validations now take place on a background thread so that UI is not blocked and is more responsive.

Another issue concerning validation was that validation errors, when building a workflow project, were not considered build errors. In .NET 4.5 validation errors will now cause the build to fail.

Pan Mode

Pan mode, a feature common in most graphic programs, has been implemented in the workflow designer. When enabled, this feature allows the developer to navigate a large workflow by clicking and dragging to move the visible portion. To activate pan mode, you can either click the button in the lower-right corner of the designer (Figure 11-6), or you can press and hold the space bar or the middle mouse button.

Image

Figure 11-6. Pan mode button

Multi-select

The means by which you can select multiple activities in the designer has been extended so that not only can you select them one at a time by holding down the Ctrl key, but you can also select an activity by dragging a rectangle around it. More importantly, multiple activity selections can now be dragged and dropped around the design surface, and they can be interacted with via the context menu. Please keep in mind that multi-select is not available when you are using pan mode.

Auto-surround with Sequence

Workflows and certain other containers, such as NoPersistScope and InvokeDelegate, can only contain a single root activity. Previously, if you wanted to add a second activity, you had to delete or cut the existing activity, add a new Sequence activity, re-add the first activity, and then finally add the second activity. With WF 4.5, you can now add a second activity and a new Sequence will be automatically created to contain the two activities.

Define and Consume ActivityDelegate Objects in the Designer

ActivityDelegate objects are used by activities to expose execution points that allow other parts of the workflow to interact with a workflow’s execution. Previously in WF4, utilizing these execution points required you to do a fair bit of coding, but with the newer version you can now define and consume activity delegates from within the designer. For a good example of this, see http://msdn.microsoft.com/en-us/library/hh305737(v=vs.110).aspx.

Working with the Rehosted Designer

If you are working with a rehosted designer in an application, this version of WF offers a couple of new features to manage what the user has access to in the designer.

Visibility of Shell Header Items

In .NET 4, you could only control what items were displayed in the shell bar at the bottom of the designer. Now you also have the ability to customize what items are displayed in the shell header. To modify what is displayed in the shell header, you need to access the DesignView and modify its WorkflowShellHeaderItemsVisibility property. The following method is a quick example of how to do it:

/// <summary>
/// Sets up the designer view for the workflow designer
/// </summary>
/// <param name="context">the work flow designer's editing Context</param>
private void SetDesignerView(EditingContext context)
{
  var designerView = context.Services.GetService<DesignerView>();
  if (designerView != null)
  {
      designerView.WorkflowShellHeaderItemsVisibility = ShellHeaderItemsVisibility.None;
  }
}

Then all you need to do is call this method, passing it the new workflow designer’s editing context, as this example shows:

private void AddDesigner()
{
  this.wd = new WorkflowDesigner();
  Grid.SetColumn(this.wd.View, 1);
  this.wd.Load(new Sequence());
  SetDesignerView(wd.Context);
  Grid1.Children.Add(this.wd.View);
}

When modifying the design view, make sure you do it after you have loaded an activity into the designer. If you do it before loading the activity, it will fail since at that point the context does not have a reference to the DesignView class in its set of services.

Opt-in for WF4.5 Features

By default, some of the new features in 4.5 are not enabled in the rehost designer in order to preserve backward compatibility. This was done to ensure existing applications don’t break when updating to the latest version and is the case even if you create a new application in .NET 4.5. For example, in Figure 11-7, you will see that there is no pan mode button in the shell bar and annotations are not listed in the context menu.

Image

Figure 11-7. Default rehosted designer

To include the 4.5 features in the rehosted designer, you will need to access the DesignerConfigurationService and either set its TargetFrameworkName property to the .NET 4.5 Framework, or, if you want more control, you can enable individual features. The following code snippet shows how to opt-in for the 4.5 features, but it disables multi-select drag and drop.

/// <summary>
/// Sets the target framework to 4.5.
/// </summary>
/// <param name="context">The workflow designer's editing context.</param>
private void SetFrameWorkTarget(EditingContext context)
{
  var dcs = context.Services.GetService<DesignerConfigurationService>();
  if (dcs != null)
  {
    dcs.TargetFrameworkName = new System.Runtime.Versioning.FrameworkName(".NET Framework, Version=4.5");
    dcs.MultipleItemsDragDropEnabled = false; //just because we can
  }
}

Then all you need to do is call this method after you have instantiated a new instance of the workflow designer.

private void AddDesigner()
{
this.wd = new WorkflowDesigner();
SetFrameWorkTarget(wd.Context);
...
}

Now if we re-run the application, you will see that the pan mode button is now available and annotations are now in the context menu (see Figure 11-8).

Image

Figure 11-8. Rehosted designer with 4.5 features

One thing to be aware of is that if you are going to modify the target framework or enable any of the 4.5 features, you need to do it before you load an activity. Otherwise, your application will throw exception. Also a small quirk, at least at the time of writing, is that if you just try to enable annotations, your application will error. This doesn’t appear to be the case when enabling any of the other features.

Additional Features

Here are a few other features that we won’t cover in detail but might be of interest to you:

  • View state information is now stored in a separate location in the XAML files, which should make it easier to remove if needed.
  • Opening the Document Outline Window (Ctrl + Alt + T) will display the components of a workflow in a tree-style outline view. Clicking on a node in this view will navigate to the corresponding activity in the designer and vice versa.
  • In the variable and argument designer, Delete is now a context menu item.

Activities

With the built-in activity library, little has changed or been added. In this release, some new capabilities have been added to Flowchart activities and a new container activity called NoPersistScope has been added.

The Flowchart activity now includes a new property called ValidateUnconnectedNodes. By default, this property is set to false. If set to true, any unconnected nodes will produce validation errors. The only other change is with the FlowSwitch and FlowDecision activities, which now have an editable DisplayName property. Though a small change, this will at least allow the activity designer to show more information about the activity’s purpose.

The NoPersistScope activity is designed to prevent a workflow from persisting if any of its child activities are executing. This is particularly useful when a workflow is accessing files or running a database transaction. Previously, the only way to manage this was by creating a NativeActivity that used a NoPersisthandle.

New Workflow Development Models

In this release, we see the full return of one workflow development model with the return of the state machine workflow and the introduction of contract-first workflow services.

State Machine Workflows

State machine workflows were originally included in Windows Workflow, but they were removed in the 4.0 release with the inclusion of flowchart workflows. They were then reintroduced in .NET 4.0.1 in the Microsoft .NET Framework 4 Platform Update1. With 4.5, state machine workflows are now included out of the box and have been updated with the following features:

  • the ability to set breakpoints on states
  • the ability to copy and paste transitions within the workflow designer
  • designer support for shared trigger transition creation

Contract-First Workflow Development

The contract-first workflow development tool has been introduced to provide better integration between web services and workflows. Essentially what it allows you to do is to define your contract in code first, and then the tool will automatically generate an activity template in the toolbox for the operations in the contract.

Let’s have a look at this in action:

  1. With Visual Studio open, select File Image New Project, select the WCF or Workflow node in the templates tree, and select the WCF Workflow Service Application template.
  2. Name the new project ProjectWorkflow and click OK.
  3. Right-click on the project in the Solution Explorer and select Add Image Interface, name the interface IProjectService, and click OK.
  4. If it is not open, open the IProjectService file, add using statements for System.ServiceModel and System.Runtime.Serialization, and then modify it as follows.

    ImageNote  The Project class has been included in the same file for convenience.

    [ServiceContract]
    public interface IProjectService
    {
      [OperationContract]
      Project AddNewProject(Project project);
      [OperationContract]
      int FindClient(string clientName);
    }

    [DataContract]
    public class Project
    {
      [DataMember]
      public int ProjectId { get; set; }
      [DataMember]
      public int ClientId { get; set; }
      [DataMember]
      public string ProjectName { get; set; }
      [DataMember]
      public DateTime StartDate { get; set; }
    }
  5. Once you have made the modifications, build the project.
  6. Again right-click on the project in the Solution Explorer and this time select Import Service Contract… . This will bring up a dialog box that will list all of the available service contracts. Drill down into the <Current Project> node, select IProjectService, and click OK. A dialog box will display informing you that the operation was successful.
  7. At this point, rebuild the project to get the generated activities to display in the toolbox (see Figure 11-9).
    Image

    Figure 11-9. Toolbox with newly added activities

  8. Next, open the file Service1.xamlx to display the workflow service in the designer.
  9. In the Properties window for the workflowservice, click the “…” button in the Implemented Contracts property.
  10. In the Type Collection Editor window that appears, click on Add new type. From the dropdown, select “Browse for Types….” Drill into the <Current Project> node, select IProjectService, and click OK.
  11. Delete the existing activities in the Sequence activity. Then from the toolbox, drag a FindClient_ReceiveAndSendReply and an AddNewProject_ReceiveAndSendReply activity onto the Sequence Service.   

If you modify the service contract at any point, the changes will not automatically be reflected in the designer or toolbox. To get the changes reflected in the designer, you will need to first open the Service Contracts folder in the project, right-click on the appropriate service, select Update Service Contract from the context menu, and then rebuild the project.

Workflow Versioning

In WF 4, there was no support for versioning workflows. If you wanted to version your workflows, you had to roll your own solution. Versioning becomes an issue when we start persisting long-running workflows. Say, for example, a workflow gets kicked off and then is persisted. If, before it gets reloaded, a new version of the activity is introduced, then when it is reloaded, it will more then likely fail.

To resolve this problem in 4.5, we have a new class to work with called WorkflowIdentity. This class provides the means within a workflow application to map a persisted workflow instance with its definition, and it is the basis for a number of new versioning features:

  • WorkflowApplication hosting can use the WorkflowIdentity to enable multiple versions of a workflow side-by-side. By loading persisted instances with the new WorkflowApplicationInstance class, the DefinitionIdentity can be used to determine the correct workflow to use for instantiating the workflow application.
  • The WorkflowServiceHost is now a multi-version host. This means that if a new version of a workflow service is deployed, new instances will be created with the new version while existing instances will run/complete under the previous version.
  • It is now possible to dynamically update the definition of a persisted workflow instance.

To get a taste for how workflow versioning works, we will look at how WorkflowIdentity can be used to load the correct version of a workflow and how a previously persisted workflow can be dynamically updated.

ImageNote  To use the SQL workflow Instance Store feature, you will need to create the database that is used for persisting workflows. To do this, copy the following code into a batch file and run from the command console:

sqlcmd -S(local) -d Master -Q "Create Database Persistence"
sqlcmd -S(local) -d Persistance -i
"C:WindowsMicrosoft.NETFrameworkv4.0.30319SQLenSqlWorkflowInstanceStoreSchema.sql"
sqlcmd -S(local) -d Persistance -i
"C:WindowsMicrosoft.NETFrameworkv4.0.30319SQLenSqlWorkflowInstanceStoreLogic.sql"

Side-by-Side Versioning

In this example, we are going to look at how versioning can be used to load the correct definition for a persisted workflow. The example is reasonably trivial and the activities are created completely in code purely for expediency and not as a requirement for versioning.

  1. To get started, create a new workflow console application, add references to System.Activities.DurableInstancing and System.Runtime.DurableInstancing, and delete the file workflow1.xaml.
  2. Add a new class and name it GetInput, add a using statement for System.Activities, and then implement the following code:
      public class GetInput<T> : NativeActivity<T>
      {
        [DefaultValue(null)]
        public string BookmarkName { get; set; }

        protected override bool CanInduceIdle
        {
          get { return true; }
        }

        protected override void Execute(NativeActivityContext context)
        {
          context.CreateBookmark(this.BookmarkName, Continue);
        }

        void Continue(NativeActivityContext context, Bookmark bookmark, object state)
        {
          Result.Set(context, state);
        }
      }
  3. Next, create some preliminary code in order to enable persistence and to create our test activities. Open the Program.cs file and add the following code:
    static void Main(string[] args)
    {
    //[Setup]

      var connectionString = @"Data Source=(local);Initial Catalog=Persistence;Integrated
    Security=True";
      var instanceStore = new SqlWorkflowInstanceStore(connectionString);
      var bookmarkId = "ReadLine";

      //[Step 1] start up version 1 of the activity and persist it.
      var application = new WorkflowApplication(CreateActivityV1( bookmarkId));
      application.InstanceStore = instanceStore;
      var instanceId = ForcePersistance(application);

      //[Step 2]
      
    // [Step 3] Finish
      Console.WriteLine(" Press any key to exit");
      Console.ReadKey();

    }

    /// <summary>
    /// Runs the application so that the workflow is persist.
    /// </summary>
    /// <param name="application">The workflow application.</param>
    /// <returns>The instance id. </returns>
    private static Guid ForcePersistance(WorkflowApplication application)
    {
      var syncEvent = new AutoResetEvent(false);
      application.PersistableIdle = (e) => PersistableIdleAction.Unload;
      application.Unloaded = (e) =>
        {
          syncEvent.Set();
          Console.WriteLine("[Application unloaded]");
        };

      application.Run();
      syncEvent.WaitOne();

      return application.Id;
    }

    /// <summary>
    /// Creates version 1 of the test activity
    /// </summary>
    /// <param name="bookmarkId">the bookmark name used by GetInput</param>
    /// <returns>A Sequence activity</returns>
    private static Activity CreateActivityV1(string bookmarkId)
    {
      var activity = new Sequence
      {
        DisplayName = "Activity 1",
        Variables = { new Variable<string>("Name"),
                      new Variable<string>("Message","Hi {0} from activity 1")
                    },
        Activities =
          {
            new WriteLine{Text="Please enter your name"},
            new GetInput<string>
            {
              BookmarkName = bookmarkId,
              DisplayName = "ReadLine",
              Result = new OutArgument<string>( new VisualBasicReference<string>("Name"))
            },
            new WriteLine
            {
              Text = new InArgument<string>(new VisualBasicValue<string>("string.Format(Message,Name)"))
            }     
          }
      };

      return activity;
    }

    /// <summary>
    /// Creates version 2 of the test activity
    /// </summary>
    /// <param name="bookmarkId">The bookname for GetInput</param>
    /// <returns>A Sequence activity</returns>
    private static Activity CreateActivityV2(string bookmarkId)
    {
      var activity = new Sequence
      {
        DisplayName = "Activity 2",
        Variables = { new Variable<string>("Name"),
                      new Variable<string>("Message","Hi {0} from activity 2")
                    },
        Activities =
        {
          new WriteLine{Text="Please enter your name"},
          new GetInput<string>
          {
            BookmarkName = bookmarkId,
            DisplayName = "ReadLine",
            Result = new OutArgument<string>( new VisualBasicReference<string>("Name"))
          },
          new If
          {
            Condition = new InArgument<bool>(new VisualBasicValue<bool>("String.IsNullOrEmpty(Name)")),
            Then = new WriteLine{Text="You have no name."},
            Else = new WriteLine
                {
                  Text = new InArgument<string>(new VisualBasicValue<string>("string.Format(Message,Name)"))
                }
          }
        }
      };

    return activity;
    }
  4. To make sure that everything is working, add some code that will load the persisted activity and complete it. Immediately after [Step 2], add this code:
    var instance = WorkflowApplication.GetInstance(instanceId, instanceStore);
    application = new WorkflowApplication(CreateActivityV1(bookmarkId));
    application.InstanceStore = instanceStore;
    var workflowCompleted = new AutoResetEvent(false);
    application.Completed = e => workflowCompleted.Set();

    try
    {
      application.Load(instance);
      string input = Console.ReadLine();
      var result = application.ResumeBookmark(bookmarkId, input);

      workflowCompleted.WaitOne();
      application.Unload();
    }
    catch (Exception ex)
    {
      Console.WriteLine(ex.Message);
    }
  5. Run the application at this point, and you should get something similar to Figure 11-10.
Image

Figure 11-10. Output after running activity

At this point, if you are curious to see what happens when you load a persisted workflow into an updated activity, modify the instantiation of the workflow application in the previous step to the following:

application = new WorkflowApplication(CreateActivityV2(bookmarkId));

Up to this point, the code we have written basically reflects the state of play in WF 4. Now we will introduce the WorkflowIdentity and see how it allows us to manage multiple workflow versions.

  1. First, we need to version the workflow that is being persisted. To do this, we will create an instance of WorkflowIdentity and add it the constructor of the workflow application by adding and modifying the code at [Step 1] as follows:
    var identityV1 = new WorkflowIdentity("SampleActivity", new Version(1, 0, 0, 0), null);
    var application = new WorkflowApplication(CreateActivityV1( bookmarkId),identityV1);
  2. For convenience, we add a collection of workflows using a Dictionary<> object. Just before [Step 2], add the following code:
    var workflows = new Dictionary<WorkflowIdentity, Activity>
      {
        {new WorkflowIdentity("SampleActivity", new Version(1, 0, 0, 0), null),
         CreateActivityV1(bookmarkId)
        },
       {new WorkflowIdentity("SampleActivity", new Version(2, 0, 0, 0), null),
        CreateActivityV2(bookmarkId)
       }
    };
  3. Finally, we will get the identity of the persisted activity from the WorkflowApplicationInstance and use it to select the activity we need to use from our workflow collection. In [Step 2], modify the first two lines as follows:
var instance = WorkflowApplication.GetInstance(instanceId, instanceStore);
var definitionIdentity = instance.DefinitionIdentity;
var activityToLoad = workflows[definitionIdentity];
application = new WorkflowApplication(activityToLoad,definitionIdentity);

Now if you run the application, it should all work as advertised.

Dynamically Updating a Workflow

There are cases when you need to update the workflow definition of a persisted workflow. This can be achieved by using the new DynamicUpdateServices class to create a DynamicUpdateMap, which can then be applied to a persistence instance when it is loaded. Following are the four steps involved in creating and applying an update map:

  1. Prepare the workflow definition for update. This is done by calling DynamicUpdateServices.PrepareForUpdate() with the current workflow. This method validates the workflow and then processes the workflow tree, identifying all the objects that need to be tagged for comparison with the updated workflow definition. Once this is done, the tree is cloned and attached to the original workflow definition.
  2. Modify the workflow definition with the changes required. You cannot apply a new workflow definition, so you need to actually modify the existing workflow definition. Given that restriction, you can add or remove activities; add, move, or delete public variables; remove arguments; and change the signatures of activity delegates.
  3. Create the update map. To create a dynamic update map, you invoke the method DynamicUpdateServices.CreateUpdateMap() passing it the modified workflow definition. This will return a DynamicUpdateMap that will contain the information the runtime needs to modify a persisted workflow instance.
  4. Apply the update map to the desired persisted workflow instance. This is done by passing in the dynamic update map to the WorkflowApplication. Load() method along with the instance you want to run. If you don’t want to run the workflow immediately, you can just update the persisted instance by calling WorkflowApplication.Unload() immediately after loading the instance.

    The code example below will cover each of these steps. As with the previous example, you will need to ensure that your database has been configured for persisting workflows for the code to work.

  5. First, create a workflow console application project, delete the workflow1.xaml file since we won’t be using it, and add references to System.Runtime.DurableInstancing and System.Activities.DurableInstancing.  
  6. As with the previous example, we have some preliminary code we need to put in place. Open the file Program.cs, add using statements for System.Threading, System.Activities.DurableInstancing, and System.Activities.DynamicUpdate, and then update the code to correspond to the following:
    class Program
    {
      static void Main(string[] args)
      {
        //Modify this connection string to match your data source
        var connectionString = @"Data Source=(local);Initial Catalog=Persistence;Integrated Security=True";
        var instanceStore = new SqlWorkflowInstanceStore(connectionString);

        //[Step 1] We will add the code for creating a dynamic update map here

        //[Step 2] start up version 1 of the activity and persist it.
        var identityV1 = new WorkflowIdentity("SampleActivity", new Version(1, 0, 0, 0), null);
        var application = new WorkflowApplication(CreateSequence(1), identityV1);
        application.InstanceStore = instanceStore;
        var instanceId = ForcePersistance(application);

        //[Step 3] Get  Persisted instance and create new WorkflowApplication
        var instance = WorkflowApplication.GetInstance(instanceId, instanceStore);
        var definitionIdentity = instance.DefinitionIdentity;
        application = new WorkflowApplication(CreateSequence(1), identityV1);
        application.InstanceStore = instanceStore;
        var workflowCompleted = new AutoResetEvent(false);
        application.Completed = e => workflowCompleted.Set();

        //[Step 4] Run and complete workflow.
        try
        {
          application.Load(instance);
          Console.WriteLine("Completing {0}", application.WorkflowDefinition.DisplayName);
          application.Run();
          workflowCompleted.WaitOne();
          application.Unload();
        }
        catch (Exception ex)
        {
          Console.WriteLine(ex.Message);
        }

        Console.ReadKey();
      }

      /// <summary>
      /// Runs the application so that the workflow is persist.
      /// </summary>
      /// <param name="application">The workflow application.</param>
      /// <returns>The instance id. </returns>
      private static Guid ForcePersistance(WorkflowApplication application)
      {
        var syncEvent = new AutoResetEvent(false);
        application.PersistableIdle = (e) => PersistableIdleAction.Unload;
        application.Unloaded = (e) =>
        {
        syncEvent.Set();
        Console.WriteLine("[Application unloaded]");
        };

        application.Run();

        syncEvent.WaitOne();

        return application.Id;
      }

      private static Sequence CreateSequence(int version)
      {
        var activity = new Sequence
        {
        DisplayName = version == 1?"Sequence 1": "Sequence 2",
        Activities =
        {
          new WriteLine{Text = "Run Process A"},
          new Persist(),
          new Delay{ Duration =  new TimeSpan(0,0,3)},
          new WriteLine{Text = "Run Process B"}
        }
        };

        if (version == 1)
        {
          activity.Activities.Add(new WriteLine { Text = "Run Process C" });
          activity.Activities.Add(new WriteLine { Text = "Run Process D" });
        }
        else
        {
          activity.Activities.Add(new WriteLine { Text = "Run Process E" });
          activity.Activities.Add(new WriteLine { Text = "Run Process F" });
          activity.Activities.Add(new WriteLine { Text = "Run Process Z" });

        }
        return activity;
      }
    }
  7. If you build and run this code, it will persist and then reload and complete version 1 of the workflow definition resulting in output similar to Figure 11-11.
    Image

    Figure 11-11. Pre-update output

  8. Next, you need to add a method that will modify an instance of the version 1 sequence so that its set of activities corresponds to that in the version 2 sequence. Add the following method in the Program class:
    private static Sequence ModifyWorkflow(Sequence workflowToUpdate)
    {
      workflowToUpdate.DisplayName = "Activity 2";
      workflowToUpdate.Activities[3] = new WriteLine { Text = "Run Process E" };
      workflowToUpdate.Activities[4] = new WriteLine { Text = "Run Process F" };
      workflowToUpdate.Activities.Add(new WriteLine { Text = "Run Process Z" });

     return workflowToUpdate;
    }
  9. Now you can create our dynamic update map. At this point, you will create an instance of the version 1 workflow, prepare it for updating, pass the instance to your ModifyWorkflow() method to get an updated version, and then use that create the update map. In the Main() method at [Step 1], add these four lines of code:
    var workflowToUpdate = CreateSequence(1);
    DynamicUpdateServices.PrepareForUpdate(workflowToUpdate);
        
    var updatedWorkflow = ModifyWorkflow(workflowToUpdate);
    var duMap = DynamicUpdateServices.CreateUpdateMap(updatedWorkflow);
  10. To use the map, you will first modify the workflow application in [Step 3] to use version 2 of the workflow as well as to update the version. To do this, update the code for creating the workflow application so it matches the following:
    var identityV2 = new WorkflowIdentity("SampleActivity", new Version(2, 0, 0, 0), null);
    application = new WorkflowApplication(CreateSequence(2),identityV2);
  11. Now in [Step 4], replace the application.Load(instance) with this code:
if (definitionIdentity.Equals(identityV1))
  application.Load(instance, duMap);
else
application.Load(instance);

If you now run this code, you will see that the persisted instance will now complete using version 2 of the workflow (see Figure 11-12).

Image

Figure 11-12. Result after applying the update map

Even though in this example we created the dynamic update map at runtime, a better option would be to create and save the map ahead of time and then apply it later. One way to do this is to serialize the update map to a file and then load when required.

//Save the map
var serializer = new DataContractSerializer(typeof(DynamicUpdateMap));
using (FileStream fs = System.IO.File.Open(@"C: empWorkflow_v2.map", FileMode.Create))
{
  serializer.WriteObject(fs, duMap);
}

//And at some point in the future...
DynamicUpdateMap loadedMap;
using (FileStream fs = File.Open(@"C: empWorkflow_v2.map", FileMode.Open))
{
  var dcSerializer = new DataContractSerializer(typeof(DynamicUpdateMap));
  loadedMap = dcSerializer.ReadObject(fs) as DynamicUpdateMap;
  if (loadedMap == null)
  {
    throw new ApplicationException("DynamicUpdateMap is null.");
  }
}

Conclusion

This release of Workflow Foundation is very much about consolidating the changes that were made in 4.0 and responding to the feedback the team has received from users, with the big-ticket items being the introduction of C# expressions in the designer and versioning. It is clear that WF is in for the long haul, so if you haven’t really given much thought to WF since it was first released, now would be a good time to revisit it.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset