There will be 2 projects created:
.Server is net5.0 and using Microsoft.NET.Sdk.Web SDK
.Client is net5.0 and connect to Server
Prerequisites
vs 2019
windows server
Table of Content
Create a web project with mvc
install Microsoft.AspNetCore.SignalR
create hubs
Add the SignalR client library
step 1:Create a web project with mvc
step 2:Install required packages for SignalR
Install Microsoft.AspNetCore.SignalR
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="5.0.0" />
</ItemGroup>
</Project>
Step 3:Create hubs
使用SignalR 即时更新状态。
首先建立SchedulerHub作为SignalR伺服端控管前后端沟通的渠道,能够接收client端的请求,并且也能主动通知 client状态异动。
public class SchedulerHub : Hub
{
private QuartzHostedService _quartzHostedService;
/// <summary>
/// 建構子
/// </summary>
/// <param name="quartzHostedService">Quartz排程服務</param>
public SchedulerHub (QuartzHostedService quartzHostedService)
{
_quartzHostedService = quartzHostedService;
}
/// <summary>
/// 要求取得Job狀態
/// </summary>
public async Task RequestJobStatus()
{
if (Clients != null)
{
var jobs = await _quartzHostedService.GetJobSchedules();
var jobSummary = jobs.Select(e =>
new JobScheduleSummary {
JobName = e.JobName,
CronExpression = e.CronExpression,
JobStatusName = e.JobStatus.GetDescription(),
JobStatusId = e.JobStatus,
JobType = e.JobType.FullName
}
);
await Clients.Caller.SendAsync("ReceiveJobStatus", jobSummary);
}
}
/// <summary>
/// 通知Job狀態改變
/// </summary>
public async Task NotifyJobStatusChange()
{
if (Clients != null)
{
await Clients.All.SendAsync("JobStatusChange");
}
}
/// <summary>
/// 手動觸發Job執行
/// </summary>
public async Task TriggerJob(string jobName)
{
await _quartzHostedService.TriggerJobAsync(jobName);
}
/// <summary>
/// 手動中斷Job執行
/// </summary>
public async Task InterruptJob(string jobName)
{
await _quartzHostedService.InterruptJobAsync(jobName);
}
/// <summary>
/// 開啟排程器
/// </summary>
public async Task StartScheduler()
{
await _quartzHostedService.StartAsync(_quartzHostedService.CancellationToken);
}
/// <summary>
/// 關閉排程器
/// </summary>
public async Task StopScheduler()
{
await _quartzHostedService.StopAsync(_quartzHostedService.CancellationToken);
}
/// <summary>
/// 用戶連線事件
/// </summary>
public override async Task OnConnectedAsync()
{
await Groups.AddToGroupAsync(Context.ConnectionId, "SignalR Users");
await NotifyJobStatusChange();
await base.OnConnectedAsync();
}
/// <summary>
/// 用戶斷線事件
/// </summary>
public override async Task OnDisconnectedAsync(Exception exception)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, "SignalR Users");
await base.OnDisconnectedAsync(exception);
}
}
Step 4:SignalR伺服端设置
于DI容器中注册JobListener、SchedulerListener及SchedulerHub后,加入SignalR服务及设定SignalR Router
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
//向DI容器註冊Quartz服務
services.AddSingleton<IJobFactory, JobFactory>();
services.AddSingleton<ISchedulerFactory, StdSchedulerFactory>();
services.AddSingleton<IJobListener, JobListener>(); // 註冊 JobListener
services.AddSingleton<ISchedulerListener, SchedulerListener>(); // 註冊 SchedulerListener
/* ... 略 ... */
// 註冊DB容器schedulerHub實體
services.AddSingleton<SchedulerHub>();
// 設定 SignalR 服務
services.AddSignalR();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
/* ... 略 ... */
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
// 設定 signalR 的 router
endpoints.MapHub<SchedulerHub>("/schedulerHub");
});
}
}
至此SignalR伺服端设置已经完成了。
Step 5:前端设置
ASP.NET Core SignalR .NET Client
Add the SignalR client library
In Solution Explorer, right-click the project, and select Add > Client-Side Library.
In the Add Client-Side Library dialog, for Provider select unpkg.
For Library, enter @aspnet/signalr
Add Client-Side Library dialog
enter @aspnet/signalr
VUE+ASP.NET Core SignalR JavaScript client
使用vue来完成前端Home/index.cshtml的互动.
前端就只要依照SignalR开发方式进行即可,首先在_Layout.cshtml中载入所需要的js档案。
@*加入 signalr.js + vue.js *@
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/3.1.7/signalr.min.js"></script>
<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/http-vue-loader"></script>
使用vue来完成前端Home/index.cshtml的互动。主要就是使用 signalR.HubConnectionBuilder建立起与后端的连线,接着定义可被后端触发的事件,以及可以直接呼叫后端SchedulerHub方法的动作,程式逻辑约略如下。
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<div id="app">
<h1 class="display-4">System Scheduler</h1>
<div class="scheduler-actions">
<button v-on:click="startScheduler" :disabled="!isSchedulerStop">Start Scheduler</button>
<button v-on:click="stopScheduler" :disabled="isSchedulerStop">Stop Scheduler</button>
<button v-on:click="refresh">Refresh {{counter}}</button>
</div>
<table class="schedule-table">
<thead class="schedule-table__header">
<tr>
<td align="center">
Job Name
</td>
<td align="center">
Job Type
</td>
<td align="center">
CRON
</td>
<td align="center">
Status
</td>
<td>
Actions
</td>
</tr>
</thead>
<tr v-for="(job,index) in jobs" :key="index">
<td align="center">
{{job.jobName}}
</td>
<td align="center">
{{job.jobType}}
</td>
<td align="center">
{{job.cronExpression}}
</td>
<td align="center" :class="isExecuting(job.jobStatusId) ? 'schedule-table--active' : ''">
{{job.jobStatusName}}
</td>
<td>
<button v-on:click="()=>trigger(job.jobName)" :disabled="!isStandby(job.jobStatusId)">Trigger</button>
<button v-on:click="()=>Interrupt(job.jobName)" :disabled="!isExecuting(job.jobStatusId)">Interrupt</button>
</td>
</tr>
</table>
<div class="scheduler-actions">
{{time}}
</div>
</div>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
connection: null,
jobs: [],
counter: 0,
time: '',
timeInterval: null
},
async mounted() {
this.connection = new signalR.HubConnectionBuilder()
.withUrl("/schedulerHub")
.build();
// 被後端呼叫接收 Job 目前狀態
this.connection.on("ReceiveJobStatus", (jobs) => {
this.jobs = jobs;
this.counter += 1;
});
// 被後端呼叫接收 Job 狀態改變的通知
this.connection.on("JobStatusChange", () => {
this.connection.invoke("RequestJobStatus").catch(err => console.error(err));
});
this.connection.start().catch(err => console.error(err));
this.time = this.getTime();
this.timeInterval = window.setInterval(() => this.time = this.getTime(), 300);
},
destroyed() {
window.clearInterval(this.timeInterval);
},
computed: {
isSchedulerStop() {
return this.jobs.findIndex(j => j.jobStatusId === 3) > -1
}
},
methods: {
isExecuting(status) {
return status === 2;
},
isStandby(status) {
return status === 1;
},
refresh() {
// 呼叫後端提供所有 Job 的狀態
this.connection.invoke("RequestJobStatus").catch(err => console.error(err));
},
trigger(jobName) {
// 呼叫後端觸發特定Job
this.connection.invoke("TriggerJob", jobName).catch(err => console.error(err));
},
Interrupt(jobName) {
// 呼叫後端終止特定Job
this.connection.invoke("InterruptJob", jobName).catch(err => console.error(err));
},
startScheduler() {
// 呼叫後端啟動排程
this.connection.invoke("StartScheduler").catch(err => console.error(err));
},
stopScheduler() {
// 呼叫後端終止排程
this.connection.invoke("StopScheduler").catch(err => console.error(err));
},
getTime() {
var dt = new Date();
var DD = ("0" + dt.getDate()).slice(-2);
var MM = ("0" + (dt.getMonth() + 1)).slice(-2);
var YYYY = dt.getFullYear();
var hh = ("0" + dt.getHours()).slice(-2);
var mm = ("0" + dt.getMinutes()).slice(-2);
var ss = ("0" + dt.getSeconds()).slice(-2);
return YYYY + "-" + MM + "-" + DD + " " + hh + ":" + mm + ":" + ss;
}
}
});
</script>
useful links
Quartz.Net 排程作业Host 于ASP.NET Core 网站中,并以 SignalR +vue实现Dashboard 监控页面