Authoring Workflows, Activities, and Expressions Using Imperative Code
A workflow definition is a tree of configured activity objects. This tree of activities can be defined many ways, including by hand-editing XAML or by using the Workflow Designer to produce XAML. Use of XAML, however, is not a requirement. Workflow definitions can also be created programmatically. This topic provides an overview of creating workflow definitions, activities, and expressions by using code. For examples of working with XAML workflows using code, see Serializing Workflows and Activities to and from XAML.
Creating Workflow Definitions
A workflow definition can be created by instantiating an instance of an activity type and configuring the activity object’s properties. For activities that do not contain child activities, this can be accomplished using a few lines of code.
Activity wf = new WriteLine
{
Text = "Hello World."
};
WorkflowInvoker.Invoke(wf);
Note
The examples in this topic use WorkflowInvoker to run the sample workflows. For more information about invoking workflows, passing arguments, and the different hosting choices that are available, see Using WorkflowInvoker and WorkflowApplication.
In this example, a workflow that consists of a single WriteLine activity is created. The WriteLine activity’s Text argument is set, and the workflow is invoked. If an activity contains child activities, the method of construction is similar. The following example uses a Sequence activity that contains two WriteLine activities.
Activity wf = new Sequence
{
Activities =
{
new WriteLine
{
Text = "Hello"
},
new WriteLine
{
Text = "World."
}
}
};
WorkflowInvoker.Invoke(wf);
Using Object Initializers
The examples in this topic use object initialization syntax. Object initialization syntax can be a useful way to create workflow definitions in code because it provides a hierarchical view of the activities in the workflow and shows the relationship between the activities. There is no requirement to use object initialization syntax when you programmatically create workflows. The following example is functionally equivalent to the previous example.
WriteLine hello = new WriteLine();
hello.Text = "Hello";
WriteLine world = new WriteLine();
world.Text = "World";
Sequence wf = new Sequence();
wf.Activities.Add(hello);
wf.Activities.Add(world);
WorkflowInvoker.Invoke(wf);
For more information about object initializers, see How to: Initialize Objects without Calling a Constructor (C# Programming Guide) and How to: Declare an Object by Using an Object Initializer.
Working with Variables, Literal Values, and Expressions
When creating a workflow definition using code, be aware of what code executes as part of the creation of the workflow definition and what code executes as part of the execution of an instance of that workflow. For example, the following workflow is intended to generate a random number and write it to the console.
Variable<int> n = new Variable<int>
{
Name = "n"
};
Activity wf = new Sequence
{
Variables = { n },
Activities =
{
new Assign<int>
{
To = n,
Value = new Random().Next(1, 101)
},
new WriteLine
{
Text = new InArgument<string>((env) => "The number is " + n.Get(env))
}
}
};
When this workflow definition code is executed, the call to Random.Next
is made and the result is stored in the workflow definition as a literal value. Many instances of this workflow can be invoked, and all would display the same number. To have the random number generation occur during workflow execution, an expression must be used that is evaluated each time the workflow runs. In the following example, a Visual Basic expression is used with a VisualBasicValue<TResult>.
new Assign<int>
{
To = n,
Value = new VisualBasicValue<int>("New Random().Next(1, 101)")
}
The expression in the previous example could also be implemented using a CSharpValue<TResult> and a C# expression.
new Assign<int>
{
To = n,
Value = new CSharpValue<int>("new Random().Next(1, 101)")
}
C# expressions must be compiled before the workflow containing them is invoked. If the C# expressions are not compiled, a NotSupportedException is thrown when the workflow is invoked with a message similar to the following: Expression Activity type 'CSharpValue`1' requires compilation in order to run. Please ensure that the workflow has been compiled.
In most scenarios involving workflows created in Visual Studio the C# expressions are compiled automatically, but in some scenarios, such as code workflows, the C# expressions must be manually compiled. For an example of how to compile C# expressions, see the Using C# expressions in code workflows section of the C# Expressions topic.
A VisualBasicValue<TResult> represents an expression in Visual Basic syntax that can be used as an r-value in an expression, and a CSharpValue<TResult> represents an expression in C# syntax that can be used as an r-value in an expression. These expressions are evaluated each time the containing activity is executed. The result of the expression is assigned to the workflow variable n
, and these results are used by the next activity in the workflow. To access the value of the workflow variable n
at run time, the ActivityContext is required. This can be accessed by using the following lambda expression.
Note
Note that both of these code are examples are using C# as the programming language, but one uses a VisualBasicValue<TResult> and one uses a CSharpValue<TResult>. VisualBasicValue<TResult> and CSharpValue<TResult> can be used in both Visual Basic and C# projects. By default, expressions created in the workflow designer match the language of the hosting project. When creating workflows in code, the desired language is at the discretion of the workflow author.
In these examples the result of the expression is assigned to the workflow variable n
, and these results are used by the next activity in the workflow. To access the value of the workflow variable n
at run time, the ActivityContext is required. This can be accessed by using the following lambda expression.
new WriteLine
{
Text = new InArgument<string>((env) => "The number is " + n.Get(env))
}
For more information about lambda expressions, see Lambda Expressions (C# reference) or Lambda Expressions (Visual Basic).
Lambda expressions are not serializable to XAML format. If an attempt to serialize a workflow with lambda expressions is made, a LambdaSerializationException is 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." To make this expression compatible with XAML, use ExpressionServices and Convert, as shown in the following example.
new WriteLine
{
//Text = new InArgument<string>((env) => "The number is " + n.Get(env))
Text = ExpressionServices.Convert((env) => "The number is " + n.Get(env))
}
A VisualBasicValue<TResult> could also be used. Note that no lambda expression is required when using a Visual Basic expression.
new WriteLine
{
//Text = new InArgument<string>((env) => "The number is " + n.Get(env))
//Text = ExpressionServices.Convert((env) => "The number is " + n.Get(env))
Text = new VisualBasicValue<string>("\"The number is \" + n.ToString()")
}
At run time, Visual Basic expressions are compiled into LINQ expressions. Both of the previous examples are serializable to XAML, but if the serialized XAML is intended to be viewed and edited in the workflow designer, use VisualBasicValue<TResult> for your expressions. Serialized workflows that use ExpressionServices.Convert
can be opened in the designer, but the value of the expression will be blank. For more information about serializing workflows to XAML, see Serializing Workflows and Activities to and from XAML.
Literal Expressions and Reference Types
Literal expressions are represented in workflows by the Literal<T> activity. The following WriteLine activities are functionally equivalent.
new WriteLine
{
Text = "Hello World."
},
new WriteLine
{
Text = new Literal<string>("Hello World.")
}
It is invalid to initialize a literal expression with any reference type except String. In the following example, an Assign activity's Value property is initialized with a literal expression using a List<string>
.
new Assign
{
To = new OutArgument<List<string>>(items),
Value = new InArgument<List<string>>(new List<string>())
},
When the workflow containing this activity is validated, the following validation error is returned: "Literal only supports value types and the immutable type System.String. The type System.Collections.Generic.List`1[System.String] cannot be used as a literal." If the workflow is invoked, an InvalidWorkflowException is thrown that contains the text of the validation error. This is a validation error because creating a literal expression with a reference type does not create a new instance of the reference type for each instance of the workflow. To resolve this, replace the literal expression with one that creates and returns a new instance of the reference type.
new Assign
{
To = new OutArgument<List<string>>(items),
Value = new InArgument<List<string>>(new VisualBasicValue<List<string>>("New List(Of String)"))
},
For more information about expressions, see Expressions.
Invoking Methods on Objects using Expressions and the InvokeMethod Activity
The InvokeMethod<TResult> activity can be used to invoke static and instance methods of classes in the .NET Framework. In a previous example in this topic, a random number was generated using the Random class.
new Assign<int>
{
To = n,
Value = new VisualBasicValue<int>("New Random().Next(1, 101)")
}
The InvokeMethod<TResult> activity could also have been used to call the Next method of the Random class.
new InvokeMethod<int>
{
TargetObject = new InArgument<Random>(new VisualBasicValue<Random>("New Random()")),
MethodName = "Next",
Parameters =
{
new InArgument<int>(1),
new InArgument<int>(101)
},
Result = n
}
Since Next is not a static method, an instance of the Random class is supplied for the TargetObject property. In this example a new instance is created using a Visual Basic expression, but it could also have been created previously and stored in a workflow variable. In this example, it would be simpler to use the Assign<T> activity instead of the InvokeMethod<TResult> activity. If the method call ultimately invoked by either the Assign<T> or InvokeMethod<TResult> activities is long running, InvokeMethod<TResult> has an advantage since it has a RunAsynchronously property. When this property is set to true
, the invoked method will run asynchronously with regard to the workflow. If other activities are in parallel, they will not be blocked while the method is asynchronously executing. Also, if the method to be invoked has no return value, then InvokeMethod<TResult> is the appropriate way to invoke the method.
Arguments and Dynamic Activities
A workflow definition is created in code by assembling activities into an activity tree and configuring any properties and arguments. Existing arguments can be bound, but new arguments cannot be added to activities. This includes workflow arguments passed to the root activity. In imperative code, workflow arguments are specified as properties on a new CLR type, and in XAML they are declared by using x:Class
and x:Member
. Because there is no new CLR type created when a workflow definition is created as a tree of in-memory objects, arguments cannot be added. However, arguments can be added to a DynamicActivity. In this example, a DynamicActivity<TResult> is created that takes two integer arguments, adds them together, and returns the result. A DynamicActivityProperty is created for each argument, and the result of the operation is assigned to the Result argument of the DynamicActivity<TResult>.
InArgument<int> Operand1 = new InArgument<int>();
InArgument<int> Operand2 = new InArgument<int>();
DynamicActivity<int> wf = new DynamicActivity<int>
{
Properties =
{
new DynamicActivityProperty
{
Name = "Operand1",
Type = typeof(InArgument<int>),
Value = Operand1
},
new DynamicActivityProperty
{
Name = "Operand2",
Type = typeof(InArgument<int>),
Value = Operand2
}
},
Implementation = () => new Sequence
{
Activities =
{
new Assign<int>
{
To = new ArgumentReference<int> { ArgumentName = "Result" },
Value = new InArgument<int>((env) => Operand1.Get(env) + Operand2.Get(env))
}
}
}
};
Dictionary<string, object> wfParams = new Dictionary<string, object>
{
{ "Operand1", 25 },
{ "Operand2", 15 }
};
int result = WorkflowInvoker.Invoke(wf, wfParams);
Console.WriteLine(result);
For more information about dynamic activities, see Creating an Activity at Runtime.
Compiled Activities
Dynamic activities are one way to define an activity that contains arguments using code, but activities can also be created in code and compiled into types. Simple activities can be created that derive from CodeActivity, and asynchronous activities that derive from AsyncCodeActivity. These activities can have arguments, return values, and define their logic using imperative code. For examples of creating these types of activities, see CodeActivity Base Class and Creating Asynchronous Activities.
Activities that derive from NativeActivity can define their logic using imperative code and they can also contain child activities that define the logic. They also have full access to the features of the runtime such as creating bookmarks. For examples of creating a NativeActivity-based activity, see NativeActivity Base Class, How to: Create an Activity, and the Custom Composite using Native Activity sample.
Activities that derive from Activity define their logic solely through the use of child activities. These activities are typically created by using the workflow designer, but can also be defined by using code. In the following example, a Square
activity is defined that derives from Activity<int>
. The Square
activity has a single InArgument<T> named Value
, and defines its logic by specifying a Sequence activity using the Implementation property. The Sequence activity contains a WriteLine activity and an Assign<T> activity. Together, these three activities implement the logic of the Square
activity.
class Square : Activity<int>
{
[RequiredArgument]
public InArgument<int> Value { get; set; }
public Square()
{
this.Implementation = () => new Sequence
{
Activities =
{
new WriteLine
{
Text = new InArgument<string>((env) => "Squaring the value: " + this.Value.Get(env))
},
new Assign<int>
{
To = new OutArgument<int>((env) => this.Result.Get(env)),
Value = new InArgument<int>((env) => this.Value.Get(env) * this.Value.Get(env))
}
}
};
}
}
In the following example, a workflow definition consisting of a single Square
activity is invoked using WorkflowInvoker.
Dictionary<string, object> inputs = new Dictionary<string, object> {{ "Value", 5}};
int result = WorkflowInvoker.Invoke(new Square(), inputs);
Console.WriteLine("Result: {0}", result);
When the workflow is invoked, the following output is displayed to the console:
Squaring the value: 5
Result: 25