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

一个简单的仪表板:ASP.NET Web API 服务配合 AngularJS 和 Angular 2 客户端

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (41投票s)

2016 年 4 月 25 日

CPOL

10分钟阅读

viewsIcon

56268

downloadIcon

324

ASP.NET Web API、SignalR 和 AngularJS / Angular 2 协同工作

引言

本文介绍了一个协作式 RESTful Web 服务与 AngularJS(下称ng1)和 Angular 2(ng2)客户端的示例。一个简单的基于 Web 的仪表板服务显示了由硬件设备控制的各种进程的状态。这些设备已连接到网络,并能够使用 HTTP 与专用于特定设备类型的 Web 服务进行通信。每个此类服务(在代码示例中为DeviceXApp)会按一定时间间隔从设备接收数据,处理数据以确定被控进程的状态,并将该状态信息转发给仪表板 Web 应用程序(DashboardApp)。DeviceXAppDashboardApp 之间的通信通过 SignalR 执行。两个 Web 应用程序均使用 ASP.NET Web API 开发。DashboardApp 的客户端侧有两种形式:ng1ng2。两个客户端都具有相同的功能和用户界面,并使用 SignalR 与相同的服务器软件进行全双工通信。

流程

系统的整体结构如下图 1 所示。

图 1. 整体结构

DevicesSimulator 控制台应用程序模拟硬件设备的通信活动,向 DeviceXApp 发送周期性的 HTTP 请求。每个设备在首次发送消息时会向 DeviceXApp 注册(假定 DeviceXApp 在设备注册之前已启动)。DeviceXApp 将设备注册数据传递给 DashboardApp,后者根据设备的 ID 和为每个类别预配置的设备 ID 范围,将设备归入适当的类别。成功注册后,设备会继续向 DeviceXApp 发送其测量数据。后者根据接收到的数据确定设备进程的状态(OkWarningError),并将其转发给 DashboardApp 以便呈现给用户。如果在预配置的时间间隔内设备未能发送其数据,DeviceXApp 将报告设备处于 NoData 状态。ng1ng2 应用程序呈现 DashboardApp 的客户端用户界面,根据设备类别显示设备信息和状态。用户界面如下图 2 所示(请不要对它有过高的要求,因为它只是一个小而不太真实的示例)。

图 2. 用户界面

每个类别表都可以展开或折叠。在示例的信息流中,我们希望尽可能避免移动不必要的数据,以减少网络负载并提高性能。因此,每个设备在启动时都会向 DeviceXApp 服务注册自己,然后只向其发送测量数据。

工作场景

设备(在本例中由 DevicesSimulator 控制台应用程序模拟)通过 HTTP GET 请求向 DeviceXApp 服务注册自己,该请求调用 DeviceXAppDeviceController.RegisterDevice() 远程方法。通过此注册,设备向服务提供其 ID、类型、名称、描述和一些附加数据(例如校准表等)。为了启用安全连接,使用了 HTTPS 协议。RequireHttpsAttribute : AuthorizationFilterAttribute 特性位于一个单独的程序集 RequiresHttps.dll 中,因为该特性在所有 Web 服务中都使用。RegisterDevice() 方法反过来通过远程调用 SignalR ConnectivityHubRegisterDeviceWithDashboard() 方法来注册设备到 DashboardApp Web 服务。每次注册后,设备会继续通过远程调用 DeviceController 类型的 DeviceData() 方法向 DeviceXApp 服务发送其更新数据。该方法根据接收到的数据获取设备状态,并远程异步调用 SignalR ConnectivityHubDashboardApp 服务方法 SetDeviceStatus()。在服务 DashboardApp 中,CommunicationHub 的方法将设备数据和状态放入单例 DeviceDataProvider 类的适当集合中。DashboardApp 通过 ng1ng2 应用程序与最终用户通信,利用全双工 SignalR 通过 DevicesHub 提供设备信息和状态。在初始化时,ng1ng2 应用程序调用 DevicesHubInit() 方法,以获取所有当前活动设备的信息。

HubClientHandler.dll 程序集包含 HubClient 类,该类为 SignalR hub 客户端提供代理。某些环境会在不活动一段时间后自动停止服务。为避免这种情况,提供了一种保持活动机制。KeepAlive.dll 程序集包含 KeepAliveController,它会向自身发送周期性的 HTTP GET 请求。保持活动机制从 Application_BeginRequest() 方法激活。默认情况下禁用,通过将 KeepAliveTimeoutInSec 配置参数设置为正值来启用。

