В данной статье я постараюсь показать пример использования библиотеки SignalR. Напомню, что в
предыдущей была описана библиотека в целом. Для демонстрации будет создано веб-приложение, которое позволяет редактировать таблицу данных одновременно нескольким пользователям в режиме реального времени.
Идея даноого примера не новая, а позаимствована
отсюда. Но в этот раз, она будет реализована с использованием SignalR. Поскольку код этого проекта был написан максимально гибко (это было запланировано заранее), то заменить часть функционала по обеспечению дуплексной связи будет просто, и всё это будет показано детально. И так приступим. Создадим пустое приложение ASP.NET Web Forms 4.5.
Устанавливаем библиотеку SignalR используя NuGet. На момент написания статьи это версия 1.1.3.
Нужно подключить систему маршрутизации, для этого в файл web.config добавим список управляемых модулей.
<system.webServer>
<modules runAllManagedModulesForAllRequests="true" />
</system.webServer>
Регистрируем маршрут для SignalR, он должен быть первым в таблице. Хотя в данном случае он единственный.
using System.Web.SessionState;
namespace SignalRwebApplication
{
public class Global : System.Web.HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
RouteTable.Routes.MapHubs();
}
}
}
Добавим нашу страницу, это обычная веб-форма, со следующим содержимым.
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Products.aspx.cs"
Inherits="SignalRwebApplication.Products" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
<title></title>
<link href="<%#ResolveUrl("~/Content/Styles/ProductsPage.css")%>" rel="stylesheet" />
<script src="<%#ResolveUrl("~/Scripts/jquery-1.6.4.js")%>"
type="text/javascript"></script>
<script src="<%#ResolveUrl("~/Scripts/jquery.signalR-1.1.3.js")%>"
type="text/javascript"></script>
<%--Данный файл автоматически генерируется на сервере.--%>
<script src="<%#ResolveUrl("~/signalr/hubs")%>" type="text/javascript"></script>
<script src="<%#ResolveUrl("~/Scripts/ProductsPage/ProductsPage.js")%>"
type="text/javascript"></script>
</head>
<>
<form id="form1" runat="server">
<div class="tdiv">
<span id="webSocketStatusSpan"></span>
<asp:Repeater ID="ProductsRepeater" runat="server">
<HeaderTemplate>
<table id="ProdTable" class="itable">
<tr id="ProdTableHd">
<th>Ид</th>
<th>Имя</th>
<th>Описание</th>
</tr>
</HeaderTemplate>
<ItemTemplate>
<tr>
<td>
<asp:Label ID="ProductsRepeaterId" ClientIDMode="Static" runat="server"
Text='<%#Eval("Id")%>'></asp:Label>
</td>
<td>
<asp:Label ID="ProductsRepeaterName" ClientIDMode="Static" runat="server"
Text='<%#Eval("Name")%>'></asp:Label>
</td>
<td>
<asp:Label ID="ProductsRepeaterDescription" ClientIDMode="Static" runat="server"
Text='<%#Eval("Description")%>'></asp:Label>
</td>
</tr>
</ItemTemplate>
<FooterTemplate>
</table>
</FooterTemplate>
</asp:Repeater>
</div>
<br />
<div class="ediv">
<ul class="elist">
<li><span>Ид</span><input type="text" id="ProductIdText" disabled="disabled" /></li>
<li><span>Имя</span><input type="text" id="ProductNameText" /></li>
<li><span>Описание</span><input type="text" id="ProductDescriptionText" /></li>
</ul>
<input id="InsertProductButton" type="button" value="Добавить запись" />
<input id="UpdateProductButton" type="button" disabled="disabled"
value="Сохранить запись" />
<input id="DeleteProductButton" type="button" disabled="disabled"
value="Удалить запись" />
</div>
<br />
<div class="mdiv">
<ul id="MessagesList">
</ul>
</div>
</form>
</>
</html>
Тут надо отметить, что менеджер NuGet автоматически добавил старую версию библиотеки jQuery (версия 1.6.4), хотя она вполне пригодна для использования. Поэтому я не стал обновлять на более новую версию. Обратите внимание на порядок использования скриптов.
<script src="<%#ResolveUrl("~/Scripts/jquery-1.6.4.js")%>"
type="text/javascript"></script>
<script src="<%#ResolveUrl("~/Scripts/jquery.signalR-1.1.3.js")%>"
type="text/javascript"></script>
<%--Данный файл автоматически генерируется на сервере.--%>
<script src="<%#ResolveUrl("~/signalr/hubs")%>" type="text/javascript"></script>
<script src="<%#ResolveUrl("~/Scripts/ProductsPage/ProductsPage.js")%>"
type="text/javascript"></script>
Сначала, как обычно, подключается библиотека jQuery. После основная библиотека SignalR, которая основана и использует первую. А вот файла hubs физически нет на сервере, он генерируется "на лету" (динамически) системой. Он содержит прокси-объекты, посредством которорых приложение будет работать с сервером. Об этом чуть позже. Теперь нам нужен код, который будет упралять всеми этими соединениями, а также посылать и принимать данные. Напомню, что в примере из
этой статьи, этим занимался обработчик HTTP-данных (ProductsDataHandler). Но в данном случае библиотека предоставляет абстракцию более высокого уровня, а именно – концентратор (Hub). Добавим его в проект.
Отредактируем его, добавив следующее содержимое.
using System;
using Microsoft.AspNet.SignalR;
namespace SignalRwebApplication
{
public class ProductMessageHub : Hub
{
//Метод обрабатывающий запросы.
public void HandleProductMessage(string receivedString)
{
string responseString = String.Empty;
//Обрабатываем входную строку.
bool dataProcessedSuccessfully =
ProductMessageHandler.HandleMessage(receivedString, ref responseString);
//В случае обновления отправляем данные всем подключённым пользователям.
if(dataProcessedSuccessfully)
Clients.All.handleProductMessage(responseString);
//Иначе, только приславшему.
else
Clients.Caller.handleProductMessage(responseString);
}
}
}
Что касается остального серверного кода, то он позаимствован из
этого проекта, вы можете посмотреть его там. Чтобы не загромаждать статью листингами я не стал его повторно приводить. Теперь о клиентском коде, вот он.
/// <reference path="../jquery-1.6.4.js" />
/// <reference path="../jquery.signalR-1.1.3.js" />
var lastSelectedTableRow;
//Кеш со ссылками на строки таблицы идентифицируемые по Ид продукта.
var productsDataTableRows = new Array();
//Объект концентратора.
var productMessageHubProxy = $.connection.productMessageHub;
//Метод который будет получать приходящие сообщения.
productMessageHubProxy.client.handleProductMessage = function (message)
{
//Метод обрабатывающий сообщения.
ReceivedMessageHandler(message);
}
var rowSelected = false, rowInserted = false;
$(document).ready(function () {
//Устанавливаем соединение с сервером.
$.connection.hub.start().done(function () {
$("#webSocketStatusSpan").text("Соединение установлено.");
});
$("#ProdTable tr").each(function (index, element) {
if ($(element).attr("id") != "ProdTableHd") {
productsDataTableRows.push(FillProductsTableRowObject(element));
};
});
$("#ProdTable tr").click(ProdTableTrClickHandler);
/////////////////////////////////////////////////////////////////////////////
$("#InsertProductButton").click(function () {
rowSelected = false;
rowInserted = true;
ClearDataForm();
$("#ProductIdText").removeAttr('disabled');
$("#UpdateProductButton").removeAttr('disabled');
$(this).attr('disabled', 'disabled');
$("#DeleteProductButton").attr('disabled', 'disabled');
});
/////////////////////////////////////////////////////////////////////////////
$("#UpdateProductButton").click(function () {
if (!rowSelected & !rowInserted)
alert("Строка для обновления не выбрана.");
else if (rowSelected & !rowInserted) {
//Тип сообщения – 2, данные для обновления.
SendProductDataMessage(2);
}
else if (!rowSelected & rowInserted) {
if ($("#ProductIdText").val() == '')
alert("Пустой идентификатор продукта.");
else {
//Тип сообщения – 1, данные для вставки.
SendProductDataMessage(1);
rowInserted = false;
$("#UpdateProductButton").attr('disabled', 'disabled');
}
}
$("#InsertProductButton").removeAttr('disabled');
});
/////////////////////////////////////////////////////////////////////////////
$("#DeleteProductButton").click(function () {
if (!rowSelected)
alert("Строка для удаления не выбрана.");
else {
if (confirm("Удалить выбранную запись?")) {
//Тип сообщения – 3, данные для удаления.
SendProductDataMessage(3);
}
}
}
);
$("#ProductNameText").change(function () {
$("#UpdateProductButton").removeAttr('disabled');
});
$("#ProductDescriptionText").change(function () {
$("#UpdateProductButton").removeAttr('disabled');
});
});
/////////////////////////////////////////////////////////////////////////////
function ProdTableTrClickHandler() {
if ($(this).attr("id") != "ProdTableHd") {
if (lastSelectedTableRow != undefined)
$(lastSelectedTableRow).removeClass("srow");
$(this).addClass("srow");
lastSelectedTableRow = this;
FillDataForm();
rowSelected = true;
rowInserted = false;
$("#ProductIdText").attr('disabled', 'disabled');
$("#InsertProductButton").removeAttr('disabled');
$("#UpdateProductButton").attr('disabled', 'disabled');
$("#DeleteProductButton").removeAttr('disabled');
}
}
/////////////////////////////////////////////////////////////////////////////
function FillProductsTableRowObject(tableRow) {
var tableRowObject = new Object();
tableRowObject.RowLink = tableRow;
tableRowObject.Id = $(tableRow)
.find("span[id*='ProductsRepeaterId']").first().text();
return tableRowObject;
}
/////////////////////////////////////////////////////////////////////////////
function FillDataForm() {
var selectedRow = $("#ProdTable tr.srow");
$("#ProductIdText").val($(selectedRow)
.find("span[id*='ProductsRepeaterId']").first().text());
$("#ProductNameText").val($(selectedRow)
.find("span[id*='ProductsRepeaterName']").first().text());
$("#ProductDescriptionText").val($(selectedRow)
.find("span[id*='ProductsRepeaterDescription']").first().text());
}
/////////////////////////////////////////////////////////////////////////////
function ClearDataForm() {
$("#ProductIdText").val('');
$("#ProductNameText").val('');
$("#ProductDescriptionText").val('');
}
/////////////////////////////////////////////////////////////////////////////
function FindTableRowById(id) {
for (i = 0; i < productsDataTableRows.length; i++) {
if (productsDataTableRows[i].Id == id)
return productsDataTableRows[i];
}
return null;
}
/////////////////////////////////////////////////////////////////////////////
function InsertTableRow(productDataMessage) {
var tableRowHtmlString = '<tr><td><span id="ProductsRepeaterId">'
+ productDataMessage.Product.Id
+ '</span></td><td><span id="ProductsRepeaterName">' + productDataMessage.Product.Name
+ '</span></td><td><span id="ProductsRepeaterDescription">'
+ productDataMessage.Product.Description
+ '</span></td></tr>';
$("#ProdTable tr:last").after(tableRowHtmlString);
$("#ProdTable tr:last").click(ProdTableTrClickHandler);
productsDataTableRows.push(FillProductsTableRowObject($("#ProdTable tr:last")));
}
/////////////////////////////////////////////////////////////////////////////
function UpdateTableRow(productDataMessage) {
var tableRow = FindTableRowById(productDataMessage.Product.Id);
$(tableRow.RowLink).find("span[id*='ProductsRepeaterName']")
.first().text(productDataMessage.Product.Name);
$(tableRow.RowLink).find("span[id*='ProductsRepeaterDescription']")
.first().text(productDataMessage.Product.Description);
}
/////////////////////////////////////////////////////////////////////////////
function RemoveTableRow(productDataMessage) {
var tableRow = FindTableRowById(productDataMessage.Product.Id);
$(tableRow.RowLink).remove();
}
/////////////////////////////////////////////////////////////////////////////
function SendProductDataMessage(messageType) {
//Создаём новое сообщение для отправки.
var productDataMessage = new Object();
productDataMessage.Product = new Object();
//Устанавливаем тип сообщения.
productDataMessage.MessageType = messageType;
//Устанавливаем введённые данные.
productDataMessage.Product.Id = $("#ProductIdText").val();
productDataMessage.Product.Name = $("#ProductNameText").val();
productDataMessage.Product.Description = $("#ProductDescriptionText").val();
SendPoductsData(JSON.stringify(productDataMessage));
}
function ReceivedMessageHandler(data) {
var productDataMessage = JSON.parse(data);
if (productDataMessage.DataProcessedSuccessfully) {
switch (productDataMessage.MessageType) {
case 1: //Новая запись.
InsertTableRow(productDataMessage);
break;
case 2: //Обновление текущей записи.
UpdateTableRow(productDataMessage);
break;
case 3: //Удаление записи.
RemoveTableRow(productDataMessage);
break;
default:
return;
}
}
SetOperationResulStatus(productDataMessage.ResponseMessage);
}
/////////////////////////////////////////////////////////////////////////////
function SetOperationResulStatus(statusString) {
var date = new Date();
var dateString = date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds();
$("#MessagesList").prepend('<li>' + dateString + ' ' + statusString + '</li>');
}
/////////////////////////////////////////////////////////////////////////////
//Метод отправляющий данные.
function SendPoductsData(productDataJsonString) {
//Отправляет данные на сервер.
productMessageHubProxy.server.handleProductMessage(productDataJsonString);
}
Разберём его по порядку. Самое главное тут объект концентратора,
//Объект концентратора.
var productMessageHubProxy = $.connection.productMessageHub;
но откуда он создаётся? Помните о порядке подключения скриптов и о файле hubs, именно там и автоматически создаётся код прокси-объектов, его можно создать и вручную (о том как это сделать читайте на сайте
asp.net). Вот, что мы получаем для данного случая.
/*!
* ASP.NET SignalR JavaScript Library v1.1.3
* http://signalr.net/
*
* Copyright Microsoft Open Technologies, Inc. All rights reserved.
* Licensed under the Apache 2.0
* https://github.com/SignalR/SignalR/blob/master/LICENSE.md
*
*/
/// <reference path="....SignalR.Client.JSScriptsjquery-1.6.4.js" />
/// <reference path="jquery.signalR.js" />
(function ($, window) {
/// <param name="$" type="jQuery" />
"use strict";
if (typeof ($.signalR) !== "function") {
throw new Error("SignalR: SignalR is not loaded. Please ensure
jquery.signalR-x.js is referenced before ~/signalr/hubs.");
}
var signalR = $.signalR;
function makeProxyCallback(hub, callback) {
return function () {
// Call the client hub method
callback.apply(hub, $.makeArray(arguments));
};
}
function registerHubProxies(instance, shouldSubscribe) {
var key, hub, memberKey, memberValue, subscriptionMethod;
for (key in instance) {
if (instance.hasOwnProperty(key)) {
hub = instance[key];
if (!(hub.hubName)) {
// Not a client hub
continue;
}
if (shouldSubscribe) {
// We want to subscribe to the hub events
subscriptionMethod = hub.on;
}
else {
// We want to unsubscribe from the hub events
subscriptionMethod = hub.off;
}
// Loop through all members on the hub and find
//client hub functions to subscribe/unsubscribe
for (memberKey in hub.client) {
if (hub.client.hasOwnProperty(memberKey)) {
memberValue = hub.client[memberKey];
if (!$.isFunction(memberValue)) {
// Not a client hub function
continue;
}
subscriptionMethod.call(hub, memberKey, makeProxyCallback(hub, memberValue));
}
}
}
}
}
$.hubConnection.prototype.createHubProxies = function () {
var proxies = {};
this.starting(function () {
// Register the hub proxies as subscribed
// (instance, shouldSubscribe)
registerHubProxies(proxies, true);
this._registerSubscribedHubs();
}).disconnected(function () {
// Unsubscribe all hub proxies when we "disconnect".
//This is to ensure that we do not re-add functional call backs.
// (instance, shouldSubscribe)
registerHubProxies(proxies, false);
});
proxies.productMessageHub = this.createHubProxy('productMessageHub');
proxies.productMessageHub.client = {};
proxies.productMessageHub.server = {
handleProductMessage: function (receivedString) {
return proxies.productMessageHub.invoke.apply(proxies.productMessageHub,
$.merge(["HandleProductMessage"], $.makeArray(arguments)));
}
};
return proxies;
};
signalR.hub = $.hubConnection("/signalr", { useDefaultPath: false });
$.extend(signalR, signalR.hub.createHubProxies());
}(window.jQuery, window));
Думаю, что теперь уже ясно, что сервер используя рефлексию генерирует скрипт прокси-кода основываясь на имени класса концентратора и его методов. Только вот имена клиентских объектов начинаются с буквы в нижнем регистре. Метод обрабатывающий приходящие данные выглядит так.
//Метод который будет получать приходящие сообщения.
productMessageHubProxy.client.handleProductMessage = function (message)
{
//Метод обрабатывающий сообщения.
ReceivedMessageHandler(message);
}
А тот, который посылает данные, так.
function SendPoductsData(productDataJsonString) {
//Отправляет данные на сервер.
productMessageHubProxy.server.handleProductMessage(productDataJsonString);
}
Осталось запустить приложение и посмотреть.
Готовый проект можно скачать
отсюда.