Exercise 6: WorkflowApplication

To this point in the labs you have focused on creating an activity and invoking it in the simplest way possible with the WorkflowInvoker class. The WorkflowInvoker.Invoke method is simple because it is synchronous and invokes the workflow on the same thread as the caller.

Another way to invoke a workflow is with the WorkflowApplication class. This class allows you to run a workflow on a separate thread and to supply delegates to be invoked when the workflow completes, goes idle, terminates or has an unhandled exception. This allows you to create multi-threaded server or client programs more easily than you can without using WF4.

In this exercise, you will modify the host application to run your SayHello activity with WorkflowApplication and observe the threading behavior. You now have two requirements for your workflow.

  1. Return a personalized greeting
  2. Return a non-zero Int32 value containing the managed thread ID that the workflow was invoked on

Using the “write the test first” approach, you will begin by writing a test that verifies your new requirement for the workflow thread ID.

Task 0 – Opening the Solution

To begin this exercise you can use the solution you finished from Exercise 5. Alternatively, you can follow the following steps to begin with Exercise 6.

  1. Start Microsoft Visual Studio 2010 from Start | All Programs | Microsoft Visual Studio 2010.
  2. Open the starting solution for Exercise 6 located under the Source\Ex6-WorkflowApplication\Begin folder and use it as the starting point for this exercise.
  3. Press CTRL+SHIFT+B to build the solution.

Task 1 – Writing the Test to Verify that the Workflow Thread ID is Returned as an Out Argument

