领先技术

使用 SignalR 构建进度栏

Dino Esposito

下载代码示例

Dino Esposito
在此专栏过去的两篇文章中,我曾讨论了如何构建 ASP.NET 解决方案,以解决从 Web 应用程序的客户端监视远程任务进度的持续问题。尽管 AJAX 取得成功并得到采用,但仍缺乏不借助 Silverlight 或 Flash 即可在 Web 应用程序中显示上下文相关进度栏的被广泛接受的综合解决方案。

老实说,可实现此目的的方法不多。如果您愿意,可以构建自己的解决方案,但基本模式不会与我在以前的专栏中所提供的模式有所不同 — 专门面向 ASP.NET MVC。这个月,我重新讨论同一主题,但我将讨论如何使用以下仍在不断完善的新库构建进度栏: SignalR。

SignalR 是 ASP.NET 团队正在开发的一个 Microsoft .NET Framework 库和 jQuery 插件,可能包括在以后版本的 ASP.NET 平台中。它提供了一些前景极为光明的功能,而这些功能正是 .NET Framework 当前不曾具有的,并且是越来越多的开发者所需要的。

SignalR 概览

SignalR 是一个集成的客户端与服务器库,基于浏览器的客户端和基于 ASP.NET 的服务器组件可以借助它来进行双向多步对话。换句话说,该对话可不受限制地进行单个无状态请求/响应数据交换;它将继续,直到明确关闭。对话通过永久连接进行,允许客户端向服务器发送多个消息,并允许服务器做出相应答复,值得注意的是,还允许服务器向客户端发送异步消息。

我将使用一个聊天应用程序来阐明 SignalR 的主要功能,这不足为奇。客户端通过向服务器发送消息来开始对话;服务器(ASP.NET 终结点)答复并持续侦听新请求。

SignalR 专用于 Web 方案,并需要客户端上有 jQuery 1.6(或更高版本)并且服务器上有 ASP.NET。您可以通过 NuGet 或通过从位于 github.com/SignalR/SignalR 上的 GitHub 存储库直接下载二进制文件来安装 SignalR。图 1 显示了具有所有 SignalR 包的 NuGet 页面。您至少需要下载 SignalR,它依赖框架的服务器端部件 SignalR.Server 和框架的 Web 客户端部件 SignalR.Js。您在图 1 中看到的其他包用于其他特定目的,例如提供 .NET 客户端、Ninject 依赖关系解析程序和基于 HTML5 Web 套接字的备用传输机制。

SignalR Packages Available on the NuGet Platform
图 1 NuGet 平台上提供的 SignalR 包

深入探讨聊天示例

在我尝试构建进度栏解决方案之前,通过查看随可下载的源代码 (archive.msdn.microsoft.com/mag201203CuttingEdge) 一起分发的聊天示例和 Web 上当前提供的(一些)相关文章中引用的其他信息来熟悉库将很有用。但请注意,SignalR 不是已发布项目。

在 ASP.NET MVC 项目的上下文中,您首先引用一些脚本文件,如下所示:

<script src="@Url.Content("~/Scripts/jquery-1.6.4.min.js")"
  type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.signalr.min.js")"
  type="text/javascript"></script>
<script src="@Url.Content("~/signalr/hubs")"
  type="text/javascript"></script>

请注意,SignalR 中没有任何内容是特定于 ASP.NET MVC 的,此库同样可用于 Web 窗体应用程序。

值得关注的是前两个链接引用特定脚本文件。 而第三个链接仍引用一些 JavaScript 内容,但该内容是动态生成的,并依赖宿主 ASP.NET 应用程序中具有的一些其他代码。 还请注意,如果您打算支持低于 Internet Explorer 8 版的版本,则需要 JSON2 库。

页面加载完成时,您便完成了客户端设置并打开连接。 图 2 显示您需要的代码。 您可能希望从 jQuery 的就绪事件中调用此代码。 代码将脚本处理程序绑定到 HTML 元素(非介入式 JavaScript)并准备 SignalR 对话。

图 2 为聊天示例建立 SignalR 库

<script type="text/javascript">
  $(document).ready(function () {    // Add handler to Send button
    $("#sendButton").click(function () {
      chat.send($('#msg').val());
    });
    // Create a proxy for the server endpoint
    var chat = $.connection.chat; 
    // Add a client-side callback to process any data
    // received from the server
    chat.addMessage = function (message) {
      $('#messages').append('<li>' + message + '</li>');
    };
    // Start the conversation
    $.connection.hub.start();
  });
