本系列介绍了由 Elsa 工作流引擎驱动的用户界面的实现。 在这一部分中,我们将进一步研究引擎如何成为 UI 的驱动力。 这个想法是工作流可以规定 UI 应提供哪些数据条目。
在上一部分 - 添加从用户任务返回数据的功能中,这是一个可以输入数据的简单工作流程列表。 在这一部分中,它将进一步扩展。
我们将引入一个显示用户任务详细信息的新页面。 用户任务屏幕可以根据用户任务的需要进行完全定制。
@page "/WorkflowInstances/{workflowinstanceid}"
@using ElsaDrivenWebApp.Shared.Components
@inject UsertaskService userTaskService
@inject ProcessService processService
@if (usertask != null)
{
switch (usertask.Signal)
{
case "usertasksample2":
<Usertasksample2 Task=usertask OnFinished="TaskFinished"></Usertasksample2>
break;
case "usertasksample2a":
<Usertasksample2a Task=usertask OnFinished="TaskFinished"></Usertasksample2a>
break;
default:
<button @onclick="() => SendSignal(usertask)">Finalize @usertask.TaskTitle</button>
break;
}
}
@code {
[Parameter]
public string workflowinstanceid { get; set; } = string.Empty;
private UsertaskViewModel? usertask;
protected async override Task OnParametersSetAsync()
{
await LoadTask();
}
private async Task LoadTask()
{
var workflowInstance = await userTaskService.GetUsertasksFor(workflowinstanceid);
usertask = workflowInstance?.UserTasks?.FirstOrDefault();
}
private async Task SendSignal(UsertaskViewModel task)
{
await userTaskService.MarkAsCompleteAsync(task.WorkflowInstanceId, task.Signal, null);
await LoadTask();
}
private async Task TaskFinished()
{
await LoadTask();
}
}
此实现与之前的版本没有太大不同。 唯一的区别是有一个特定的页面用于处理用户任务。
根据信号名称选择组件。 它将获取数据并将其返回给引擎。
如果信号未知,则回退方案只是发送完整的信号而不发送任何数据。
此信号和 UI 耦合显示了当前实现的问题之一。 引擎的配置和 UI 之间存在紧密耦合。 引擎决定信号并假设 UI 将按预期提供数据。 UI考虑到在某些情况下,它可以在不发送任何数据的情况下完成任务。
这种耦合并不是什么新鲜事。 大多数情况下,UI 和后端之间会存在紧密耦合。 如果您了解这一点,则可以设置部署它们的 DevOps 环境。
在用户任务活动的实现中,有更多的属性我们到现在为止还没有使用过。
[ActivityInput(
Hint = "The definition of the data expected to be returned",
UIHint = ActivityInputUIHints.MultiLine,
Category = "Task",
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string UIDefinition { get; set; }
UIDefinition
该属性允许配置用户任务保存 UI 定义。 这是 UI 工作流程的建议。 UI 是否遵守它不是工作流程关心的问题。
在当前的实现中,UI-Definition 由dynamicUserTaskComponent
使用。 该组件利用动态 Blazor 表单的功能。
Dynamic Blazor Form
是一个实验性组件,可创建基于 JSON 结构的表单。 表单中的输入会生成 JSON 对象。
(稍后我也会写一系列关于这个组件的文档)
在下一个代码片段中可以看到 UI 定义的示例。
{
"groups": [
{
"name": "demo",
"layoutHint": "",
"subGroups": [
{
"layoutHint": "",
"index": 1,
"items": [
{
"index": 1,
"span": 4,
"path": "$.number",
"typeName": "NumberInput",
"layoutHint": "",
"text": "number",
"groups": [
],
"customData": {
}
},
{
"index": 2,
"span": 8,
"path": "$.myText",
"typeName": "TextInput",
"layoutHint": "",
"text": "initial text",
"groups": [
],
"customData": {
}
}
]
},
{
"layoutHint": "",
"index": 2,
"items": [
{
"index": 1,
"span": 4,
"path": "$.datefield",
"typeName": "DateInput",
"layoutHint": "",
"text": "date",
"groups": [
],
"customData": {
}
},
{
"index": 2,
"span": 8,
"path": "$.otherText",
"typeName": "TextInput",
"layoutHint": "",
"text": "next text",
"groups": [
],
"customData": {
}
}
]
}
]
}
],
"title": "random text"
}
包含建议的 UI 结构的 Json 被转换为表单。 JSONPath — XPath for JSON (goessner.net) 表示法确定每个元素结果的结构。
DynamicUserTask
@inject UsertaskService userTaskService
@if(Usertask !=null)
{
<DynamicForm @ref="df" Layout="@(GetLayout())" @bind-Value=taskData></DynamicForm>
<div class="row">
<div class="col-md-12 text-right">
<button @onclick="() => SendSignalAndData()">Continue</button>
</div>
</div>
}
@code {
[Parameter]
public UsertaskViewModel? Usertask { get; set; }
[Parameter]
public EventCallback OnFinished { get; set; }
private JToken taskData { get; set; } = new JObject();
private DynamicForm df { get; set; }
private DynamicLayout? GetLayout()
{
return JsonConvert.DeserializeObject<DynamicLayout>(Usertask.UIDefinition);
}
private async Task SendSignalAndData()
{
await userTaskService.MarkAsCompleteAsync(Usertask.WorkflowInstanceId, Usertask.Signal, taskData);
Usertask = null;
taskData = new JObject();
df.ResetData();
await OnFinished.InvokeAsync();
}
}
该组件的工作非常简单。 它获取布局,生成表单,并在绑定上,只要表单的输入字段更改其值,组件就会更新结果模型。
Dynamic Blazor 表单允许您定义要在表单内使用的 UI 组件的实现。 您可以利用默认的 HTML 组件或使用 Radzen 组件库等。 混合和匹配,甚至创建您的类型,都是可能的。
您要使用的组件需要在startup.cs中注册。
builder.Services.AddScoped(sp =>
new DynamicElementsRepository()
.GetHTMLDefaultSettings()
.Add("TextInput", typeof(TextInput))
.Add("NumberInput", typeof(NumberInput))
.Add("BoolInput", typeof(BoolInput))
.Add("DateInput", typeof(DateInput)));
这意味着可以在 UI 定义中添加新组件,并让 UI 映射到这些类型的特定组件。
当使用已知的 UI 定义名称创建新的用户任务时,无需更新 Web 应用程序。 应用程序现在可以创建表单并将结果发送回引擎。
时机问题
在工作流详细信息页面的实现中,UI 组件激活任务完成事件。 该组件将带有数据的信号发送到引擎。 之后,它将引发 TaskFinished
事件。 详细信息页面将通过加载下一个用户任务来对此进行操作。
但如果
- 引擎还没有完成任务。
- 详细信息页面将再次显示相同的用户任务。
- 工作流程已结束
- 没有什么可展示的。
- 下一步的执行是一个长时间运行的后台活动
- —用户必须等待,但是在这种情况下要显示什么?
有一些解决方案可以缓解这些问题。 不幸的是,他们中的大多数人都求助于在引擎和客户端之间进行更多的通信。 例如,了解工作流的上次执行时间。 因此,在发送信号时,客户端知道工作流何时更新,因为执行时间会有所不同。 但这也意味着客户端需要保存状态。
调度还是执行?
有时,由于工作流程的目的,问题并不那么严重。 例如,当所有活动都是用户任务时,可以将信号发送为“执行”。这种类型的请求意味着该请求将触发工作流的执行,只有当工作流停止运行时才会返回结果。 这个执行方法是同步调用。 此方案适用于类似向导的流程,其中数据存储在工作流实例本身中,并且不依赖于可能运行缓慢的活动。
如果工作流包含长时间运行的步骤,则调度是最佳选择,但这需要某种同步。 或者,使用丑陋的轮询方法或用于检索下一步的任务延迟。 假设在一定时间内,工作流将被执行。
幸运的是,有一种机制可以优雅地处理客户端和引擎之间的通信。 SignalR
实现可以处理它。
下一部分将介绍SignalR
的实现。