Welcome to MSDN Blogs Sign in | Join | Help

Getting Started : ASPX Association and Initiation Forms for SharePoint 2007 Workflows

I realize that this topic may be somewhat of a bore for some of you out there that have already ventured down this road, but I have found that there are still many customers just now getting into SharePoint, and Workflow and then needing to create their own ASP.Net forms for association and initiation data.

The hardest thing for me a lot of times is trying to figure out where to go to find the information about this, and for those who are new to the topic, where do you first begin?

Luckily, SharePoint 2007 has been out for a while and the world is now looking toward SharePoint 2010 but, let’s not forget the mass of humanity that may not move there for a while…or those that will be moving eventually, but that need to get started right now with their workflows.

I have put together a short list of links that have really helped me out a lot getting started understanding the mechanics of creating these form types and their integration with SharePoint 2007 workflows.

First, you can start with a few articles on MSDN:

How to: Implement a SharePoint Workflow with ASP.NET Forms

http://msdn.microsoft.com/en-us/library/dd206915.aspx

Workflow Forms Overview

http://msdn.microsoft.com/en-us/library/ms457061.aspx

Creating an Application Page in WSS 3.0

http://msdn.microsoft.com/en-us/library/bb418732.aspx

Those articles begin to build a lot of information in your mind about the things you will need to do to your page.  However, the best step-by-step guide I’ve seen is at Robert Shelton’s blog:

http://rshelton.com/archive/2007/10/05/sharepoint-document-workflow-with-visual-studio-workshop-documents-download.aspx

Robert actually has an entire series of articles on SharePoint tutorials here that are very good.

As I went through this document, I stumbled upon something else quite interested.  As you are going through Roberts tutorial you might notice that there is code in there (such as for page-to-workflow serialization etc) that would be pretty common code for just about any type of ASP.Net form.  I then discovered Serge Lucas’ submission to CodePlex titled ‘Generic Framework for SharePoint Workflow aspx forms’ at http://aspxsharepointwf.codeplex.com/.

On this site, you will find sample code, the framework and a link to a screencast that demonstrates how to use the framework.

Posted by larrywa | 0 Comments

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!

Posted by larrywa | 1 Comments

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.

Posted by larrywa | 0 Comments

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!

Posted by larrywa | 0 Comments

WF 4.0 Beta 1 : How to Create a Declarative Sequential Service Library

Wow! It's been a long time since I've blogged about anything! It seems every time I think of something to blog about, I find some other blog that already talks about the same thing. I guess it's best I get over this because you never know who will find who's blog.

After being in the trenches for so long with WF 3.0/3.5, SharePoint WF and WCF, I have finally had the opportunity to start digging into WF 4 & Visual Studio 2010 Beta 1. Workflow is headed for a very bright and exciting future, but like all new things, it's going to take some getting used to.

So today, let's start by creating a 'Declarative Sequential Service Library' (workflow exposed as a WCF service). But first, lets understand a few things about this type of service.

  • Remember this is beta 1 and some functionality that might make this easier to implement does not yet exist.
  • Remember that a declarative service is all about creating a workflow, exposed as a WCF service. This is something that takes a bit of effort in WF 3.5.
  • Remember that when we say declarative, we mean, no code. And that is one of the key things here that tripped me up, because I kept trying to do things in code….and I couldn't.

What will this example service represent?

Since spend a week every year at summer camp, I've decided to create a simple service that will confirm space in merit badge classes for summer camp. In our case to keep this simple, we will simply confirm space for any merit badge request. We will not be doing any sort of correlation in this example, we'll save that for later blog entries.

Creating the Service

  1. Open Visual Studio 2010 Beta 1 and select File | New | Project | Visual C# | WCF | Declarative Sequential Service Library. Give your project the name 'ConfirmMeritBadge' and select OK. What you will be presented with is the new WF 4.0 designer surface with a sequence activity that contains a receive and send activity that work together somewhat as a unit.

clip_image001

As it stands at this point, you'll notice that you have an Operation Name of GetData, a Value of 'data' and no Correlates with value. You may recall from Visual Studio (2008/2010) that if you were to have selected just to create a WCF service, it would provide for you a contract named IService with an operation named GetData. This is the same sort of thing that is happening here. We are going to change these values though.

Here is the data type we will be referencing within our service (I created this by creating a class library named MeritBadgeType and putting it in the same Visual Studio solution):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Runtime.Serialization;

namespace MeritBadgeType
{
    [DataContract]
    public class MeritBadge
    {
        private string _mBadgeName = default(string);
        private Int16 _mBadgeSession = default(Int16);

        [DataMember]
        public string BadgeName
        {
            get { return _mBadgeName; }
            set { _mBadgeName = value; }
        }

        [DataMember]
        public Int16 BadgeSession
        {
            get { return _mBadgeSession; }
            set { _mBadgeSession = value; }
        }
    }
}

 

  1. Right click the Service1.xamlx file in the Visual Studio solution and rename it to MeritBadgeService.xamlx.
  2. Add a reference to the assembly (MeritBadgeType) to the declarative service. I do this by selecting 'Add Reference' and then selecting my MeritBadgeType project in the same Visual Studio solution.
  3. Build the solution. This may sound odd at this point, but if you don't do a build, you will not be able to see your referenced data type when you create your workflow variable.
  4. Click in the workflow designer where you see an activity named 'Sequential Service'. This will highlight the entire sequence activity. In the properties window, set the DisplayName property to 'Confirm Badge'.
  5. We need to create two workflow scoped level variables to hold our merit badge object and a boolean confirmation. With the Confirm Badge activity still selected, in the lower left hand corner of the workflow designer, select the 'Variables' button.
  6. Create two variables by selecting 'Add New Variable':

