源代码文件:
Calculator.zip
结构化编程有顺序、分支和循环三种基本结构,这次我们来看看在WF中如何构造分支结构。我们以构建一个计算器为例,采用传统WinForm与WF相结合的设计方法,同时演示如何编译Workflow的类库。
建立calculator工作流
打开VS2008,建立一个新的空解决方案,命名为Calculator。然后在里面添加一个Sequential Workflow Library,命名为CalculatorWorkflow,如图所示:
我们的这个计算器要执行四则运算,每执行一次工作流会运算其中一个操作符。每个操作数都是整形的,结果以double型返回。
第一步,我们要定义工作流的公共属性,以实现外界参数的传入。下面是Workflow1.cs代码部分的内容:
using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Collections;
using System.Drawing;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Serialization;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Design;
using System.Workflow.Runtime;
using System.Workflow.Activities;
using System.Workflow.Activities.Rules;
namespace CalculatorWorkflow
{
public sealed partial class Workflow1: SequentialWorkflowActivity
{
private string _operation = string.Empty;
private int _operand1;
private int _operand2;
private double _result;
public string Operation
{
get { return _operation; }
set { _operation = value; }
}
public int Operand1
{
get { return _operand1; }
set { _operand1 = value; }
}
public int Operand2
{
get { return _operand2; }
set { _operand2 = value; }
}
public double Result
{
get { return _result; }
set { _result = value; }
}
public Workflow1()
{
InitializeComponent();
}
}
}
添加IfElse Activities
这个工作流需要做出一个简单的判断,就是算术运算的运算符。为了完成这个任务,我们可以采用IfElseActivity。
打开Workflow1.cs的设计界面,从工具箱中拖放一个标注了IfElse的activity到工作流里,如图所示:
这个IfElseActivity实际上就是一个能够容纳其它Activity的容器,里面包含了一组(两个或者更多)IfElseBranchActivity的实例。每一个代表着一个判决树可能的结果。这些分支的顺序很重要,因为判决是从最左边的分支开始的。我们再添加两个分支。右击它,点击Add Branch,如是两次即可。
第一个分支我们用它来代表加法运算,它将检查Operation属性是否为“+”。选中最左侧的这个分支,把它的名字改为IfElseBranchActivityIsAdd(非必须,但最好是选择含义清晰的名称)。下面我们有两种方法来定义一个条件,代码条件(Code Condition)和声明规则条件(Declarative Rule Condition)。前者与上篇文章中的CodeActivity很像,它通过代码来实现一个能够评定布尔类型结果的事件句柄,发挥作用;后者也需要评定布尔型结果,但是它与工作流的代码是分离的。采用声明规则条件的好处就是可以在运行时对它进行修改,而无需重新编译。这个例子中,我们也会采用这种形式。这种规则的完整类名是System.Workflow.Activities.Rules.RuleConditionReference。
在属性窗口中,打开Condition属性下拉列表,并选择Declarative Rule Condition,然后展开它,在ConditionName中填入IsAdd,然后点击Expression后的…按钮,填入this.Operation==”+”,如下图所示:
此处输入代码的时候仍然支持IntelliSense。以此类推,将另外的三个分支分别设成如下的属性:
分支 |
ConditionName |
Expression |
IfElseBranchActivityIsMinus |
IsMinus |
this.Operation=="-" |
IfElseBranchActivityIsMultiply |
IsMultiply |
this.Operation=="x" |
IfElseBranchActivityIsDivide |
IsDivide |
this.Operation=="/" |
添加运算逻辑(Calculation Logic)
有了分支的架构,现在需要为每一个分支添加实际的业务操作。从工具栏拖动一个CodeActivity,放置到第一个分支容器内部,将其改名为codeActivityAdd,并且在ExecuteCode属性中填入AddOperation,添加如下的代码:
private void AddOperation(object sender, EventArgs e)
{
_result = _operand1 + _operand2;
}
如法炮制将其余三个运算逻辑也完成。注意按照规范来进行命名。把这四个运算模块写完之后,还可以增加一个抛出异常的模块,即如果传进来的运算符不是四则运算符号,就抛出一个异常来。再添加一个分支,命名为IfElseBranchActivityIsUnknown,条件留空,相当于多重if-elseif最后的那个else。再放进去一个CodeActivity,添加如下的代码:
private void UnknownOperation(object sender, EventArgs e)
{
throw new ArgumentException(string.Format("Invalid operation : {0} ", Operation));
}
全部搞定之后,这个流程图看起来应该如下所示:
创建计算器客户端
现在向这个解决方案中再添加一个Windows Form Application的项目,并为其添加CalculatorWorkflow的项目引用以及下面三个程序集的引用:
l System.Workflow.Activities
l System.Workflow.ComponentModel
l System.Workflow.Runtime
最后整个解决方案看起来应该是下面这个样子:
OK,下面又回到了我们熟悉的WinForm编程,我就不多说什么了,搞出下面的界面来先。那些button的命名不重要,把文本框命名为txtNumber,方便后面的讨论。
我们没有必要为每一个button去写Click事件。事实上,使用四个事件句柄就可以了。即:
l NumericButton_Click:赋给所有的数字按钮(0~9);
l OperationButton_Click:赋给所有的运算符按钮(+、-、x、/);
l Equals_Click:赋给等号按钮(=);
l Clear_Click:赋给清除按钮(C)。
现在可以向窗体的后台代码文件里添加一些成员变量以及Workflow的初始化代码。完整列表如下。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Workflow.Runtime;
using System.Threading;
namespace SimpleCalculator
{
public partial class MainUI : Form
{
private WorkflowRuntime _workflowRuntime;
private AutoResetEvent _waitHandle = new AutoResetEvent(false);
private string _operation = string.Empty;
private int _operand1;
private int _operand2;
private double _result;
public MainUI()
{
InitializeComponent();
// 启动工作流运行时。每个应用程序域只能进行一次
InitializeWorkflowRuntime();
}
private void InitializeWorkflowRuntime()
{
_workflowRuntime = new WorkflowRuntime();
_workflowRuntime.WorkflowCompleted += delegate(object sender, WorkflowCompletedEventArgs e)
{
_result = Convert.ToDouble(e.OutputParameters["Result"]);
_waitHandle.Set();
};
_workflowRuntime.WorkflowTerminated += delegate(object sender, WorkflowTerminatedEventArgs e)
{
MessageBox.Show(string.Format("工作流中断: {0}", e.Exception.Message), "工作流错误");
_waitHandle.Set();
};
}
}
}
初始化工作流的那部分代码在前面的Hello Workflow中被我略过,这个地方继续54它,现在只要会套用就可以了。我们在不同应用程序中要做的工作就是将WorkflowCompleted匿名委托里返回的OutputParameters里的有用信息提取出来即可。在前面的介绍中我们知道它是一个Dictionary<string, object>的类型。在每个应用程序域中只能存在一个WorkflowRuntime的实例,因此将它放在窗体的构造函数中是比较理想的。
紧接着,我们回到设计界面,将上面提到的四个事件句柄添加给每个按钮,并填充它的实际功能代码。
/// <summary>
/// 处理数字按钮的事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void NumericButton_Click(object sender, EventArgs e)
{
txtNumber.AppendText(((Button)sender).Text.Trim());
}
/// <summary>
/// 处理运算符按钮的事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OperationButton_Click(object sender, EventArgs e)
{
try
{
_operand1 = int.Parse(txtNumber.Text);
_operation = ((Button)sender).Text.Trim();
txtNumber.Clear();
}
catch (Exception exception)
{
MessageBox.Show(string.Format("运算符操作错误: {0}", exception.Message));
}
}
/// <summary>
/// 处理等号按钮的事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Equals_Click(object sender, EventArgs e)
{
try
{
_operand2 = int.Parse(txtNumber.Text);
// 创建一个带有输入参数的字典
Dictionary<string, object> wfArguments = new Dictionary<string, object>();
wfArguments.Add("Operand1", _operand1);
wfArguments.Add("Operand2", _operand2);
wfArguments.Add("Operation", _operation);
// 申请一个工作流实例并启动它
WorkflowInstance instance = kflowRuntime.CreateWorkflow(typeof(CalculatorWorkflow.Workflow1), wfArguments);
instance.Start();
// 等待处理完成
_waitHandle.WaitOne();
// 显示结果
Clear();
txtNumber.Text = _result.ToString();
}
catch (Exception ex)
{
MessageBox.Show(string.Format("运算错误: {0}", ex.Message));
}
}
/// <summary>
/// 重设当前的状态
/// </summary>
private void Clear()
{
txtNumber.Clear();
_operand1 = 0;
_operand2 = 0;
_operation = string.Empty;
}
/// <summary>
/// 清除按钮的事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Clear_Click(object sender, EventArgs e)
{
Clear();
}
Equals_Click()就是工作流创建和启动的地方,入口参数以字典对象的形式传递给工作流,这和我们在Hello Workflow中所看到的是一样的。运算的结果就会以Result属性的方式保存在WorkflowCompleted事件句柄中,返回到UI。