使用命令性代码创作工作流、活动和表达式

工作流定义是已配置活动对象的树。 可以通过多种方法定义此活动树,包括手动编辑 XAML 或使用工作流设计器生成 XAML。 但是,使用 XAML 并不是必需的。 还可以以编程方式创建工作流定义。 本主题概述如何使用代码创建工作流定义、活动和表达式。 有关使用代码处理 XAML 工作流的示例,请参阅 对 XAML 的工作流和活动进行序列化

创建工作流定义

可以通过实例化活动类型的实例并配置活动对象的属性来创建工作流定义。 对于不包含子活动的活动,可以使用几行代码来完成此作。

Activity wf = new WriteLine
{
    Text = "Hello World."
};

WorkflowInvoker.Invoke(wf);

注释

本主题中的示例使用 WorkflowInvoker 来运行示例工作流。 有关调用工作流、传递参数以及可用的不同托管选项的详细信息,请参阅 Using WorkflowInvoker 和 WorkflowApplication

在此示例中,将创建包含单个 WriteLine 活动的工作流。 设置了 WriteLine 活动的 Text 自变量,并调用工作流。 如果一个活动包含子活动,其构造方法是类似的。 以下示例使用一个包含两个 Sequence 活动的 WriteLine 活动。

Activity wf = new Sequence
{
    Activities =
    {
        new WriteLine
        {
            Text = "Hello"
        },
        new WriteLine
        {
            Text = "World."
        }
    }
};

WorkflowInvoker.Invoke(wf);

使用对象初始值设定项

本主题中的示例使用对象初始化语法。 对于用代码创建工作流定义而言,对象初始化语法非常有用,因为它为工作流中的活动提供了分层视图,可以显示活动之间的关系。 通过编程方式创建工作流时,不要求必须使用对象初始化语法。 下面的示例与前面的示例在功能上是等效的。

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);

有关对象初始值设定项的详细信息,请参阅 如何:在不使用调用构造函数的情况下初始化对象(C# 编程指南) 以及如何 :使用对象初始值设定项声明对象

处理变量、字面值和表达式

使用代码创建工作流定义时,请注意在创建工作流定义的过程中执行哪些代码,以及在执行该工作流实例的过程中执行哪些代码。 例如,以下工作流旨在生成一个随机数并将其写入控制台。

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))
        }
    }
};

执行此工作流定义代码时,将调用 Random.Next 并结果以文本值的形式存储在工作流定义中。 可以调用此工作流的许多实例,并且所有实例都将显示相同的数字。 若要在工作流执行期间生成随机数,必须使用一个每次运行工作流时都会计算的表达式。 在以下示例中,一个 Visual Basic 表达式与VisualBasicValue<TResult>一起使用。

new Assign<int>
{
    To = n,
    Value = new VisualBasicValue<int>("New Random().Next(1, 101)")
}

上一示例中的表达式还可以使用 CSharpValue<TResult> 和 C# 表达式来实现。

new Assign<int>  
{  
    To = n,  
    Value = new CSharpValue<int>("new Random().Next(1, 101)")  
}  

在调用包含 C# 表达式的工作流之前,必须编译 C# 表达式。 如果未编译 C# 表达式,则当使用类似于以下内容NotSupportedException的消息调用工作流时,将引发一个Expression Activity type 'CSharpValue`1' requires compilation in order to run. Please ensure that the workflow has been compiled.:在大多数情况下,涉及在 Visual Studio 中创建的工作流时,C# 表达式会自动编译,但在某些情况下(如代码工作流)必须手动编译 C# 表达式。 有关如何编译 C# 表达式的示例,请参阅 C# 表达式主题的代码工作流部分中的“使用 C# 表达式”。