Variable Name Type
wfMeritBadge To set the type, select the drop-down in the Variable Type column and select 'Browse for Types'. A 'type' browser dialog box will appear and you will need to select the 'MeritBadgeType.MeritBadge' data type. Select Ok.
wfBadgeConfirm The type will be a boolean that you can just select from the Variable Type drop-down.

clip_image002

 

  1. Click on the Receive Request receive activity to highlight it. Change it's DisplayName to 'Receive Badge Request'.
  2. Click on the SendResponse send activity and set it's DisplayName to 'Badge Reply'.

Setting up the Activities

  1. Now that we have everything named like we want it, we need to start setting up our internal activities to have operation and contract names like we wish for it to have. Select the Receive Badge Request activity.
  2. Set the Operation Name property to 'ReserveBadgeSpace'.
  3. Set the ServiceContractName property to 'IConfirmBadge'.
  4. Set the Value property to 'wfMeritBadge'
  5. Click on the Badge Reply Send activity.
  6. Set the Value property to 'wfBadgeConfirm'.

    At this point, you may be asking, what did we just do here? Why couldn't I just browse to a contract or operation name? Well, in Beta 1, you do not have the capability to physically add your own contract, in code, and then browse to it. You also cannot browse to a referenced contract type. Basically, when you put this information into the properties of the Receive activity, the service contract and operation will be created on the fly, sort of like the Workflow First approach that was implemented in .Net 3.5.

Now comes the tricky part. If I were to go and try to add a service reference to this service in a client app, what it would show me is that my actually service name is IService1, not IConfirmService. So how do we get this to work?

  1. Drag and drop an Assign activity below the Receive Badge Request activity. Set the DisplayName to Confirmed.
  2. In the Assign activity, set the To property to wfBadgeConfirm and the Value property to True. There is of course a lot more we could do in this service…we could go out to a database to figure out if there is room left for more students etc, but for now, I'll just set the return to true.

In the end, your declarative service should look something like this:

clip_image003

Setting up the Service Name

  1. In the ConfirmMeritBadge project, open up the web.config file. I'll just do it with an XML editor instead of the WCF Configuration Editor. You will see this:

clip_image004

Notice that in the config file, we need to change the name of the service being referenced to 'MeritBadgeService' which is the same name as the .xamlx file. Another thing you might notice is that you don't see an endpoint configured with an address or binding type. This is because, out of the box, this declarative service assumes you will be using basicHttpBinding and the address will be http://localhost:<someport>/MeritBadgeService.xamlx. Also notice that metadata exposure is enabled.

  1. Save the web.config file and close it.
  2. Close the MeritBadgeService.xamlx file.
  3. Right click on the MeritBadgeService.xamlx file in the solution explorer and choose Open With.
  4. Choose XML Editor and select Ok.

clip_image005

  1. Change the ConfigurationName and Name settings to MeritBadgeService. Save the file and close it.

What we just did here (remember this is Beta 1 bits) is, to change the exposed service name, we had to go into the source code and change the service name. To re-open the MeritBadgeService.xamlx file in the workflow designer, double-click on it in the solution explorer. Rebuild the entire solution.

Testing the Service

  1. First we perform a simple test just to make sure the service will work. Here, we are hosting via IIS, so right click on the ConfirmMeritBadge project and select 'View In Browser'.
  2. You will be presented with a directory listing of your service. You cannot click on the .xamlx file and view it in the browser because there is no menu item for this. You could just type in the address to the .xamlx file if you know the Visual Studio generated port number, but I'll do it this way first to see what port number will be used.
  3. In the browser window, click on the MeritBadgeService.xamlx file. What you should see (if everything is working right is)

clip_image006

…and if you click on the wsdl link, you'll be able to see that indeed the service name is 'MeritBadgeService':

clip_image007

What about a real test?

For a real test, you can pretty much choose any type of client where you can add a service reference to (and that can call a basicHttp endpoint). What you need to do is add a service reference to the address: http://localhost:1068/MeritBadgeService.xamlx from the above example and the service reference will be added to your project. More info in the next post about how to set this up.

Posted by larrywa | 3 Comments

Update: Hosting a Workflow Service in IIS

It was January when I initially posted the steps on how to host a workflow, exposed as a WCF service, within IIS.  Since then, and more specifically, with the .Net 3.5 SP1 update, a nice feature has been added to ease this process.

So what I will do here is take the same workflow service, or really it can be any workflow service, and go through the steps of how to host it in IIS.

The Workflow Service Project

So, as you may recall, we created a WCF Sequential Workflow Service Library from within Visual Studio 2008.  We then changed the interface (contract) file to IProductOrder and we changed the name of our workflow file to OrderProcessWF.cs.  Just to make sure we are on the same page, I'll show the code again:

IProductOrder.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization;
using System.ServiceModel;

namespace WFHostedInIIS
{

    //Service contract

    [ServiceContract(Namespace = "http://ncdevguyblog/Workflow")]
    public interface IProductOrder
    {

        [OperationContract]
        void SubmitOrder(OrderInfo order);

        [OperationContract]
        OrderInfo GetOrder(string orderID);

    }
    //Data type (contract)
    [DataContract]
    public class OrderInfo
    {

        private string orderID = default(string);
        private string fName = default(string);
        private string lName = default(string);
        [DataMember]
        public string OrderID
        {
            get { return orderID; }
            set { orderID = value; }
        }

        [DataMember]
        public string FName
        {
            get { return fName; }
            set { fName = value; }
        }

