InfoPath : Retrieve and Send Data from/to a SharePoint 2007 Workflow

Yes, I know this title sounds a bit odd, and the real title should be ‘When you are using an InfoPath task edit form, how do you use InfoPath data connection bindings to both retrieve and send data to a SharePoint 2007 workflow?’

So one day I was building an InfoPath 2007 task edit form and I wanted to send data to this form from inside of my workflow by using something like:

taskProperties.ExtendedProperties[“txtProductDescription”] = “Product ABC”;

I implemented an ItemMetadata.xml file with an ‘ows_txtProductDescription’ field, added a secondary data connection for it in my InfoPath form and followed all the normal steps and when I opened the form when my task was created, the data was there as expected.

However, what I didn’t realize was that when I closed the form and completed the task and then re-opened the form just to look at my previous settings, the data was gone!  The form appeared as if it were a new form with default settings.  To make matters worse, inside of the workflow I discovered that I could not use the task After properties to get the data out of the txtProductDescription field, it was always null.  And this is where my multi-day odyssey of searching for the answer began.

To make things simple here, I’m just going to start with a simple form that has no binding and show you how to set things up.  I hate to admit it took me several days and talking to several people to figure this out, so I hope it helps you shorten your path to a solution also.

Technologies Used: 

  • Microsoft Office SharePoint Server 2007 Enterprise Edition, SP1
  • InfoPath 2007 – Browser enabled forms
  • Visual Studio 2008 SP1 / C#

To begin with, here is my simple form entitled OrderRequestTask.xsn

 

image

I have three InfoPath controls here here, a Textbox named ‘txtProductDescription’, a Drop-Down List named ‘drpStatus’ and a button for submitting the data.   This is a browser enabled form.

Create the ItemMetadata.xml file

Before we create our data connections we will need to create the ItemMetadata.xml file.  This is what the file will look like:

image

Note the ‘ows_’ in front of the field names.  This is a required prefix. Make sure you save this file as an XML file.

Setting Up the Data Connections

I will be using two data connections:

Update – this is a data connection that is used to submit the control(s) data into the workflow.  Follow these steps to setup this data connection:

1.  On the Design Task link (the Design Tasks toolbox should be on the right hand side of the form in InfoPath), select Data Source.

2. Select the Manage Data Connections… link.

3.  In the Data Connections dialog box, select Add.

4.  Select the Create a New Connection to and then Submit Data radio button.

5.  Select Next.

6.  Select ‘To the hosting environment, such as an ASP.Net page or a hosting application’ radio button.  Basically at this point, InfoPath has no idea we are dealing with SharePoint, what we are saying is that we are going to submit the data back to whatever host opened the form, which will be the SharePoint task list.

7.  Change the name of the data connection from Submit to Update and select Finish.  This will complete your ‘main’ data connection.  You can only have one main data connection and this is the only connection that can submit data to a form or other entity.

ItemMetadata – this data connection will read from an XML file named ItemMetadata.xml that will contain a reference to the controls on the form that can receive data from the workflow.  It is very important here that you name this xml file EXACTLY ItemMetadata.xml, spelling is important, case is important, format is important….if any of these are incorrect, the data connection will not work.  Another point, each form (whatever type of form it may be) that is receiving data from the workflow will have it’s own ItemMetadata.xml file, named ItemMetadata.xml so what I do is I have a separate directory for each form setup.  Now technically, you could have a single ItemMetadata.xml file with several fields and only use the ones you need but that could get a bit confusing down the road.

8.  The Data Connection dialog box should already be open, if not, select Manage Data Connections again to open it up.

9.  In the Data Connections dialog box, select Add.

10.  Select the Create a New Connection to and then Receive Data radio button.

11.  Select Next.

12.  Select ‘XML Document’.

13.  Select Next.

14.  Browse to where your ItemMetadata.xml file is and select it.

15.  Select Next.

16.  Make sure the ‘Include the data as a resource file in the form template or template part’ radio button is selected.  What this means is that the ItemMetadata.xml file is actually going to become a part of the InfoPath form itself.  Therefore, if you were to move this file after you get your form published, it would not matter.  If you ever update your ItemMetadata.xml file you will also need to go through the steps above to refresh your data connection anyway.

17.  Select Next then Finish.  Now your Data Connections dialog box should look like this:

image

 

Binding the Controls to the Data Connections

As it stands right now, the way I did the drag and drop of the fields onto the form and setup my main data connection, made it so that both of these fields will submit their data into the workflow without any other modification from me.  However, I want to be able to both receive data from the workflow AND submit the data to a workflow.   This is where I spent so much time trying to figure out essentially, how to bind the controls to both data sources.

If you right click on the txtProductDescription field and select ‘Change Binding’ you will see the following dialog box:

image

You can see here that this field is data bound to the Main data source.  You could of course choose the data source dropdown and then choose the ItemMetadata (secondary) data source but then the control will only receive data from the workflow and will not submit data to the workflow.  Perplexing right?

To correct this situation, do the following:

1.  Make sure the Text Box Binding dialog box above is closed.

2.  With the txtProductDescription box still selected, move over to the Design Tasks toolbox and make sure the Data Connections window is open like below:

image

3.  Right-click on txtProductDescription.

4.  Select Properties:

image

5.  In the Field or Group Properties dialog box, select the Fx (function) button:

image

6.  In the Insert Formula dialog box, select the Insert Field or Group button:

 image

7.  From the Select Field or Group dialog box, select the Data Source drop-down and select ItemMetadata (Secondary) menu item.

8.  Select the ows_txtProductDescription field and then select OK

image

9.  Select OK in the Insert Formula field and then OK and the Field or Group Properties dialog box.