VisualBasicValue<TResult> 个表示 Visual Basic 语法中的表达式,该表达式可用作表达式中的 r 值,一个 CSharpValue<TResult> 表示 C# 语法中的表达式,该表达式可用作表达式中的 r 值。 每次执行包含活动时,都会计算这些表达式。 表达式的结果分配给工作流变量 n,这些结果由工作流中的下一个活动使用。 若要在运行时访问工作流变量 n 的值,需要用到该值 ActivityContext 。 这可以通过使用以下 lambda 表达式进行访问。

注释

请注意,这两个代码示例都使用 C# 作为编程语言,但一个使用 VisualBasicValue<TResult> 而另一个使用 CSharpValue<TResult>VisualBasicValue<TResult>CSharpValue<TResult> 可以在 Visual Basic 和 C# 项目中使用。 默认情况下,在工作流设计器中创建的表达式与宿主项目的语言匹配。 在代码中创建工作流时,所需语言由工作流作者自行决定。

在这些示例中,表达式的结果分配给工作流变量 n,这些结果由工作流中的下一个活动使用。 若要在运行时访问工作流变量 n 的值,需要用到该值 ActivityContext 。 这可以通过使用以下 lambda 表达式进行访问。

new WriteLine
{
    Text = new InArgument<string>((env) => "The number is " + n.Get(env))
}

有关 lambda 表达式的详细信息,请参阅 Lambda 表达式(C# 参考)Lambda 表达式(Visual Basic)。

Lambda 表达式不可序列化为 XAML 格式。 如果尝试使用 lambda 表达式序列化工作流, LambdaSerializationException 则会引发以下消息:“此工作流包含代码中指定的 lambda 表达式。 这些表达式不可序列化 XAML。 若要使工作流 XAML 可序列化,请使用 VisualBasicValue/VisualBasicReference 或 ExpressionServices.Convert(lambda)。 这将将 lambda 表达式转换为表达式活动。若要使此表达式与 XAML 兼容,请使用 ExpressionServicesConvert,如以下示例所示。

new WriteLine
{
    //Text = new InArgument<string>((env) => "The number is " + n.Get(env))
    Text = ExpressionServices.Convert((env) => "The number is " + n.Get(env))
}

也可以使用一个VisualBasicValue<TResult>。 请注意,使用 Visual Basic 表达式时不需要 lambda 表达式。

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()")
}

在运行时,Visual Basic 表达式编译为 LINQ 表达式。 前面两个示例都可序列化为 XAML,但是,如果序列化后的 XAML 用于在工作流设计器中查看和编辑,则对表达式使用 VisualBasicValue<TResult>。 可以在设计器中打开使用的 ExpressionServices.Convert 序列化工作流,但表达式的值将为空。 有关将工作流和活动序列化为 XAML 的详细信息,请参阅 将工作流和活动序列化到 XAML

文本表达式和引用类型

文本表达式由 Literal<T> 活动在工作流中表示。 以下活动 WriteLine 在功能上是等效的。

new WriteLine  
{  
    Text = "Hello World."  
},  
new WriteLine  
{  
    Text = new Literal<string>("Hello World.")  
}  

初始化文本表达式时,使用任何引用类型(除了 String)都是无效的。 在下例中,Assign 活动的 Value 属性是通过 List<string> 用文本表达式初始化的。

new Assign  
{  
    To = new OutArgument<List<string>>(items),  
    Value = new InArgument<List<string>>(new List<string>())  
},  

