AngularJS SignalR Web Api
▌背景
最近因工作需要在Client端即時呈現AD資料異動的資料,
也剛好在學習AngularJS, 因此就選擇以AngularJS來實作SignalR在前端的部分。
之前有使用jQuery來處理這一部分的需求,這次實作後發現整體開發架構和流程並沒有太大的變化, 如果有興趣也可以參考之前的相關文章,應該還是有參考價值 ^__^。
l Visual Studio 2015
Ent.
l AngularJS 1.4.6
l ASP.NET SignalR 2.2.0
l ASP.NET SignalR Client
2.2.0
l Fiddler
將資料以Http Post的方式送到後端的Web Api時,將自動推送給所有在前端連線到SingnalR Hub的Clients。
如下圖,我使用Fiddler送了兩筆AD的資料,此時網頁便會自動刷新呈現這兩筆資訊。
建立一支Web Api,並加入以下Nuget套件:
▋加入OWIN Startup類別
在Configuration定義SignalR的路由和設定。
public class Startup
{
public void Configuration(IAppBuilder app)
{
var config =
//app.MapSignalR();
app.Map("/SignalR", map =>
{
map.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
var hubConfiguration = new HubConfiguration
{
EnableJSONP = true,
EnableDetailedErrors
= true
};
map.RunSignalR(hubConfiguration);
});
}
}
|
如果有需要使用CORS,也在App_Start\WebApiConfig.cs 的註冊方法中加入以下程式碼:
//Enable CORS
config.EnableCors();
|
▋加入DTO Model
前後端溝通的Model …
public class AdUser
{
public String Cn { get; set; }
public String Sn { get; set; }
public String GivenName { get; set; }
public String DisplayName { get; set; }
public String Status { get; set; }
}
|
▋建立SignalR Hub
在Web Api5專案下,新增一個SignalR資料夾,我們將Hub於此。
實作一個SignalR Hub,
重點如下:
l 在類別屬性上指定HubName :updateModifiedAdUsersHub (前端會使用到)。
l 可選擇針對某一個client或所有已建立連線的Clients推送訊息(資料)。
l 黃色底的方法,即為前端要呼叫的Hub Method。
l 我們在Update這個方法,預留一個輸入參數:connId (Client ID), 當有帶入connId時,只會針對該Client做推送。
[HubName("updateModifiedAdUsersHub")]
public class UpdateModifiedAdUsersHub: Hub
{
static IHubContext HubContext = GlobalHost.ConnectionManager.GetHubContext<UpdateModifiedAdUsersHub>();
public void Update(
IEnumerable<AdUser> adUsers, String connId)
{
if (!String.IsNullOrEmpty(connId))
{
//Send back to certain client with
Connection id
HubContext.Clients.Client(connId).updateModifiedAdUsers(adUsers);
}
else
{
//Send back to all connections
HubContext.Clients.All.updateModifiedAdUsers(adUsers);
}
}
}
|
▋Web Api :
Controller
完成SingnalR Hub後,實作Web Api的Http method,
這個Post method主要作用在於,其他系統可藉由此服務送回有異動的AD資料集合到Server side, Server side再推送這些異動資料給所有Client。
[EnableCors(origins: "*", headers: "*", methods: "*")]
public class AdCenterController : ApiController
{
[HttpPost]
// POST: api/AdCenter
public async Task<HttpResponseMessage> Post(IEnumerable<AdUser> adUsers)
{
try
{
new UpdateModifiedAdUsersHub().Update(adUsers, connId:String.Empty);
return Request.CreateResponse(HttpStatusCode.OK);
}
catch (Exception)
{
return Request.CreateResponse(HttpStatusCode.InternalServerError);
}
}
}
|
請新增一網站 (本範例以MVC專案為例),並加入以下套件:
▋AngularJS : Controllers
放一個Root Controller放Server的SingnalR服務 URL
root-controller.js
var app =
angular.module('app')
.run(function ($rootScope) {
$rootScope.SignalRUrl = "http://localhost:9999/SignalR";
});
|
adUser-controller
n 重點是紅色黃底的部分,也就是在後端定義的Hub name和方法!
n 重新Binding資料後,必須呼叫一次 $scope.$apply()
var app =
angular.module('app', [])
.controller('AdUserCtrl', function ($scope,
$rootScope, $http, $window) {
$scope.adUsers = [{
"Cn": "",
"Sn": "",
"GivenName": "",
"DisplayName": "",
"Status": ""
}];
jQuery.support.cors = true;
//AngularJS寫法
$window.jQuery.connection.hub.url =
$rootScope.SignalRUrl;
var myHub = $window.jQuery.connection.updateModifiedAdUsersHub;
$window.jQuery.connection.hub.start();
//一般寫法
//$.connection.hub.url = $rootScope.SignalRUrl;
//var myHub = $.connection.updatemodifiedadusershub;
//$.connection.hub.start();
myHub.client.updateModifiedAdUsers = function (data) {
$scope.adUsers = data;
$scope.$apply();
};
}
);
|
▋View
View沒什麼特別的,只是帶出後端送過來的資料。
但是要注意引用的 .js 順序:
1.
jquery.signalR.XXX.js
必須在最前面。
2.
必須引用後端的SignalR/js
:
<script src="http://localhost:9999/SignalR/js" type="text/javascript"></script>
3.
如果是MVC專案,因為master page有Rendor Script Section,所以務必在引用.js時,包上@secion scripts。 否則會出現以下錯誤:
@section
scripts{
<script src="~/Scripts/jquery.signalR-2.2.0.js"></script>
<script src="~/ScriptsAngularJs/adUser-controller.js"></script>
<script src="~/ScriptsAngularJs/root-controller.js"></script>
<script src="http://localhost:9999/SignalR/js" type="text/javascript"></script>
}
<div ng-app="app">
<div ng-controller="AdUserCtrl">
<table data-toggle="table" class="table">
<tr class="tr-class-1">
<td>姓名</td>
<td>姓氏</td>
<td>名字</td>
<td>顯示名稱</td>
<td>狀態</td>
</tr>
<tr class="tr-class-3" ng-repeat="item in
adUsers">
<td>{{item.Cn}}</td>
<td>{{item.Sn}}</td>
<td>{{item.GivenName}}</td>
<td>{{item.DisplayName}}</td>
<td>{{item.Status}}</td>
</tr>
</table>
</div>
</div>
|
完成以上後,請將後端的Web Api打開,然後開啟多個網頁指向同一個MVC頁面。
在Fiddler pose 以下JSON:
[{"Cn":"JB.Lin","Sn":"Lin","GivenName":"JB","DisplayName":"The
Force
JB","Status":"New"},{"Cn":"Lily.Yang","Sn":"Yang","GivenName":"Lily","DisplayName":"The
Force Lily","Status":"Updated"}]
|
就可以看到所有Client同時刷新並取得Server side的資料。
▋Modify SignalR Hub
還記得我們在updateModifiedAdUsersHub裡面的推送方法:Update(IEnumerable<AdUser> adUsers, String
connId) 有預留以connId(Client ID)針對Client推送資訊。
可是我們要如何得知Client ID呢?
很簡單,覆寫OnConnected() 方法,在Client一與後端的SignalR Hub連線時,我們馬上紀錄其Client
ID即可。
另外覆寫OnDisconnected(bool stopCalled)方法, 將已斷線的Client ID從記錄中移除。
為了簡化程式碼,我們將Client ID保存在記憶體為例。 (只列出調整的部分)
[HubName("updateModifiedAdUsersHub")]
public class UpdateModifiedAdUsersHub: Hub
{
//只列出調整的程式碼 …
public override Task OnConnected()
{
//Get the clientId
var clientId = Context.ConnectionId;
//Keep it in memory
clientIds.Add(clientId);
return base.OnConnected();
}
public override Task OnDisconnected(bool stopCalled)
{
//Get the clientId
var clientId = Context.ConnectionId;
//Remove it from memory
clientIds.Remove(clientId);
//custom logic here
return base.OnDisconnected(stopCalled);
}
public override Task OnReconnected()
{
return base.OnReconnected();
}
}
|
如此我們已可在Client連線時取得Client ID。
▋Web Api : Controller
調整原本的Web Api Post Method,使其可以另行選擇是否帶入Client ID以針對特定Client作推送。
[HttpPost]
public async Task<HttpResponseMessage> Post(IEnumerable<AdUser> adUsers)
{
//Get
the POST params
NameValueCollection nvc =
HttpUtility.ParseQueryString(Request.RequestUri.Query);
var connId = nvc["connId"] ?? String.Empty; //connection id for SingleR
try
{
new UpdateModifiedAdUsersHub().Update(adUsers,
connId:String.Empty);
return Request.CreateResponse(HttpStatusCode.OK);
}
catch (Exception)
{
return Request.CreateResponse(HttpStatusCode.InternalServerError);
}
}
|
▋再次測試
我們這次只針對其中一個Client作額外的推送, 並在JSON加入第三筆資料。
[{"Cn":"JB.Lin","Sn":"Lin","GivenName":"JB","DisplayName":"The
Force
JB","Status":"New"},{"Cn":"Lily.Yang","Sn":"Yang","GivenName":"Lily","DisplayName":"The
Force Lily","Status":"Updated"},{"Cn":"Leia.Lin","Sn":"Lin","GivenName":"Leia","DisplayName":"Leia
Skywalker","Status":"New"}]
|
POST Url : http://localhost:9999/api/AdCenter/Post?connId=b0472efc-838f-48d7-a237-2548558a23b4
結果如下,可以看到只有其中一個Client有做刷新的動作。
即時推送的功能,在許多系統應該是不可或缺的功能了。
當然其實際應用也可以非常多元和變化,是個相當有趣的Web應用。