10.  Repeat steps 2 – 9, except choose the drpStatus drop-down field in the form.  Note that if you don’t want or need to set this value from the workflow, you can just leave the binding as is.

11. Now you can save and publish your form.  Since this is a task edit form and the workflow is going to determine where/how to pickup this file via it’s configuration files (feature.xml and workflow.xml), we will be publishing this form to a network location.

Publishing the Form

1.  Select the File –> Publish menu item.

2.  Select ‘To a network location’ then Next.

image

3.  Browse to where you want to publish the file and select Next.

4.  On the this page, make absolutely sure you clear out the text field in this window.  If you do not, SharePoint will more than likely not let anyone open the form due to permissions.

image

5.  Select Next.  You will see a dialog box warning you of possible user access problems. Just select OK.

6.  Select Publish.

7.  Select Close.

At this point, I will defer to the experience of the student at to where they put the form so that the workflow can access it.  More than likely you will have a sub-directory under your workflow projects feature directory to put the form in.  You will then need to re-deploy your workflow project.

 

What about the code in the workflow itself?

Inside of the workflow in my CreateTask handler, I will have code that looks like this:

   1:  this.taskProperties.ExtendedProperties["ows_txtProductDescription"] = this.productDescription;
   2:  this.taskProperties.ExtendedProperties["ows_drpStatus"] = "Completed"

In my OnTaskChanged handler, I would have code that looks something like this:

   1:  string strProdDesc = this.taskAfterProperties.ExtendedProperties["txtProductDescription"].ToString();
   2:  bool statusComplete = this.taskAfterProperties.ExtendedProperties.ContainsValue("Completed");

A couple of things to note in the OnTaskChanged event is that I do not include the ‘ows_’ prefix in the field name and I am able to use ‘ContainsValue’ to get to the boolean status value.

 

In conclusion, I hope this helps someone out there prevent the time loss I had when attempting to figure this out.  And as I’ve said before, if you know a better way of doing it, let me know!

Working with the Workflow Foundation (3.5) Delay Activity (why won’t my expired workflow automatically reload?)

I was recently working with a customer who made the statement ‘Whenever I use a delay activity in my workflow, the workflow will automatically unload itself but will only reload into memory when a particular workflow runtime host is running.  It was our understanding that if a workflow had been unloaded due to a delay activity, that any runtime that had the persistence service added to it would just pick up the unloaded workflow whenever the timeout expired’. 

The answer to why this was happening was very easy but it was quite interesting to see how these workflows reacted to the runtime scenarios being used.  It was just interesting enough to think I would let others know in case they run into similar issues.

The scenario is to have two workflow sequential libraries and three hosts for these workflows. I placed all of these projects into one Visual Studio 2008 Solution.

image

WorkflowDelay1 – This project is a sequential workflow library with the following workflow:

image

This workflow has two code activities to write out to the console the workflow instance ID and a delay activity set to 30 seconds.  In order to run this workflow, I have a console based host project named WorkflowHostDelay1.  The code for the host is shown here (program.cs).  I also added a project reference to the host that points to the WorkflowDelay1 project:

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Text;
   5:  using System.Workflow.Runtime;
   6:  using System.Workflow.Runtime.Hosting;
   7:  using System.Threading;
   8:   
   9:  namespace WorkflowHostDelay1
  10:  {
  11:      class Program
  12:      {
  13:          static string connectionString = "Initial Catalog=TrackingStore;" + 
  14:              "Data Source=.\\SQLEXPRESS; Integrated Security=SSPI;";
  15:   
  16:          static void Main(string[] args)
  17:          {
  18:              using (WorkflowRuntime workflowRuntime = new WorkflowRuntime())
  19:              {
  20:                  AutoResetEvent waitHandle = new AutoResetEvent(false);
  21:                  workflowRuntime.WorkflowCompleted += 
  22:                      delegate(object sender, WorkflowCompletedEventArgs e) 
  23:                      { 
  24:                          waitHandle.Set(); 
  25:                      };
  26:                  workflowRuntime.WorkflowTerminated += 
  27:                      delegate(object sender, WorkflowTerminatedEventArgs e)
  28:                  {
  29:                      Console.WriteLine(e.Exception.Message);
  30:                      waitHandle.Set();
  31:                  };
  32:   
  33:                  workflowRuntime.AddService(new SqlWorkflowPersistenceService(connectionString, 
  34:                      true, new TimeSpan(0, 0, 30), new TimeSpan(0, 0, 10)));
  35:   
  36:                  workflowRuntime.StartRuntime();
  37:                  WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(WorkflowDelay1.WFDelay1));
  38:                  instance.Start();
  39:                  
  40:                  Console.Title = "WorkflowDelay1";
  41:                  Console.BackgroundColor = ConsoleColor.DarkGreen;
  42:                  Console.ForegroundColor = ConsoleColor.Yellow;
  43:                  Console.Clear();
  44:                  Console.WriteLine();
  45:                  Console.WriteLine("WorkflowDelay1 host is running");
  46:                  Console.WriteLine("Press <enter> to exit.");
  47:                  Console.ReadLine();
  48:   
  49:                  waitHandle.WaitOne();
  50:   
  51:              }
  52:   
  53:          }
  54:      }
  55:  }

This host code simply adds the SQLWorkflowPersistenceService and creates/starts and instance of WorkflowDelay1.WFDelay1.  I’ve set the timespans for the OwnershipTimeout and LoadInterval shorter than normal just for testing.

My other workflow project, WorkflowDelay2 with a workflow type of WFDelay2 is very similar to the first workflow type except with one additional code activity added.  This workflow has a delay activity with a timeout of 1 minute:

image