验证包含此活动的工作流时,将返回以下验证错误:“文本仅支持值类型和不可变类型 System.String。 类型 System.Collections.Generic.List`1[System.String] 不能用作字面值。如果调用工作流,将会抛出一个包含验证错误文本的InvalidWorkflowException。 这是一个验证错误,因为创建具有引用类型的文本表达式不会为每个工作流实例创建引用类型的新实例。 若要解决此问题,请将文本表达式替换为创建并返回引用类型的新实例的表达式。

new Assign  
{  
    To = new OutArgument<List<string>>(items),  
    Value = new InArgument<List<string>>(new VisualBasicValue<List<string>>("New List(Of String)"))  
},  

有关表达式的详细信息,请参阅 表达式

使用表达式和 InvokeMethod 活动调用对象上的方法

活动 InvokeMethod<TResult> 可用于调用 .NET Framework 中类的静态和实例方法。 在本主题前面的示例中,使用 Random 类生成随机数。

new Assign<int>
{
    To = n,
    Value = new VisualBasicValue<int>("New Random().Next(1, 101)")
}

InvokeMethod<TResult> 活动还可以用于调用 Next 类的 Random 方法。

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  
}  

由于 Next 不是静态方法,因此将在 Random 属性中提供 TargetObject 类的实例。 在此示例中,将使用 Visual Basic 表达式创建一个新实例,但也可能以前创建并存储在工作流变量中。 在此示例中,使用 Assign<T> 活动而不是 InvokeMethod<TResult> 活动会更简单。 如果最终通过长时间运行的 Assign<T>InvokeMethod<TResult> 活动调用该方法,因为有 InvokeMethod<TResult> 属性,所以 RunAsynchronously 具有优势。 如果此属性设置为 true,则调用的方法将在工作流方面异步运行。 如果其他活动是并行的,则该方法异步执行时不会阻止它们。 此外,如果要调用的方法没有返回值,则 InvokeMethod<TResult> 调用该方法的合适方式。

参数与动态活动

工作流定义通过在代码中将活动组合到活动树中并配置任何属性和参数来创建。 现有参数可以绑定,但无法将新参数添加到活动。 这包括传递给根活动的工作流参数。 在命令式代码中,工作流参数被指定为新 CLR 类型的属性,而在 XAML 中,它们通过使用x:Classx:Member来声明。 由于在工作流定义创建为内存中对象的树时没有创建新的 CLR 类型,因此无法添加参数。 但是,可以将参数添加到DynamicActivity中。 在本示例中,将要创建一个 DynamicActivity<TResult>,它接受两个整型自变量并将它们相加,然后返回结果。 为每个参数创建一个DynamicActivityProperty,并将操作结果分配给ResultDynamicActivity<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);

有关动态活动的详细信息,请参阅 在运行时创建活动

编译活动

动态活动是定义包含使用代码的参数的活动的一种方法,但活动也可以在代码中创建并编译为类型。 可以创建派生自 CodeActivity的简单活动以及派生自 AsyncCodeActivity的异步活动。 这些活动可以具有参数、返回值,并使用命令性代码定义其逻辑。 有关创建此类活动的示例,请参阅 CodeActivity 基类创建异步活动

派生自 NativeActivity 的活动可以使用命令性代码定义其逻辑,还可以包含定义逻辑的子活动。 它们还可以完全访问运行时的功能,例如创建书签。 有关创建基于 NativeActivity 的活动的示例,请参阅 NativeActivity 基类如何:创建活动使用本机活动的自定义复合示例。

派生自Activity的活动仅通过使用子活动来定义其逻辑。 这些活动通常使用工作流设计器创建,但也可以使用代码定义。 在下面的示例中, Square 定义派生自 Activity<int>的活动。 Square 活动具有名为 InArgument<T> 的单一 Value,并且通过使用 Sequence 属性指定 Implementation 活动来定义其逻辑。 活动 Sequence 包含一个 WriteLine 活动和一个 Assign<T> 活动。 这三个活动共同实现了 Square 活动的逻辑。

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))  
                }  
            }  
        };  
    }  
}  

在以下示例中,包含单个Square活动的工作流定义通过WorkflowInvoker被调用。

Dictionary<string, object> inputs = new Dictionary<string, object> {{ "Value", 5}};  
int result = WorkflowInvoker.Invoke(new Square(), inputs);  
Console.WriteLine("Result: {0}", result);  

调用工作流时,向控制台显示以下输出:

对值进行平方处理:5
结果:25