</script>

值得注意的是,$.connection 对象在 SignalR 脚本文件中定义。 而 chat 对象在某种意义上是动态对象,其代码是动态生成的并通过 Hub 脚本引用注入客户端页面。 chat 对象从根本上说是服务器端对象的 JavaScript 代理。 此时应很清楚,图 2 中的客户端代码如果没有强大的服务器端对应部分,则没有多大意义(和作用)。

ASP.NET 项目应包括对 SignalR 程序集及其依赖项(例如 Microsoft.Web.Infrastructure)的引用。 服务器端代码包括与您创建的 JavaScript 对象匹配的托管类。 若要引用图 2 中的代码,您需要拥有与客户端 Chat 对象具有相同的接口的服务器端对象。 此服务器类将从 SignalR 程序集中定义的 Hub 类继承。 下面是类签名:

using System;
using SignalR.Hubs;
namespace SignalrProgressDemo.Progress
{
  public class Chat : Hub
  {
    public void Send(String message)
    {
      Clients.addMessage(message);
    }
  }
}

类中的每个公共方法必须与客户端上的 JavaScript 方法匹配。 或者,至少在 JavaScript 对象上调用的任何方法都必须在服务器类上具有匹配方法。 因此,正如前面所定义的那样,您在图 2 的脚本代码中看到调用的 Send 方法以调用 Chat 对象的 Send 方法结束。 为了将数据发送回客户端,服务器代码将使用 Hub 类上的 Clients 属性。 Clients 成员是动态类型,这使它能够引用动态确定的对象。 具体来说,Clients 属性包含对在以下客户端对象的接口之后构建的服务器端对象的引用: Chat 对象。 因为图 2 中的 Chat 对象具有 addMessage 方法,所以要求服务器端 Chat 对象也公开相同的 addMessage 方法。

准备进度栏演示

现在,让我们使用 SignalR 构建通知系统,该系统将在可能较长的任务期间向客户端报告服务器上执行的任何进度。 作为第一步,让我们创建封装任务的服务器端类。 您为此类分配的名称(任意选择)将影响稍后编写的客户端代码。 这仅仅意味着您有另一个原因来仔细选择类名。 更重要的是,此类将继承自 SignalR 提供的名为 Hub 的类。 下面是签名:

public class BookingHub : Hub
{
  ...
}

BookingHub 类将具有一些公共方法,大部分是接受对预期目的有意义的输入参数的任意序列的 void 方法。 Hub 类上的每个公共方法表示客户端要调用的可能终结点。 例如,让我们添加一个方法来预订机票:

public void BookFlight(String from, String to)
{
  ...
}

此方法需要包含执行给定操作(即预订机票)的所有逻辑。 代码还将包含各个阶段的调用,它们将以某种方式将任何进度报告回客户端。 假设 BookFlight 方法的框架如下所示:

public void BookFlight(String from, String to)
{
  // Book first leg  var ref1 = BookFlight(from, to);  // Book return flight
  var ref2 = BookFlight(to, from);
  // Handle payment
  PayFlight(ref1, ref2);
}

通过结合使用这些主操作,您希望通知用户执行的进度。 Hub 基类提供了一个名为 Clients、定义为动态类型的属性。 换句话说,您将调用此对象上的方法来回调客户端。 但此方法的构成和形式由客户端本身决定。 然后,让我们转到客户端。

正如所提到的,在客户端页面中,您将具有一些在页面加载时运行的脚本代码。 如果您使用 jQuery,则 $(document).ready 事件是运行此代码的好位置。 首先,获取服务器对象的代理:

var bookingHub = $.connection.bookingHub;
// Some config work
...
// Open the connection
$.connection.hub.start();

您在 $.connection SignalR 本机组件上引用的对象的名称只是动态创建的代理,它将 BookingHub 对象的公共接口公开给客户端。 此代理是通过页面的 <script> 部分中具有的 signalr/hubs 链接生成的。 用于名称的命名约定是 camelCase,这意味着 C# 中的 BookingHub 类将成为 JavaScript 中的 bookingHub 对象。 在此对象上,您将找到与服务器对象的公共接口匹配的方法。 另外,对于方法,命名约定使用相同的名称,而非 camelCased。 您可以将单击处理程序添加到 HTML 按钮中并通过 AJAX 启动服务器操作,如下所示:

bookingHub.bookFlight("fco", "jfk");

现在,您可以定义客户端方法来处理任何响应。 例如,您可以在客户端代理上定义接收消息并通过 HTML 范围标记显示消息的 displayMessage 方法:

bookingHub.displayMessage = function (message) {
  $("#msg").html(message);
};

请注意,您负责 display­Message 方法的签名。 您决定传递什么内容和要求任何输入是什么类型。

在结束之前,只剩下最后一个问题: 谁调用 displayMessage 以及谁最终负责传递数据? 答案是服务器端 Hub 代码。 您通过 Clients 对象从 Hub 对象中调用 displayMessage(以及您希望采用的任何其他回调方法)。 图 3 显示了 Hub 类的最终版本。

图 3 Hub 类的最终版本

public void BookFlight(String from, String to)
{
  // Book first leg
  Clients.displayMessage(    String.Format("Booking flight: {0}-{1} ...", from, to));
  Thread.Sleep(2000);
  // Book return
  Clients.displayMessage(    String.Format("Booking flight: {0}-{1} ...", to, from));
  Thread.Sleep(3000);
  // Book return
  Clients.displayMessage(    String.Format("Booking flight: {0}-{1} ...", to, from));
  Thread.Sleep(2000);
  // Some return value
  Clients.displayMessage("Flight booked successfully.");
}

请注意,在本例中,displayMessage 名称必须与您在 JavaScript 代码中使用的大小写完全匹配。 如果您将它错误键入为 DisplayMessage 之类,则不会获得任何异常,但也不会执行任何代码。

Hub 代码作为 Task 对象实现,因此它获取自己的线程来运行,并且不会影响 ASP.NET 线程池。

如果服务器任务导致计划了异步工作,则它会从标准工作线程池中选取线程。 优点是,SignalR 请求处理程序是异步的,这意味着当它们处于等待状态来等待新消息时,它们根本不使用线程。 当收到消息并且有要执行的工作时,将使用 ASP.NET 工作线程。

使用 HTML 的真正进度栏

在过去的专栏中以及在本专栏中,我频繁使用术语进度栏,而没有显示作为客户端 UI 示例的经典规杆。 具有规杆只是非常好的视觉效果,并且在异步基础结构中不需要更复杂的代码。 不过,图 4 显示了动态构建给定百分比值的规杆的 JavaScript 代码。 您可以通过合适的 CSS 类来更改 HTML 元素的外观。

图 4 创建基于 HTML 的规杆

var GaugeBar = GaugeBar || {};
GaugeBar.generate = function (percentage) {
  if (typeof (percentage) != "number")
    return;
  if (percentage > 100 || percentage < 0)
    return;
  var colspan = 1;
  var markup = "<table class='gauge-bar-table'><tr>" +
    "<td style='width:" + percentage.toString() +
    "%' class='gauge-bar-completed'></td>";
  if (percentage < 100) {
    markup += "<td class='gauge-bar-tobedone' style='width:" +
      (100 - percentage).toString() +
      "%'></td>";
    colspan++;
  }
  markup += "</tr><tr class='gauge-bar-statusline'><td colspan='" +
    colspan.toString() +
    "'>" +
    percentage.toString() +
    "% completed</td></tr></table>";
  return markup;
}

您从按钮单击处理程序调用此方法:

bookingHub.updateGaugeBar = function (perc) {
  $("#bar").html(GaugeBar.generate(perc));
};

因此,updateGaugeBar 方法是从使用不同的客户端回调来报告进度的另一个 Hub 方法调用的。 您可以将以前使用的 displayMessage 替换为 Hub 方法中的 updateGaugeBar。

不仅是 Web 客户端

我主要将 SignalR 作为需要 Web 前端的 API 进行介绍。 虽然这可能是您可能希望在其中使用它的最引人注目的方案,但 SignalR 绝不局限于仅支持 Web 客户端。 您可以为 .NET 桌面应用程序下载客户端,而且将很快发布支持 Windows Phone 客户端的另一个客户端。

本专栏仅从 SignalR 提供了最简单有效的编程方法的意义上对其进行了简单介绍。 在以后的专栏中,我将研究它执行的一些不可思议的深层功能以及包如何沿线路移动。 请继续关注。

Dino Esposito是《Programming Microsoft ASP.NET MVC3》(Microsoft Press,2011 年)的作者,同时也是《Microsoft .NET: Architecting Applications for the Enterprise》(Microsoft Press,2008 年)的合著者。 他定居于意大利,经常在世界各地的业内活动中发表演讲。 请关注他的 Twitter:twitter.com/despos

衷心感谢以下技术专家对本文的审阅: Damian Edwards