在 Microsoft Dynamics 365 for Finance and Operations (D365 F&O) 中,工作流是定义业务流程自动化、文档审批和流转路径的核心机制。
本指南将详细演示如何为一个名为 Demo_WFDocument 的自定义文档表创建一个完整的工作流。
重要提示: 本教程假定你在与文档表相同的模型中创建工作流。如果跨模型操作,请注意,截至本文撰写时,某些步骤(特别是覆写 canSubmitToWorkflow() 方法)可能需要进行覆盖 (Overlaying) 操作。
让我们开始吧!
第 1 步:定义工作流状态
首先,我们需要一个基础枚举来定义文档在工作流中的各个状态。
- 创建一个新的基础枚举,例如
Demo_WFDocumentStatus。 - 为其添加状态值,例如:
Draft(草稿),Submitted(已提交),ChangeRequest(请求更改),Rejected(已拒绝),Approved(已批准)。

接下来,在你的文档表 (Demo_WFDocument) 上:
- 添加一个新字段,使用刚刚创建的
Demo_WFDocumentStatus枚举作为类型。 - 将此字段设置为只读 (Read-Only),因为它的值将由工作流系统自动更新。
- (可选) 在文档对应的窗体 (Form) 上添加这个新字段,以便用户可以直观地看到当前状态。

