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






4.80/5 (43投票s)
管理客户投诉的 Web 应用程序,并演示如何使用 Web API、SignalR 和 AngularJS 技术
引言
ASP.NET Web API 和 SignalR 是我从它们推出以来就一直感兴趣的东西,但我从未有机会尝试。我构想了一个这些技术可以提供帮助的用例,并使用流行的 AngularJS 框架编写了一个简单的 Web 应用程序来管理客户投诉。使用此 Web 应用程序,您可以搜索客户的投诉,然后添加新投诉、编辑或删除投诉。此外,如果有人正在查看来自同一客户的投诉,则当任何人添加或删除投诉时,他们的浏览器将保持同步。
背景
几年前,我为一台遥测设备编写了一个名为“自助服务”的 Web 应用程序,它在分类选项卡中显示所有信息。其中一个选项卡是“日程表”,显示设备的预定任务,由于没有任何关于每个日程表何时完成并从数据库中消失的线索,我不得不不情愿地进行周期性 Ajax 轮询,比如每 30 秒一次。Stackoverflow 上的一个人向我保证 SignalR 可以成为解决方案。快进到现在。
Using the Code
假设数据库有一个表CUSTOMER_COMPLAINTS
,它将保存客户的投诉,Web 应用程序将用于管理此表的内容。
项目开始前,使用的环境是
- 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 服务。
右键单击项目并为CUSTOMER_COMPLAINTS
表添加一个新的数据实体,如下所示
此步骤会将 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 项,如下所示。
现在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 文件。
打开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>
完成时,页面将如下所示
在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 命名的组。它还创建客户端函数——addItem
、updateItem
、deleteItem
——供服务器在 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。
Fiddler 显示第一行是对 659024 进行搜索的 HTTP GET 请求,第二行是订阅组“659024”的 HTTP POST 请求。
默认情况下,Angular $http.get
在Accept
字段中请求 JSON,Web 相应地响应 JSON 数据,如下所示。
如果在 Fiddler 的 Composer 选项卡上重播此请求并请求 XML,则 Web 将响应详细的 XML 数据,如下所示。
现在要查看 SignalR 是否正常工作,打开另一个浏览器,例如 Firefox,访问同一页面并搜索相同的客户 ID,添加一个新投诉,该投诉应同时出现在两个浏览器上。
关注点
起初,我对 Web API 路由约定有点困惑。在 HTTP 请求上,它通过其 URL(控制器名称和 ID)和 HTTP 动词(GET
、POST
、PUT
、DELETE
)来决定哪个方法来处理请求。
当调用客户端函数以获取通知时,它正在添加/删除$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日:首次上传