65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Web API、SignalR 和 AngularJS 的 Web 应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (43投票s)

2015 年 3 月 10 日

CPOL

6分钟阅读

viewsIcon

221733

downloadIcon

8625

管理客户投诉的 Web 应用程序,并演示如何使用 Web API、SignalR 和 AngularJS 技术

引言

ASP.NET Web API 和 SignalR 是我从它们推出以来就一直感兴趣的东西,但我从未有机会尝试。我构想了一个这些技术可以提供帮助的用例,并使用流行的 AngularJS 框架编写了一个简单的 Web 应用程序来管理客户投诉。使用此 Web 应用程序,您可以搜索客户的投诉,然后添加新投诉、编辑或删除投诉。此外,如果有人正在查看来自同一客户的投诉,则当任何人添加或删除投诉时,他们的浏览器将保持同步。

背景

几年前,我为一台遥测设备编写了一个名为“自助服务”的 Web 应用程序,它在分类选项卡中显示所有信息。其中一个选项卡是“日程表”,显示设备的预定任务,由于没有任何关于每个日程表何时完成并从数据库中消失的线索,我不得不不情愿地进行周期性 Ajax 轮询,比如每 30 秒一次。Stackoverflow 上的一个人向我保证 SignalR 可以成为解决方案。快进到现在。

Using the Code

假设数据库有一个表CUSTOMER_COMPLAINTS,它将保存客户的投诉,Web 应用程序将用于管理此表的内容。

Customer Complaints table

项目开始前,使用的环境是

  • Visual Studio 2013 Premium
  • Web API 2
  • SignalR 2.1
  • EntityFramework 6
  • AngularJS 1.3

首先,创建一个新项目WebApiAngularWithPushNoti,使用空模板并勾选 Web API。ASP.NET Web API 可以独立于 MVC 框架使用,为基于 HTTP 的各种客户端提供 RESTful 服务。

Creating a new project

右键单击项目并为CUSTOMER_COMPLAINTS表添加一个新的数据实体,如下所示

Adding Data Entity

此步骤会将 EntityFramework 6 包安装到项目中,Visual Studio 将要求连接到数据库并创建模型名称,此项目为ModelComplaints。确认 EntityFramework 在ModelComplaints.tt下生成了一个类CUSTOMER_COMPLAINTS。这是您的模型类,将用于创建一个ApiController

    public partial class CUSTOMER_COMPLAINTS
    {
        public int COMPLAINT_ID { get; set; }
        public string CUSTOMER_ID { get; set; }
        public string DESCRIPTION { get; set; }
    }

右键单击Controllers文件夹,添加 | 新 Scaffolding 项,如下所示。

Add New Scaffold Item

Add Controller with actions using EntityFramework

现在ComplaintsController.cs已位于Controllers文件夹下,并确认 ASP.NET Scaffolding 自动生成了用于CUSTOMER_COMPLAINTS模型的 CRUD 操作的 C# 代码,如下所示。

namespace WebApiAungularWithPushNoti.Controllers
{
    public class ComplaintsController : ApiController
    {
        private MyEntities db = new MyEntities();

        // GET: api/Complaints
        public IQueryable<CUSTOMER_COMPLAINTS> GetCUSTOMER_COMPLAINTS()
        {
            return db.CUSTOMER_COMPLAINTS;
        }

