文档审批工作流系统






4.09/5 (24投票s)
通用的、完整的文档审批工作流系统,使用 Windows Workflow Foundation (WF)。
文章目的
展示工作流基础的快速视图,并强调在创建第一个有用的工作流应用程序之前,我们应该了解的基本知识,
并强调在创建第一个有用的工作流应用程序之前,
以及强调设计指南以创建可重用的工作流应用程序。
最后,通过一个示例项目来体验工作流,该项目通用且适用于任何
文档工作流系统,从客户端应用程序到使用数据库的管理应用程序。
数据库。
工作流基础,简而言之
我们可以简单地将工作流基础看作是活动集合,这些活动可以通过条件和规则共同工作以实现特定目的。
在一起通过条件和规则以实现特定目的。
工作流基础不是带循环块的if else块,如果它以这种简单的算法实现,我们将付出巨大的代价而没有任何好处。使用工作流基础的底线是:
这简单的算法,它将使我们付出巨大的代价而没有任何好处。使用
工作流基础的底线是,简化一个
复杂的算法,变成简单的可视化可编辑、可维护的工作流。
用工作流取代了我的旧设计
我工作中大量使用了命令模式,作为请求的封装在一个
名为“命令”的单一对象中,该对象用参数初始化,
然后会调用execute方法来检索结果,或执行特定任务。
但我总是在共享属性、信息和资源,或者其他命令的结果方面遇到问题,
结果(s)的命令,
一些命令等待其他命令完成,而另一些命令则从其他命令的结束点开始。
其他命令的结束点开始,
为了解决这个问题,我总是使用一个中介,在大多数情况下是一个CommandManager类,它包含一组命令,并且知道所有命令的执行顺序,并通过容器范围共享所有参数,每个命令都有一个指向CommandManager的引用,
CommandManager类,它有一个命令集合,并且知道所有
命令的执行顺序,并通过容器范围共享所有参数,每个命令都有
Command Manager的引用,
因此它可以非常轻松地访问所有其他命令的所有属性,或者为其他命令或外部客户端应用程序设置任何属性。
属性为其他命令或外部客户端应用程序。
最后
为了使设计良好,我总是让所有命令都继承自CommandBase
抽象类,以便为我提供所有命令的契约,这样CommandManager就可以
执行和初始化所有子命令。
有趣的是,
每次我尝试创建命令对象时,我都会发现自己创建了相同的通用模式,并且想知道是否有办法将这些库可视化以更好地使用?
通用模式,并想知道是否有办法将这些库可视化以更好地使用?
当我第一次看到顺序工作流活动时,我说:“啊哈!”
这正是我想要的,命令(活动)有一个容器命令管理器
(SequentialWorkflowActivity),所有命令都具有相同的接口或基类
(Activity),并且所有命令都可以通过联系命令管理器获取数据。
顺序工作流
最后,将所有内容可视化,以便快速开发和更好地维护,这真是太棒了。
太棒了。
工作流基础的什么地方给我留下了深刻印象?
当然,工作流基础中的一切都给我留下了深刻印象,但是运行时添加服务的理念,然后通过引用服务在工作流中使用此服务
在运行时,然后通过引用该服务
使用WorkflowRuntime对象的公共方法GetService,或通过特殊的
活动,如CallExternalMethodActivity、HandleExternalEventActivity,这是一个
很棒的主意,
因为您只需将所有抽象类和接口分离到一个单独的程序集,然后让工作流和核心应用程序引用该程序集,从而提高了工作流的可重用性,因为工作流是根据接口编写的,但您可以在运行时注册任何实现此接口的具体类。
单独的程序集,然后让工作流和核心应用程序引用该
程序集,这样你就提高了工作流的可重用性,因为工作流是
根据接口(s)编码的,但你可以在运行时注册任何具体的类,
实现了这个接口,
你可以将 .Net 2 中引入的奇妙Provider 模式与奇妙的工作流基础集成,通过在 web.config 的 workflow 部分注册任何服务(这个话题超出了这个简单项目)。一旦你理解了如何在工作流中使用自定义服务,你就会理解所有
美妙的工作流基础,通过在 web.config 中注册任何服务,在工作流
部分(这个话题超出了这个简单项目)。一旦你理解了如何使用
工作流中的自定义服务,你将理解所有
其他服务,包括“开箱即用”服务,如 WorkflowPersistenceService、
WorkflowSchedulerService、TrackingService 等。
你应该知道的基础知识
在开发第一个实际应用程序之前,
正如我所提到的,将工作流用作一堆“if else”语句,根本不是一个好主意,并且您应该知道关于工作流的最低限度信息。您应该知道:
根本不是一个好主意,并且您应该知道关于工作流的最低限度信息。您应该知道
• 如何将参数传递给工作流,以及在工作流完成后检索参数。
已完成。
• 如何创建自定义活动以完成特定任务。
• 如何使用自定义服务,以及如何在工作流内部使用该服务。
• 如何在工作流处理过程中将数据从主机应用程序传递到工作流,或从工作流传递到应用程序。我认为这是您创建有用的工作流应用程序所应了解的最低限度。对于工作流的高级工作,您可能需要了解:
通过工作流处理。我认为这是您创建有用的工作流应用程序所应了解的最低限度。对于工作流的高级工作,您可能需要了解,
有用的工作流应用程序。对于工作流的高级工作,您可能需要现在知道,
• 使用 WorkflowPersistenceService,以及加载和卸载到数据库,
• 使用事务范围实现数据一致性。
• 创建错误处理程序,以防工作流内部发生错误。是的,关于工作流要说的很多,特别是如果您在 Asp.NET 应用程序或 Web 服务中使用工作流,
提到,工作流,特别是如果您在 Asp.NET 应用程序或
在 webservice 中,
或者您考虑在工作流完成后更新简单的 Windows 窗体
或在特定事件中。但这并非本文目的。
使用工作流基础的设计指南
我尝试将工作流应用于我以前使用我的面向对象编程技能编写的对象,但我发现如果不对我的代码进行任何准备,就很难使用工作流,就像我第一次尝试使用单元测试来测试“不可测试”的应用程序一样。
在面向对象编程中,使用工作流,但我发现很难使用
工作流,无需对我的代码进行任何准备,就像我第一次尝试使用单元
测试来测试“不可测试”的应用程序一样。
我发现我必须通过依赖注入、构造函数注入和模拟对象来使我的代码可测试,
构造函数注入和模拟对象,
现在,要使用工作流,您必须改变您的开发方式。因此,您应该将对象行为从具体类转移到服务,而不是 100% 使用面向对象编程。
100% 的面向对象编程,您应该将对象行为从具体的
类转移到服务,
所以你的代码将更面向服务
在使用工作流之前准备代码的步骤
基本 3 步
1) 将所有对象行为分离到服务中。
2) 提取这些服务的接口(在 Visual Studio 5 中使用重构),因此所有
您的服务都应实现服务接口。
3) 将这些接口分离到单独的程序集中。
考虑这个简单的例子
假设你总是使用一个名为
客户的类,拥有以下属性:
ID, 姓名, 地址,
并具有以下方法:
Add()、Update()、Delete()、Attach()、Register()、Unregister() 等。
要使用工作流,您应该将此对象的状态和标识(如 ID、姓名、地址)分离,并将行为(公共方法、事件等)移动到实现 ICustomerService 的 CustomerService 中。
地址并将行为(公共方法、事件等)移动到CustomerService,它
实现了ICustomerService。
在这种情况下,您可以非常轻松地将工作流与您准备好的代码一起使用。
在源示例中,
您将看到Document对象继承自IDocumentService,它们都用于描述整个文档处理过程。
它们都用于描述整个文档流程
示例项目简述
该解决方案示例包含 3 个基本项目以及另外 2 个用于通用对象和接口的项目。
接口。
该应用程序的理念是
用户将通过其客户端应用程序创建文档,
只要文档处于打开状态,用户仍可更新其文档。
然后,用户将文档踢入工作流,然后根据文档属性,工作流将选择被委托批准该文档的管理员,并将数据保存到数据库中,然后等待管理员完成整个流程。
属性,工作流将选择被委派批准该文档的管理员,
并将数据保存到数据库,然后等待管理员
完成循环。
管理员在登录界面(示例无需密码)后,将看到所有
正在等待他处理的文档,
然后他将批准或拒绝该文档,
因此,文档将被标记为已批准或重新打开状态,以便用户可以
更新文档、删除文档或再次提交到工作流,
客户端应用示例
在主表单中创建文档
private void btnCreateDocument_Click(object sender, EventArgs e) { if (ValidateForm()) { Document _document = new Document(); _document.Category = (Category)this.cmbCategory.SelectedItem; _document.Title = this.txtTitle.Text.Trim(); _document.Description = this.txtDescription.Text.Trim(); _document.Depositor = User.CurrentUser.UserName; _service.CreateDocument(_document); ConfirmSuccess(); } else { MessageBox.Show("Please complete, the form's fields", "Validation, Error", MessageBoxButtons.OK , MessageBoxIcon.Error); } }
IDocument 服务类图

