A workflow models a business process and coordinates the flow of work to be performed, but it is the role of the workflow’s activities to implement the actual execution of that work. A WF activity provides the most basic unit of work for building execution logic through code and provides a consistent approach rather than just defining logic with standard code.
Chapter 2 covered a couple of activities just to get you familiar with how they work, but if you are used to writing code, you will have noticed that most of the activities implemented many of the same logical patterns as code constructs. The only difference is that activities provide a more declarative way of implementing logic because they visually represent the way they are to be used within a workflow. And just like code, activities are re-usable in the sense that once they are defined, they can be used to model behavior for more than one workflow.
Activities also have an exception handling very similar to how programming languages handle exceptions. I also mentioned that the WF activity is the basic building block for defining a workflow, so understanding activities allows developers to express custom workflows that target specific business processes. This chapter focuses on understanding the different types of WF activities, how they are defined, and how they can be used for building workflows for modeling complex business process scenarios. I will also cover how to build custom activities that are built to be domain specific, meaning that they are geared to focus on a certain type of business model. See Figure 3-1.
Figure 3-1. Activity hierarchy
Activity Basics
WF activities are nothing more than objects defined within the .NET Framework, and they are derived from the namespace System.Object. The namespace for WF activities is System.Activities and it contains the components used to define and implement activities. The class Activity contained within System.Activities provides the abstract class for all activities (see Table 3-1).
Table 3-1. System.Activities.Activity
Namespace | Description |
---|---|
System.Activities.CodeActivity | Mimics WF3.x base activity. Simple execution of code for an activity to execute within a workflow. |
System.Activities.NativeActivity | Provides access to the WF runtime. |
System.Activities.DynamicActivity | Creates an “on-the-fly” activity development experience. |
System.Activities.AsyncCodeActivity | Layers on asynchronous coding model in .Net 4.0 for processing execution of activities asynchronous. |
Activities can be built to take advantage of one of the namespaces represented in Table 3-1. Here are the details:
Although most of the activities defined will inherit from System.Activities.Activity and will be defined using the workflow designer, other custom activities will derive from the base classes, such as:
The concept for how WF activities process data is similar to regular code. Activities require the following:
Variables
Just as a method written in code can have variables defined within it, activities also have variables that they use. Variables are used as placeholders for holding temporary data that can be used as part of the overall results for executing business logic. Variables contain a variable type that represents the type of data that it will house and an optional default value that can be used in case there is no value passed into the variable.
One of the key benefits WF4 added to variables is scope availability. A variable’s activity scope determines what activities can reference the variable within a workflow. Figure 3-2 demonstrates how two different variables, variable1 and variable2, can have limited scope within an activity by using the Scope drop-down menu and selecting the desired parent activity. You will notice that variable2 has a scope of the Sequence activity, which is the container for the Pick activity; however, if the variable is not needed in both branches of the Pick activity, it would be better to reduce the variables scope like the other variable, variable2, which only gives scope to Branch2 within the Pick activity rather than the entire Sequence activity.
Figure 3-2. Variable scoping
Arguments are used for receiving and returning data to the WF runtime. Arguments provide functionality within a workflow much like a function coding construct. Data can be passed into a function, and a function can also return data back to the line of code that called it. When workflows expect to receive data as input, the argument is defined as an InArgument object. When data is returned from a workflow, an OutArgument object is used, and when data needs to be received and returned within the same argument for a workflow, an InOutArgument object is used. Therefore, WF arguments can be one of three types of directions within an activity:
Arguments also have a type attribute, just as variables do, that represents the data that they can hold as well as a default value (which is optional). The tab towards the bottom of the WF designer reveals how to access the arguments for a workflow. Figure 3-3 demonstrates three different string arguments that can be used within the workflow.
Figure 3-3. Variable scoping
Expressions
Expressions specify the execution of logic that an activity will perform. Most of the time expressions take advantage of the variable and arguments for processing information; other times expressions use data received from external sources that originate outside of the workflow. Expressions in WF4 use Visual Basic syntax for modeling logic but WF4.5 provides an alternative to this by allowing C# syntax to be used as well. Expressions can also be extended, so a custom expression’s syntax can be used instead of requiring workflow authors to use C# or VB syntax. Chapter 5 will cover how expressions can be customized and used within activities.
As you read further, you will see how variables, arguments, and expressions assist in activity execution through the activities in the chapters.
Note You may notice that the name of the arguments use the prefix “arg” and variables use the prefix “var” for workflows in the examples. This is solely done to minimize confusion between arguments and variables.
Just as a workflow instance running within the WF runtime has a lifecycle, so does a workflow activity. An activity is started in the executing state, which means that the activity has been invoked to execute. Once all work has completed within the activity, including any work with its child activities or outside communication through bookmarks, the activity changes to the closed state. When an activity is requested to be cancelled, the requested activity resolves to a canceled state. If there is an exception thrown during its execution, the activity then goes into a faulted state. See Figure 3-4.
Figure 3-4. Activity lifecycle
Authoring Activities
One of the really powerful features introduced in WF4 is the ability to author activities through code and XAML, an XML representation for a WF activity. WF3.x had activities that were used declaratively and had a code back-end for implementing how the activity executed. Activities could also be built using XML, noted as “.xoml”, but there was still code associated with the workflow. WF4 helped clear up the confusion by clearly drawing a line between activities that are authored through code and activities that are strictly authored through XAML.
The real power of WF is designing workflows declaratively using the WF designer, but workflows can also be declared strictly through code.
Listing 3-1 demonstrates the code for implementing a basic WriteLine activity by instantiating the variable wfActivity, declared as a System.Activities.Activity and setting it to a new WriteLine activity. The WriteLine activity has a Text property that is used to write “Hello from Workflow” to a console window. Finally, WorkflowInvoker.Invoke is used to call the wfActivity just as a simple method call.
Listing 3-1. Simple Hello from Workflow
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;
using System.Activities.Statements;
namespace Apress.Example.Chapter3
{
public partial class ImperativeCodeWorkflow
{
public void SimpleHelloWorld()
{
Activity wfActivity = new WriteLine
{
Text = "Hello from Workflow."
};
WorkflowInvoker.Invoke(wfActivity);
}
}
}
This next example goes a little more in depth by using data within a workflow and performing a simple addition. Three variables are used to hold data that will be used during the addition calculation (see Listing 3-2).
Listing 3-2. Declaring Variables
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;
using System.Activities.Statements;
using Microsoft.VisualBasic.Activities;
namespace Apress.Example.Chapter3
{
public partial class ImperativeCodeWorkflow
{
public void AdditionActivity()
{
Variable<int> Add1 = new Variable<int>
{
Name = "Add1",
Default = 5
};
Variable<int> Add2 = new Variable<int>
{
Name = "Add2",
Default = 5
};
Variable<int> Sum = new Variable<int>
{
Name = "Sum"
};
After the variables are declared, a Sequence activity is instantiated and used as a container for holding child activities. The three variables declared in Listing 3-2 are then added to the Sequence activity, along with an Assign activity that will be used to assign an argument declared as an integer type to the Sum variable that is calculated from the expression of adding variables Add1 and Add2. The calculated value is then written to the console window by adding the WriteLine activity to the Sequence activity. See Listing 3-3.
Listing 3-3. Sequence Activity in Code
Activity wfSequence = new Sequence
{
Variables = { Add1,Add2,Sum },
Activities =
{
new Assign<int>
{
To = Sum,
Value = new InArgument<int> (ad) => Add1.Get(ad) + Add2.Get(ad))
},
new WriteLine
{
Text = new InArgument<string> ((sm) =>string.Format("The sum of {0} and {1} is {2} ",Add1.Get(sm),Add1.Get(sm),Sum.Get(sm)))
}
}
};
WorkflowInvoker.Invoke(wfSequence);
}
This code can be called simply using the following code:
var imperativeCode = new ImperativeCodeWorkflow();
imperativeCode.AdditionActivity();
Declaring activities as DynamicActivity instead of through imperative code provides a flexible way for executing activities because they allow the creation of arguments and values for the arguments to be created externally from the workflow. Dynamic activities provide properties that can be set instead for processing logic within the activity (see Table 3-2).
Table 3-2. Important DynamicActivity Properties
Properties | Description |
---|---|
DisplayName | Used primarily as metadata for describing the activity. Inherited from System.Activities.Activity. |
Implementation | Abstracts out a way to get or set the activities that make up the business logic executed for the activity. |
Name | Name of the activity that the WF designer displays. |
Properties | Sets or gets properties used as arguments for the activity. Takes a collection of type System.Collections.ObjectModel.KeyedCollection(Of String, DynamicActivityProperty) |
Let’s take a look at how you can take advantage of building a dynamic activity to provide the flexibility for enlisting outside arguments. Let’s use the same logic that was built using imperative code activity for adding two integers together. To get started, the first thing to do is to add the arguments and variables that the dynamic activity will use. The code in Listing 3-4 demonstrates adding two in arguments, argAdd1 and argAdd2 and one out argument, AdditionResult. Two variables, varAdd1 and varAdd2, are also included to hold the argument values passed in to the workflow; however, they are not really needed in this simple example. Notice that each variable is given a default value of 5, just in case arguments are not passed to the workflow activity.
Listing 3-4. Sequence Activity in Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;
using System.Activities.Statements;
using Microsoft.VisualBasic.Activities;
using System.Activities.Expressions;
namespace Apress.Example.Chapter3
{
public partial class DynamicCodeActivity
{
public Activity AdditionActivity()
{
var argAdd1 = new InArgument<int> ();
var argAdd2 = new InArgument<int> ();
var AdditionResult = new OutArgument<int> ();
Variable<int>varAdd1=new Variable<int>
{
Name = "varAdd1",
Default = 5
};
Variable<int>varAdd2 = new Variable<int>
{
Name = "varAdd2",
Default = 5
};
The implementation for the workflow activity is pretty much the same as the addition activity from Listing 3-3, except that arguments can now be passed into the activity externally so that any two numbers can be added together through the hosted WF runtime. After the activity is instantiated, the following properties are set for the activity:
Listing 3-5 demonstrates adding the DisplayName property, “Add two Integers”, which the WF runtime will use to associate the activity to any exceptions that may occur. There are also two arguments of type InArgument, argAdd1 and argAdd2, and one OutArgument type for returning data back to the WF runtime that are added using the DynamicActivityProperty . The name given to each argument is important because this is what the WF runtime will use for referencing the arguments with the activity.
Listing 3-5. Instantiatingthe DynamicActivity
var MathAddActivity = new DynamicActivity()
{
DisplayName = "Add two Integers",
Properties =
{
new DynamicActivityProperty
{
Name = "argAdd1",
Type = typeof(InArgument<int>),
Value = argAdd1
},
new DynamicActivityProperty
{
Name = "argAdd2",
Type = typeof(InArgument <int>),
Value = argAdd2
},
new DynamicActivityProperty
{
Name = "argAdditionResult",
Type = typeof(OutArgument<int>),
Value = argAdditionResult
}
},
After the Properties property for the activity has been set, the next property that needs to be set for defining the execution logic for the activity is Implementation.
The Implementation property creates a new Sequence activity with two new variables, varAdd1 and varAdd2, that were defined in Listing 3-4. The Sequence activity adds three Assign activities and one WriteLine activity to its Activities property, which represents included child activities. The first Assign activity assigns the variable varAdd1 to the value passed into the activity through the Add1 InArgument. The second Assign activity assigns the variable varAdd2 to the value also passed into the activity through the Add2 InArgument. The last Assign activity assigns the value generated by the expression executed for adding the two set variables together. The value is assigned to the OutArgument so it can be returned as output from the activity. Finally, the WriteLine activity simply confirms the addition logic that was executed by showing the values for the arguments that were passed in and the argument value that is passed back out from the activity. See Listing 3-6.
Listing 3-6. Implementing the Dynamic Activity
Implementation = () => new Sequence
{
Variables =
{
varAdd1,
varAdd2
},
Activities =
{
new Assign<int>
{
To = varAdd1,
Value = new ArgumentValue<int>
{
ArgumentName = "argAdd1"
}
},
new Assign<int>
{
To = varAdd2,
Value = new ArgumentValue<int>
{
ArgumentName = "argAdd2"
}
},
new Assign<int>
{
To = new ArgumentReference<int>
{
ArgumentName = "argAdditionResult"
},
Value = new InArgument<int> (ad) =>varAdd1.Get(ad)+varAdd2.Get(ad))
},
new WriteLine
{
Text = new InArgument<string> ((env) =>string.Format("The sum of {0} and {1} is {2} "
,varAdd1.Get(env)
,varAdd2.Get(env)
,argAdditionResult.Get(env)))
}
}
}
};
return MathAddActivity;
}
}
}
Next let’s review the code for passing in the arguments and getting the results from the activity. The code in Listing 3-7 instantiates a DynamicCodeActivity class defined in Listing 3-4 and calls the activity AdditionActivity.
Listing 3-7. Executingthe Dynamic Activity
using System;
using System.Linq;
using System.Activities;
using System.Activities.Statements;
using System.Collections.Generic;
namespace Apress.Example.Chapter3
{
class Program
{
static void Main(string[] args)
{
CallDynamicAdditionActivity();
}
private static void CallDynamicAdditionActivity()
{
var dynamicActivity = new DynamicCodeActivity();
var actAddition = dynamicActivity.AdditionActivity();
var result = WorkflowInvoker.Invoke(actAddition,
new Dictionary<string, object>{ { "argAdd1", 3 }, { "argAdd2", 5 } });
Console.WriteLine(string.Format("The workflow returned {0}", result["argAdditionResult"]));
Console.ReadKey();
var imperativeCode = new ImperativeCodeWorkflow();
imperativeCode.AdditionActivity();
}
}
}
The dynamic activity returned is then executed by executing WorkflowInvoker.Invoke and passing in two arguments, argAdd1 and argAdd2, that were defined in Listing 3-5. The integers that are passed in will be added together. Notice the Dictionary object’s signature <string, object> that is used for passing arguments into workflows. This string part of the signature is used to set the name of the arguments, which must be known so the arguments can be sent to the activity. The object part of the signature sets the values 3 and 5 which are hard-coded for demonstration purposes; however, they are outside of the implementation for the activity. These values can be set through user input or read in from an outside source, making this activity re-usable for logic that adds two integers together and returns a sum.
Tip There might be some confusion between building workflows through imperative code and defining dynamic activities through code. Workflows built through imperative code do not have the flexibility of dynamic activities because they are hard-coded for wiring up of properties and arguments. Dynamic activities provide dependency injection by allowing arguments to be defined outside of the scope of the activities execution.
WF4 introduced activities authored using code plus another powerful feature for creating workflows entirely from XML. Workflows authored in XML can now be created and executed without being compiled; they can also be modified during runtime. So what strategies does this provide for developers? If you are familiar with the WF Rules Engine that was made available in WF3.x, it allowed rules to be created and then stored within a central location like SQL Server. Rules could be retrieved at a later time from storage and compiled so they could process business logic for .NET applications. The real advantage in implementing a rules engine so rules can be processed within applications is so rules can be changed during runtime. The same concept applies when authoring dynamic activities through XAML. However, instead of building rules that are defined through code, workflows can be created; this provides a better approach for executing business logic. Workflows can be built not only by developers but also non-technical users that model the logic declaratively instead of having to learn the rules syntax for building rules. Workflows can now be run on the fly using the System.Activities.XamlIntegration.ActivityXamlServices object and changed during runtime.
ActivityXamlServices is a static object that allows XML to be loaded and returns an Activity type that can be processed as if the workflow activity was created through code, but without the workflow having to be compiled. Figure 3-5 demonstrates how a workflow can be created using the WF designer declaratively that models the dynamic activity authored through code.
Figure 3-5. Adding a sequence activity and arguments
The first activity that is added to the WF designer is a Sequence activity that will serve as the container for the other child activities (see Figure 3-5). The workflow also uses the three arguments found in Listing 3-6.
There are also the two variables that will be used for executing the logic for adding the two input arguments (see Figure 3-6).
Figure 3-6. Adding WF variables
The completed workflow defined in code is completely demonstrated within the designer in Figure 3-7.
Figure 3-7. Complete workflow
At this point, you can review the XAML it has produced using the WF designer. Browsing through the XAML, you can easily pick out different activities, declared arguments, variables, and the expressions.
Listing 3-8. XAML for the Custom Addition Activity
<Activity mc:Ignorable="sap sap2010 sads" x:Class= "Apress.Chapter3 .Activity1"
xmlns=" http://schemas.microsoft.com/netfx/2009/xaml/activities "
xmlns:mc=" http://schemas.openxmlformats.org/markup-compatibility/2006 "
xmlns:mca="clr-namespace:Microsoft.CSharp.Activities;assembly= System.Activities"
xmlns:sads=" http://schemas.microsoft.com/netfx/2010/xaml/activities/debugger "
xmlns:sap=" http://schemas.microsoft.com/netfx/2009/xaml/activities/presentation "
xmlns:sap2010=" http://schemas.microsoft.com/netfx/2010/xaml/activities/presentation "
xmlns:scg="clr-namespace:System.Collections.Generic;assembly= mscorlib"
xmlns:sco = "clr-namespace:System.Collections.ObjectModel;assembly = mscorlib"
xmlns:x = " http://schemas.microsoft.com/winfx/2006/xaml ">
<x:Members>
<x:Property Name = "argAdd1" Type = "InArgument(x:Int32)" />
<x:Property Name = "argAdd2" Type = "InArgument(x:Int32)" />
<x:Property Name = "argAdditionResult" Type = "OutArgument(x:Int32)" />
</x:Members>
<sap2010:ExpressionActivityEditor.ExpressionActivityEditor> C#</sap2010:ExpressionActivityEditor.ExpressionActivityEditor>
<sap2010:WorkflowViewState.IdRef> Apress.Chapter3 .Activity1_1</sap2010:WorkflowViewState.IdRef>
<TextExpression.NamespacesForImplementation>
<sco:Collection x:TypeArguments = "x:String">
<x:String> System</x:String>
<x:String> System.Collections.Generic</x:String>
<x:String> System.Data</x:String>
<x:String> System.Linq</x:String>
<x:String> System.Text</x:String>
</sco:Collection>
</TextExpression.NamespacesForImplementation>
<TextExpression.ReferencesForImplementation>
<sco:Collection x:TypeArguments = "AssemblyReference">
<AssemblyReference> Microsoft.CSharp</AssemblyReference>
<AssemblyReference> System</AssemblyReference>
<AssemblyReference> System.Activities</AssemblyReference>
<AssemblyReference> System.Core</AssemblyReference>
<AssemblyReference> System.Data</AssemblyReference>
<AssemblyReference> System.Runtime.Serialization</AssemblyReference>
<AssemblyReference> System.ServiceModel</AssemblyReference>
<AssemblyReference> System.ServiceModel.Activities</AssemblyReference>
<AssemblyReference> System.Xaml</AssemblyReference>
<AssemblyReference> System.Xml</AssemblyReference>
<AssemblyReference> System.Xml.Linq</AssemblyReference>
<AssemblyReference> mscorlib</AssemblyReference>
<AssemblyReference> Apress.Chapter3 </AssemblyReference>
</sco:Collection>
</TextExpression.ReferencesForImplementation>
<Sequence sap2010:WorkflowViewState.IdRef = "Sequence_1">
<Sequence.Variables>
<Variable x:TypeArguments = "x:Int32" Default = "5" Name = "varAdd1" />
<Variable x:TypeArguments = "x:Int32" Default = "5" Name = "varAdd2" />
</Sequence.Variables>
<Assign>
<Assign.To>
<OutArgument x:TypeArguments = "x:Int32">
<mca:CSharpReference x:TypeArguments = "x:Int32"> varAdd1</mca:CSharpReference>
</OutArgument>
</Assign.To>
<Assign.Value>
<InArgument x:TypeArguments = "x:Int32">
<mca:CSharpValue x:TypeArguments = "x:Int32"> argAdd1</mca:CSharpValue>
</InArgument>
</Assign.Value>
<sap2010:WorkflowViewState.IdRef> Assign_1</sap2010:WorkflowViewState.IdRef>
</Assign>
<Assign>
<Assign.To>
<OutArgument x:TypeArguments = "x:Int32">
<mca:CSharpReference x:TypeArguments = "x:Int32"> varAdd2</mca:CSharpReference>
</OutArgument>
</Assign.To>
<Assign.Value>
<InArgument x:TypeArguments = "x:Int32">
<mca:CSharpValue x:TypeArguments = "x:Int32"> argAdd2</mca:CSharpValue>
</InArgument>
</Assign.Value>
<sap2010:WorkflowViewState.IdRef> Assign_2</sap2010:WorkflowViewState.IdRef>
</Assign>
<Assign>
<Assign.To>
<OutArgument x:TypeArguments = "x:Int32">
<mca:CSharpReference x:TypeArguments = "x:Int32"> argAdditionResult</mca:CSharpReference>
</OutArgument>
</Assign.To>
<Assign.Value>
<InArgument x:TypeArguments = "x:Int32">
<mca:CSharpValue x:TypeArguments = "x:Int32"> varAdd1 + varAdd2</mca:CSharpValue>
</InArgument>
</Assign.Value>
<sap2010:WorkflowViewState.IdRef> Assign_3</sap2010:WorkflowViewState.IdRef>
</Assign>
<WriteLine>
<InArgument x:TypeArguments = "x:String">
<mca:CSharpValue x:TypeArguments = "x:String"> string.Format("The sum of {0} and {1} is {2}",
varAdd1,varAdd2,argAdditionResult)</mca:CSharpValue>
</InArgument>
<sap2010:WorkflowViewState.IdRef> WriteLine_1</sap2010:WorkflowViewState.IdRef>
</WriteLine>
<sads:DebugSymbol.Symbol> d1JjOlx1c2Vyc1xid2hpdGVcZG9jdW1lbnRzXHZpc3VhbCBzdHVkaW8
gMTFcUHJvamVjdHNcQXByZXNzLkNoYXB0ZXIzXEFjdGl2aXR5MS54YW1sDiwDXw4CAQEuMy42AgEDLzMvNgIBA
jEFPQ4CASU + BUoOAgEYSwVXDgIBC1gFXRECAQQ5CzlPAgEsNAs0VwIBJkYLRk8CAR9BC0FXAgEZUwtTVw
IBEk4LTmECAQxaCVqXAQIBBQ==</sads:DebugSymbol.Symbol>
</Sequence>
<sap2010:WorkflowViewState.ViewStateManager>
<sap2010:ViewStateManager>
<sap2010:ViewStateData Id = "Assign_1" sap:VirtualizedContainerService.HintSize = "242,62" />
<sap2010:ViewStateData Id = "Assign_2" sap:VirtualizedContainerService.HintSize = "242,62" />
<sap2010:ViewStateData Id = "Assign_3" sap:VirtualizedContainerService.HintSize = "242,62" />
<sap2010:ViewStateData Id = "WriteLine_1" sap:VirtualizedContainerService.HintSize = "242,62" />
<sap2010:ViewStateData Id = "Sequence_1" sap:VirtualizedContainerService.HintSize = "264,492">
<sap:WorkflowViewStateService.ViewState>
<scg:Dictionary x:TypeArguments = "x:String, x:Object">
<x:Boolean x:Key = "IsExpanded"> True</x:Boolean>
</scg:Dictionary>
</sap:WorkflowViewStateService.ViewState>
</sap2010:ViewStateData>
<sap2010:ViewStateData Id = "Apress.Chapter3 .Activity1_1"
sap:VirtualizedContainerService.HintSize = "304,572" />
</sap2010:ViewStateManager>
</sap2010:WorkflowViewState.ViewStateManager>
</Activity>
Now the XAML can be run without being compiled by finding the path of the XAML file and using the following code:
var act = ActivityXamlServices.Load(@"Activity1.xaml");
var retArg = WorkflowInvoker.Invoke(act, new Dictionary <string, object>
{
{ "argAdd1", 3 }, { "argAdd2", 5 }
});
var result = Convert.ToInt32(retArg["argAdditionResult"]);
In this case, the Activity1.xaml file is located within the same file path as the executing assembly, and because of the implementation of the WriteLine activity within the XAML, the console window also opens and displays the processed equation for adding two numbers (see Figure 3-8).
Figure 3-8. Activity processed as XAML
Caution XAML activities cannot serialize Lambda expressions (syntax used for defining a nameless function). Therefore, if they exist, a LambdaSerializationException will be thrown with the following message: “This workflow contains lambda expressions specified in code. These expressions are not XAML serializable. In order to make your workflow XAML-serializable, either use VisualBasicValue/VisualBasicReference or ExpressionServices.Convert(lambda). This will convert your lambda expressions into expression activities.”
Let’s now explore how to make sure the WF activities authored will work before they are actually added into a workflow. Unit testing is a methodology commonly used by developers for testing their code before it is implemented within a solution. Unit testing WF activities was a challenge before WF4. However, in in WF4 you can use the WorkflowInvoker to execute an activity, the same way a C# method is executed through code. Visual Studio also comes with a template for a test project (see Figure 3-9) that can be added to a Visual Studio solution.
Figure 3-9. Adding a test project
A test project includes by default a file called UnitTestProject1. Sometimes it is a good practice to add another unit test file if the functionality being tested can be categorized into more than one category. So let’s say that you are building functionality around adding inventory into a system, and later you need to build functionality for managing users within the system. You might have one unit test dedicated to testing the functionality for inventory and another unit test for testing user management.
Listing 3-9. Default Unit Test Code
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Apress.Example.Chapter3.Activity.Test
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
}
}
}
Listing 3-9 shows the default code that a new unit test contains by default. Essentially it is a boilerplate for building your own unit tests for testing custom code. The code in Listing 3-9 contains a test class and test method; an obvious giveaway is the attribute TestClass given for the class UnitTest1 and the attribute TestMethod given to the method TestMethod1. To test the addition activity, the System.Activity namespace needs to be referenced within the test project (see Figure 3-10).
Figure 3-10. Test project System.Activities reference
As code is being tested, there are ways to confirm different types of results to make sure the right results are returned or not returned. Anticipating different results requires different types of tests. The most common are the following:
To verify that tests are returning the correct results, it is important to use an Assert statement to confirm certain result patterns. Table 3-3 illustrates the different types of Asserts that are available.
Table 3-3. Assert Types
Assert Types | Description |
---|---|
Assert | Provides methods for verifying a pass/fail result. |
Collection Assert | Used for testing collections of objects. |
StringAssert | Provides methods for testing strings. |
AssertFailedException | Exception thrown when a test fails. |
AssertInconclusiveException | Exception thrown when a test result is inconclusive or when the results cannot be defined as a pass or fail. |
The following two test methods show how to use Asserts. TestMethod2 compares an empty collection of strings to a StringBuilder, but because the Assert is AreNotEqual, the test will pass.
[TestMethod]
public void TestMethod2()
{
Assert.AreNotEqual(new List <string> (), new System.Text.StringBuilder());
}
[TestMethod]
public void TestMethod3()
{
Assert.AreEqual("123", "123");
}
TestMethod2 will pass as well because it asserts that “123” equals “123”. To test the activity in Listing 3-8, the same code can be used for invoking the activity.
[TestMethod]
public void TestMethod1()
{
var act = ActivityXamlServices.Load(@"C:ApressChapter3SolutionApress.Example.Chapter3Apress.Example.Chapter3inDebugWorkflow1.xaml");
var retArg = WorkflowInvoker.Invoke(act, new Dictionary <string, object>
{
{ "argAdd1", 3 },
{ "argAdd2", 5 }
});
var result = Convert.ToInt32(retArg["AdditionResult"]);
Assert.AreEqual(result, 3 + 5);
}
The only thing that needs to change is the file path for loading the XAML from, because with the new test project, the XAML that defines the activity is still located within the build directory for the project that was used for authoring the activity. After running a test, the results can be viewed within the Test Results window. Figure 3-11 shows that the Addition activity results were good because it successfully passed its basic task of adding 3 and 5 together.
Figure 3-11. Test results
Communicating with Activities
Chapter 2 briefly touched on ways of communicating with workflows, but because the execution of logic takes place inside of an activity, communication to activities flows from the WF runtime, either through arguments or bookmarks.
Bookmarks
Bookmarks allow event-driven communication to occur to an activity within a workflow, from an outside source, using the WF runtime as the channel of communication. Bookmarks were introduced in WF4 to settle the complexity around defining what WF3.x referred to as “external events.” The concept of bookmarks is pretty simple as it closely resembles how a real bookmark works for keeping track of what page of a book was the last to be read.
Bookmarks in WF apply the same meaning except that a bookmark in WF holds the last place a workflow executed, usually because the workflow is waiting on some external event to happen so it can start back up. The cool thing about using bookmarks, though, is when a workflow stops and waits for an external event to fire (for example, a manager needs to approve a work order), the workflow is considered idle and can therefore be persisted within SQL Server. Then a day later, when the manager decides he or she is ready to approve the work order, the workflow is loaded back into memory and the workflow picks up exactly where it left off, which is from the bookmark.
There is no out-of-box activity for handling bookmarks, but one can easily be created that handles most of the functionality needed for implementing a bookmark. Listing 3-10 demonstrates how to build a custom activity that works with bookmarks within a workflow. The WaitForResponse activity inherits from NativeActivity <TResult>, which allows any object to be returned from the WF runtime using a Bookmark. Going back to the work order scenario, once the manager approves the work order, a work order object is returned back through the WF runtime to the Bookmark so the work order can continue to be processed. The most important information that defines a Bookmark object is a bookmark’s name. The name is used to reflect on a Bookmark from the WF runtime. Instead of building a bookmark activity for every Bookmark needed within a workflow, building an activity that handles bookmarks is a generic approach, so the name of the Bookmark can be defined through code or using the WF designer rather than a hard-coded value. Once a Bookmark is created, it can pass with it a .NET object, which can be used as data and processed within a workflow. The ResponseName property is used to define the actual bookmark name, so that the WF runtime can associate with the same name to correspond with the activity for external events (see Figure 3-12).
Listing 3-10. Bookmark Activity
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;
namespace Apress.Example.Chapter3
{
public sealed class WaitForResponse<TResult> : NativeActivity< TResult>
{
public WaitForResponse()
: base()
{
}
public string ResponseName { get; set; }
protected override bool CanInduceIdle
{ //override when the custom activity is allowed to make he workflow go idle
get
{
return true;
}
}
protected override void Execute(NativeActivityContext context)
{
context.CreateBookmark(this.ResponseName, new BookmarkCallback(this.ReceivedResponse));
}
void ReceivedResponse(NativeActivityContext context, Bookmark bookmark, object obj)
{
this.Result.Set(context, (TResult)obj);
}
}
}
Figure 3-12. WaitForResponse Bookmark Activity
A Pick activity is the ideal place for setting up a bookmark because it has a Trigger and Action signature. Figure 3-13 shows the WaitforResponse activity that was added within the Trigger of Branch1 of the Pick activity. A Delay activity is brought into Branch2 to set how long to wait for an external event. If the Delay activity interval expires, then whatever activity is provided in Branch2’s Action container will execute. If the Bookmark is triggered within the timer interval, then whatever activities are added within the Action container for Branch1 will be executed.
Figure 3-13. WaitForResponse Bookmark activity
Once the WaitforResponse activity is configured with ResponseName (name of the Bookmark) and Result (usually a variable within the scope of the activity for the object that is passed with the bookmark), a Bookmark is set and ready to receive an external event. Recall that bookmarks cannot be used with the WF host, WorkflowInvoker.Invoke. Instead, the WorkflowApplication host provides a ResumeBookmark method, which is called for initiating an external event from the hosted WF runtime. When ResumeBookmark is called, two arguments can be passed, indicating the following:
Implementing Activities
The activities that come with WF are considered the out-of-box activities, so getting familiar with how to use them is crucial for building workflows within WF. As mentioned, most of the workflows you will build will be authored from out-of-box activities, and that is primarily because they mimic the same logical patterns as code constructs. This section of the chapter and the chapters following will walk through labs to demonstrate concepts for using the provided activities within WF. This way there are different examples to reference while trying to implement WF activities within custom workflows. Most of the activities demonstrated in this chapter will be from the Primitives category for building a foundation for using the out-of-box activities; however, later chapters will demonstrate more advanced activities. Let’s get started by creating a new Visual Studio 2011 solution.
This project will be used for walking through the rest of the labs in this chapter.
After activities have been thoroughly unit tested, workflows can be built using the tested activities and executed, but developers will also want to step through workflow’s to debug them even further and before they go into production. WF has first-class experience similar to debugging C# code with using breakpoints, except the breakpoints are applied to activities within a workflow rather than lines of code. Breakpoints can be set on an activity, and once the activity receives scope, the workflow execution pauses on the breakpoint. Execution can then be controlled for step-by-step interaction for visually seeing the execution pattern of execution for the workflow.
WF also has a handy out-of-the-box activity called WriteLine, which is great for debugging. The WriteLine activity writes custom information to a console, which can also be used for understanding activity flow. There is also a monitoring extension called Tracking that can be added to the WF runtime to monitor custom information on a workflow; however, WF tracking has its own dedicated chapter later within the book.
DEBUGGING ACTIVITIES
This lab walks through a simple workflow that is used to demonstrate debugging a workflow using breakpoints and WriteLine activities. The workflow will loop 10 times, each time grabbing a different random number between 0–10. A condition will check if the random number is greater or less than 5. There will be various places where breakpoints will be applied and WriteLine activities will be used to indicate the paths for each random number that is generated.
To walk through these activities, use the new solution called Chapter3.Activities created in Visual Studio 2012.
Figure 3-14. Setting the variables for debugging the workflow
Tip As workflows become larger, it is smart to name some of the DisplayName properties for workflows that contain more than one of the same activities. If not, variable scopes can start getting confusing, as illustrated in Figure 3-15.
Figure 3-15. Confusing scope
Figure 3-16. Adding an activity breakpoint
Figure 3-17. Debugging an activity using a breakpoint
Figure 3-18. Debugging flow using WriteLine activities
Figure 3-19. Removing a breakpoint
Figure 3-20. Breakpoints on WriteLine activities
Designing a good error handling strategy within code is just as important as developing the functional code it supports. Exception management is a proactive approach for anticipating when exceptions occur and how they are handled during the execution of code. There are different types of exceptions that can occur and usually they are handled differently based on the exception type. WF has a first-class implementation for handling exceptions within workflows, and WF’s exception management closely resembles the constructs used within standard code.
Listing 3-11. Try, Catch, Finally
private static void DoSomeThing()
{
try
{
}
catch (Exception ex)
{
}
finally
{
}
}
Listing 3-11 shows an example for how to handle exceptions in code, and its verbiage pretty much describes how the try, catch and finally blocks are executed.
Caution The finally block of the TryCatch does not perform like regular C# code. If an error occurs within the catch block, the finally block will not fire. This is not the case with C# code, as the finally block will fire regardless.
WF is declarative, so in order to implement the same functionality for handling exceptions, WF uses out-of-box activities to declaratively add exception management. There are three activities that are used for handling errors with in WF:
By default, a workflow will throw an error that will bubble up to the application hosting the workflow, as long as it is running the same execution thread. So using WorkflowInvoker.Invoke to host a workflow that has an unmanaged exception will have the exception bubbled up to the hosting application; however, using WorkflowApplication, which runs asynchronously to its hosted application, will not receive the error unless the hosted application subscribes to the WF runtime’s OnUnhandledException event.
HANDLING EXCEPTIONS IN WF
To demonstrate this, use the same workflow solution, Chapter3.Activities.
public InArgument <string> Text {get; set; }
and
string text = context.GetValue(this.Text);
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;
namespace Apress.Example.Chapter3
{
public sealed class ExceptionActivity : CodeActivity
{
protected override void Execute(CodeActivityContext context)
{
throw new Exception("Here is an exception");
}
}
}
Figure 3-21. Exception Activity
using System;
using System.Linq;
using System.Activities;
using System.Activities.Statements;
namespace Chapter3.Activities
{
class Program
{
static void Main(string[] args)
{
try
{
Activity workflow1 = new Workflow1();
WorkflowInvoker.Invoke(workflow1);
Console.ReadKey();
}
catch (Exception ex)
{
throw ex;
}
}
}
}
Figure 3-22. TryCatch activity
Figure 3-23. Catches System.Exception
Figure 3-24. Handling the error
throw new Exception("Here is an exception");
to
throw new ApplicationException ("Here is an Application Exception");
Figure 3-25. Catching another exception type (ApplicationException)
Figure 3-26. Browsing .Net types
Figure 3-27. Adding the Rethrow activity
Activity workflow1 = new Workflow1();
Activity workflow1 = new ApplicationExceptionWorkflow();
Console.ReadKey();
using System;
using System.Linq;
using System.Activities;
using System.Activities.Statements;
namespace Chapter3.Activities
{
class Program
{
static void Main(string[] args)
{
try
{
Activity workflow1 = new ApplicationExceptionWorkflow();
WorkflowInvoker.Invoke(workflow1);
}
catch(ApplicationException ex)
{
Console.WriteLine(string.Format("Application Exception --{0}-- has fired!", ex.Message));
}
catch (Exception ex)
{
Console.WriteLine("Exception {0} has been bubbled up!", ex.Message);
}
finally
{
Console.ReadKey();
}
}
}
}
argInSeconds> 0.
varApplicationException
Figure 3-28. Alert workflow
Figure 3-29. Success!
Summary
This chapter focused on describing WF activities and the namespaces that establish the base classes that all activities inherit from when created. It also covered how WF4 activities could author workflows, either through code or the XML file format called XAML. By building workflows from XAML, workflows can be changed during runtime, allowing for changes to how logic is processed within running applications. The chapter also covered the data model used for getting data back and forth from a workflow and a means for communicating with the application hosting the workflow through the WF runtime using a WF concept called bookmarks. Bookmarks will be covered in more depth in later chapters, but this chapter supplied the code for building a custom bookmark activity that will also be demonstrated in the next chapter on state-machine workflows. After establishing a good foundation for activities, the focus changed to how activities can be unit tested and on patterns around debugging and implementing exception management within activities. Finally, you discovered how to take advantage of the some of the activities provided within WF, categorized as Primitive activities.
The many WF activities that are provided out-of-box will be used for modeling the majority of the business processes you will encounter. The reason is because these activities closely mimic the constructs provided with coding languages and written using syntax for writing logic. Instead, WF activities provide an alternative way for developers to build code declaratively. In the next chapter, you will discover state-machine workflows within WF and the advantages in using them to model event-driven and humanistic business processes that require interaction with human behavior. The next chapters start focusing on the different types of workflows that can be built and why to use one type of workflow over the other.