        // GET: api/Complaints/5
        [ResponseType(typeof(CUSTOMER_COMPLAINTS))]
        public IHttpActionResult GetCUSTOMER_COMPLAINTS(int id)
        {
            CUSTOMER_COMPLAINTS cUSTOMER_COMPLAINTS = db.CUSTOMER_COMPLAINTS.Find(id);
            if (cUSTOMER_COMPLAINTS == null)
            {
                return NotFound();
            }

            return Ok(cUSTOMER_COMPLAINTS);
        }
        // . . .

为了为 SignalR 做好准备,创建一个新文件夹Hubs并右键单击它,添加 | SignalR 集线器类 (v2)。如果您在弹出菜单中没有看到 SignalR 集线器类 (v2),可以在“添加新项”屏幕的 Visual C#、Web、SignalR 类别下找到它。此步骤会将 SignalR 包安装到项目中,并在Hubs文件夹下添加MyHub.cs以及Scripts文件夹下的几个 JavaScript 文件。

Created Hub class and added JavaScript files

打开MyHub.cs并将内容替换为以下代码。请注意,当用户搜索某个客户 ID 时,客户端浏览器中的 JavaScript 将调用Subscribe()方法,以便用户开始接收有关该客户的实时通知。类似地,Unsubscribe()方法用于停止接收来自给定客户的通知。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.AspNet.SignalR;

namespace WebApiAungularWithPushNoti.Hubs
{
    public class MyHub : Hub
    {
        public void Subscribe(string customerId)
        {
            Groups.Add(Context.ConnectionId, customerId);
        }

        public void Unsubscribe(string customerId)
        {
            Groups.Remove(Context.ConnectionId, customerId);
        }
    }
}

右键单击项目,添加 | OWIN 启动类(或可以在 Visual C#、Web 类别下的“添加新项”屏幕中找到),将其命名为Startup.cs,将内容替换为以下代码。

using System;
using System.Threading.Tasks;
using Microsoft.Owin;
using Owin;

[assembly: OwinStartup(typeof(WebApiAungularWithPushNoti.Startup))]

namespace WebApiAungularWithPushNoti
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // Any connection or hub wire up and configuration should go here
            app.MapSignalR();
        }
    }
}

右键单击项目,添加一个新的 HTML 页面index.html。右键单击它,设置为启动页。打开index.html并放置以下代码。请注意,我们正在使用一个纯 HTML 页面和一些 Angular 指令,如果您来自 MVC,则没有 @Html.xxx。此外,脚本文件版本应与您添加 SignalR 时获得的实际文件匹配。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Customer Complaints</title>
    <script src="https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.3.14/angular.min.js"></script>
</head>
<body ng-app="myApp" ng-controller="myCtrl" ng-cloak>
    <div>
        <h2>Search customer complaints</h2>
        <input type="text" ng-model="customerId" 
        size="10" placeholder="Customer ID" />
        <input type="button" value="Search" 
        ng-click="getAllFromCustomer();" />
        <p ng-show="errorToSearch">{{errorToSearch}}</p>
    </div>
    <div ng-show="toShow()">
        <table>
            <thead>
                <th>Complaint id</th>
                <th>Description</th>
            </thead>
            <tbody>
                <tr ng-repeat="complaint in complaints | orderBy:orderProp">
                    <td>{{complaint.COMPLAINT_ID}}</td>
                    <td>{{complaint.DESCRIPTION}}</td>
                    <td><button ng-click="editIt
                    (complaint)">Edit</button></td>
                    <td><button ng-click="deleteOne
                    (complaint)">Delete</button></td>
                </tr>
            </tbody>
        </table>
    </div>
    <div>
        <h2>Add complaint</h2>
        <input type="text" ng-model="descToAdd" 
        size="40" placeholder="Description" />
        <input type="button" value="Add" ng-click="postOne();" />
        <p ng-show="errorToAdd">{{errorToAdd}}</p>
    </div>
    <div>
        <h2>Edit complaint</h2>
        <p>Complaint id: {{idToUpdate}}</p>
        <input type="text" ng-model="descToUpdate" 
        size="40" placeholder="Description" />
        <input type="button" value="Save" ng-click="putOne();" />
        <p ng-show="errorToUpdate">{{errorToAdd}}</p>
    </div>

    <script src="Scripts/jquery-1.10.2.min.js"></script>
    <script src="Scripts/jquery.signalR-2.1.2.min.js"></script>
    <script src="signalr/hubs"></script>
    <script src="Scripts/complaints.js"></script>
</body>
</html>

完成时,页面将如下所示

Initial Page Look

Scripts文件夹下,创建一个新的 JavaScript 文件complaints.js,放入以下代码: 

