在第一章中,使用了工作流设计器完成了一个简单的工作流,现在我们要使用代码区实现一个同样功能的工作流。任何工作流都可以使用代码或者设计器去实现,而使用哪种方式就是喜好问题了。但是,使用代码去实现工作流会让你更加了解工作流是怎么运作的。
创建一个控制台应用程序
创建一个控制台应用程序(不使用工作流模板),如图Figure2-1所示。
添加引用System.Activities。这样可以让你能在项目中使用工作流活动。然后在Program.cs中添加命名空间应用:
using System;
using System.Activities;
using System.Activities.Statements;
using System.Activities.Expressions;
在main()函数中添加如下代码:
WorkflowInvoker.Invoke(CreateWorkflow());
Console.WriteLine("Press ENTER to exit");
Console.ReadLine();
注意到这跟第一章的main()函数是一样的。如果你愿意,你可以简单的从之前的程序中复制到这里。这里有一个不同点。创建工作流是是调用CreateWorkflow()而不是调用new Workflow1():
WorkflowInvoker.Invoke(CreateWorkflow());
Workflow1是在Workflow1.xaml文件中定义的,是通过工作流设计器生成的。CreateWorkflow()是一个你现在要实现的方法。
定义工作流
正如我在上一章描述的,一个工作流只是一个属性的集合,更准确的说,它是一个集合的类和类成员属性。为了让你更清楚工作流,我会一层一层地向你展示。我会解析代码的作用是什么。添加以下方法到Program.cs文件中:
private static Activity CreateWorkflow()
{
Variable<int> numberBells = new Variable<int>()
{
Name = "numberBells",
Default = DateTime.Now.Hour
};
Variable<int> counter = new Variable<int>()
{
Name = "counter",
Default = 1
};
return new Sequence()
{
};
}
CreateWorkflow()方法首先创建了两个int类型的Variable<T>(变量),它们名字为numberBells和counter。这两个variables是为活动服务的。
CreateWorkflow()方法返回一个Activity,而Acitivy正是类WorkflowInvoker所期待的。它实际返回一个匿名的Sequence类。类Activity是一个基类,而其他所有的工作流活动都派生自这个类,包括Sequence。所以可以返回一个Sequence类代替Activity基类。
实现第一层
到目前为止,你已经定义了一个空的Sequence活动。现在在这个空的Sequence中定义它的活动。代码如下所示:
return new Sequence()
{
DisplayName = "Main Sequence",
Variables = { numberBells, counter },
Activities =
{
new WriteLine()
{
DisplayName = "Hello",
Text = "Hello, World"
},
new If()
{
DisplayName = "Adjust to PM",
//Code to be added here in Level 2
},
new While()
{
DisplayName = "Sound Bells",
//Code to be added here in Level 2
},
new WriteLine()
{
DisplayName = "Display Time",
Text = "The time is " + DateTime.Now.ToString()
},
new If()
{
DisplayName = "Greeting",
//Code to be added here in Level 2
}
}
};
提示:本实施主要依赖于创建匿名类的实例。例如Sequence、WriteLine和If是一些匿名的类。这种实现的方法类似于一种技术——功能建设(functional construction),功能建设是用来建立XML树的。如果你觉得这项技术有点奇怪,你可以在MSDN中找关于functional construction的文档(本人能力有限,这里翻译的有点模糊,下面是原文,有兴趣的可以看一下,可能会理解的更好。)
■ Note This implementation relies heavily on creating anonymous class instances. Classes such as Sequence, WriteLine, and If are instantiated but never named. This approach is similar to the technique called functional construction, which is used to build XML trees. If it seems strange to you, you might want to review some of the documentation on functional construction on MSDN.
代码首先定义了DisplayName和关联了variables到活动中。然后是初始化成员Activities作为一个集合活动。它包含的活动如下表Table2-1。
对于WriteLine活动,Text属性应经定义了,而其他的活动,我们将在第二层中定义。
实现第二层
对于第一个If活动,输入以下代码:
DisplayName = "Adjust for PM",
//Code to be added here in Level 2
Condition = ExpressionServices.Convert<bool>(env => numberBells.Get(env) > 12),
Then = new Assign <int>()
{
DisplayName = "Adjust Bell"
//Code to be added here in Level 3
}
代码定义了Condition和Then属性(没有Else分支)。Assign将在下一层中定义。对于Condition属性的定义,在这里,可能需要解析一下。
表达式
ExpressionServices中的静态方法Convert<T>是用来创建一个InArgument<T>类的,而InArgument<T>类正是Condition属性所希望得到的。这个类和方法用了泛型,因此可以适用于任何数据类型。在这种情况下,我们需要使用bool类型,因为If活动的Condition属性的值只可以去true或者false。
表达式使用了lambda表达式(类似于Linq)去从工作流环境中提取数据。=>是lambda表达式的操作符,左边的参数是输入参数,实际的表达式在=>操作符的右边。当env试图去验证Condition时,运行时会提供给env一个值。(不懂的可以去看linq的lambda表达式)
工作流实际上没有状态的,它不会存储任何的数据。为了去得到实际的数据,你需要去调用Get()方法。这要求一个ActivityContext类,它用来与其他正在运行的工作流区分开来,使得只取自己数据。Get(env)返回的数据与12比较,看是否大于12。
在While活动中输入以下代码:
DisplayName = "Sound Bells",
//Code to be added here in Level 2
Condition = ExpressionServices.Convert<bool>(env => counter.Get(env) <= numberBells.Get(env)),
Body = new Sequence()
{
DisplayName = "Sound Bell",
//Code to be added here in Levl 3
While活动中的Condition属性类似于If活动的Condition属性,它同样用了ExpressionServices类来创建一个InArgument<T>类,同样是bool类型的。在这种情况下,它去验证是否counter <= numberBells。因为他们都是variables,他们都要使用Get(env)方法去获得他们的数据。
对于第二个If活动(名为“Greeting”),输入以下代码:
DisplayName = "Greeting",
//Code to be added here in Level 2
Condition = ExpressionServices.Convert<bool>(env => DateTime.Now.Hour >= 18),
Then = new WriteLine() { Text = "Good evening"},
Else = new WriteLine() { Text = "Good day"}
对于这个Condition属性,env参数没有使用到,但它仍需要在表达式中定义。用当前时间去判断是否过了6:00PM。对于Then和Else属性,这里使用了WriteLine活动去赋值。一个是输出“Good Evening”;另一个是输出“Good Day”。
实现第三层
对于第一个If活动(名为“Adjust for PM”),你创建了一个空的Assign活动在Then属性中。输入以下代码:
DisplayName = "Adjust Bell",
//Code to be added here in Level 3
To = new OutArgument<int>(numberBells),
Value = new InArgument<int>(env => numberBells.Get(env) - 12)
Assign活动
Assign类是一个泛型类,因此可以支持任何数据类型。在这种情况下,它赋值给一个整型,因此它被创建为Assign<int>。To和Value属性也应该用<int>来创建。To属性是一个OutArgument类,OutArgument类把一个Variable类放到他的构造函数中。对于这个构造函数,它使用了一个lambda表达式。
Sequence活动
在While活动中,你为body属性创建了一个空的Sequence活动,这个Sequence活动会不断的执行,直到循环退出。输入以下代码:
DisplayName = "Sound Bell",
//Code to be added here in Levl 3
Activities =
{
new WriteLine()
{
Text = new InArgument<string>(env => counter.Get(env).ToString())
},
new Assign<int>()
{
DisplayName = "Increment Counter",
To = new OutArgument<int>(counter),
Value = new InArgument<int>(env => counter.Get(env) + 1)
},
new Delay()
{
Duration = TimeSpan.FromSeconds(1)
}
}
代码添加了三个活动到Sequence中。
1、 一个WriteLine活动去显示counter
2、 一个Assign活动去递增counter
3、 一个Delay活动去让程序停止一段时间
对于WriteLine活动,他的Text属性需要一个字符串的值,因此它创建了一个InArgument<string>类,到现在,你应该知道怎么去使用lambda表达式了吧,Get(env)方法返回一个整型,用ToString()方法去把整型转变为string类型。
对于Delay活动,Duration属性是一个TimeSpan类,TimeSpan类是可以通过FromSeconds()静态方法来创建的。
运行程序
点击F5,运行程序,根据你时间的不同,你的结果应该类似于下面的结果:
下面是完整的Program.cs代码单:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;
using System.Activities.Statements;
using System.Activities.Expressions;
namespace Chapter02
{
class Program
{
static void Main(string[] args)
{
WorkflowInvoker.Invoke(CreateWorkflow());
Console.WriteLine("Press ENTER to exit");
Console.ReadLine();
}
private static Activity CreateWorkflow()
{
Variable<int> numberBells = new Variable<int>()
{
Name = "numberBells",
Default = DateTime.Now.Hour
};
Variable<int> counter = new Variable<int>()
{
Name = "counter",
Default = 1
};
return new Sequence()
{
DisplayName = "Main Sequence",
Variables = { numberBells, counter },
Activities =
{
new WriteLine()
{
DisplayName = "Hello",
Text = "Hello, World"
},
new If()
{
DisplayName = "Adjust for PM",
//Code to be added here in Level 2
Condition = ExpressionServices.Convert<bool>(env => numberBells.Get(env) > 12),
Then = new Assign<int>()
{
DisplayName = "Adjust Bell",
//Code to be added here in Level 3
To = new OutArgument<int>(numberBells),
Value = new InArgument<int>(env => numberBells.Get(env) - 12)
}
},
new While()
{
DisplayName = "Sound Bells",
//Code to be added here in Level 2
Condition = ExpressionServices.Convert<bool>(env => counter.Get(env) <= numberBells.Get(env)),
Body = new Sequence()
{
DisplayName = "Sound Bell",
//Code to be added here in Levl 3
Activities =
{
new WriteLine()
{
Text = new InArgument<string>(env => counter.Get(env).ToString())
},
new Assign<int>()
{
DisplayName = "Increment Counter",
To = new OutArgument<int>(counter),
Value = new InArgument<int>(env => counter.Get(env) + 1)
},
new Delay()
{
Duration = TimeSpan.FromSeconds(1)
}
}
}
},
new WriteLine()
{
DisplayName = "Display Time",
Text = "The time is " + DateTime.Now.ToString()
},
new If()
{
DisplayName = "Greeting",
//Code to be added here in Level 2
Condition = ExpressionServices.Convert<bool>(env => DateTime.Now.Hour >= 18),
Then = new WriteLine() { Text = "Good evening"},
Else = new WriteLine() { Text = "Good day"}
}
}
};
}
}
}
回顾
这本书的例子一些会使用设计器来设计工作流,一些会使用代码来创建工作流,使用设计器会比用代码实现要容易。但是,当你对工作流非常熟悉时,你会发现使用代码来时创建要比用设计器来实现更加快。两种方法的结果都是一样的,两种都可以运行的非常好。