        [DataMember]
        public string LName
        {
            get { return lName; }
            set { lName = value; }
        }

    }

}

OrderProcessWF.cs (designer mode)

image

app.config file

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.web>
    <compilation debug="true" />
  </system.web>
  <system.serviceModel>
    <services>
      <service behaviorConfiguration="WFHostedInIIS.Workflow1Behavior"
        name="WFHostedInIIS.OrderProcessWF">
        <endpoint address="" binding="wsHttpContextBinding" contract="WFHostedInIIS.IProductOrder">
          <identity>
            <dns value="localhost" />
          </identity>
        </endpoint>
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
        <host>
          <baseAddresses>
            <add baseAddress="
http://localhost:8731/Design_Time_Addresses/WFHostedInIIS/OrderProcessWF/" />
          </baseAddresses>
        </host>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="WFHostedInIIS.Workflow1Behavior"  >
          <serviceMetadata httpGetEnabled="true" />
          <serviceDebug includeExceptionDetailInFaults="false" />
          <serviceCredentials>
            <windowsAuthentication
                allowAnonymousLogons="false"
                includeWindowsGroups="true" />
          </serviceCredentials>
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

So at this point, I'm assuming you have your project created, compiled and ready for work.

Publishing the Project to IIS

With .Net 3.5 SP1, Microsoft has added a mechanism (wizard) in which we can publish our services to be hosted in IIS.  Basically what they did was, they used the ASP.Net publishing wizard and added it as a context menu item.

1. First, assuming your project is open, what you will need to do is go into the project properties and de-select the checkbox under the WCF Options tab.  This will prevent Visual Studio from trying to provide its default WCF service host.  Since we will have IIS for our host, we don't need this. BTW, this checkbox is a new option in .Net 3.5 SP1.

WcfOptions

2.  Click on the Debug tab and make sure the Command line arguments do not have any reference to the WCFTestClient.exe.  You will normally see this there because Visual Studio adds this to WCF service projects created with the Visual Studio project templates.

DebugOptions

3.  Right click on WFHostedInIIS project and select 'Publish'.

publishmenu

4.  In the dialog that appears, enter http://localhost/IISHostingOfWorkflow as the Target Location and click the Publish button.

PublishWindow

5.  Right click the WFHostedInIIS solution and select Add | Existing Web Site...

6.  Select the IIS application 'IISHostingOfWorkflow' and select Open.

inetmgr

7.   To do an initial test, right click on the .svc file and select 'View in Browser'. If everything is working correctly, you should see a service page appear in your web browser.

BrowserView

8.  Instead of going through the whole process of creating a test client, we can use the WCFTestClient to test our methods. Click on the website project http://localhost/IISHostingOfWorkflow' and then select Property Pages.

9.  Click on Start Options.

10. Set Start external program to 'C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\WcfTestClient.exe' and Command line arguments to 'http://localhost/IISHostingOfWorkflow/WFHostedInIis.OrderProcessWF.svc'.

startoptions

11.  To test the service with the WCFTestClient interface, right click on the website project and select Set As Startup Project.

12.  Select F5 to begin debugging. The test client should appear and you are able to enter data into the fields for testing.

TestClient2

So there we have it.  We could of course build some other sort of client interface, add a service reference and point to the .svc file as the address, but the point is, compared to the old way of doing it, we have relatively few steps to get this up and running.

Posted by larrywa | 1 Comments

Changing the Look

I was told recently by someone reading my blog that some of my code was being cut off on the right hand side of the blog border. I've changed the look now to allow for easier reading. Hope this helps!
Posted by larrywa | 1 Comments

HowTo: Create a Custom Action for an Office SharePoint Designer Workflow - Part 3

 

Ok, so if you've been following along up to this point in grand anticipation, this is going to be a happy time for you...we now get to test our custom action.

Step 1 – Create the Workflow

1. Open SPD to the site http://<yourmachineanddomain.com>/FleetCustomers by clicking File > Open Site.

2. Click on the root folder and then select File->New->Workflow.

3. Give the Workflow the name 'Fleet Customer" and attach it to the 'Fleet Customer Lib Request' document library. Make sure the workflow can be started manually.

SPD1

4. Click Next.

5. Name the Step ‘Create Doc Lib’.

6. Select the Actions button and then select 'Create Fleet Document Library'.

7. Click Name of the doclib and using the Function button, select Current Item and then ‘Customer Name'.

SPD2 

8. Click Link to site and type in a site name of http://<yourmachineanddomain.com>/FleetCustomers . (the /fleetcustomers sub-site is hard coded in the application)

Your Action should look like this:

SPD3

9. Check the Workflow for Errors. Select Finish.

Step 2 – Testing the Workflow

1. Go back to the site http://<yourmachineanddomain.com>/FleetCustomers via Internet Explorer.

2. Click on Fleet Customer Lib Request in the Documents menu and create a new document. It does not matter at this point what the content of the Word document is or the name that you save it as. What is important is that you fill in the Customer Name and Title on the document properties ribbon bar.

This information will be used to create the new doc lib.

3. Put in a customer name of 'MyFleetCustomer' and a title of 'I want a new truck'.

4. Save the document to the Fleet Customer Lib Request document library.

5. Execute the Fleet Customer workflow against the new document by clicking the drop-down arrow adjacent to the document name and then selecting Workflows then Fleet Customer.

6. You will be prompted to start the workflow, select the Start button.

7. Go back to the root site.

8. Click on Tasks. You should see a new task that was created for the administrator to read.

9. Click on the Fleet Customers site menu item.

10. Click on View All Site Content. You should see the new MyFleetCustomer document library.

