本系列介绍了由 Elsa 工作流引擎驱动的用户界面的实现。
在第 2 部分 - 创建用户任务活动中,完成了大量编码。 下一步是扩展代码以适应将用户数据返回到工作流程的可能性。
这次,让我们按照应用程序中采取的步骤,看看各个元素如何协同工作以恢复工作流程(从 UI 开始)。
@page "/WorkflowInstances"
@using ElsaDrivenWebApp.Shared.Components
<h3>WorkflowInstances</h3>
@inject UsertaskService userTaskService
@inject ProcessService processService
@if (workflowInstances == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<tr>
<td><input @bind="inputValue" /></td>
<td><button @onclick="() => AddSample2Task()">Start Sample 2 workflow</button></td>
</tr>
</table>
<table class="table">
<thead>
<tr>
<th>Title</th>
<th>Instance name</th>
<th>State</th>
<th>Description</th>
</tr>
</thead>
<tbody>
@foreach (var workflowInstance in workflowInstances)
{
<tr>
<td>
@workflowInstance.DefinitionId
</td>
<td>
@workflowInstance.WorkflowName
</td>
<td>
@workflowInstance.State
</td>
<td>
@foreach (var usertask in workflowInstance.UserTasks)
{
<button @onclick="() => Open(usertask)">@usertask.TaskTitle - @usertask.TaskDescription</button>
<br />
}
</td>
</tr>
}
</tbody>
</table>
<Usertasksample2 Task=selectedUsertasksample2 OnFinished="TaskFinished2"></Usertasksample2>
<Usertasksample2a Task=selectedUsertasksample2a OnFinished="TaskFinished2a"></Usertasksample2a>
}
@code {
private List<WorkfowInstanceUsertaskViewModel>? workflowInstances;
private string? inputValue;
private UsertaskViewModel? selectedUsertasksample2 { get; set; } = null;
private UsertaskViewModel? selectedUsertasksample2a { get; set; } = null;
protected override async Task OnInitializedAsync()
{
await LoadTasks();
}
private async Task LoadTasks()
{
var workflowInstancesArray = await userTaskService.GetWorkflowsWaitingOnUserTask();
workflowInstances = workflowInstancesArray.ToList();
}
private async Task TaskFinished2()
{
selectedUsertasksample2 = null;
await LoadTasks();
}
private async Task TaskFinished2a()
{
selectedUsertasksample2a = null;
await LoadTasks();
}
private async Task Open(UsertaskViewModel task)
{
selectedUsertasksample2 = null;
switch (task.Signal)
{
case "usertasksample2":
selectedUsertasksample2 = task;
break;
case "usertasksample2a":
selectedUsertasksample2a = task;
break;
default:
await SendSignal(task);
break;
}
}
private async Task SendSignal(UsertaskViewModel task)
{
await userTaskService.MarkAsCompleteAsync(task.WorkflowInstanceId, task.Signal, null);
await LoadTasks();
}
private async Task AddSample2Task()
{
var settings = new Envelope<SampleSettings>(new SampleSettings { Name = inputValue });
await processService.SendSignal("sample2", settings);
await LoadTasks();
}
}
尽管这是 Blazor 实现,但路由和步骤仍然适用于其他框架。
- 获取等待 Usertask 活动的工作流实例
- 为用户任务呈现适当的 UI
- 向引擎发送更新信号和数据
在初始化组件时,UsertaskService
加载用户任务。 在这种情况下,服务的实现就是返回工作流实例。 另一种方法是获取用户任务列表,即使工作流实例相同。
事后看来,这将是一个更好的方法,因为客户端可以控制如何显示用户任务。 😉
控制器返回 UsertaskViewmodel
,UI 可以显示它们。 在每个显示的项目上,添加一个 open(usertask)
事件处理程序。 在此情况下,甚至 UI 也会激活适当的 UI 控件。
Ui控件打开bij
后UI能够与引擎进行交互。 它将使用 UsertaskService
将信号发送到引擎。 然后,它使用 JsonConvert
将组件中输入的数据转换为 JSON 对象,并将其作为请求中的数据结果发送。
@using Newtonsoft.Json
@inject UsertaskService userTaskService
@if (Task != null)
{
<form>
<div class="form-group">
<label for="exampleInputEmail1">Email address</label>
<input type="email" @bind=data.Email class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp" placeholder="Enter email">
<small id="emailHelp" class="form-text text-muted">We'll never share your email with anyone else.</small>
</div> <div class="form-group">
<label for="exampleInputPassword1">Password</label>
<input type="password" @bind=data.Password class="form-control" id="exampleInputPassword1" placeholder="Password">
</div>
<button @onclick="() => SendSignalAndData()">Continue</button>
</form>
}
@code {
[Parameter]
public EventCallback OnFinished { get; set; }
[Parameter]
public UsertaskViewModel Task { get; set; }
UsertaskSample2DataModel data = new UsertaskSample2DataModel();
private async Task SendSignalAndData()
{
await userTaskService.MarkAsCompleteAsync(Task.WorkflowInstanceId, Task.Signal, JsonConvert.SerializeObject(data));
await OnFinished.InvokeAsync();
Task = null;
}
class UsertaskSample2DataModel
{
public string Email { get; set; }
public string Password { get; set; }
}
}
之前提到过,数据对象需要包装在信封中,以便它可以为信号添加一些元数据。
public async Task MarkAsCompleteAsync(string workflowInstanceId, string signal, JToken signalData)
{
var data = new MarkAsCompletedPostModel
{
WorkflowInstanceId = workflowInstanceId,
Input = signalData == null ? JValue.CreateNull() : signalData
};
var content = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json");
await httpClient.PostAsync($"/v1/usertask-signals/{signal}/execute", content);
}
//combined for clarity
public class MarkAsCompletedPostModel
{
[JsonProperty("workflowInstanceId")]
public string WorkflowInstanceId { get; set; } = string.Empty;
[JsonProperty("input")]
public JToken Input { get; set; } = JValue.CreateNull();
}
现在,引擎可以使用元数据来确定需要恢复哪个工作流实例,更具体地说,根据信号确定哪个活动。
因此,在设计流程和信号时,请注意信号发送。 避免使用相同的名称,因为它可能会意外激活多个任务,因为它们使用相同的信号名称。
在信封中,没有指示活动 ID,这意味着引擎将激活工作流实例中具有该信号名称且已暂停的所有内容。
在控制器中的实现相当简单
[HttpPost("{signalName}/execute")]
[ElsaJsonFormatter]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ExecuteSignalResponse))]
[SwaggerOperation(
Summary = "Signals all workflows waiting on the specified signal name synchronously.",
Description = "Signals all workflows waiting on the specified signal name synchronously.",
OperationId = "UsertaskSignals.Execute",
Tags = new[] { "UsertaskSignals" })
]
public async Task<IActionResult> Handle(string signalName, ExecuteSignalRequest request,
CancellationToken cancellationToken = default)
{
var collectedWorkflows = await invoker.ExecuteWorkflowsAsync(signalName, request.Input,
request.WorkflowInstanceId, request.CorrelationId, cancellationToken);
return Ok(collectedWorkflows.ToList());
}
接收到数据和信号。 接下来,调用者可以根据 Usertask
信号执行工作流。 此调用程序 (IUserTaskSignalInvoker
) 是书签实现的一部分。 它将收集等待特定书签类型和信号的工作流程。 然后,它将激活工作流实例中关联活动的 OnResume
方法。
protected override IActivityExecutionResult OnResume(ActivityExecutionContext context)
{
var triggeredSignal = context.GetInput<Signal>()!;
SignalInput = triggeredSignal.Input;
Output = triggeredSignal.Input;
context.LogOutputProperty(this, nameof(Output), Output);
return Done();
}
对于用户任务来说,没有什么可做的。 它只是将输入和输出设置为接收到的数据。 并且,之后它将指示完成作为用户任务的结果。
这就是用户任务中处理数据的方式!