The host code (WorkflowHostDelay2) is pretty much the same as the first set of host code.  The only difference is the workflow type being started.  I added a project reference to this host to point to the WorkflowDelay2 project.

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Text;
   5:  using System.Workflow.Runtime;
   6:  using System.Workflow.Runtime.Hosting;
   7:  using System.Threading;
   8:   
   9:  namespace WorkflowHostDelay2
  10:  {
  11:      class Program
  12:      {
  13:          static string connectionString = "Initial Catalog=TrackingStore;" + 
  14:              "Data Source=.\\SQLEXPRESS; Integrated Security=SSPI;";
  15:   
  16:          static void Main(string[] args)
  17:          {
  18:              using (WorkflowRuntime workflowRuntime = new WorkflowRuntime())
  19:              {
  20:                  AutoResetEvent waitHandle = new AutoResetEvent(false);
  21:                  workflowRuntime.WorkflowCompleted += 
  22:                      delegate(object sender, WorkflowCompletedEventArgs e) 
  23:                      { 
  24:                          waitHandle.Set(); 
  25:                      };
  26:                  workflowRuntime.WorkflowTerminated += 
  27:                      delegate(object sender, WorkflowTerminatedEventArgs e)
  28:                  {
  29:                      Console.WriteLine(e.Exception.Message);
  30:                      waitHandle.Set();
  31:                  };
  32:   
  33:                  workflowRuntime.AddService(new SqlWorkflowPersistenceService(connectionString, 
  34:                      true, new TimeSpan(0, 0, 30), new TimeSpan(0, 0, 10)));
  35:                  workflowRuntime.StartRuntime();
  36:                  WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(WorkflowDelay2.WFDelay2));
  37:                  instance.Start();
  38:                  
  39:                  Console.Title = "WorkflowDelay2";
  40:                  Console.BackgroundColor = ConsoleColor.DarkGreen;
  41:                  Console.ForegroundColor = ConsoleColor.Yellow;
  42:                  Console.Clear();
  43:                  Console.WriteLine();
  44:                  Console.WriteLine("WorkflowDelay2 host is running");
  45:                  Console.WriteLine("Press <enter> to exit.");
  46:                  Console.ReadLine();
  47:   
  48:                  waitHandle.WaitOne();
  49:   
  50:              }
  51:   
  52:          }
  53:      }
  54:  }

 

So how do we test how the delay activities work?  What I am going to do, is start up both WorkflowHostDelay1 and WorkflowHostDelay2 long enough for the workflows to start and then be unloaded into the persistence InstanceState table.  Once I see that the workflows are persisted, I will shut down the console based hosts which means that the workflows will timeout while sitting in the database.

After I do this, taking a snapshot of the InstanceState table I see:

image

You can see here that these workflows are not owned by anyone at this point.

What I wanted to do next was start up a new instance of a workflow runtime (any runtime) with a persistence service and see what would happen with the workflows in the database.  I created another host project named HostWithOnlyaRuntime that looks like this:

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Text;
   5:  using System.Workflow.Runtime;
   6:  using System.Workflow.Runtime.Hosting;
   7:  using System.Threading;
   8:   
   9:  namespace HostWithOnlyaRuntime
  10:  {
  11:      class Program
  12:      {
  13:          static string connectionString = "Initial Catalog=TrackingStore;" + 
  14:              "Data Source=.\\SQLEXPRESS; Integrated Security=SSPI;";
  15:   
  16:          static void Main(string[] args)
  17:          {
  18:              using (WorkflowRuntime workflowRuntime = new WorkflowRuntime())
  19:              {
  20:                  //AutoResetEvent waitHandle = new AutoResetEvent(false);
  21:                  
  22:                  workflowRuntime.AddService(new SqlWorkflowPersistenceService(connectionString, 
  23:                      true, new TimeSpan(0, 0, 30), new TimeSpan(0, 0, 10)));
  24:                  workflowRuntime.StartRuntime();
  25:   
  26:                  Console.Title = "HostWithOnlyaRuntimeowDelay2";
  27:                  Console.BackgroundColor = ConsoleColor.White;
  28:                  Console.ForegroundColor = ConsoleColor.Black;
  29:                  Console.Clear();
  30:                  Console.WriteLine();
  31:                  Console.WriteLine("HostWithOnlyaRuntimeowDelay2 host is running");
  32:                  Console.WriteLine("Press <enter> to exit.");
  33:                  Console.ReadLine();
  34:   
  35:                  workflowRuntime.StopRuntime();
  36:   
  37:                  //waitHandle.WaitOne();
  38:   
  39:              }
  40:   
  41:          }
  42:      }
  43:  }

This code just starts up the runtime, adds the persistence service and then starts the runtime.  At this point, if I look at my records in the database I see that although it appears a have a new ownerID and it is locked until a certain point in time, the workflows themselves do not actually ever reload.

image

As long as I have this host running, these records will remain locked, which means no other host can start the workflows either.

To prove this point, comment out the code to start up a new workflow in WorkflowHostDelay2 (we don’t need to start a new workflow this time) and then startup the host.  The WorkflowHostDelay2 host will sit there indefinitely and not reload the workflow it originally started.   But wait, why would this host have to reload the same workflow that it had originally started?