So there you have it! In this three part series, we have created a custom activity that represents our new action, created an .ACTIONS file that will be used by SharePoint to know what to display on the menu and which fields the action needs and, we've tested the workflow.

I hope you find this information useful. Let me know if you find anything wrong our have better suggestions on how to do something.

Posted by larrywa | 1 Comments

HowTo: Create a Custom Action for an Office SharePoint Designer Workflow - Part 2

So, in my last post we went through the first steps of what we need to do to create a custom action for Office SharePoint Designer (SPD).  In Part 2, we will add the necessary .ACTION file and make a modification to our web applications web.config file to get our component successfully registered and authorized.

Creating the .ACTION file

1. Create a new text file named CreateFleetDocLib.ACTIONS and save it to:

C:\MSDNBlog\SPD\CreateFleetDocLib\CreateFleetDocLib

We are only saving this file here so it is contained in the same project. It is not compiled with the project.

2. Add the following code to the text file you just created

<?xml version="1.0" encoding="utf-8"?>
<WorkflowInfo>
<Actions Sequential="then" Parallel="and">
<Action Name="Create Fleet Document Library" ClassName="CreateFleetDocLib.FleetDocLibActivity" Assembly="CreateFleetDocLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a796aa76103ee0c2" AppliesTo="all" Category="Labs">

<RuleDesigner Sentence="Create fleet document library %1 on %2.">
<FieldBind Field="CompanyName" Text="Name of the doclib" Id="2" DesignerType="TextArea"/>
<FieldBind Field="Url" Text="Link to site" DesignerType="TextArea" Id="1"/>
</RuleDesigner>

<Parameters>
<Parameter Name="Url" Type="System.String, mscorlib" Direction="In" />
<Parameter Name="CompanyName" Type="System.String, mscorlib" Direction="In" /> </Parameters>

</Action>
</Actions>
</WorkflowInfo>

A couple of things to note here about two of the elements in this file:

<RuleDesigner> - this element represents the text you will see in the actions menu inside of the SPD wizard.

<FieldBind> - this element represents the fields that you will need to fill in in the designer once you have this action item dropped into the designer form.  The important thing here is that the 'Field' attribute value needs to be the same as a <Parameter> 'Name' attribute in the section below it.  So for example, we have a Field="CompanyName" and a Parameter Name="CompanyName".

3. Browse to the C:\Windows\Assembly directory and right click on the CreateFleetDocLib assembly and select Properties. Copy the Public Key Token to the clipboard.

4. In the CreateFleetDocLib.ACTIONS file, replace the PublicKeyToken with the Public key token you copied to the clipboard.

5. Copy the CreateFleetDocLib.ACTIONS file into the following directory:

                C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\1033\Workflow

Modifying web.config

1. Browse to the directory:

           C:\Inetpub\wwwroot\wss\VirtualDirectories\ <yourmachineanddomain>

2. Open up the web.config file for editing.

3. Add the following to the AuthorizedTypes section of the web.config file.

<authorizedType Assembly="CreateFleetDocLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a796aa76103ee0c2" Namespace="CreateFleetDocLib" TypeName="*" Authorized="True" />

4. Save the web.config file

5. Restart IIS (iisreset).

Posted by larrywa | 0 Comments

HowTo: Create a Custom Action for an Office SharePoint Designer Workflow - Part 1

Sorry about the long title, but I thought it may help when someone is doing a search for such a topic.  On with the show....

Invariably, if you are using Office SharePoint Designer (SPD) to create workflows, your power users are going to say 'Hey, I really like this but I want to have an Action that does xyz'.  Nobody ever seems just satisfied with what we get out of the box, but that's the beauty of the Microsoft platform, its extensible!

So in part 1, we will talk about the first steps to creating a custom action.

Scenario
Suppose we have a situation where we have a car dealership that caters to customers that purchase fleets of vehicles.  The idea here would be that whenever a new fleet customer wants to sign an order, we would want to take that order form and use it to create a unique site (or document library) for this client.  I'd like to be able to create an action where I could pick from that would allow me to pick up the customers name from the document and use that to create my new document library.

Part 1 - Creating our Custom Action

1. First, we need to setup a sample site in WSS (3.0) or MOSS to use as our base site. Go ahead and create a site http://<yourmachineanddomain.com/FleetCustomers. Create a new document library named 'Fleet Customer Doc Lib'.  In this document library, create a new custom column (single line of text) named 'Customer Name'

A custom Action is actually just a custom workflow activity that is registered with the system using an .ACTIONS file, so now we will create a custom activity.

2. Create a new workflow activity library project in Visual Studio 2008 in some location on your machine for example 'C:\MSDNBlog\SPD' Give it the name 'CreateFleetDocLib'.

3. Rename Activity.cs to FleetDocLibActivity.cs. Open the code view to the activity.

4. Change the base class from SequenceActivity to Activity.

5. Create two Dependency properties (both strings):

Property Name Data Type Category Description
Url string Input Property Url of base site
CompanyName string Input Property Used as doc lib name

This can also be done by using Insert Snippet > Other > Workflow > Dependency Property – Property.

6. To make sure we require validation on each of these entries (meaning we have to have input), put the following above each of the properties:

[ValidationOption(ValidationOption.Required)]

The code for the dependency properties should look like this:

#region Dep Props

public static DependencyProperty UrlProperty = System.Workflow.ComponentModel.DependencyProperty.Register("Url", typeof(string),typeof(FleetDocLibActivity));

