使用 SharePoint 2013 工作流服务 JS API





5.00/5 (10投票s)
使用 SharePoint 2013 工作流服务 API 的代码示例。
引言
SharePoint 2013 中包含的工作流服务 API 不仅允许您启动和停止工作流,还可以实际编辑和创建它们、在网站之间复制它们、导出到 WSP、与列表关联等等。此 API 是 SharePoint 客户端对象模型的一部分,因此不仅可以从 C# 访问,还可以从 JavaScript 访问。不幸的是,该 API 的文档并不完善,而且有点麻烦,因此需要一段时间才能弄清楚如何使用它,甚至更长的时间来编写可工作的代码。本文旨在提供 API 用法的现成代码示例(使用 JavaScript),以便开发人员可以专注于实际业务目标,而不是浪费时间在 API 的困难之处。
背景
SharePoint 2013 支持两种类型的工作流:旧的 WF3 工作流(也称为 SharePoint 2010 工作流)和新的 WF4 工作流(也称为 SharePoint 2013 工作流)。工作流服务 API 主要用于处理新的 WF4 工作流,但也包含少量用于处理旧 WF3 工作流的方法。
新的 WF4 工作流在很多方面都更好,但同时也要明白,它们也缺少一些旧工作流拥有的功能。至少有两个重要的限制与 WF4 工作流相关,您可能需要考虑:
- 某些工作流操作,例如“设置审批状态”,对于 WF4 工作流不支持,而在 SP2010 工作流中却工作得很好。请参阅 MSDN 上的完整列表。
- SP2013 工作流无法像 SP2010 工作流那样全局发布。例如,如果您想创建一个工作流并在某个网站集的不同网站中使用它,您必须将其复制到每个网站并单独管理。下面的示例可以帮助自动化此过程。
还有其他区别(例如 SP2013 可重用工作流不能与内容类型关联),但它们不像我上面列出的两个那样关键。
API 本身由 4 个服务组成:
- WorkflowDeploymentService - 提供创建新工作流定义和管理现有工作流定义的方法,例如修改、删除、发布、标记为弃用、导出到 WSP 等。
- WorkflowSubscriptionService - 在这里您可以找到与创建和管理工作流关联相关的方法,即附加和分离工作流与列表。
- WorkflowInstanceService - 使用此服务,您可以启动、取消、暂停、恢复和终止工作流。
- InteropService - 提供调用旧 SP2010 工作流的方法。
示例
示例列表
示例 1:枚举网站的工作流定义
要枚举网站的工作流定义,可以使用 WorkflowDeploymentService 的 enumerateDefinitions 方法。
完整代码
(function () {
// Getting client context and loading the current web
var context = SP.ClientContext.get_current();
var web = context.get_web();
context.load(web);
// Workflow Services API entry point - WorkflowServiceManager object
var servicesManager = SP.WorkflowServices.WorkflowServicesManager.newObject(context, web);
context.load(servicesManager);
var workflowDefinitions = servicesManager.getWorkflowDeploymentService().enumerateDefinitions(false);
context.load(workflowDefinitions);
context.executeQueryAsync(function (sender, args) {
// enumerateDefinition returns ClientCollection object
var definitionsEnum = workflowDefinitions.getEnumerator();
var empty = true;
console.log('Site ' + web.get_url() + ':');
// Going through the definitions
while (definitionsEnum.moveNext()) {
var def = definitionsEnum.get_current();
// Displaying information about this definition - DisplayName and Id
console.log(def.get_displayName() + " (id: " + def.get_id() + ")");
empty = false;
}
if (empty) {
console.log('No 2013 workflows found.');
}
}, errFunc);
// Error handling
function errFunc(sender, args) {
alert("Error occured! " + args.get_message());
};
})();
注意:此代码将不会返回旧的 SharePoint 2010 工作流。
这是我在一个测试网站上执行上述代码的结果:
正如您所看到的,甚至可以直接从控制台运行此代码。在这种情况下,您必须确保页面中包含 "/layouts/15/SP.WorkflowServices.js"。
示例 2:在网站之间复制工作流
此示例展示了如何复制 SP2013 工作流到不同网站。由于 SP2013 可重用工作流无法全局发布(这只能为 SP2010 工作流完成),因此当您想在多个网站中使用相同的工作流时,此代码在许多场景中都可能有用。
以下代码将一个工作流从当前网站复制到其子网站(不是复制到网站集中的所有网站,而只是当前网站的下一级)。
(function () {
// please, provide actual id of the workflow you're going to copy
var myDefinitionId = 'PUT-YOUR-GUID-HERE';
var context = SP.ClientContext.get_current();
// get current web and it's subwebs
var web = context.get_web();
var webs = web.get_webs();
context.load(web);
context.load(webs);
// get WorkflowServiceManager object for the root web
var servicesManager = SP.WorkflowServices.WorkflowServicesManager.newObject(context, web);
context.load(servicesManager);
// load the workflow definition
var workflowDefinition = servicesManager.getWorkflowDeploymentService().getDefinition(myDefinitionId);
context.load(workflowDefinition);
context.executeQueryAsync(function (sender, args) {
// Looks meaningless, but without this "fix" you workflow
// will not have the displayName and it's Id will appear
// instead of the displayName
workflowDefinition.set_displayName(workflowDefinition.get_displayName());
// enumerate through subsites
var websEnum = webs.getEnumerator();
while (websEnum.moveNext()) {
var subweb = websEnum.get_current();
// save a copy of the workflow definition to the subsite
saveWorkflowDefinition(context, subweb, workflowDefinition);
}
}, errFunc);
function errFunc(sender, args) {
alert("Error occured! " + args.get_message());
};
function saveWorkflowDefinition(context, web, myDefinition) {
// we have to repeat the procedure of getting WorkflowServicesManager object because
// it should be retrieved for each web separately
var servicesManager = SP.WorkflowServices.WorkflowServicesManager.newObject(context, web);
context.load(servicesManager);
// Save and publish the definition
servicesManager.getWorkflowDeploymentService().saveDefinition(myDefinition);
servicesManager.getWorkflowDeploymentService().publishDefinition(myDefinition.get_id());
context.executeQueryAsync(function (sender, args) {
console.log('Definition saved to site ' + web.get_url());
}, errFunc);
}
})();
注意:此代码有一个陷阱:在检索到工作流定义并将其保存到其他网站时,必须将显示名称设置为相同的值,否则该定义将没有名称。
我运行了此代码,在当前网站上有一个名为“Reusable workflow test”的测试工作流。当前网站有两个子网站(project1 和 project2),其中没有这样的工作流。
执行结果
在此之后,工作流出现在子网站上。
我已仔细检查:工作流是正确的,并且可以与列表关联。
以防万一,我已将此工作流关联到列表并启动了它,它已成功完成。
示例 3:在网站之间复制工作流,并动态自定义
这基本上与上一个示例相同,唯一的区别是工作流不是“按原样”复制的,而是在过程中进行了修改。
在大多数情况下,这可以通过修改工作流的 XAML 代码来实现,XAML 代码本质上是一段 XML。因此,您可以进行简单的字符串替换即可。
假设您的工作流中有一个“启动任务进程”操作,并且您希望在部署工作流的不同网站上具有不同的参与者。
首先,您应该检索工作流的 XAML 定义(为此请使用 workflowDefinition.get_xaml() 属性),并确定需要更改的位置。在上面的示例中,您会在工作流中找到类似这样的内容:
<local:CompositeTask.AssignedTo>
<InArgument x:TypeArguments="sco:Collection(x:String)">
<local:ExpandInitFormUsers>
<local:ExpandInitFormUsers.Users>
<InArgument x:TypeArguments="sco:Collection(x:String)">
<p:BuildCollection x:TypeArguments="x:String"
Collection="{x:Null}" Result="{x:Null}">
<p:BuildCollection.Values>
<InArgument x:TypeArguments="x:String">i:0#.f|membership
|user@test.onmicrosoft.com</InArgument>
</p:BuildCollection.Values>
</p:BuildCollection>
</InArgument>
</local:ExpandInitFormUsers.Users>
</local:ExpandInitFormUsers>
</InArgument>
</local:CompositeTask.AssignedTo>
显然,您需要将 i:0#.f|membership|user@test.onmicrosoft.com
替换为其他内容,但仅限于此片段的替换范围。
现在,如果您对正则表达式有基本了解,可以使用这个简单的脚本来完成:
var regex = /(<local:CompositeTask.*?<local:CompositeTask.AssignedTo>.*?<InArgument x:TypeArguments="x:String">)([^\<]+)/;
definition.set_xaml(definition.get_xaml().replace(regex, "\\$1i:0#.f|membership|user2@test.onmicrosoft.com");
不可否认,正则表达式因其可读性和可维护性方面的问题而臭名昭著。因此,如果您打算对工作流 XAML 进行一些真正复杂的操作,请考虑使用托管客户端对象模型(即 C#)或至少使用类似 JsXML 这样的库。
示例 4:添加工作流关联(订阅)
您可以将此示例视为对 示例 2:在网站之间复制工作流 的补充,因为很明显,通常在复制工作流后,您很可能会立即将其附加到列表。
代码
(function() {
// please, provide actual id of the workflow you're going to copy
var workflowDefinitionId = 'PUT-YOUR-GUID-HERE';
var listGuid = 'PUT-YOUR-GUID-HERE';
var web = context.get_web();
context.load(web);
// Get WorkflowServicesManager object for the specified web
var servicesManager = SP.WorkflowServices.WorkflowServicesManager.newObject(context, web);
context.load(servicesManager);
// Workflow History list
var historyList = web.get_lists().getByTitle('Workflow History');
context.load(historyList, 'Id');
// Workflow Tasks list
var tasksList = web.get_lists().getByTitle('Tasks');
context.load(tasksList, 'Id');
context.executeQueryAsync(function (sender, args) {
// Creating the subscription
var sub = new SP.WorkflowServices.WorkflowSubscription(context);
sub.set_name('Test subscription');
sub.set_enabled(true);
sub.set_definitionId(workflowDefinitionId);
sub.set_statusFieldName('WF status');
sub.set_eventSourceId(listGuid);
sub.set_eventTypes(["WorkflowStart"]);
// These 3 are MANDATORY! Otherwise the workflow will fail to complete
sub.setProperty("TaskListId", tasksList.get_id().toString());
sub.setProperty("HistoryListId", historyList.get_id().toString());
sub.setProperty("FormData", "");
// Associate the workflow with the list
servicesManager.getSubscriptionService().publishSubscriptionForList(sub, listGuid);
context.executeQueryAsync(function (sender, args) {
console.log('Workflow association has been created successfully. Web: ' + web.get_url());
}, errFunc);
}, errFunc);
})();
注意:此代码的丑陋之处在于,您应该在创建的工作流订阅中添加三个强制性的附加属性(“FormData”、“TaskListId”和“HistoryListId”),否则,即使订阅成功创建,工作流也无法工作(启动后会立即失败)。这些属性没有在任何地方记录,我花了相当长的时间才弄清楚这一点。
示例 5:启动工作流
要启动 SP2013 工作流,您应该使用 WorkflowInstanceService 对象的 startWorkflow 方法。
以下是操作方法(此代码在当前网站的指定列表的第一个项目上启动指定的工作流):
(function () {
var definitionId = 'PUT-YOUR-GUID-HERE';
var listGuid = 'PUT-YOUR-GUID-HERE';
var context = SP.ClientContext.get_current();
var web = context.get_web();
context.load(web);
var list = web.get_lists().getById(listGuid);
context.load(list);
var items = list.getItems(SP.CamlQuery.createAllItemsQuery());
context.load(items);
var servicesManager = SP.WorkflowServices.WorkflowServicesManager.newObject(context, web);
context.load(servicesManager);
var subs =
servicesManager.getWorkflowSubscriptionService().enumerateSubscriptionsByDefinition(definitionId);
context.load(subs);
context.executeQueryAsync(function (sender, args) {
if (resultsElement.innerHTML.indexOf('Loading') !== -1)
resultsElement.innerHTML = '';
var subsEnum = subs.getEnumerator();
while (subsEnum.moveNext()) {
var sub = subsEnum.get_current();
console.log('Web: ' + web.get_url() + ', Subscription: ' +
sub.get_name() + ', id: ' + sub.get_id());
var initiationParams = {};
servicesManager.getWorkflowInstanceService().startWorkflowOnListItem(
sub, items.getItemAtIndex(0).get_id(), initiationParams);
context.executeQueryAsync(function (sender, args) {
console.log('Workflow started.');
}, errFunc);
}
}, errFunc);
function (sender, args) {
alert("Error occured! " + args.get_message() +
'\r\nStack trace: ' + args.get_stackTrace());
}
})();
注意:如果您计划在不同的网站中操作,请确保使用同步的 SP.ClientContext 和 SP.Web 对象,否则工作流将无法启动。
结论
SharePoint 2013 中的工作流服务 API 是一个强大的工具,它不仅可以用于启动/停止工作流等常规任务,还可以用于广泛的维护和自动化目的,包括解决方案升级场景、自动化测试、动态生成工作流等。考虑到 Apps 和客户端对象模型的日益重要性,它对于现代 SharePoint 开发尤其有价值。
请记住,该 API 并不完美,并且有其自身的陷阱!
祝您的解决方案好运!