In this task, you will create a test that verifies that the workflow returns a non-zero integer in the output arguments named WorkflowThread.

  1. Open SayHelloFixture.cs and add the following namespace directives

    C#

    using System.Threading; using System.Diagnostics;

    Visual Basic

    Imports System.Threading Imports System.Diagnostics

  2. Add the ShouldReturnWorkflowThread test as shown

    (Code Snippet - Introduction to WF4Lab - ShouldReturnWorkflowThread Test CSharp)

    C#

    /// <summary> /// Verifies that the workflow returns an Out Argument /// Name: WorkflowThread /// Type: Int32 /// Value: Non-Zero /// </summary> [TestMethod] public void ShouldReturnWorkflowThread() { var output = WorkflowInvoker.Invoke( new SayHello() { UserName = "Test" }); Assert.IsTrue(output.ContainsKey("WorkflowThread"), "SayHello must contain an OutArgument named WorkflowThread"); // Don't know for sure what it is yet var outarg = output["WorkflowThread"]; Assert.IsInstanceOfType(outarg, typeof(Int32), "WorkflowThread must be of type Int32"); Assert.AreNotEqual(0, outarg, "WorkflowThread must not be zero"); Debug.WriteLine("Test thread is " + Thread.CurrentThread.ManagedThreadId); Debug.WriteLine("Workflow thread is " + outarg.ToString()); }
    FakePre-520707ac83f0469eb1d48d90ebd63eda-bce10ad534984b498b89bc77415eacceFakePre-baacb314573f4199b6644114428f1547-d99c2cf4d49c4675b8d4ee759a1c469aFakePre-2ef0cf6b3c5b42199c4612508a5ccda1-c48aaeef68df40bfab48fb8c6cf20e6eFakePre-6bc3b8c7844143949a65177d2e76d18c-adc6571ae0a04dc3b8abee8df8f749d0
    

    (Code Snippet-Introduction to WF4Lab-ShouldReturnWorkflowThread Test VB)

    Visual Basic

    ''' <summary> ''' Verifies that the SayHello workflow contains an Out Argument ''' Name: WorkflowThread ''' Type: Int32 ''' Value: Non-Zero ''' </summary> ''' <remarks></remarks> <TestMethod()> Public Sub ShouldReturnWorkflowThread() Dim output = WorkflowInvoker.Invoke( _ New SayHello() With {.UserName = "Test"}) Assert.IsTrue(output.ContainsKey("WorkflowThread"), "SayHello must contain an OutArgument named WorkflowThread") ' Don't know for sure what it is yet Dim outarg = output("WorkflowThread") Assert.IsInstanceOfType(outarg, GetType(Int32), "WorkflowThread must be of type Int32") Assert.AreNotEqual(0, output("WorkflowThread"), "WorkflowThread must not be zero") Debug.WriteLine("Test thread is " & _ Thread.CurrentThread.ManagedThreadId) Debug.WriteLine("Workflow thread is " & outarg.ToString()) End Sub
    FakePre-f50e2ea63ef54862a165804fe777c6d8-d7c7f2718805466c8e44428c385a024eFakePre-3a0c34454df446b9aa37818ce73ed193-edc9cc38ea8248bba7e68509aceadc3dFakePre-15128d02a37e4309abdc51aa29412dea-c882f6483c2d45b4a96d9e0a3d3d7efcFakePre-b644bd0e30d241958b06f54184fec397-ec219470626e4cefbdfc78d34e6a5188
    

  3. Press CTRL+R, A to run all tests. You should see the new test fail because you have not added the WorkflowThread out argument yet.

    Figure 32

    One test passes, one test fails because you have no WorkflowThread argument

Task 2 – Returning the WorkflowThread as an Argument

Now that you have a test to verify the behavior, you need modify your workflow to make the test pass.

  1. Open SayHello.xaml and add an Out argument named WorkflowThread of type Int32

    Figure 33

    Add the WorkflowThread Out argument

    Until now, your workflow has been just one activity. Now you need two activities, one to assign the greeting and another to assign the workflow thread. You need to modify the workflow to use an activity that will contain the two assign activities and there are several activities you could choose from to do this, but let’s begin with the simplest one, the Sequence activity.

  2. You cannot drop a sequence on the designer until you remove the Assign activity that is already there. You will use this assign activity inside the sequence so you need to cut it, drop the sequence and then and paste it back. Right click on the Assign activity and select Cut.

    Figure 34

    Cutting the Assign an Activity

  3. Drag a Sequence and drop it on the design surface

    Figure 35

    Drop a Sequence on the designer

  4. Right click inside the sequence and select Paste to put the Assign activity inside the sequence.

    Figure 36

    Paste the assign activity inside the sequence

  5. Import the System.Threading namespace into your workflow. Click on Imports and add System.Threading. To do this type the first few letters of the namespace and when the correct namespace is highlighted press Enter

    Figure 37

    Click on Imports and add System.Threading

    Note:
    Do I have to add System.Threading to Imports?

    No, this is optional just as it would be in any C# or VB project. If you don’t import the namespace you will have to fully qualify classes from the name space as in System.Threading.Thread

  6. Now you need to assign the current managed thread ID to the WorkflowThread out argument. Drop an Assign activity on the Sequence below the first Assign activity and set the properties as shown

    Figure 38

    Set the properties of the second Assign activity as shown

  7. Press CTRL+SHIFT+B to build the solution
  8. Press CTRL+R,A to run all tests. The tests will now pass. If you want to see the thread returned by the workflow, double click on the ShouldReturnWorkflowThread test. The Debug output will appear in the test results. The actual thread ID will vary on your computer but you will notice that the thread ID for the test is the same as the thread ID for the Workflow because WorkflowInvoker invokes the workflow synchronously on the calling thread.

    Figure 39

    Debug trace output appears in test results

Task 3 – Modifying the Test to use WorkflowApplication

Your test is good but it has one weakness. It verifies that the WorkflowThread returned is non-zero but it does not verify that it returns the actual managed thread ID that the workflow ran on. For example, your test would pass if the workflow always returned 1 in the WorkflowThread out argument.

If you want to verify the actual thread ID you will need to use WorkflowApplication to run the workflow. In this task you will modify the test to capture the thread of the workflow by obtaining it during a call to the WorkflowApplication.Completed action and then comparing that value to the value returned from the workflow.

  1. Open SayHelloFixture.cs (C#) or SayHelloFixture.vb (VB) and locate the ShouldReturnWorkflowThread test. Modify it as shown to use the WorkflowApplication class to run the workflow. This code will use the WorkflowApplication.Completed action to capture the output arguments and thread ID.

    (Code Snippet -Introduction to WF4Lab -ShouldReturnWorkflowThread WFApp Test CSharp)

    C#

    /// <summary> /// Verifies that the workflow returns an Out Argument /// Name: WorkflowThread /// Type: Int32 /// Value: Non-Zero, matches thread used for Completed action /// </summary> [TestMethod] public void ShouldReturnWorkflowThread() { AutoResetEvent sync = new AutoResetEvent(false); Int32 actionThreadID = 0; IDictionary<string, object> output = null; WorkflowApplication workflowApp = new WorkflowApplication( new SayHello() { UserName = "Test" }); // Create an Action<T> using a lambda expression // To be invoked when the workflow completes workflowApp.Completed = (e) => { output = e.Outputs; actionThreadID = Thread.CurrentThread.ManagedThreadId; // Signal the test thread the workflow is done sync.Set(); }; workflowApp.Run(); // Wait for the sync event for 1 second sync.WaitOne(TimeSpan.FromSeconds(1)); Assert.IsNotNull(output, "output not set, workflow may have timed out"); Assert.IsTrue(output.ContainsKey("WorkflowThread"), "SayHello must contain an OutArgument named WorkflowThread"); // Don't know for sure what it is yet var outarg = output["WorkflowThread"]; Assert.IsInstanceOfType(outarg, typeof(Int32), "WorkflowThread must be of type Int32"); Assert.AreEqual(actionThreadID, (int)outarg, "WorkflowThread should equal actionThreadID"); Debug.WriteLine("Test thread is " + Thread.CurrentThread.ManagedThreadId); Debug.WriteLine("Workflow thread is " + outarg.ToString()); }
    FakePre-53dd6d03b6814faa9351aa2da13ca9c3-b88baf94b6234e4a866a08a50854d4e7FakePre-59bb5108c2384a0290017a0a10e26167-ce01b34bf2ea44c5afcc8eac922942a3FakePre-4f46f486b2d94958a99b763d8dba8ad0-89d41740f7574257bf252304e0b87437FakePre-41341ed0cdf740a1ba18bf75e81e02bf-9f271ab09f33401a8e5a0e2013d8e550FakePre-b33c424a70284e87bc846bd8337cebe4-650fdd385f65472ba57c85d19217b2ecFakePre-1d703f9b9cae47ae8ffd57b6f997d9e5-3646a7f03d10427eab0e3d191be0151cFakePre-f251d63890474415ab9dbbe42056e93f-73996135ddb3462e9bc50725efe7884eFakePre-55701d2a8f774496869dac363f60477e-ecf7d580243b4315802bf8af7a494de2FakePre-b5f64f4a28fe4bd0834e1baeb07f0c49-9094896b712a4507b28b00212997a2bbFakePre-80562cf1cd484930a0f2c225b21a9138-6af7dd3fcfaa41cd9a2b246de551fee5
    

    (Code Snippet-Introduction to WF4LabShouldReturnWorkflowThread WFApp Test VB)

    Visual Basic

    ''' <summary> ''' Verifies that the SayHello workflow contains an Out Argument ''' Name: WorkflowThread ''' Type: Int32 ''' Value: Non-Zero, matches thread used for Completed action ''' </summary> ''' <remarks></remarks> <TestMethod()> Public Sub ShouldReturnWorkflowThread() Dim sync As New AutoResetEvent(False) Dim actionThreadID As Int32 = 0 Dim output As IDictionary(Of String, Object) = Nothing Dim workflowApp As New WorkflowApplication( _ New SayHello() With {.UserName = "Test"}) workflowApp.Completed = _ Function(e) output = e.Outputs actionThreadID = Thread.CurrentThread.ManagedThreadId ' Signal the test thread the workflow is done sync.Set() ' VB requires a lambda expression to return a value ' It is not used with Action(Of T) Return Nothing End Function workflowApp.Run() ' Wait for the sync event for 1 second sync.WaitOne(TimeSpan.FromSeconds(1)) Assert.IsNotNull(output, "output not set, workflow may have timed out") Assert.IsTrue(output.ContainsKey("WorkflowThread"), "SayHello must contain an OutArgument named WorkflowThread") ' Don't know for sure what it is yet Dim outarg = output("WorkflowThread") Assert.IsInstanceOfType(outarg, GetType(Int32), "WorkflowThread must be of type Int32") Assert.AreEqual(actionThreadID, DirectCast(output("WorkflowThread"), Integer), "WorkflowThread should equal actionThreadID") Debug.WriteLine("Test thread is " & _ Thread.CurrentThread.ManagedThreadId) Debug.WriteLine("Workflow thread is " & outarg.ToString()) End Sub
    FakePre-027778a4de4d4a0185e64188b84c07ad-d9b19afb8a3840fba568354629ec61fdFakePre-735c4e3c5098474fb796538cc2a6d682-b4478170936c4ea7a64718d83f6e6434FakePre-bb65327f01204233a968b1a0e8f84acd-dc62a56fc90c4fc19b2596cd754b8712FakePre-7d0ff0c4ebad4dd5a19592cbcbbe330a-47217c15dc794361a1b43c1a077d862cFakePre-58c8633344ec40fb9da51bcfe94b683d-6c88718c026e4d1c9881965e385ca3b1FakePre-6ec5486d93434d7293e5df69d0247f2e-21fc292d42df4ef9a554462f3d934142FakePre-c77b19dc4b264a7f84170c15d986e7b2-9b443a4a47d74c348d9e824f204797d7FakePre-0d4ef58816c34ad29c096df1ed4bc119-d4af02453baf4fed840279b8feaa7af2FakePre-f886904596274473bcf14c208be0775a-f5bfc73e297a4f6ebb51bd88cfb88522FakePre-9306dd9677b9480b8fcf6efe32b2ee69-0ded9d6bb4fd4761b580c9af69b05fe6FakePre-bea3f5397056430c9d96a2b2f0fc60b5-8bfcaf83bfd34bba8b936f887b290b40
    

    Note:
    Delegates, not events

    WorkflowApplication.Completed and other properties of WorkflowApplication are not events but delegates. Handling them requires you to provide a method, anonymous method or lambda expression.

Next Step

Exercise 6: Verification