[Description("Base URL of site")]
[Category("Input Property")]
[Browsable(true)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)[ValidationOption(ValidationOption.Required)]
public string Url
{
   
get
   
{
   
return ((string)(base.GetValue
    
(FleetDocLibActivity.UrlProperty)));
   
}

    set
   
{
   
base.SetValue(FleetDocLibActivity.UrlProperty, value);
   
}
}

public static DependencyProperty CompanyNameProperty = System.Workflow.ComponentModel.DependencyProperty.Register("CompanyName",typeof(string), typeof(FleetDocLibActivity));

[Description("Name of the fleet company")]
[Category("Input Property")]
[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[ValidationOption(ValidationOption.Required)]
public string CompanyName
{
  
get
   {
    return ((string)(base.GetValue   
   
(FleetDocLibActivity.CompanyNameProperty)));
    }

   set
  
{
   
base.SetValue(FleetDocLibActivity.CompanyNameProperty, value);
  
}
}

#endregion

7. Add an EventLog variable to the FleetDocLibActivity class along with other variables used for the sites and webs to be accessed.

#region Member Variables

private EventLog _eventLog;

//main site
SPSite mainSite = null;
SPWeb rootweb = null;

//sub site
SPSite subSite = null;
SPWeb subWeb = null;

#endregion

8. Insert the following code inside of the FleetDocLibActivity class (after the constructor)

protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
    //Set up the Event Logging object            
    _eventLog = new EventLog("Workflow");
    _eventLog.Source = "SharePoint Workflow";

    try
    {
        //Send the email                
        CreateDocLib();
    }
    finally
    {
        //Dispose of the Event Logging

        // object         
        _eventLog.Dispose();
    }
    //Indicate the activity has closed            
    return ActivityExecutionStatus.Closed;
}

private void CreateDocLib()
{

    try
    {
        mainSite = new SPSite(Url);
        rootweb = mainSite.OpenWeb();

        //sub site
        subSite = new SPSite(Url + "/FleetCustomers");
        subWeb = subSite.OpenWeb();

        //get the list collection for the main and sub
        // sites

       SPListCollection lists = subWeb.Lists;
        SPListCollection rootLists = rootweb.Lists;

        //we are going to create a new doc lib in the
        //sub-site, create a new empty template 
        //type then set the type to be a doc library
        SPListTemplateType listTemplateType = new SPListTemplateType();
        listTemplateType = SPListTemplateType.DocumentLibrary;
        lists.Add(CompanyName, "", listTemplateType);

        SPList taskList = null;

        //look for the tasks lists in the root web
        //also note that SPQuery could be used here 
        foreach (SPList lst in rootLists)
        {
            if (lst.Title.Equals("Tasks",
                      StringComparison.InvariantCultureIgnoreCase))
            {
                taskList = lst;
                break;
            }
        }
        if (taskList != null)
        {
            //add a new task item for the administrator
            SPListItem newItem = taskList.Items.Add();
            newItem["Title"] = "Required completion of Doc Lib " + CompanyName;
            newItem["Customer Name"] = CompanyName;
            newItem.Update();
            _eventLog.WriteEntry("Task assigned to admin");
        }

        _eventLog.WriteEntry("Workflow success: Document Library created at: " + rootweb.Title.ToString());

    }
    catch (System.Exception Ex)
    {
       //Log exceptions in the Event Log                
        _eventLog.WriteEntry("Workflow Error :" + Ex.Message.ToString(), EventLogEntryType.Information);

    }
    finally
    {

       mainSite.Dispose();
        subSite.Dispose();
        rootweb.Dispose();
        subWeb.Dispose();
    }
} //end CreateDocLib

9.  Add the following using statements to the file:

using System.Diagnostics;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Workflow;
using Microsoft.SharePoint.WorkflowActions;

10. Add a reference to your project to these (note that you may have to browse for this assemblies) SharePoint assemblies:

Under the .NET tab

Windows SharePoint Services
Windows SharePoint Services Workflow Actions

- If you can’t find the assemblies you can browse directly to C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\ISAPI\
- Microsoft.SharePoint.dll
- microsoft.sharepoint.WorkflowActions.dll

11. Go into the CreateFleetDocLib project properties and create a new strong named assembly file named FleetLib.snk.

12. Build the assembly and resolve any errors.

