第 4 部分 - 让 Usertask 确定 UI 布局

本系列介绍了由 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 的实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值