2015年9月25日 星期五

以AngularJS實作SignalR Client side即時在前端刷新資料


 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 HubClients

如下圖,我使用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 ApiHttp 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 ControllerServerSingnalR服務 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 pageRendor 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的資料。



修改後端以針對特定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應用。

 











沒有留言:

張貼留言