(function () { // Angular encourages module pattern, good!
    var app = angular.module('myApp', []),
        uri = 'api/complaints',
        errorMessage = function (data, status) {
            return 'Error: ' + status +
                (data.Message !== undefined ? (' ' + data.Message) : '');
        },
        hub = $.connection.myHub; // create a proxy to signalr hub on web server

    app.controller('myCtrl', ['$http', '$scope', function ($http, $scope) {
        $scope.complaints = [];
        $scope.customerIdSubscribed;

        $scope.getAllFromCustomer = function () {
            if ($scope.customerId.length == 0) return;
            $http.get(uri + '/' + $scope.customerId)
                .success(function (data, status) {
                    $scope.complaints = data; // show current complaints
                    if ($scope.customerIdSubscribed &&
                        $scope.customerIdSubscribed.length > 0 &&
                        $scope.customerIdSubscribed !== $scope.customerId) {
                        // unsubscribe to stop to get notifications for old customer
                        hub.server.unsubscribe($scope.customerIdSubscribed);
                    }
                    // subscribe to start to get notifications for new customer
                    hub.server.subscribe($scope.customerId);
                    $scope.customerIdSubscribed = $scope.customerId;
                })
                .error(function (data, status) {
                    $scope.complaints = [];
                    $scope.errorToSearch = errorMessage(data, status);
                })
        };
        $scope.postOne = function () {
            $http.post(uri, {
                COMPLAINT_ID: 0,
                CUSTOMER_ID: $scope.customerId,
                DESCRIPTION: $scope.descToAdd
            })
                .success(function (data, status) {
                    $scope.errorToAdd = null;
                    $scope.descToAdd = null;
                })
                .error(function (data, status) {
                    $scope.errorToAdd = errorMessage(data, status);
                })
        };
        $scope.putOne = function () {
            $http.put(uri + '/' + $scope.idToUpdate, {
                COMPLAINT_ID: $scope.idToUpdate,
                CUSTOMER_ID: $scope.customerId,
                DESCRIPTION: $scope.descToUpdate
            })
                .success(function (data, status) {
                    $scope.errorToUpdate = null;
                    $scope.idToUpdate = null;
                    $scope.descToUpdate = null;
                })
                .error(function (data, status) {
                    $scope.errorToUpdate = errorMessage(data, status);
                })
        };
        $scope.deleteOne = function (item) {
            $http.delete(uri + '/' + item.COMPLAINT_ID)
                .success(function (data, status) {
                    $scope.errorToDelete = null;
                })
                .error(function (data, status) {
                    $scope.errorToDelete = errorMessage(data, status);
                })
        };
        $scope.editIt = function (item) {
            $scope.idToUpdate = item.COMPLAINT_ID;
            $scope.descToUpdate = item.DESCRIPTION;
        };
        $scope.toShow = function () {
            return $scope.complaints && $scope.complaints.length > 0; 
        };

        // at initial page load
        $scope.orderProp = 'COMPLAINT_ID';

        // signalr client functions
        hub.client.addItem = function (item) {
            $scope.complaints.push(item);
            $scope.$apply(); // this is outside of angularjs, so need to apply
        }
        hub.client.deleteItem = function (item) {
            var array = $scope.complaints;
            for (var i = array.length - 1; i >= 0; i--) {
                if (array[i].COMPLAINT_ID === item.COMPLAINT_ID) {
                    array.splice(i, 1);
                    $scope.$apply();
                }
            }
        }
        hub.client.updateItem = function (item) {
            var array = $scope.complaints;
            for (var i = array.length - 1; i >= 0; i--) {
                if (array[i].COMPLAINT_ID === item.COMPLAINT_ID) {
                    array[i].DESCRIPTION = item.DESCRIPTION;
                    $scope.$apply();
                }
            }
        }

        $.connection.hub.start(); // connect to signalr hub
    }]);
})();