第 2 步:实现核心表逻辑
我们需要在文档表 (Demo_WFDocument) 上实现一些关键方法:
-
覆写
canSubmitToWorkflow()方法: 这个方法决定了一个记录是否满足提交到工作流的条件。在这里,我们设定只有当WorkflowStatus字段为Draft时才允许提交。
public boolean canSubmitToWorkflow(str _workflowType = '') { boolean ret = super(_workflowType); // 只有草稿状态的文档才能提交 ret = this.WorkflowStatus == Demo_WFDocumentStatus::Draft; return ret; }
- (可选) 添加状态更新辅助方法: 为了方便起见,可以添加一个静态方法,允许通过记录 ID 来更新工作流状态。这在实现其他业务逻辑时可能很有用。
public static void updateWorkflowStatus(RecId _documentRecId, Demo_WFDocumentStatus _status) { ttsbegin; Demo_WFDocument document; update_recordset document setting WorkflowStatus = _status where document.RecId == _documentRecId; ttscommit; }
第 3 步:创建数据查询
工作流需要一个查询 (Query) 来定义它将使用哪些表和字段。
- 创建一个新的查询。
- 将你的文档表 (
Demo_WFDocument) 添加为数据源。 -
关键: 将查询的
Dynamic Fields属性设置为Yes,以确保所有表字段都包含在内。

第 4 步:定义工作流类别
工作流类别 (Workflow Category) 决定了你创建的工作流将出现在 D365 F&O 的哪个模块下。
- 你可以使用现有的类别,但通常建议为新功能创建一个新的类别。
- 创建新的工作流类别,并设置其
Module属性(例如,本例中设置为车队管理FleetManagement)。

提示: 如果为新项目创建工作流,可能需要扩展 Module 属性背后的基础枚举,并确保存在相应的工作流配置窗体。这个过程相对简单,可以参考 这篇相关文章(注意:链接来自原文)。
第 5 步:构建工作流类型
工作流类型 (Workflow Type) 是核心,它描述了工作流的特性和可用的元素。Visual Studio 提供了一个向导来简化创建过程:

- Category: 选择上一步创建的工作流类别。
- Query: 选择为文档表创建的查询。
- Document menu item: 选择文档表对应窗体的菜单项 (Menu Item)。
完成向导后,项目中会生成几个关键对象:

- Workflow Type 本身: 主要关注其属性,特别是标签 (Label) 和帮助文本 (Help Text),确保它们清晰有意义。其他属性通常指向事件处理程序。
-
Document 类 (
...Document): 通常无需修改,它实现了返回关联查询的方法。 -
EventHandler 类 (
...EventHandler): 处理工作流类型级别的事件(稍后会添加代码)。 -
SubmitManager 类 (
...SubmitManager): 处理文档提交操作。我们将用一个更通用的类替换它。 - CancelMenuItem (Action Menu Item): 用于取消文档工作流,更新其标签和帮助文本。
-
SubmitMenuItem (Action Menu Item): 用于提交文档到工作流,默认调用
SubmitManager。我们需要修改它指向我们新的提交管理器类。 更新其标签和帮助文本。
优化提交逻辑:使用通用的 Submission Manager
/// <summary> /// The Demo_FAAcceptanceWorkflowSubmitManager menu item action event handler. /// </summary> public class Demo_FAAcceptanceWorkflowSubmitManager { private Demo_FAAcceptanceFormTable document; private WorkflowVersionTable versionTable; private WorkflowComment comment; private WorkflowWorkItemTable workItem; private SysUserId userId; private boolean isSubmission; private WorkflowTypeName workflowType; /// <summary> /// Main method /// </summary> /// <param name = "_args">calling arguments</param> public static void main(Args _args) { if (_args.record().TableId != tableNum(Demo_FAAcceptanceFormTable)) { throw error('@DemoZ:DemoZ0476'); } Demo_FAAcceptanceFormTable document = _args.record(); FormRun caller = _args.caller() as FormRun; boolean isSubmission = _args.parmEnum(); MenuItemName menuItem = _args.menuItemName(); Demo_FAAcceptanceWorkflowSubmitManager manager = Demo_FAAcceptanceWorkflowSubmitManager::construct(); manager.init(document, isSubmission, caller.getActiveWorkflowConfiguration(), caller.getActiveWorkflowWorkItem()); if (manager.openSubmitDialog(menuItem)) { manager.performSubmit(menuItem); } caller.updateWorkflowControls(); FormDataSource Demo_HRMEmployeeRequest_DS; Demo_HRMEmployeeRequest_DS = FormDataUtil::getFormDataSource(document); if (Demo_HRMEmployeeRequest_DS) { Demo_HRMEmployeeRequest_DS.research(true); Demo_HRMEmployeeRequest_DS.refresh(); } } /// <summary> /// Construct method /// </summary> /// <returns>new instance of submission manager</returns> public static Demo_FAAcceptanceWorkflowSubmitManager construct() { return new Demo_FAAcceptanceWorkflowSubmitManager(); } /// <summary> /// parameter method for document /// </summary> /// <param name = "_document">new document value</param> /// <returns>current document</returns> public Demo_FAAcceptanceFormTable parmDocument(Demo_FAAcceptanceFormTable _document = document) { document = _document; return document; } /// <summary> /// parameter method for version /// </summary> /// <param name = "_versionTable">new version table value</param> /// <returns>current version table</returns> public WorkflowVersionTable parmVersionTable(WorkflowVersionTable _versionTable = versionTable) { versionTable = _versionTable; return versionTable; } /// <summary> /// parameter method for comment /// </summary> /// <param name = "_comment">new comment value</param> /// <returns>current comment value</returns> public WorkflowComment parmComment(WorkflowComment _comment = comment) { comment = _comment; return comment; } /// <summary> /// parameter method for work item /// </summary> /// <param name = "_workItem">new work item value</param> /// <returns>current work item value</returns> public WorkflowWorkItemTable parmWorkItem(WorkflowWorkItemTable _workItem = workItem) { workItem = _workItem; return workItem; } /// <summary> /// parameter method for user /// </summary> /// <param name = "_userId">new user value</param> /// <returns>current user value</returns> public SysUserId parmUserId(SysUserId _userId = userId) { userId = _userId; return userId; } /// <summary> /// parameter method for isSubmission flag /// </summary> /// <param name = "_isSubmission">flag value</param> /// <returns>current flag value</returns> public boolean parmIsSubmission(boolean _isSubmission = isSubmission) { isSubmission = _isSubmission; return isSubmission; } /// <summary> /// parameter method for workflow type /// </summary> /// <param name = "_workflowType">new workflow type value</param> /// <returns>current workflow type</returns> public WorkflowTypeName parmWorkflowType(WorkflowTypeName _workflowType = workflowType) { workflowType = _workflowType; return workflowType; } /// <summary> /// Opens the submit dialog and returns result /// </summary> /// <returns>true if dialog closed okay</returns> protected boolean openSubmitDialog(MenuItemName _menuItemName) { if (isSubmission) { return this.openSubmitDialogSubmit(); } else { return this.openSubmitDialogResubmit(_menuItemName); } } /// <summary> /// Open submission dialog /// </summary> /// <returns>true if dialog closed okay</returns> private boolean openSubmitDialogSubmit() { WorkflowSubmitDialog submitDialog = WorkflowSubmitDialog::construct(this.parmVersionTable()); submitDialog.run(); this.parmComment(submitDialog.parmWorkflowComment()); return submitDialog.parmIsClosedOK(); } /// <summary> /// Open resubmit dialog /// </summary> /// <returns>true if dialog closed okay</returns> private boolean openSubmitDialogResubmit(MenuItemName _menuItemName) { WorkflowWorkItemActionDialog actionDialog = WorkflowWorkItemActionDialog::construct(workItem, WorkflowWorkItemActionType::Resubmit, new MenuFunction(_menuItemName, MenuItemType::Action)); actionDialog.run(); this.parmComment(actionDialog.parmWorkflowComment()); this.parmUserId(actionDialog.parmTargetUser()); return actionDialog.parmIsClosedOK(); } /// <summary> /// initializes manager /// </summary> /// <param name = "_document">document</param> /// <param name = "_menuItem">calling menu item</param> /// <param name = "_versionTable">workflow version</param> /// <param name = "_workItem">workflow item</param> protected void init(Demo_FAAcceptanceFormTable _document, boolean _isSubmission, WorkflowVersionTable _versionTable, WorkflowWorkitemTable _workItem) { this.parmDocument(_document); this.parmIsSubmission(_isSubmission); this.parmVersionTable(_versionTable); this.parmWorkItem(_workItem); this.parmWorkflowType(this.parmVersionTable().WorkflowTable().TemplateName); } /// <summary> /// perform workflow submission /// </summary> protected void performSubmit(MenuItemName _menuItemName) { if (isSubmission) { this.performSubmitSubmit(); } else { this.performSubmitResubmit(_menuItemName); } } /// <summary> /// perform workflow submit /// </summary> private void performSubmitSubmit() { if (this.parmWorkflowType() && Demo_FAAcceptanceFormTable::findRecId(document.RecId).WorkflowState == Demo_FAAcceptanceWorkflowState::NotSubmitted) { Workflow::activateFromWorkflowType(workflowType, document.RecId, comment, NoYes::No); Demo_FAAcceptanceFormTable::UpdateWorkflowState(document.RecId, Demo_FAAcceptanceWorkflowState::Submitted); } } /// <summary> /// perform workflow resubmit /// </summary> private void performSubmitResubmit(MenuItemName _menuItemName) { if (this.parmWorkItem()) { WorkflowWorkItemActionManager::dispatchWorkItemAction(workItem, comment, userId, WorkflowWorkItemActionType::Resubmit, _menuItemName); Demo_FAAcceptanceFormTable::updateWorkflowState(document.RecId, Demo_FAAcceptanceWorkflowState::Submitted); } } }
重要配置:
- 修改向导生成的
SubmitManager类代码为上述代码。 - 修改
SubmitMenuItem操作菜单项:- 将其
Object Type设置为Class。 - 将其
Object设置为我们新创建的Demo_FAAcceptanceWorkflowSubmitManagerr类名。 - 设置其
Enum Type Parameter为NoYes。 - 设置其
Enum Parameter为Yes(表示这是初始提交)。
- 将其
- 任何用于重新提交的操作菜单项(例如稍后创建的审批拒绝后的重新提交按钮)应将
Enum Parameter设置为No。

第 6 步:处理工作流类型事件
在工作流类型向导生成的 EventHandler 类中,我们需要添加代码来响应关键的工作流生命周期事件:
/// <summary> /// The Demo_FAAcceptanceWorkflowEventHandler workflow event handler. /// </summary> public class Demo_FAAcceptanceWorkflowEventHandler implements WorkflowCanceledEventHandler, WorkflowCompletedEventHandler, WorkflowStartedEventHandler { public void started(WorkflowEventArgs _workflowEventArgs) { RecId documentRecId = _workflowEventArgs.parmWorkflowContext().parmRecId(); Demo_FAAcceptanceFormTable::UpdateWorkflowState(documentRecId, Demo_FAAcceptanceWorkflowState::Submitted); } public void canceled(WorkflowEventArgs _workflowEventArgs) { RecId documentRecId = _workflowEventArgs.parmWorkflowContext().parmRecId(); Demo_FAAcceptanceFormTable::UpdateWorkflowState(documentRecId, Demo_FAAcceptanceWorkflowState::NotSubmitted); } public void completed(WorkflowEventArgs _workflowEventArgs) { RecId documentRecId = _workflowEventArgs.parmWorkflowContext().parmRecId(); Demo_FAAcceptanceFormTable::UpdateWorkflowState(documentRecId, Demo_FAAcceptanceWorkflowState::Completed); } }
第 7 步:创建工作流审批元素
审批 (Approval) 是工作流中常见的元素,允许用户对文档进行批准或拒绝。同样,可以使用向导来创建:
注意: 在运行此向导之前,可能需要先编译你的项目,以确保之前创建的类(如 Document 类)可供选择。

-
Workflow document: 选择工作流类型向导创建的
...Document类。 - Document preview field group: 选择文档表中用于在工作流历史记录中显示标识信息的字段组。
- Document menu item: 再次选择文档表对应窗体的菜单项。
此向导也会生成一系列对象:

-
EventHandler 类 (
...EventHandler): 处理审批特定事件(稍后添加代码)。 -
ResubmitActionMgr 类: 处理重新提交。我们将配置相应的菜单项以使用我们通用的
Demo_WFDocumentSubmitManager类。 - Approve (Action Menu Item): 用于批准操作,更新标签/帮助文本。
- DelegateMenuItem (Action Menu Item): 用于委派操作,更新标签/帮助文本。
- Reject (Action Menu Item): 用于拒绝操作,更新标签/帮助文本。
- RequestChange (Action Menu Item): 用于请求更改操作,更新标签/帮助文本。
- ResubmitMenuItem (Action Menu Item): 用于重新提交。我们需要修改它以使用通用的提交管理器。
配置 ResubmitMenuItem:
- 将其
Object Type设置为Class。 - 将其
Object设置为之前创建的类SubmitManager。 - 设置其
Enum Type Parameter为NoYes。 - 设置其
Enum Parameter为No(表示这是重新提交)。 -
删除向导生成的
ResubmitActionMgr类。

第 8 步:处理工作流审批事件
在审批元素向导生成的 EventHandler 类中,添加处理审批结果的代码。根据原文示例,这里在取消和完成时都更新为 Rejected(请根据你的实际业务逻辑调整):
/// <summary> /// The OXC_FAAcceptanceWorkflowApprovalEventHandler workflow outcome event handler. /// </summary> public final class OXC_FAAcceptanceWorkflowApprovalEventHandler implements WorkflowElementCanceledEventHandler, WorkflowElemChangeRequestedEventHandler, WorkflowElementCompletedEventHandler, WorkflowElementReturnedEventHandler, WorkflowElementStartedEventHandler, WorkflowElementDeniedEventHandler, WorkflowWorkItemsCreatedEventHandler { public void started(WorkflowElementEventArgs _workflowElementEventArgs) { } public void canceled(WorkflowElementEventArgs _workflowElementEventArgs) { RecId documentRecId = _workflowElementEventArgs.parmWorkflowContext().parmRecId(); OXC_FAAcceptanceFormTable::UpdateWorkflowState(documentRecId, OXC_FAAcceptanceWorkflowState::NotSubmitted); } public void completed(WorkflowElementEventArgs _workflowElementEventArgs) { RecId documentRecId = _workflowElementEventArgs.parmWorkflowContext().parmRecId(); OXC_FAAcceptanceFormTable::UpdateWorkflowState(documentRecId, OXC_FAAcceptanceWorkflowState::Approved); } public void denied(WorkflowElementEventArgs _workflowElementEventArgs) { RecId documentRecId = _workflowElementEventArgs.parmWorkflowContext().parmRecId(); OXC_FAAcceptanceFormTable::UpdateWorkflowState(documentRecId, OXC_FAAcceptanceWorkflowState::Rejected); } public void changeRequested(WorkflowElementEventArgs _workflowElementEventArgs) { RecId documentRecId = _workflowElementEventArgs.parmWorkflowContext().parmRecId(); OXC_FAAcceptanceFormTable::UpdateWorkflowState(documentRecId, OXC_FAAcceptanceWorkflowState::ChangeRequest); } public void returned(WorkflowElementEventArgs _workflowElementEventArgs) { RecId documentRecId = _workflowElementEventArgs.parmWorkflowContext().parmRecId(); OXC_FAAcceptanceFormTable::UpdateWorkflowState(documentRecId, OXC_FAAcceptanceWorkflowState::Rejected); } public void created(WorkflowWorkItemsEventArgs _workflowWorkItemsEventArgs) { // TODO: Write code to execute once work items are created. } }
第 9 步:将审批元素添加到工作流类型
回到你的工作流类型定义,在 AOT (Application Object Tree) 中找到 Supported Element Types 节点。
- 创建一个新的节点,引用你刚刚创建的工作流审批元素。
- 设置其
Name和ElementType属性,指向审批元素的名称。

第 10 步:在窗体上启用工作流
最后一步是在文档表对应的窗体上启用工作流支持:
- 打开窗体设计器。
- 选中代表主数据源 (你的
Demo_WFDocument表) 的Design节点。 - 在属性窗口中,设置以下属性:
-
Workflow Datasource: 设置为你的文档表数据源名称。 -
Workflow Enabled: 设置为Yes。 -
Workflow Type: 选择你创建的工作流类型名称。
-

第 11 步:配置和激活实际的工作流
完成以上所有开发步骤并成功编译项目后,你需要在 D365 F&O 前端配置并激活工作流:
- 导航到你在工作流类别中指定的模块(本例中是车队管理)。
- 找到工作流配置相关的菜单项 (通常在 “设置” > “工作流” 下)。
- 点击 “新建”,从列表中选择你创建的工作流类型。
- 系统可能会提示输入凭据,然后会打开工作流编辑器。
- 在编辑器中:
- 从左侧工具箱将你创建的 “审批” 元素拖拽到画布上。
- 将 “开始” 节点连接到 “审批” 元素。
- 将 “审批” 元素连接到 “结束” 节点。
- 配置审批元素的属性(例如,分配给谁、完成策略等)。
- 解决编辑器底部显示的所有警告或错误。
- 点击 “保存并关闭”。
- 在弹出的对话框中选择 “激活新版本”。
现在,当你创建或查看 Demo_WFDocument 表的记录时,窗体的菜单栏上应该会出现工作流相关的按钮了!