13. Using the Visual Studio command prompt, place the library into the GAC by running the following command (NOTE: Don't forget the double quotes around the path to the assembly):

gacutil /i "C:\MSDNBlog\SPD\CreateFleetDocLib\CreateFleetDocLib\bin\Debug\CreateFleetDocLib.dll"

At this point, we have built the activity that will be used to handle our custom action.  In the Part II, we will create our custom action file.

Posted by larrywa | 1 Comments

Where's my SvcTraceViewer.exe?

I decided to move into the brave new world of Windows Server 2008. My intention was to setup a new VPC image to do some testing with WCF.

As I recalled from my experiments with WPF, there is an issue with XAML intellisense be disabled if you install the new Windows Server 2008 / .Net 3.5 SDK after Visual Studio 2008.  So, I decided to install the SDK first. There is a solution to the XAML issue by the way at:

http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=2892404&SiteID=1

Much to my surprise, after I installed the SDK, I could not find one of my favorite tools (SvcTraceViewer.exe) in the sdk bin directory. I went back and checked to make sure that I installed all the .Net pieces correctly but still it wasn't there. It wasn't until AFTER I installed Visual Studio 2008 that I realized that when you install Visual Studio, Visual Studio will install these WCF tools for you.

I also noticed that on Windows 2008 Server, that after installing Visual Studio 2008, a new directory was created under C:\Program Files\Microsoft SDKs\Windows named v6.0a. Under this bin directory is where I found the WCF tools. This is sort of interesting because on my Vista machine, I just have one directory v6.1 and it's bin directory has all the tools.

So, either way, Visual Studio should install the final tools for you.

I thought maybe this piece of information might be useful for other who might be wondering why the SDK install doesn't provide these tools.

Posted by larrywa | 0 Comments

HOWTO: Host a Workflow Service in IIS (the whole story)

 

    I started out in my attempt to figure out how to host a workflow service in IIS by looking at the following MSDN article:

     

    How to: Host a Workflow Service in IIS

    http://msdn2.microsoft.com/en-us/library/bb675264.aspx

     

    This gave me 'most' of the information I needed but as is usual for me, it's the small missing pieces of information that end up creating the most grief for me.  So, I thought I would put down all the steps it took, from start to finish in hopes that this may help someone else.

     

    What we are going to be doing is hosting an existing, pre-compiled workflow as a service in IIS (on Vista). What this implies is that we are going to create a workflow library project.  This is what tripped me up because there is a particular way you need to do this otherwise you have to end up adding a lot of your own code.

     

    Setting up things in IIS

  1. First, you have to make sure you have IIS installed on the machine and that it's running.  Therefore you need to go through the Control Panel | Programs and Features | Turn Windows features on and off and select Internet Information Services.
     
  2. Open the Internet Information Services Manager by going to Control Panel | Administrative Tools | Internet Information Services (IIS) Manager.
     
  3. Create a new virtual directory by right clicking on Default Web Site and selecting 'Add Virtual Directory'. Here you will need to give this virtual directory an alias name and point it to a physical directory on the machine.  For the same of our sample, I'm going to give the alias the name of 'IISWorkflowHost' and point it to a directory I've already named on my machine 'C:\IISHostForWorkflow'.  I have named the alias and directory differently the help you understand they don't need to necessary be named the same thing and to also show how we reference our service later on in the blog.
     
  4. Double click on the Authentication icon in the IIS section.  You need to enable Windows Authentication.
  5.  

  6. Looking in the IIS Manager (left hand tree view), you will see the new folder for IISWorkflowHost.  This is just a directory however and to get this to work correctly you need to right click on the directory and select 'Convert to Application'.  You can just select OK from the dialog box that pops up or you can change the AppPool if desired.  For this example, we'll just accept the defaults.  This step was one of the things I was missing when I tried to get it working initially.
     
  7. Create a Bin directory under the C:\IISHostForWorkflow directory.  This is where you will end up putting the workflow assembly that we will create later.  Before we do any further additions to our virtual directory, let's go create our workflow service.
  8.  

     

    Creating the Workflow Service

  9. Open Visual Studio 2008 and select File | New | Project | Visual C# | WCF | Sequential Workflow Service Library.  Give it the name of WFToHostInIIS and put it in the directory C:\IISHostedWFExample. Select OK.
     
  10. Rename Workflow1.cs to OrderProcessWF.cs.
     
  11. Rename IWorkflow1.cs (the contract file) to IProductOrder.cs.
     
  12. We need to replace ALL the code in the IProductOrder.cs file with:
  13.  

     

    using System;

    using System.Collections.Generic;

    using System.Linq;

    using System.Text;

    using System.Runtime.Serialization;

    using System.ServiceModel;

     

    namespace WFToHostInIIS

    {

        //Service contract

        [ServiceContract(Namespace = "http://ncdevguyblog/Workflow")]

        public interface IProductOrder

        {

            [OperationContract]

            void SubmitOrder(OrderInfo order);

     

            [OperationContract]

            OrderInfo GetOrder(string orderID);

        }

     

        //Data type (contract)

        [DataContract]

        public class OrderInfo

        {

            private string orderID = default(string);

            private string fName = default(string);

            private string lName = default(string);

     

            [DataMember]

            public string OrderID

            {

                get { return orderID; }

                set { orderID = value; }

            }

     

            [DataMember]

            public string FName

            {

                get { return fName; }

                set { fName = value; }

            }

     

            [DataMember]

            public string LName

            {

                get { return lName; }

                set { lName = value; }

            }

        }

    }

     

    This service exposes two methods, one for submitting an order, the other for getting order info. For simplicity, we are only going to implement SubmitOrder.

     

  14. Open OrderProcessWF in the workflow designer.  Click on the Receive activity that you see on the designer surface and rename it to RcvSubmitOrder.
     
  15. Click on the ServiceOperationInfo property, select the ellipse on the right hand side and Import the IProductOrder contract. You may not actually need to import it, but if the SubmitOrder method doesn't show up in the Operations list, you'll need to import.
     
  16. Select the SubmitOrder operation. Select OK.
     
  17. Click on the 'order' parameter and then select the ellipse that will take you to the property/field binding dialog box.  Select Bind to a new Member tab and then create a new property named WFOrderInfo. Select OK

 

  1. Open the app.config file.  Everywhere you see 'IWorkflow1', replace that with 'IProductOrder'.
     
  2. For the <service name="WFToHostInIIS.Workflow1", change this to <service name="WFToHostInIIS.OrderProcessWF".
     
  3. For the base addresses:
  4.           <baseAddresses>

                <add baseAddress="http://localhost:8731/Design_Time_Addresses/WFToHostInIIS/Workflow1/" />

              </baseAddresses>

     

    Change this to:
     

              <baseAddresses>

                <add baseAddress="http://localhost:8731/Design_Time_Addresses/WFToHostInIIS/OrderProcessWF/" />

              </baseAddresses>

     

    Note that your port number may be different in your case. This does not matter.  In the end, your app.config would look something like this:

     

    <?xml version="1.0" encoding="utf-8" ?>

    <configuration>

      <system.web>

        <compilation debug="true" />

      </system.web>

      <system.serviceModel>

        <services>

          <service name="WFToHostInIIS.OrderProcessWF" behaviorConfiguration="WFToHostInIIS.Workflow1Behavior">

            <host>

              <baseAddresses>

                <add baseAddress="http://localhost:8731/Design_Time_Addresses/WFToHostInIIS/OrderProcessWF/" />

              </baseAddresses>

            </host>

            <endpoint address=""

                      binding="wsHttpContextBinding"

                      contract="WFToHostInIIS.IProductOrder">

              <!--

                  Upon deployment, the following identity element should be removed or replaced to reflect the

                  identity under which the deployed service runs.  If removed, WCF will infer an appropriate identity

                  automatically.

              -->

              <identity>

                <dns value="localhost"/>

              </identity>

            </endpoint>

            <endpoint address="mex"

                      binding="mexHttpBinding"

                      contract="IMetadataExchange" />

          </service>

        </services>

        <behaviors>

          <serviceBehaviors>

            <behavior name="WFToHostInIIS.Workflow1Behavior"  >

              <serviceMetadata httpGetEnabled="true" />

              <serviceDebug includeExceptionDetailInFaults="false" />

              <serviceCredentials>

                <windowsAuthentication

                    allowAnonymousLogons="false"

                    includeWindowsGroups="true" />

              </serviceCredentials>

            </behavior>

          </serviceBehaviors>

        </behaviors>

      </system.serviceModel>

    </configuration>

     

     

    Finishing the configuration for IIS

  5. Since we are still within Visual Studio, we are going to create a new web.config file to put in the virtual directory/application we created. Just choose File | New | File | Web | Web Configuration File.  Now what you are going to see is a configuration file that has a LOT of things that you don't need for this  example. Just replace the entire contents of this file with:
     
  6. <?xml version="1.0" encoding="UTF-8"?>

    <configuration>

       

    </configuration>
     

  7. We need to add our own code to the web.config file within the <configuration> section.  Paste in the following code:
  8. <system.serviceModel>

    <services>

    <service name="WFToHostInIIS.OrderProcessWF" behaviorConfiguration="ServiceBehavior">

    <endpoint address="ContextOverHttp" binding="wsHttpContextBinding" contract="WFToHostInIIS.IProductOrder" />

    </service>

    </services>

     

    <behaviors>

    <serviceBehaviors>

    <behavior name="ServiceBehavior">

    <serviceMetadata httpGetEnabled="true" />

    <serviceDebug includeExceptionDetailInFaults="true" />

    <serviceCredentials>

    <windowsAuthentication allowAnonymousLogons="false" includeWindowsGroups="true" />

    </serviceCredentials>

    <!-- Comment out the following behavior to disable persistence store -->

    <workflowRuntime name="WorkflowServiceHostRuntime" validateOnCreate="true" enablePerformanceCounters="true">

    <services>

    <add type="System.Workflow.Runtime.Hosting.SqlWorkflowPersistenceService, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionString="Data Source=localhost;Initial Catalog=NetFx35Samples_ServiceWorkflowStore;Integrated Security=True;Pooling=False" LoadIntervalSeconds="1" UnLoadOnIdle="true" />

    </services>

    </workflowRuntime>

    </behavior>

    </serviceBehaviors>

    </behaviors>

    </system.serviceModel>

    <system.web>

    <compilation>

    <assemblies>

    <add assembly="System.WorkflowServices, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />

    </assemblies>

    </compilation>

    <identity impersonate="false" />

    </system.web>

    <system.webServer>

    <directoryBrowse enabled="true" />

    </system.webServer>

     

     

      3.  Save this file to C:\IISHostForWorkflow.

     

    A couple of things to note regarding this web.config file:

    1. I have added the OOB SQL Persistence service to this example. It is not necessary to do that, so you can remove it if you want. If you do use it and you're using SQLExpress, you need to change the Data Source setting.  This example also assumes you've installed and setup the .Net 3.5 Samples located at:

     

    Windows Communication Foundation (WCF), Windows Workflow Foundation (WF) and Windows CardSpace Samples
    http://www.microsoft.com/downloads/details.aspx?FamilyID=2611a6ff-fd2d-4f5b-a672-c002f1c09ccd&DisplayLang=en

     

    1. Notice that the service name points to the same name as the app.config file in our workflow project.
    2. Notice that both endpoint contracts point to the IProductOrder contract.

     

  9. Create a new file for the C:\IISHostForWorkflow directory named OrderProcess.svc.  Do this within Visual Studio. I just started by creating a new text file and just renaming it.
  10.  

  11. Paste in the following code:
  12. <%@ServiceHost language=c# Debug="true" Service="WFToHostInIIS.OrderProcessWF" Factory="System.ServiceModel.Activation.WorkflowServiceHostFactory" %>

     

    Notice how the service that we are referencing here is our workflow.

     

  13. Save the file. 
     
  14. Copy your workflow assembly file (from the projects bin\Debug directory) to your virtual directories Bin directory.
     
  15. To make sure that everything is working appropriately, open Internet Explorer and browse to:
  16. http://localhost/IISWorkflowHost/OrderProcess.svc. 

     

    What you should see is a window similar to what you would see if you browsed to a web service location.

     

     


    Creating a Test Client

     

  17. Create a new Visual C# Windows Console Application named 'ConsoleServiceTest' and place it in the C:\TestWorkflowService directory.
     
  18. Open a command prompt that will allow you to run svcutil.exe. I'm going to just open mine to the Windows SDK directory and run the command:
     
  19. Svcutil /config:app.config http://localhost/IISWorkflowHost/OrderProcess.svc. 

    Two files will be created in the directory where you are running svcutil from, an app.config file and an OrderProcessWF.cs file. Add these two files to your console app project. You could also just right click on the project and choose 'Add Service Reference' and find the service.

     

  20. Add a reference to System.ServiceModel and System.Runtime.Serialization to the project.
  21.  

  22. Paste the following code into the Main method in Program.cs:
     
  23. WFToHostInIIS.OrderInfo oInfo = new WFToHostInIIS.OrderInfo();

    oInfo.FName = "John";

    oInfo.LName = "Doe";

    oInfo.OrderID = "1234";

     

    try

      {

      ProductOrderClient client = new ProductOrderClient();

     client.SubmitOrder(oInfo);

      }

      catch (Exception e)

      {

       Console.WriteLine("Exception: {0}",e.Message);

      }

     

  24. You can step through this using any method you want.
  25.  


     

Posted by larrywa | 13 Comments

New BizTalk Server Adapters site

Not sure if you've had the time to go out and view the new BizTalk Adapters site.

 http://msdn2.microsoft.com/en-us/biztalk/bb544997.aspx

The new site includes a simplified categorization of the adapters, better organization, and some new adapter content. Very shortly this site will replace the old adapter site (http://msdn2.microsoft.com/en-us/biztalk/aa937652.aspx).

Expect to see new links added for R2 when it releases. 

Posted by larrywa | 0 Comments

Book Review: Inside Microsoft Windows SharePoint Services 3.0

 

I just recently began my journey down the road of learning about WSS and MOSS. My primary focus of course is for the workflow portion of the product, but what I have found while building that base of knowledge, is that you do need to know quite a bit about SharePoint just to be able to understand what you can do with custom workflows.

 

I picked up this book written by Ted Pattison and Daniel Larson and I've been very impressed with the content, the level of detail and code samples. I can see that this is a book I am going to reference again and again.  I was actually working with a client who had WSS questions the week I purchased this book and the answers were right there in front of me. Talk about good timing!

 

This book is definitely for the developer but it does have topics and admin might be interested in like SharePoint architecture, deployment and application security.  And of course the chapter on workflow was most excellent…the text was very clear at explaining several of the properties and fields that are critical pieces of custom workflows.

 

I'm not sure what those folks who have lots of experience with WSS think of the book, but for me, it was a good purchase.

Posted by larrywa | 0 Comments

Versioning Techniques for Workflows

As of right now, there are no documents (whitepapers) available that pertain specifically to workflow versioning other than what you will find on MSDN at http://msdn2.microsoft.com/En-US/library/aa349375.aspx.

 

However, out on the forum http://wf.netfx3.com, there have been several posts related to versioning and to give you an executive summary of what you need to do, there are basically two ways (without doing lots of coding) to handle versioning:

 

  1. Use workflows that execute based on XAML activitation.  This is where you just read in a .xoml file via an XMLTextReader through the CreateWorkflow method.  As I stated in class, there is no concept of versioning in a .xoml file and there is no concept of security reading the file in this way either, except for the fact that whoever puts the .xoml file on the machine where the host reads it from must have permissions to do so.
  2. Use .Net versioning techniques where you modify the version in the assembly.cs file prior to deploying it. This should allow in process workflows to continue/finish execution and new workflows to startup with the new workflow version.

 

Here is a link to assembly versioning steps in .Net:

http://msdn2.microsoft.com/en-us/library/51ket42z(vs.71).aspx

 

So, that was the executive summary, but even within these two types of versioning tactics, there are some other things to consider:

 

  1. Are you trying to modify the workflow object structure (ie. Add new dependency properties or methods) or are you trying to add/remove activities?  If you only need to add/remove activities in certain workflow instances, you can still use the WorkflowChanges class to modify these at runtime.  If you are add/removing/changing properties etc of a workflow then you cannot expect a workflow that is currently persisted to be able to be reloaded into a new object structure.
  2. Are you going to be providing workflows or workflow apps to your customers or internal groups and then later wanting to enhance these workflows, without changing the one they have? If you need to do this, it might make more sense to come up with some sort of rules based workflow where you can modify the rules, storing the rules in a database such as we saw with the External Ruleset Demo tool. You can of course change the workflow and rules using the WorkflowChanges class but this only changes the workflow for that one running instance of the workflow, meaning that you would take a performance hit each time you ran the workflow because it would have to be modifying the running workflow instead of executing a compiled version.
  3. If you are going to be using XAML activation, are you going to be linking it to an activity class library? Without using some activity library, just using XAML activation is going to severely limit what you can do with your .xoml file. Since most business logic needs to take place in code, this will most likely mean the code will need to be in a custom activity, which will need to be linked inside of the .xoml file.  So, essentially when you need to update the activity assembly you are once again looking at having to use .Net assembly versioning techniques.

 

 

So, how does an persisted, unloaded workflow know which version of the assembly to use when it is reloaded?  When the workflow gets serialized to the database serialized (binary), the assembly and type information gets serialized as well.  This is normal .NET serialization (BinaryFormatter), not anything workflow-specific.  Upon deserialization,  .NET will automatically load the correct assembly based on version (assuming the correct version of the assembly exists in the GAC or config and can be resolved).

 

The workflow runtime actually knows nothing about workflow versions or any of that.  The instant we serialize a workflow, it’s no longer WF – it’s .NET from then on up to the point after the WF is deserialized again.

Posted by larrywa | 2 Comments
More Posts Next page »
 
Page view tracker