文档数据库图

文档服务类图

[Serializable] public class DocumentService : IDocumentService { public Document GetDocumentByID(int id) { return DocumentDalc.GetDocumentByID(id); } public Documents GetCreatedDocuments(string depositor, Status status) { return DocumentDalc.GetAllCreatedDocuments(depositor, status); } public Documents GetCreatedDocuments(string depositor) { return DocumentDalc.GetAllCreatedDocuments(depositor); } public Documents GetAllNeedToApproveDocuments(string userName) { return DocumentDalc.GetAllNeedToApproveDocuments(userName); } public DocumentWorkflowItem GetWorkflowItem(Document document) { DocumentWorkflowItem wfItem = new DocumentWorkflowItem(document); return wfItem; } #region IDocumentService Members public void CreateDocument(Document document) { DocumentDalc.CreateDocumentRecord(document); if (this.Created != null) Created.Invoke(this, new DocumentEventArguments(document)); } public void UpdateDocument(Document document) { DocumentDalc.UpdateDocumentRecord(document); if (this.Updated != null) Updated.Invoke(this, new DocumentEventArguments(document)); } public void ArchiveDocument(Document document) { document.Status = Status.Archived; DocumentDalc.UpdateDocumentRecord(document); if (this.Archived != null) Archived.Invoke(this, new DocumentEventArguments(document)); } public void DeleteDocument(Document document) { document.Status = Status.Deleted; DocumentDalc.UpdateDocumentRecord(document); if (this.Deleted != null) Deleted.Invoke(this, new DocumentEventArguments(document)); } public void CreateInWorkflowDocumentForUser(Document document, string approvalUser, Guid workflowID) { DocumentDalc.CreateInWorkflowDocumentRecordForUser(document, workflowID, approvalUser); if (this.SentToWorkflow != null) SentToWorkflow.Invoke(this, new DocumentEventArguments(document)); } public void CreateInWorkflowDocumentForGroup(Document document, string approvalGroup, Guid workflowID) { DocumentDalc.CreateInWorkflowDocumentRecordForGroup(document, workflowID, approvalGroup); if (this.SentToWorkflow != null) SentToWorkflow.Invoke(this, new DocumentEventArguments(document)); } public void ApproveDocumentWorkflow(Document document, string approvalUser, Guid workflowID) { DocumentDalc.CreateApprovedDocumentRecordForUser(document, workflowID, approvalUser); if (this.Approved != null) Approved.Invoke(this, new DocumentEventArguments(document)); } public void RejectDocumentWorkflow(Document document, string approvalUser, Guid workflowID) { DocumentDalc.CreateRejectedDocumentRecordForUser(document, workflowID, approvalUser); if (this.Rejected != null) Rejected.Invoke(this, new DocumentEventArguments(document)); } #endregion #region IDocumentService Members public event EventHandler<externalexternaldocumenteventargument __designer:dtid="1688849860263990" /> RaiseApproveDocumentWorkflowEvent; public event EventHandler<externalexternaldocumenteventargument /> RaiseRejectDocumentWorkflowEvent; #endregion public void RaiseApproveDocumentWorkflow(DocumentWorkflowItem wfItem) { ExternalExternalDocumentEventArgument arg = new ExternalExternalDocumentEventArgument(wfItem.WorkflowID, wfItem.Document, User.CurrentUser); if (RaiseApproveDocumentWorkflowEvent != null) { RaiseApproveDocumentWorkflowEvent.Invoke(this, arg); } } public void RaiseRejectDocumentWorkflow(DocumentWorkflowItem wfItem) { ExternalExternalDocumentEventArgument arg = new ExternalExternalDocumentEventArgument(wfItem.WorkflowID, wfItem.Document, User.CurrentUser); if (this.RaiseRejectDocumentWorkflowEvent != null) { RaiseRejectDocumentWorkflowEvent.Invoke(this, arg); } } #region IDocumentService Members public event EventHandler<documenteventarguments /> Created; public event EventHandler<documenteventarguments /> Updated; public event EventHandler<documenteventarguments /> Archived; public event EventHandler<documenteventarguments /> Deleted; public event EventHandler<documenteventarguments /> SentToWorkflow; public event EventHandler<documenteventarguments /> Approved; public event EventHandler<documenteventarguments /> Rejected; #endregion }
管理应用程序程序

private void approveToolStripMenuItem_Click(object sender, EventArgs e) { _documentID = int.Parse(dgrdDocuments.CurrentRow.Cells["ID"].Value.ToString()); DialogResult result = MessageBox.Show("Are you sure that you want to Approve this document", "confirmation", MessageBoxButtons.YesNo, MessageBoxIcon.Question); Document doc = null; if (result == DialogResult.Yes) { doc = _service.GetDocumentByID(this._documentID); if (doc.Status != DocumentBaseLibrary.Status.InWorkflow) { MessageBox.Show(string.Format("This document {0} is not in Workflow state", doc), "Error", MessageBoxButtons.OK , MessageBoxIcon.Error); return; } Approve(doc); } } private void Approve(Document doc) { DocumentWorkflowItem wfItem=_service.GetWorkflowItem(doc); // Get the type of the workflow Type type = typeof(DocumentWorkflow.DocumentWorkflow); // Start the workflow instance Guid id = wfItem.WorkflowID; try { WorkflowInstance inst = theWorkflowRuntime.GetWorkflow(id); inst.Load(); this._service.RaiseApproveDocumentWorkflow(wfItem); } catch(Exception ex) { MessageBox.Show("The Document cannot be loaded. " + ex.ToString(), "Administration", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); } }
结论
工作流基础,它完全消除了应用程序规则和决策的所有复杂性
应用规则和决策的复杂性。
但是,为了实现最大的可重用性,您必须
通过创建服务,并分离抽象服务到单独的程序集来准备您的代码。
程序集