AngularJS    SignalR   Web Api  
▌背景
最近因工作需要在Client端即時呈現AD資料異動的資料,
也剛好在學習AngularJS, 因此就選擇以AngularJS來實作SignalR在前端的部分。
之前有使用jQuery來處理這一部分的需求,這次實作後發現整體開發架構和流程並沒有太大的變化, 如果有興趣也可以參考之前的相關文章,應該還是有參考價值 ^__^。 
▌相關文章
File upload with Html5 & WebApi & SignalR
(1)
File upload with Html5 & WebApi & SignalR (2)
File upload with Html5 & WebApi & SignalR (3)
▌環境
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的資料,此時網頁便會自動刷新呈現這兩筆資訊。
▌實作 (Server side)
建立一支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); 
            } 
        } 
} 
 | 
 
▌實作 (Client)
請新增一網站 (本範例以MVC專案為例),並加入以下套件:
AngularJS 
jQuery 
 | 
 
▋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>
@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的資料。
▌修改後端以針對特定Client推送訊息
▋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有做刷新的動作。
▌Summary
即時推送的功能,在許多系統應該是不可或缺的功能了。
當然其實際應用也可以非常多元和變化,是個相當有趣的Web應用。





沒有留言:
張貼留言