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.
- Return a personalized greeting
- 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.
- Start Microsoft Visual Studio 2010 from Start | All Programs | Microsoft Visual Studio 2010.
- 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.
- 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.
Open SayHelloFixture.cs and add the following namespace directives
using System.Threading; using System.Diagnostics;
Imports System.Threading Imports System.Diagnostics
Add the ShouldReturnWorkflowThread test as shown
(Code Snippet - Introduction to WF4Lab - ShouldReturnWorkflowThread Test CSharp)
/// <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)
''' <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
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.
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.
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
Drag a Sequence and drop it on the design surface
Figure 35
Drop a Sequence on the designer
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
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
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
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
- Press CTRL+SHIFT+B to build the solution
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.
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)
/// <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 WF4Lab–ShouldReturnWorkflowThread WFApp Test VB)
''' <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
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