The answer is, it doesn’t.  Steve Danielson (Microsoft – http://blogs.msdn.com/sdanie/) pointed out a few things.

What’s going on here is that when HostWithOnlyaRuntime starts, the SQLWorkflowPersistenceService puts a lock on these workflows because it wants to load them, but it can’t because this runtime has no reference to the workflow types WorkflowDelay1.WFDelay1 and WorkflowDelay2.WFDelay2.  To allow HostWithOnlyaRuntime to reload these workflows, all I would have to do is add a project reference to the WorkflowDelay1 and WorkflowDelay2 projects.

image

In my customers case, he had two different application servers but on one of the machines, he did not have a reference to one of the workflow types.  On this application server that was missing the workflow type reference, when the host would start, it would leave expired workflows in the database.

I hope this helps someone who might be facing a similar scenario.

Versioning Workflow Services

A couple of years ago (that sounds like an eternity in computer years), I had written up an article on the versioning of workflows http://blogs.msdn.com/ncdevguy/archive/2007/06/14/versioning-techniques-for-workflows.aspx.

Since then, a lot of things have happened, I’ve been bouncing back and forth between WCF, SharePoint Workflows, WCF exposed workflows and so on. One thing I’ve come to discover though is that when you are talking with customers who are using the Workflow Foundation, most of them still don’t have a concrete handle on how to version their workflows and especially workflows that are exposed as WCF services.

So with the help of Xingang Liao of Ensemble Studios (China), we came up with a similar way of doing the versioning that we thought may help others who are still trying to figure this out. We of course are of the opinion that this may not be the best way to do this and if you see anything to make the code more efficient or an even better way to do this, let me know.

We’ll start with this context:

Framework Version: .Net Framework 3.5 SP1

Host: Console based application that uses WorkflowServiceHost as its host. This console based approach can of course be applied to a Windows Service (NT Service) based approach.

 

Creating the Solution

1. Create a new empty workflow Visual Studio 2008 Solution named WFVersioningApp by selecting File | New | Project | Visual C# | Workflow | Empty Workflow Project and giving it the before mentioned name.

Creating the Workflow Project

1.  Here, I created a new Sequential Workflow Library named WFProject and with a workflow (VersionWF.cs) that looks like this.  The objective here is to have a workflow that has two Receive activities, the first one of which will be used to start the workflow, the workflow will then be unloaded and another WCF operation will be called ( RestartWF – into the second Receive activity) to reload and start the workflow back up.  I also added a custom activity I wrote that would log to the Application Event log but you really could put any type of activity here that would allow you to see that indeed the correct version of the workflow has been awakened.

image

I placed a code activity inside of the first Receive activity just to hard-code a value for the event log.  So basically, I don’t have any real code inside of my workflow code-behind that is really important for this demonstration.

2.  Next, I created a new file for my WCF service.

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Text;
   5:  using System.ServiceModel;
   6:   
   7:  namespace WFProject
   8:  {
   9:      [ServiceContract]
  10:      public interface IVersionWorkflow
  11:      {
  12:          [OperationContract]
  13:          void KickoffWF();
  14:   
  15:          [OperationContract]
  16:          void RestartWF();
  17:      }
  18:      
  19:  }

Notice here that both these operations return void and are Request Response type of operations.  You will need to add a reference to System.ServiceModel to your workflow project.

3.  Add an app.config file to the project (which will later be used in your console based host ~ I just like to have this in two places in the beginning).  In this app.config file you need to add a services section.  You’ll notice in this example that I have two services listed, I’ll explain that later in the article.

   1:  <system.serviceModel>
   2:      
   3:          <services>
   4:              <service name="WFProject.VersionWF" behaviorConfiguration="ServiceBehavior" >
   5:                  <endpoint address="net.tcp://localhost:8800/WFProject/VersionWF" binding="netTcpContextBinding" contract="WFProject.IVersionWorkflow" />
   6:              </service>
   7:        <service name="WFProject.VersionWF_V2" behaviorConfiguration="ServiceBehaviorV2" >
   8:          <endpoint address="net.tcp://localhost:8800/WFProject/VersionWFV2" binding="netTcpContextBinding" contract="WFProject.IVersionWorkflow" />
   9:        </service>
  10:          </services>
  11:          <behaviors>
  12:              <serviceBehaviors>
  13:                  <behavior name="ServiceBehavior"  >
  14:                      <serviceMetadata httpGetEnabled="true" httpGetUrl="http://localhost:8888/WFProject/VersionWF"/>
  15:                      <serviceDebug includeExceptionDetailInFaults="true" />
  16:                      <serviceCredentials>
  17:                          <windowsAuthentication  allowAnonymousLogons="false" includeWindowsGroups="true" />
  18:                      </serviceCredentials>
  19:                  </behavior>
  20:          <behavior name="ServiceBehaviorV2"  >
  21:            <serviceMetadata httpGetEnabled="true" httpGetUrl="http://localhost:8888/WFProject/VersionWFV2"/>
  22:            <serviceDebug includeExceptionDetailInFaults="true" />
  23:            <serviceCredentials>
  24:              <windowsAuthentication  allowAnonymousLogons="false" includeWindowsGroups="true" />
  25:            </serviceCredentials>
  26:          </behavior>
  27:          </serviceBehaviors>
  28:          </behaviors>
  29:      </system.serviceModel>

A couple of things to point out in the above config file settings:

a.  I am actually using a net.tcp address for the service but I’m using an http address for the clients who will be getting the service information. 
b.  The authentication information in this example is not really relevant to our discussion here.

c.  I am not using a single base address, I am using relative addresses for each service configuration.

4.  I created a WFProject.snk file so this assembly could be strong named and placed in the GAC, hence allowing execution side-by-side with other versions.

5.  I confirmed that in my workflow projects Assembly.cs file that the AssemblyVersion is 1.0.0.0.

Creating the Console Host

1. Create a new Console based Windows application named WorkflowHost in the same solution.

2.  Copy the app.config file from the WFProject project into the WorkflowHost project.

3.  Add references to the following assemblies:

image

4.  Create a new class (in a new file) named WFVersionServiceHost.  This class will be derived from WorkflowServiceHost and will be a our base class for our service host.

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Text;
   5:  using System.ServiceModel;
   6:  using System.ServiceModel.Description;
   7:  using System.Workflow.Runtime;
   8:  using System.Workflow.Runtime.Hosting;
   9:   
  10:  namespace WorkflowHost
  11:  {
  12:      public class WFVersionServiceHost : WorkflowServiceHost
  13:      {
  14:          public static string ConfigurationName;
  15:          public static ServiceDescription _serviceDesc;
  16:   
  17:          static string connectionString = "Initial Catalog=TrackingStore;" + "Data Source=.\\SQLEXPRESS; Integrated Security=SSPI;";
  18:   
  19:          public WFVersionServiceHost(Type workflowType, params Uri[] baseAddress)
  20:              : base(workflowType, baseAddress)
  21:          {
  22:              
  23:              WorkflowRuntime wfRuntime = _serviceDesc.Behaviors.Find<WorkflowRuntimeBehavior>().WorkflowRuntime;
  24:   
  25:              wfRuntime.WorkflowTerminated
  26:                             += delegate(object sender, WorkflowTerminatedEventArgs e)
  27:                             {
  28:                                 Console.WriteLine("WorkflowTerminated: " + e.Exception.Message);
  29:                             };
  30:              wfRuntime.WorkflowCompleted
  31:                           += delegate(object sender, WorkflowCompletedEventArgs e)
  32:                           {
  33:                               Console.WriteLine("WorkflowCompleted: " + e.WorkflowInstance.InstanceId.ToString());
  34:                           };
  35:              wfRuntime.WorkflowUnloaded
  36:                           += delegate(object sender, WorkflowEventArgs e)
  37:                           {
  38:                               Console.WriteLine("WorkflowUnloaded: " + e.WorkflowInstance.InstanceId.ToString());
  39:                           };
  40:              
  41:                         
  42:              wfRuntime.AddService(new SqlWorkflowPersistenceService(connectionString, true, new TimeSpan(0, 0, 30), new TimeSpan(0, 5, 0)));
  43:   
  44:          }
  45:          
  46:          protected override ServiceDescription CreateDescription(out IDictionary<string, ContractDescription> implementedContracts)
  47:          {
  48:              ServiceDescription sd = base.CreateDescription(out implementedContracts);
  49:              
  50:              sd.ConfigurationName = WFVersionServiceHost.ConfigurationName;
  51:   
  52:              _serviceDesc = sd;
  53:   
  54:              return sd;
  55:          }
  56:   
  57:      }
  58:  }

 

A couple of things to note about the base class:

a.  I wanted to create a custom host to make it a bit easier to capture the workflow terminated/completed etc events. 

b.  The override to CreateDescription is used so that from our program.cs file, we can specify a new configuration name for each version of our workflow.  This configuration name represents the <service name=””> setting in our app.config file.

c.  Subscribing to the workflow runtime events need to take place in the constructor because if you put this code in the CreateDescription method, the workflow runtime has not yet been created.

d.  On line 42, notice that for each version of the workflow, I add the SQLWorkflowPersistenceService.  As far as I know, there is no way to add this once for all hosts.  If you know a way, please comment!

5.  Here is the program.cs file:

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Text;
   5:  using System.Threading;
   6:  using System.Workflow.Runtime;
   7:  using System.Workflow.Runtime.Hosting;
   8:  using System.ServiceModel;
   9:  using System.ServiceModel.Description;
  10:  using System.Reflection;
  11:   
  12:  namespace WorkflowHost
  13:  {
  14:      class Program
  15:      {
  16:         static void Main(string[] args)
  17:          {
  18:              //get the 'types' related to the version of the assemblies
  19:              Type serviceTypeV1 = Type.GetType("WFProject.VersionWF, WFProject, Version=1.0.0.0, Culture=neutral, PublicKeyToken=88a8d45b55e52cbf", true, true);
  20:              Type serviceTypeV2 = Type.GetType("WFProject.VersionWF, WFProject, Version=2.0.0.0, Culture=neutral, PublicKeyToken=88a8d45b55e52cbf", true, true);
  21:              
  22:              //the configuration name will match the <service name=""> value in the 
  23:              //app.config file
  24:              WFVersionServiceHost.ConfigurationName = "WFProject.VersionWF";
  25:              //create an instance of the workflow service host
  26:              WFVersionServiceHost wshV1 = new WFVersionServiceHost(serviceTypeV1);
  27:              //open the host
  28:              wshV1.Open();
  29:   
  30:              //notice we have to listen for both versions of the component at different
  31:              //addresses
  32:              WFVersionServiceHost.ConfigurationName = "WFProject.VersionWF_V2";
  33:              WFVersionServiceHost wshV2 = new WFVersionServiceHost(serviceTypeV2);
  34:              wshV2.Open();
  35:   
  36:        
  37:              
  38:              Console.Title = "WF Version Service";
  39:              Console.BackgroundColor = ConsoleColor.DarkGreen;
  40:              Console.ForegroundColor = ConsoleColor.Yellow;
  41:              Console.Clear();
  42:              Console.WriteLine();
  43:              Console.WriteLine("WF Version Service is ready.");
  44:              Console.WriteLine("WF Version V2 Service is ready.");
  45:              Console.WriteLine("Press <enter> to exit.");
  46:              Console.ReadLine();
  47:              wshV1.Close();
  48:              wshV2.Close();
  49:              
  50:              
  51:   
  52:          }
  53:      }
  54:  }

Comments:

a.  Note that for each version of our workflow project, I get the type.

b.  Next, set the ConfigurationName.  This will match the section in the app.config file for the service name.

c.  Create an instance of the WFVersionServiceHost, passing in the service type.

d.  Open the service host.

Once again, let’s look at the corresponding section in the app.config file:

   1:  <services>
   2:      <service name="WFProject.VersionWF" behaviorConfiguration="ServiceBehavior" >
   3:          <endpoint address="net.tcp://localhost:8800/WFProject/VersionWF" binding="netTcpContextBinding" contract="WFProject.IVersionWorkflow" />
   4:      </service>
   5:                <service name="WFProject.VersionWF_V2" behaviorConfiguration="ServiceBehaviorV2" >
   6:          <endpoint address="net.tcp://localhost:8800/WFProject/VersionWFV2" binding="netTcpContextBinding" contract="WFProject.IVersionWorkflow" />
   7:        </service>
   8:  </services>

Notice how the service names match up with the ConfigurationName on lines 24 and 32 in the program.cs file.  They are both still using the same WCF service contract and the same workflow class name.

 

Create the Client Application

1.  For this, I created a new WinForm application named WFVersioningApp and added it to the solution.  The form looks like this:

image

What happens with this form is that I will associate a customers name with my unique workflow InstanceId.  The WF Version field is just for me to use inside of my winform code to determine which proxy I want to use to reload the workflows.   The Endpoint Addr field is used at the endpoint address for the proxy I am using.  We will have a different net.tcp endpoint address for each version of our workflow.

2.  The Winform code looks like this:

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.ComponentModel;
   4:  using System.Data;
   5:  using System.Drawing;
   6:  using System.Linq;
   7:  using System.Text;
   8:  using System.Windows.Forms;
   9:  using System.ServiceModel;
  10:  using System.ServiceModel.Channels;
  11:  using System.Workflow.Runtime;
  12:  using System.Data.SqlClient;
  13:  using WFVersioningApp.WFReference;
  14:  using WFVersioningApp.WFReferenceV2;
  15:  using System.Xml;
  16:   
  17:  namespace WFVersioningApp
  18:  {
  19:      public partial class WFForm : Form
  20:      {
  21:   
  22:          //Customer myCust;
  23:          //proxy type located in reference.cs
  24:          WFReference.VersionWorkflowClient proxy_v1;
  25:          WFReferenceV2.VersionWorkflowClient proxy_v2;
  26:          WFReference.VersionWorkflowClient proxy2_v1;
  27:          WFReferenceV2.VersionWorkflowClient proxy2_v2;
  28:   
  29:          IDictionary<string, string> basicContext;
  30:          private string _wfInstId = default(string);
  31:          private string _epAddress = default(string);
  32:   
  33:          public WFForm()
  34:          {
  35:              InitializeComponent();
  36:          }
  37:   
  38:          private void WFForm_Load(object sender, EventArgs e)
  39:          {
  40:              // TODO: This line of code loads data into the 'trackingStoreDataSet1.tblVersion' table. You can move, or remove it, as needed.
  41:              
  42:              this.tblVersionTableAdapter.Fill(this.trackingStoreDataSet1.tblVersion);
  43:   
  44:          }
  45:   
  46:          private void btnStartNewWF_Click(object sender, EventArgs e)
  47:          {
  48:              IContextManager contextManager = null;
  49:   
  50:              _epAddress = this.txtEndpoint.Text;
  51:   
  52:              if (this.txtVersion.Text == "1")
  53:              {
  54:                  proxy_v1 = new WFReference.VersionWorkflowClient();
  55:                  contextManager = proxy_v1.InnerChannel.GetProperty<IContextManager>();
  56:                  proxy_v1.KickoffWF();
  57:                  proxy_v1.Close();
  58:              }
  59:              else
  60:              {
  61:                  proxy_v2 = new WFReferenceV2.VersionWorkflowClient();
  62:                  contextManager = proxy_v2.InnerChannel.GetProperty<IContextManager>();
  63:                  proxy_v2.KickoffWF();
  64:                  proxy_v2.Close();
  65:              }
  66:   
  67:   
  68:   
  69:              // This comes back as an instantiated object, but has 0 elements in it
  70:              basicContext = contextManager.GetContext();
  71:              _wfInstId = contextManager.GetContext()["instanceId"];
  72:   
  73:              AddCustomerToDatabase();
  74:              
  75:          }
  76:   
  77:          private void btnRestartWF_Click(object sender, EventArgs e)
  78:          {
  79:              string _wfId = default(string);
  80:              string _wfVersion = default(string);
  81:              IContextManager contextManager = null;
  82:   
  83:              Dictionary<string, string> _myDict = new Dictionary<string, string>();
  84:   
  85:              #region DataGrid Logic
  86:              DataGridViewSelectedRowCollection rows = dataGridView1.SelectedRows;
  87:   
  88:              if (rows.Count > 1)
  89:              {
  90:                  MessageBox.Show("You can only select one item");
  91:                  return;
  92:              }
  93:              else if(rows.Count < 1)
  94:              {
  95:                  MessageBox.Show("You must select at least one row in the grid");
  96:                  return;
  97:              }
  98:   
  99:              //get the value from the workflow id column
 100:              _wfId = rows[0].Cells[1].Value.ToString();
 101:   
 102:              //get the version number
 103:              _wfVersion = rows[0].Cells[2].Value.ToString();
 104:   
 105:              if (_wfId.Length <= 0)
 106:              {
 107:                  MessageBox.Show("Invalid workflow instance ID");
 108:                  return;
 109:              }
 110:   
 111:              #endregion
 112:   
 113:              _myDict.Add("instanceId", _wfId);
 114:              
 115:   
 116:              switch (_wfVersion)
 117:              {
 118:                  case "1":
 119:                      proxy2_v1 = new WFReference.VersionWorkflowClient();
 120:                      contextManager = proxy2_v1.InnerChannel.GetProperty<IContextManager>();
 121:                      contextManager.SetContext(_myDict);
 122:                      proxy2_v1.RestartWF();
 123:                      break;
 124:                  case "2":
 125:   
 126:                      proxy2_v2 = new WFReferenceV2.VersionWorkflowClient();
 127:                      contextManager = proxy2_v2.InnerChannel.GetProperty<IContextManager>();
 128:                      contextManager.SetContext(_myDict);
 129:                      proxy2_v2.RestartWF();
 130:                      break;
 131:                  default:
 132:                      break;
 133:   
 134:              }
 135:   
 136:              try
 137:              {
 138:                  //the workflow has already been deleted but we need to delete our own records
 139:                  DeleteCustomerFromDatabase(_wfId);
 140:              }
 141:              catch (System.Exception ex)
 142:              {
 143:                  MessageBox.Show("Exception during workflow restart: " + ex.Message);
 144:              }
 145:              finally
 146:              {
 147:   
 148:                  if (proxy2_v1 != null)
 149:                  {
 150:                      if (proxy2_v1.State == CommunicationState.Opened)
 151:                      {
 152:                          proxy2_v1.Close();
 153:                      }
 154:                  }
 155:   
 156:                  if (proxy2_v2 != null)
 157:                  {
 158:                      if (proxy2_v2.State == CommunicationState.Opened)
 159:                      {
 160:                          proxy2_v2.Close();
 161:                      }
 162:                  }
 163:              }
 164:   
 165:          }
 166:   
 167:          private void btnClose_Click(object sender, EventArgs e)
 168:          {
 169:              this.Close();
 170:          }
 171:          #region Database methods
 172:          private void AddCustomerToDatabase()
 173:          {
 174:              SqlCommand cmd = new SqlCommand();
 175:              cmd.CommandType = CommandType.StoredProcedure;
 176:              cmd.CommandText = "dbo.InsertCustomer";
 177:              cmd.Connection = this.tblVersionTableAdapter.Connection;
 178:   
 179:              try
 180:              {
 181:                  if (cmd.Connection.State != ConnectionState.Open)
 182:                  {
 183:                      cmd.Connection.Open();
 184:                  }
 185:                  
 186:                  cmd.Parameters.AddWithValue("@CustomerName", this.txtCustomerName.Text);
 187:                  cmd.Parameters.AddWithValue("@WFId", _wfInstId);
 188:                  cmd.Parameters.AddWithValue("@Version", this.txtVersion.Text);
 189:                  cmd.Parameters.AddWithValue("@EndpointAddr", _epAddress);
 190:                  
 191:   
 192:                  cmd.CommandType = CommandType.StoredProcedure;
 193:                  cmd.ExecuteNonQuery();
 194:   
 195:              }
 196:              catch (Exception ex)
 197:              {
 198:                  Console.WriteLine("InsertCustomerName error :{0}", ex.Message);
 199:              }
 200:              finally
 201:              {
 202:                  cmd.Connection.Close();
 203:              }
 204:   
 205:              this.dataGridView1.DataSource = this.tblVersionTableAdapter.GetData();
 206:   
 207:   
 208:          }
 209:          private void DeleteCustomerFromDatabase(string wfInstId)
 210:          {
 211:              //when the workflow has completed, remove it from the database
 212:              SqlCommand cmd = new SqlCommand();
 213:              cmd.CommandType = CommandType.StoredProcedure;
 214:              cmd.CommandText = "dbo.DeleteCustomerWorkflow";
 215:              cmd.Connection = this.tblVersionTableAdapter.Connection;
 216:   
 217:              try
 218:              {
 219:                  if (cmd.Connection.State != ConnectionState.Open)
 220:                  {
 221:                      cmd.Connection.Open();
 222:                  }
 223:   
 224:                  cmd.Parameters.AddWithValue("@WFId", wfInstId);
 225:                  cmd.CommandType = CommandType.StoredProcedure;
 226:                  cmd.ExecuteNonQuery();
 227:   
 228:              }
 229:              catch (Exception ex)
 230:              {
 231:                  Console.WriteLine("DeleteCustomerWorkflow error :{0}", ex.Message);
 232:              }
 233:              finally
 234:              {
 235:                  cmd.Connection.Close();
 236:              }
 237:   
 238:              this.dataGridView1.DataSource = this.tblVersionTableAdapter.GetData();
 239:   
 240:          }
 241:          #endregion
 242:   
 243:   
 244:      }
 245:  }

 

There is certainly a lot of code here, none of which is terribly complex (or production ready!) but I’ll explain what I consider to be the important pieces:

a.  Lines 24 – 27.  I wanted to make sure here that I was truly using different proxy instances whenever I call back into the workflow.  You will have to add two different service references to the project, one for each endpoint (remember to use the http addresses from the WorkflowHost projects app.config file to find the service metadata).

b.  Lines 52 through 58.  Whenever I want to create a version 1 workflow, I need to create an proxy to the first endpoints client and then from that, I get the ContextManager (which will be empty at this point).  When I call KickoffWF, the context will be filled with the instance id of the workflow returned by the first Receive activity.  Note that in real life, you are not typically going to have code that allows people to keep creating versions of workflow version 1.0.0.0.  Imagine that this is the code that your client app started with and then as you added new versions of your workflow, you would have to add such code as you see here to at minimum be able to restart version 1.0.0.0 of the workflow.

c.  Lines 70 and 71, this is where we get the workflow instance ID out of the context.

d.  Line 73, call the AddCustomerToDatabase method.  I have added a tblVersion table to the TrackingStore database along with a few stored procs to handle adding and deleting the customer name and corresponding workflow instance id information. (I’ll show the SQL script down below)

e.  Line 113.  In the btnRestartWF_Click method, I grab the workflow instance ID out of the grid and fill in a string dictionary to be used to set my context for the next Receive activity call.

f.  Line 119 – 122.  Create a new proxy instance.  The only difference in this code and the code that initially kicked the workflow off is that here (line 121) I ‘set’ the context value to be the instance id of the workflow to reload.

 

Testing the Workflow Versions

1.  In order to test this, what I did was I wrote out strings in my logging activity that specifically said ‘v1’.  I then built the component and deployed it to the GAC.   I ran through my client and created a v1 workflow.

2.  I then went and changed the strings for the logging activity to ‘v2’ and updated the Assembly.cs file to 2.0.0.0. and redeployed to the GAC.

3.  I ran the client and created a few workflows that were version 2.0. 

4.  I then, selected a version 1.0 workflow from the grid and restarted that workflow.  I was able to look in my Application event viewer and see that ‘v1’ had been restarted.

 

The SQL Script – I put this code in with my TrackingStore database (persistence and tracking database)

   1:  USE [TrackingStore]
   2:  GO
   3:  /****** Object:  Table [dbo].[tblVersion]    Script Date: 09/29/2009 19:35:36 ******/
   4:  SET ANSI_NULLS ON
   5:  GO
   6:  SET QUOTED_IDENTIFIER ON
   7:  GO
   8:  CREATE TABLE [dbo].[tblVersion](
   9:      [CustomerName] [nvarchar](50) NOT NULL,
  10:      [WorkflowID] [uniqueidentifier] NOT NULL,
  11:      [WorkflowVersion] [smallint] NULL,
  12:      [EndpointAddress] [nvarchar](50) NULL,
  13:   CONSTRAINT [PK_tblVersion] PRIMARY KEY CLUSTERED 
  14:  (
  15:      [WorkflowID] ASC
  16:  )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
  17:  ) ON [PRIMARY]
  18:   
  19:  GO
  20:  /****** Object:  StoredProcedure [dbo].[DeleteCustomerWorkflow]    Script Date: 09/29/2009 19:36:26 ******/
  21:  SET ANSI_NULLS ON
  22:  GO
  23:  SET QUOTED_IDENTIFIER ON
  24:  GO
  25:  -- =============================================
  26:  -- Author:        <Author,,Name>
  27:  -- Create date: <Create Date,,>
  28:  -- Description:    <Description,,>
  29:  -- =============================================
  30:  CREATE PROCEDURE [dbo].[DeleteCustomerWorkflow] @WFId uniqueidentifier 
  31:  AS
  32:  BEGIN
  33:      DELETE FROM [dbo].[tblVersion] WHERE WorkflowID=@WFId
  34:  END
  35:   
  36:   
  37:  GO
  38:  /****** Object:  StoredProcedure [dbo].[InsertCustomer]    Script Date: 09/29/2009 19:37:06 ******/
  39:  SET ANSI_NULLS ON
  40:  GO
  41:  SET QUOTED_IDENTIFIER ON
  42:  GO
  43:  -- =============================================
  44:  -- Author:        <Author,,Name>
  45:  -- Create date: <Create Date,,>
  46:  -- Description:    <Description,,>
  47:  -- =============================================
  48:  CREATE PROCEDURE [dbo].[InsertCustomer] @CustomerName nvarchar(50),
  49:  @WFId uniqueidentifier,
  50:  @Version smallint,
  51:  @EndpointAddr nvarchar(50)
  52:   
  53:  AS
  54:  BEGIN
  55:      -- SET NOCOUNT ON added to prevent extra result sets from
  56:      -- interfering with SELECT statements.
  57:      SET NOCOUNT ON;
  58:   
  59:      SET TRANSACTION ISOLATION LEVEL READ COMMITTED
  60:          
  61:      declare @localized_string_InsertCustomer_Failed_GetType nvarchar(256)
  62:      set @localized_string_InsertCustomer_Failed_GetType = N'GetTypeId failed'
  63:   
  64:      declare @localized_string_InsertCustomer_InsertFailed nvarchar(256)
  65:      set @localized_string_InsertCustomer_InsertFailed = N'Failed inserting Customer into TrackingStore'
  66:   
  67:   
  68:      DECLARE @local_tran        bit
  69:              ,@error            int
  70:              ,@error_desc    nvarchar(256)
  71:              ,@ret            smallint
  72:   
  73:  IF @@TRANCOUNT > 0
  74:          SET @local_tran = 0
  75:      ELSE
  76:       BEGIN
  77:          BEGIN TRANSACTION
  78:          SET @local_tran = 1        
  79:       END
  80:   
  81:      INSERT        [dbo].[tblVersion] (
  82:                      [CustomerName]
  83:                      ,[WorkflowID]
  84:                      ,[WorkflowVersion]
  85:                      ,[EndpointAddress]
  86:      )
  87:  VALUES
  88:  (
  89:  @CustomerName,
  90:  @WFId,
  91:  @Version,
  92:  @EndpointAddr
  93:  )
  94:   
  95:  IF @@ERROR NOT IN ( 3604 /* ignore dup key */, 0 )
  96:       BEGIN
  97:          SELECT @error_desc = @localized_string_InsertCustomer_InsertFailed
  98:          GOTO FAILED
  99:       END
 100:   
 101:      IF @local_tran = 1
 102:          COMMIT TRANSACTION
 103:   
 104:      SET @ret = 0
 105:      GOTO DONE
 106:   
 107:  FAILED:
 108:      IF @local_tran = 1
 109:          ROLLBACK TRANSACTION
 110:   
 111:      RAISERROR( @error_desc, 16, -1 )
 112:   
 113:      SET @ret = -1
 114:      GOTO DONE
 115:   
 116:  DONE:
 117:      RETURN @ret
 118:  END

In Conclusion

I’m sure that there are those of you out there that can find flaws in this method, suggest improvements, or have other ways you’ve done this but I’ve found few concrete methods distributed in blogs.  I (and Xingang) were just hoping this could help someone along the way and maybe with suggestions from others, even improve what we have.  Comments welcome!