【IT168 专稿】无论是系统架构师还是普通程序员在构建应用程序时都期望系统的业务逻辑层构建得灵活而易于维护。因此,尽可能消除代码重复几乎成为传统软件开发过程中的金科玉律。为此,人们付出巨大的努力,包括投入大量的时间来研究如何设计出适当的模式以适合开发需求。当然,视选择的开发平台的不同,人们为之付出的努力程度也存在不同程度的差异。
微软于2008年正式推出的ASP.NET MVC框架正是适合上述需求的平台之一。借助于这个框架,我们得以能够最小的努力来创建灵活和易于维护的应用程序。特别是,它还提供的功能能够帮助我们最大程度地避免代码重复。
在本文中,我们将构建一个简单的新用户注册的ASP.NET 3.5 Web网站示例。通过这个例子,我们的主旨在于探讨如何能够通过把ASP.NET MVC框架和微软Windows工作流(Windows Workflow Foundation)架构结合到一起来开发出极富弹性的用户界面型工作流应用程序。具体地说,此示例应用程序将演示如何避免校验逻辑以及如何最大程度地实现应用程序本身与工作流业务逻辑部分的解耦。
一 数据持久性考虑
在编写多步骤用户界面工作流程应用程序方面,存在多种可选方案可用于实现用户输入数据的持久性存储。下面给出了多种可选方案的列表,以及它们各自存在的优缺点。
方案1 把每个步骤中的数据存储到服务器端数据库中。
优点:如果用户在操作过程中因为某种中断(可能因连接掉线或浏览器崩溃等原因所致)突然离开,那么,在下次回来时他还可以继续之前的操作—这将明显地提高系统的可靠性。【注意】这种方案的一个局限性是,通常这种类型的方法需要使用由客户端会话ID或MAC地址构造的Cookie结构。然而,如果用户下一次从另一台不同的机器访问网站时,他将无法继续操作。对此,我们有一种解决方法是,在第一步中就要求用户提供凭证数据—我们可以使用此数据来识别用户的下一次系统进入。
缺点:①在正式提交前耗费服务器内存存储用户不必要的数据—有可能需要定期刷新数据库中储存的数据,以防用户永远不会回来;②需要服务器回寄支持(但我们可以通过AJAX调用来克服之)。
方案2 在浏览器会话(Session)中存储每个步骤的数据,成功验证后,最后再一起储存到服务器端数据库中。
优点:由于数据没有存储到数据库中,所以不必要创建独立的进程实现一次性存储所有不必要的数据。
缺点:①提高了服务器端内存消耗;②在用户不启用会话(多种原因:浏览器崩溃,会话超时,应用程序池循环利用等)时导致不可靠性。
方案3 先序列化页面的数据,成功验证后,最后再一起存储到服务器端数据库中。
优点:由于数据没有存储到后台数据库中,所以不必要创建独立的进程实现一次性存储所有不必要的数据。
缺点:单个网页的尺寸可能会因之急剧膨胀,因而会影响系统的响应时间。
方案4 在同一个页面中的多个面板(对于jQuery框架来是指Div元素)中存储每个步骤的数据,成功验证后,最后再一起存储存到数据库中。
优点:①用户界面响应性能好;②不需要服务器回寄,因而最有利于提高服务器资源和数据库的利用率。
缺点:①在客户端浏览器崩溃时用户并不总是有释放数据的机会;②重新安排页面顺序以及改变相应的操作流程的可能性很小。
值得注意的是,选择上述任何方案之一最终还将取决于系统功能和具体的业务逻辑需求。本文中将构建的示例应用程序将采取上表中的第3种存储方案。
本文中将构建的示例应用程序极其简单—它将展示一个简单的使用三个步骤的界面来实现新雇员注册的工作流程。
二 模型绑定
模型绑定是ASP.NET MVC框架提供的极其强大的可定制功能。借助于模型绑定,在HTTP请求中传入的数据将被自动填充到模型类中。作为开发人员,我们没有必要担心提取Forms集合或querystring中数值所可能需要付出的大量编码的任务。基本上,存在两种类型的模型绑定支持:显式绑定和隐式绑定。通过调用TryUpdateModel方法,我们可以显式地触发绑定过程。通过把动作(Action)方法的参数设置为与模型类属性一样的名字,或通过把模型类自身作为动作(Action)方法的参数,我们可以隐式地取得HTTP请求中提供的数值并使之可用。
在本文示例应用程序中,“Employee”承担模型类的任务。我们在OnActionExecuting事件中调用了TryUpdateModel方法,以便Employee模型类对象能够使用用户输入的数据得以自动填充。
三 基于模型状态进行客户端校验
常规的校验方法通常是在客户端使用Javascript脚本语言来执行数据输入的验证。有时候,我们还可能需要使用JavaScript在客户端进行业务规则的验证。仅当客户端验证不可靠时,才使用服务器端验证。显然,服务器端验证是被安排在类似C#这样的服务器端语言代码实现中。这种方法存在一个明显的问题是,在一段时间内,我们很有可能需要重复地使用数据以及业务规则验证逻辑等。
为了解决上面这样的问题,我们可以在模型中加入方法,交由它来维护和强制执行所有的验证工作。这样一来,当需要改变验证逻辑时,仅需要在模型部分执行相应的改变逻辑,但却能够自动影响到系统中所有相关内容。
ASP.NET MVC框架提供给我们许多种方案来执行校验逻辑任务,而这些工作仅需要使用我们所熟悉的C#编码即可。这样一来,校验规则被交由HTTP请求管道进行处理,从而使我们可以在客户端向用户提供校验相关的提示信息。
ASP.NET MVC中使用一个临时存储区,命名为“ModelState”,用于存储所有的绑定操作模式期间发生的验证错误。调用方法ModelState.IsValid可以确定模型绑定操作是否发生了任何验证错误。如果有任何错误,则此方法将返回false。另外,ASP.NET MVC框架也提供了相应的机制用于向终端用户展示ModelState中存储的错误信息。为使模型部分(即C#类)使用此功能,模型应该实现IDataErrorInfo接口。
在我们的示例应用程序中,模型“Employee”需要实现如下的接口:
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel;
namespace WizardTypeUITwo.Models
{
[Serializable]
public class Employee : IDataErrorInfo
{
//个人信息
public string Name { get; set; }
public int? Age { get; set; }
//职业信息
public string CurrentEmplrName { get; set; }
public int? Salary { get; set; }
//联系信息
public string Email { get; set; }
public int? Mobile { get; set; }
public string this[string columnName]
{
get
{
if ((columnName == "Name") && string.IsNullOrEmpty(Name))
return "Please enter a name";
if ((columnName == "Age") && !Age.HasValue)
return "Please enter a numeric age";
if ((columnName == "CurrentEmplrName") && string.IsNullOrEmpty(
CurrentEmplrName))
return "Please enter current employer name";
if ((columnName == "Salary") && !Salary.HasValue)
return "Please enter salary";
if ((columnName == "Email") && !IsValidEmailAddress(Email))
return "Please enter a valid email address";
if ((columnName == "Mobile") && !Mobile.HasValue)
return "Please enter mobile number";
return null;
}
}
private static bool IsValidEmailAddress(string email)
{
//请根据您的具体情况进一步改进这里的逻辑
return (email != null) && (email.IndexOf("@") > 0);
}
//遵循默认编译规则
public string Error { get { return null; } }
}
}
在本文雇员注册示例程序中,页面Step1.aspx对应于第一个视图。借助于HTML辅助方法,我们会及时地通知用户相应的验证错误信息。
<label for="Name">Name:</label>
<%= Html.TextBox("Name", Model.Name) %>
<%= Html.ValidationMessage("Name", "*") %>
通过在服务器端执行任何操作之前调用ModelState.IsValid方法,我们可以限制用户进入到系统注册流程中的下一个界面。在本文示例应用程序中,只有在填充适当的信息之后,才允许用户进入到下一个步(页面Step2.aspx)。
四 状态管理问题
正如前面提及的,多步骤web表单应用的一个关键的问题就是用户浏览多个不同的网页时的数据持久性存储问题。借助于ASP.NET MVC框架引入的一个新功能—TempData数据结构,可以存储HTTP重定向中使用的数据。TempData数据结构的一个重要特征是,存储在其中的数据只能在下一次(就此一次)HTTP请求中保持着存储状态。在之后的其他的HTTP请求中,将被自动删除。
在本文雇员注册示例程序中,我们结合使用TempData数据结构和另外两个关键事件—OnActionExecuting和OnResultExecuted共同完成临时存储的任务。
需要明确的是,在每一个行为(Action)方法执行之前将执行OnActionExecuting事件,而OnResultExecuted事件是在每个行为方法执行之后才执行的。
五 解耦流程逻辑
对于一个涉及到多步骤实现用户导航的应用程序(如电子商贸网络应用程序中的从购物车到订单确认程序,一些使用多步骤进行新用户注册导航的网站,以及一些在企业内网中实现的审批程序,等等)来说,可以采用的方案有:或者把工作流逻辑封装在应用程序逻辑内部,或者使之干脆独立出应用程序逻辑。
使用工作流逻辑减小应用程序逻辑中的耦合度将会提供极大的灵活性。通过这种方案,我们可以轻而易举地变更工作流程而不触及修改应用程序代码。这将使应用程序很容易维护。
接下来,我们简单地介绍一下本文示例中引用的几个工作流。
图1 工作流Workflow1
![](http://sy0.img.it168.com/it168_default.png)
在本文雇员注册示例程序中,正常的工作流程顺序包括三个步骤(见下图中的Workflow3)顺序依次为:Step1,Step2,Step 3及最后部分。另外,我们还有两个工作流:Workflow 1,Workflow 2。工作流Workflow 1的执行顺序为:Step1,Step2及最后部分。工作流Workflow 2的执行顺序为:Step1,Step3及最后部分。所有这些工作流都被抽象出来,使用Windows工作流的状态机工作流加以封装。
![](http://sy0.img.it168.com/it168_default.png)
在我们的应用程序中,工作流程中的每一步的命名类似于网页的命名。在工作流程步骤和网页间我们建立了一一对应的关系,这样方便工作流程驱动用户界面导航。实际开发中,不可能总会存在这种一一对应。在这种情况下,我们可以把这些步骤映射到某种配置信息—在配置信息更改网页要容易得多。
六 科学的架构设计带来系统的灵活性
在本文示例程序中,控制器类EmployeeController中定义的方法Process代表了整个应用程序的核心。根据用户行为的不同(不论他是点击“Next”按钮还是点击“Back”按钮),它都能够使用一个工具类WorkflowUtil与工作流进行通信,并且能够确定系统中的相应的导航。
{
if (wrkFlw == null && selectedWrkflw != "")
{
wrkFlw = new WorkflowUtil(selectedWrkflw);
}
if ((nextButton != null) && ModelState.IsValid)
{
page = wrkFlw.MoveNext();
return View(page, employeeObj);
}
else if (backButton != null)
{
ModelState.Clear();
page = wrkFlw.MovePrev();
return View(page, employeeObj);
}
else
return View(page, employeeObj);
}
注意,我们将创建的用于导航的工作流的类型取决于基于配置文件中的具体配置。下面给出了配置信息的示例片断:
<add key="WorkflowType" value="Workflow3"/>
</appSettings>
另外,工具类WorkflowUtil中的StartWorkflow方法负责启动实现各个工作流的任务。下面给出了关键片断代码:
{
case "Workflow1":
wfInstance = _wfRuntime.CreateWorkflow(typeof(EmpRegWorkflow.Workflow1));
break;
case "Workflow2":
wfInstance = _wfRuntime.CreateWorkflow(typeof(EmpRegWorkflow.Workflow2));
break;
case "Workflow3":
wfInstance = _wfRuntime.CreateWorkflow(typeof(EmpRegWorkflow.Workflow3));
break;
default:
wfInstance = _wfRuntime.CreateWorkflow(typeof(EmpRegWorkflow.Workflow3));
break;
}
wfInstance.Start();
显而易见,只需通过改变单一的配置信息—例如,把“Workflow1”改变为“Workflow2”,我们即可以改变雇员注册程序的整个导航路径。
根据当前状态,Windows工作流运行时能够自动把工作流推进到下一个工作流,并且会返回其对应的名称。相应地,再由过程方法将把用户导航到下一个网页。有关Windows工作流基础和事件驱动的活动的详细讨论并非本文的目的,所以我们在此不去赘述,读者可以参考MSDN等内容。
如果不是借助于Windows工作流,那么我们将需要在EmployeeController控制器类中定义更多的方法,以映射到工作流中的不同步骤,于是导致应用程序的逻辑代码零碎而且迅速膨胀,也就相应地导致了难以维护的紧耦合的应用程序。
在我们的示例中,如果我们希望将年龄(Age)字段从步骤1移到步骤二中,这是一个非常简单的事情—“剪切和粘贴”即可。根本不需要你更改应用程序代码,验证代码,及测试等内容—只需要一会儿的功能!
![](http://sy0.img.it168.com/it168_default.png)
也就是说,你只需要把Step1.aspx中的代码片断剪切到页面Step2.aspx中。如下所示:
<label for="Age">Age:</label>
<%= Html.TextBox("Age", Model.Age) %>
<%= Html.ValidationMessage("Age", "*") %>
</p>
而且,上述更改立即在应用程序中起作用,绝不会妨碍到系统流程。这就是灵活性!—这也体现了ASP.NET MVC框架的强大功能。
七 小结
通过有效地组合利用ASP.NET MVC框架和Windows工作流架构,我们可以建立更加灵活的向导型用户界面工作流应用程序。这种设计完全可以满足中小型系统流程变更的要求。有兴趣的读者可以试着把在本文实例中介绍的方法应用于另外一些类似的要求根据用户输入相应变更程序流程的动态工作流应用程序中。
如果我们在建立一个工作流应用程序之初,能够认真地分析业务流程中所有可能的潜在的变化并把这些相应变化转移到Windows工作流中进行管理,那么,系统的灵活性将极大地增强,从而使系统流程的更改不再付出巨大的代价。
最后提醒你的是,ASP.NET MVC框架与Windows工作流架构的组合非常适合于克服UI类型的工作流问题中的低效率问题,但是对于昂贵的BPM(Business Process Management,即业务流程管理系统)应用开发这样的场所,本文中介绍的ASP.NET MVC框架与Windows工作流架构二位一体的组合并不太适合。