Angular

这些相当小的 ng 应用程序是为了提供 SignalR 通信示例,并感受 ng1ng2 方法之间的差异而开发的。如上所述,我们的 ng1ng2 应用程序支持与 DashboardApp 服务的双工 SignalR 通信。它们的视觉部分相当简单(在图 2 中为 ng2 呈现;对于 ng1,它相同,只是将 (ng2) 替换为 (ng1))。它包含一个带有可展开/折叠行集合的表。每个这样的集合代表一个设备类别,而每行代表一个设备。为每个类别和设备都显示了其名称、描述和最后状态。类别状态反映了属于该类别的设备的最高状态。表会定期更新,其中包含 DashboardApp 推送的数据。在表上方放置了一个显示服务器时间的 HTML <p> 标签和一个带有显式更新的按钮(它看起来不太吸引人,但该按钮说明了从 ng 应用程序到 DashboardApp 的直接调用)。

AngularJS (ng1)

为了启用 ng1 应用程序,适当的 jQuery 和 Angular JavaScript 文件被放置在 .\DevicesManagement\DashboardApp\Scripts 文件夹下的相应文件夹中。文件 .\Scripts\_Custom\ng1.js 包含 ng1 应用程序代码。该应用程序从 .\DevicesManagement\DashboardApp\ng1.html 文件激活,并包含三个主要方法,即:

app.run() 初始化全局变量并启动与 DashboardApp 的连接,使用 devicesHub
devicesServer = app.factory('devicesHubProxy', ... 提供 SignalR 通信机制。  
devicesServer.controller('DevicesController', ... 调用 DashboardApp devicesHubgetServerTime() 方法,订阅来自 devicesHuballallDevicesserverTime 推送调用,使用 devicesHub.on('pushMethodName' , ...,并调用 DashboardApp devicesHubinit() 方法(应在订阅推送调用后调用)。

控制器还包含在 ng1.html 文件中使用的变量和函数的赋值。

为了确保与 SignalR 的通信,ng1.html 文件包含相关引用。

<!-- Reference the SignalR library. -->
<script src="node_modules/jquery.signalR-2.2.0.min.js"></script>

<!-- Reference the autogenerated SignalR hub script. -->
<script src="/signalr/hubs"></script>

Angular 2 (ng2)

撰写本文时,我无意提供 ng2 教程。为此,我推荐一本我认为非常出色的书 [1]。这项工作旨在说明 ng2 应用程序的 SignalR 通信方面。ng2 平台尚未正式发布。在我们的应用程序中使用了版本 2.0.0.-beta.6。ng2 是根据 [1] 中的建议,通过 Node.js 安装的。特别是使用了 Node Package Managementnpm)实用程序。命令

"C:\Program Files\nodejs"\npm init -y

DashboardApp 的工作文件夹 .\Devicesmanagement\DashboardApp 下执行,创建 package.json 配置文件的初始版本。然后手动编辑此文件 - 请参阅其在 .\DashboardApp 文件夹中的修改版本。

命令

"C:\Program Files\nodejs"\npm install

创建 node_module 文件夹,其中包含 package.json 文件中指定的依赖项。新创建的 node_module 文件夹包含 JavaScript 和 TypeScript 文件子文件夹,供我们的应用程序引用。然后,在 .\DashboardApp 文件夹中创建了 config.js 文件来配置 ng2 应用程序。在我们的例子中,它指出我们的应用程序代码在 app 文件夹中,而启动代码位于文件 .\app\main.ts 中。这样的包配置允许我们在 index.html 文件中使用以下行:

<script>System.import('app')</script>

该行加载 .\app\main.ts 的内容。文件 index.html 作为 ng2 应用程序的主要 HTML 文件。其 <head> 标签提供了对相应 JavaScript 文件和样式表 CSS 文件的引用。为了简化,引用的 JavaScript 文件被移动到 .\DashboardApp\node_module 文件夹,该文件夹的其余内容已被删除。文件 index.html 包含与 ng1.htmlng1 情况下的 SignalR 相关引用相同的引用。

ng2 应用程序位于 .\DashboardApp\app 文件夹及其子文件夹中。ng2 应用程序的文件结构如下所示:

DeviceManagement
    DashboardApp
        app
            components
                application			
                    application.css
                    application.html
                    application.ts
			
            services
                communication.ts
				
            main.ts

文件 main.ts 是应用程序的起点。它启动在文件 application.ts 中实现的 ApplicationComponentApplicationComponent 使用负责 SignalR 支持的 CommunicationService(文件 communication.ts)。ApplicationComponent 的代码如下所示:

import {bootstrap} from 'angular2/platform/browser';
import {Component} from 'angular2/core';
import {NgFor} from 'angular2/common';
import {CommunicationService} from 'app/services/communication.ts';

@Component({
    selector: 'main1',
    providers: [CommunicationService],
    templateUrl: 'app/components/application/application.html',
    styleUrls: ['app/components/application/application.css', 'Styles/styles.css'],
    directives: [NgFor]
})
export default class ApplicationComponent {
	
    public currentServerTime: string;

    private comm: CommunicationService;

    public imagesUrl: Array<string> = ['expand.png', 'collapse.png'];
    public imageIndex: Array<int> = [];
    public image: Array<string> = [];
    public categoryIndicator: Array<int> = [];
    public categoryIndicatorAsString: Array<string> = [];
    public allCategoriesInfo: Array<any> = [];
    public allDevicesInfo: Array<any> = [];
    public allDevices: Array<any> = [];
	
    constructor(private comm: CommunicationService) {
		
        if (comm) {		
            this.comm = comm;

            comm.getServerTime(data => this.currentServerTime = data);	
	
            comm.subscribeToBrowserFunction('devicesHub', 'all', lst => {
		
                this.allCategoriesInfo = lst[0];

                if (this.allCategoriesInfo != null)
                    for (var i = 0; i < this.allCategoriesInfo.length; i++) {
                        this.imageIndex.push(0);
                        this.image.push(this.imagesUrl[0]);
                        this.categoryIndicator.push(0);
                        this.categoryIndicatorAsString.push('NoData');
                    }
	
                    this.allDevicesInfo = lst[1];	
			
                    this.allDevicesFunc(lst[2]);
                });
	
                comm.subscribeToBrowserFunction('devicesHub', 'allDevices', devices => {
                    this.allDevicesFunc(devices);
                }
            });
	
            comm.subscribeToBrowserFunction('devicesHub', 'serverTime', timeAsString => {
	    	    this.currentServerTime = timeAsString;
            });
	
            comm.init();
        }
    }
	
    allDevicesFunc(devices: Array<any>) {
		
        this.allDevices = devices;
			
        if (this.allDevicesInfo != null && this.allDevices != null && this.allCategoriesInfo != null && 
            this.allDevicesInfo.length == this.allDevices.length && this.allDevices.length > 0) {
			
            // Overall category indicator
            for (var i = 0; i < this.allCategoriesInfo.length; i++) {
                this.categoryIndicator[i] = 0;

            for (var j = 0; j < this.allDevices.length; j++) {

                if (this.allCategoriesInfo[i].Id == this.allDevicesInfo[j].CategoryId &&
                    this.allDevices[j].Status >= this.categoryIndicator[i]) {
						
                        this.categoryIndicator[i] = this.allDevices[j].Status;
                        this.categoryIndicatorAsString[i] = this.allDevices[j].StatusAsString;
                    }
                }
            }
        }
    }
			
    toggleImage(categoryIndex: int) {
        this.imageIndex[categoryIndex] = (this.imageIndex[categoryIndex] + 1) % 2;
        this.image[categoryIndex] = this.imagesUrl[this.imageIndex[categoryIndex]];
    }
	
    getServerTime() {
        this.comm.getServerTime(data => this.currentServerTime = data);
    }
}

文件 application.ts

 

文件 application.htmlapplication.css 为应用程序提供 HTML 和 CSS 标记。ApplicationComponent 的元数据引用了这些文件,并包含选择器 main1,该选择器在 index.html<body> 中定义为一个标签。ApplicationComponent 的构造函数调用 DashboardAppDevicesHubgetServerTime()init() 方法,并订阅 allallDevicesserverTime 推送调用,因此其行为类似于 ng1 应用程序中的 DevicesController(正如您可能知道的,ng2 中不支持 ng1 控制器)。

CommunicationService(文件 communication.ts)负责与 DashboardApp 进行全双工 SignalR 通信。

export class CommunicationService {

    getServerTime(callback: Function) {
        // Start hub, call .NET method of this hub and call callback on its result.
        $.connection.hub.start().then(() => 
            $.connection.devicesHub.server.getServerTime().then(t => callback(t)));		
    }
	
    init() {
        $.connection.hub.start().then(() => $.connection.devicesHub.server.init());			
    }
	
    subscribeToBrowserFunction(hubName: string, funcName: string, callback: Function) {
        var connection = $.hubConnection('');
        connection.createHubProxy(hubName).on(funcName, data => callback(data));
        connection.start();
    }
}

文件 communication.ts

 

在上面的代码片段中,方法 subscribeToBrowserFunction() 提供了订阅服务器推送调用的基础架构,并将其连接到其处理程序 callback。方法 getServerTime()init() 执行对 DashboardAppDevicesHub 类型的相应方法的远程调用。

 

目前,ng2 应用程序及其引用的文件尚未集成到 DashboardApp Visual Studio 项目中,它们仅通过 index.html 文件链接。ng2 应用程序在运行时不会被转译(即从 TypeScript 编译为 JavaScript)。尽管很明显,预转译允许 ng2 应用程序在任何浏览器中运行,包括那些尚不支持 TypeScript 的浏览器,但我们决定避免繁琐的转译操作,希望浏览器对 ng2 的完全支持只是 ng2 最终发布以及短暂时间的问题。因此,假设在浏览器中进行转译。

注意:目前并非所有现代浏览器都支持转译。在 Windows 上,这个简单的 ng2 应用程序可以在 Google Chrome 和 Mozilla Firefox 中正常运行。Microsoft Edge 无法展开类别,而 Internet Explorer 完全无法运行基于 TypeScript 的 ng2 应用程序。以上所有浏览器都能毫无问题地运行 ng1 应用程序。

运行示例

该示例应使用 Visual Studio(VS 2013 或 VS 2015)进行构建。然后应启动多个启动项目:DeviceXAppDashboardAppDevicesSimulator。为了简单起见,可以使用 IIS Express。等待服务完全启动,然后按 DevicesSimulator 中的任意键开始设备模拟。或者,构建后可以从命令行启动服务。为此,对于 DashboardAppDeviceXApp,以管理员身份启动命令控制台 cmd.exe,将 IIS Express 文件夹(很可能是 C:\Program Files (x86)\IIS Express)设置为当前目录,并为所需站点运行 iisexpress(这些站点应在 IIS Express 配置文件 ..\Documents\IISExpress\config\applicationhost.config 中进行描述)。

C:\...>iisexpress /site:DashboardApp
C:\...>iisexpress /site:DeviceXApp

然后启动 DevicesSimulator,并在服务就绪时按任意键。

要调用客户端应用程序,您应该浏览:

  • 对于 ng1https://:44300/ng1.html ,以及

  • 对于 ng2https://:44300

What Next?

可以将许多功能添加到此示例中,使其成为更现实的解决方案,例如登录、使用数据库持久化 DeviceXApp 中的设备状态、发布到云端(我曾简要尝试从 Visual Studio 将 DeviceXAppDashboardApp 发布为 Microsoft Azure Web Apps - 只有 ng1 版本奏效。显然,浏览器转译尚不支持)。还将 ng2 应用程序完全集成到 Visual Studio 解决方案中并提供适当的转译过程也将是一件好事。

DeviceXApp 中,处理来自设备的请求非常基础(因为这不是本文的重点):每个请求在接收时进行处理,并执行 DashboardApp 方法的调用。对于真实世界的应用程序,应实现更复杂的机制来处理多个并行设备请求。

结论

本文介绍了简单的 ASP.NET Web API 应用程序,配合 AngularJS 和 Angular 2 客户端。这些应用程序使用 SignalR 进行客户端之间的通信,并使用纯 HTTP 请求接收来自外部世界(一些外部设备或其模拟器)的信息。AngularJS 和 Angular 2 客户端共享同一服务这一事实,使我们能够比较 Angular 的两个版本。

谢谢

我非常感谢 Kosta Trosman 对仪表板布局的建议,Meir Bechor 提供的及时且小小的提示,当然还有 Michael Molotsky 在本文范围内进行的非常有益的讨论。

参考文献

[1] Yakov Fain 和 Anton Moiseev 著《Angular 2 Development with TypeScript》,2016 年出版

注意

本文的代码曾托管在 CodePlex,现在已不可用。现在可以直接下载代码。一些引用可以在 NuGet 中找到。

© . All rights reserved.