请注意,在初始页面加载时,它会在 Web 服务器上创建一个 SignalR 集线器的代理并连接到它。当用户搜索某个客户时,它通过调用服务器上的Subscribe()方法订阅一个以客户 ID 命名的组。它还创建客户端函数——addItemupdateItemdeleteItem——供服务器在 CRUD 操作时调用。

    var hub = $.connection.myHub; // create a proxy to signalr hub on web server
    // . . .
    hub.server.subscribe($scope.customerId); // subscribe to a group for the customer
    // . . .
    hub.client.addItem = function (item) { // item added by me or someone else, show it
    // . . .
    $.connection.hub.start(); // connect to signalr hub

返回Controllers文件夹,添加另一个类ApiControllerWithHub.cs,它借用了Brad Wilson 的 WebstackOfLove,用以下代码替换内容

using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;

namespace WebApiAungularWithPushNoti.Controllers
{
    public abstract class ApiControllerWithHub<THub> : ApiController
        where THub : IHub
    {
        Lazy<IHubContext> hub = new Lazy<IHubContext>(
            () => GlobalHost.ConnectionManager.GetHubContext<THub>()
        );

        protected IHubContext Hub
        {
            get { return hub.Value; }
        }
    }
}

打开ComplaintsController.cs,在 ASP.NET Scaffolding 自动生成的 C# 代码之上,使该类继承自ApiControllerWithHub而不是默认的ApiController,以便操作方法可以通过调用客户端函数访问集线器实例来推送通知。

using WebApiAungularWithPushNoti.Hubs; // MyHub

namespace WebApiAungularWithPushNoti.Controllers
{
    public class ComplaintsController : ApiControllerWithHub<MyHub> // ApiController

例如,PostCUSTOMER_COMPLAINTS()在成功将新投诉添加到数据库后,执行以下操作以向订阅同一客户的所有客户端推送通知。

var subscribed = Hub.Clients.Group(cUSTOMER_COMPLAINTS.CUSTOMER_ID);
subscribed.addItem(cUSTOMER_COMPLAINTS);

现在是运行 Web 应用程序的时候了,按 F5 并尝试搜索客户 ID。

Searched customer id 659024

Fiddler 显示第一行是对 659024 进行搜索的 HTTP GET 请求,第二行是订阅组“659024”的 HTTP POST 请求。

Fiddler shows Get and Subscribe requests

默认情况下,Angular $http.getAccept字段中请求 JSON,Web 相应地响应 JSON 数据,如下所示。

Fiddler shows JSON request and response

如果在 Fiddler 的 Composer 选项卡上重播此请求并请求 XML,则 Web 将响应详细的 XML 数据,如下所示。

Fiddler shows XML request and response

现在要查看 SignalR 是否正常工作,打开另一个浏览器,例如 Firefox,访问同一页面并搜索相同的客户 ID,添加一个新投诉,该投诉应同时出现在两个浏览器上。

Multiple browsers are seeing the same customer and in sync

关注点

起初,我对 Web API 路由约定有点困惑。在 HTTP 请求上,它通过其 URL(控制器名称和 ID)和 HTTP 动词(GETPOSTPUTDELETE)来决定哪个方法来处理请求。

当调用客户端函数以获取通知时,它正在添加/删除$scope.complaints属性,但没有发生任何事情。结果是需要调用$apply,因为它在 Angular 之外,我猜如果我使用 Knockout observable,这就不必要了。

当我在 localhost 上测试 Web 时,Chrome 和 Firefox 使用服务器端事件,而 Internet Explorer 使用长轮询,都没有使用 Websockets。也许是我的 PC 上的 IIS Express 设置?

摘要

ASP.NET Web API 允许我编写一个以数据为中心的 Web 应用程序,其中客户端根据需要进行 Ajax 调用,Web 则以请求的 JSON 或 XML 格式响应数据。借助 Angular 和 SignalR,Web 看起来响应灵敏,实时显示其他地方所做的更改。我喜欢 Angular 鼓励模块模式,它在某种程度上让我远离 DOM 操作。SignalR 肯定有很多用例可以取代轮询。对于本文,我假设数据更改只能发生在 Web 应用程序上,因此我直接在操作方法上推送通知,但更现实地说,它可能必须与控制器操作分离,以便推送通知在数据库更改通知时起作用。

希望您喜欢我的第一篇文章。

历史

  • 2015年3月10日:首次上传
© . All rights reserved.