Tutorial: Transmissão de servidor com SignalR 1.x do ASP.NET

por Patrick Fletcher, Tom Dykstra

Aviso

Esta documentação não é para a versão mais recente do SignalR. Dê uma olhada em ASP.NET Core SignalR.

Este tutorial mostra como criar um aplicativo Web que usa ASP.NET SignalR para fornecer a funcionalidade de difusão do servidor. A difusão do servidor significa que as comunicações enviadas aos clientes são iniciadas pelo servidor. Esse cenário requer uma abordagem de programação diferente dos cenários ponto a ponto, como aplicativos de chat, nos quais as comunicações enviadas aos clientes são iniciadas por um ou mais clientes.

O aplicativo que você criará neste tutorial simula um ticker de ações, um cenário típico para a funcionalidade de difusão do servidor.

Os comentários sobre o tutorial são bem-vindos. Se você tiver dúvidas que não estão diretamente relacionadas ao tutorial, poderá postá-las no fórum do ASP.NET SignalR ou StackOverflow.com.

Visão geral

O pacote NuGet Microsoft.AspNet.SignalR.Sample instala um aplicativo de ticker de ações simulado de exemplo em um projeto do Visual Studio. Na primeira parte deste tutorial, você criará uma versão simplificada desse aplicativo do zero. No restante do tutorial, você instalará o pacote NuGet e examinará os recursos e o código adicionais que ele cria.

O aplicativo de ticker de ações é um representante de um tipo de aplicativo em tempo real no qual você deseja "enviar por push" ou transmitir periodicamente notificações do servidor para todos os clientes conectados.

O aplicativo que você criará na primeira parte deste tutorial exibe uma grade com dados de estoque.

Versão inicial do StockTicker

Periodicamente, o servidor atualiza aleatoriamente os preços das ações e envia as atualizações para todos os clientes conectados. No navegador, os números e símbolos nas colunas Alterar e % mudar dinamicamente em resposta às notificações do servidor. Se você abrir navegadores adicionais para a mesma URL, todos eles mostrarão os mesmos dados e as mesmas alterações nos dados simultaneamente.

Este tutorial contém as seguintes seções:

Observação

Se você não quiser trabalhar nas etapas de criação do aplicativo, instale o pacote SignalR.Sample em um novo projeto De aplicativo Web vazio ASP.NET e leia estas etapas para obter explicações sobre o código. A primeira parte do tutorial aborda um subconjunto do código SignalR.Sample e a segunda parte explica os principais recursos da funcionalidade adicional no pacote SignalR.Sample.

Pré-requisitos

Antes de começar, verifique se você tem o Visual Studio 2012 ou 2010 SP1 instalado no computador. Se você não tiver o Visual Studio, consulte Downloads do ASP.NET para obter o Visual Studio 2012 Express for Web gratuito.

Se você tiver o Visual Studio 2010, verifique se o NuGet está instalado.

Criar o projeto

  1. No menu Arquivo , clique em Novo Projeto.

  2. Na caixa de diálogo Novo Projeto , expanda C# em Modelos e selecione Web.

  3. Selecione o modelo ASP.NET Aplicativo Web Vazio , nomeie o projeto SignalR.StockTicker e clique em OK.

    Caixa de diálogo Novo Projeto

Adicionar os pacotes NuGet do SignalR

Adicionar os pacotes NuGet SignalR e JQuery

Você pode adicionar a funcionalidade do SignalR a um projeto instalando um pacote NuGet.

  1. Clique em Ferramentas | Gerenciador de Pacotes NuGet | Console do Gerenciador de Pacotes.

  2. Insira o comando a seguir no gerenciador de pacotes.

    Install-Package Microsoft.AspNet.SignalR -Version 1.1.3
    

    O pacote SignalR instala vários outros pacotes NuGet como dependências. Quando a instalação for concluída, você terá todos os componentes do servidor e do cliente necessários para usar o SignalR em um aplicativo ASP.NET.

Configurar o código de servidor

Nesta seção, você configura o código que é executado no servidor.

Criar a classe Stock

Comece criando a classe de modelo stock que você usará para armazenar e transmitir informações sobre um estoque.

  1. Crie um novo arquivo de classe na pasta do projeto, nomeie-o stock.cs e substitua o código do modelo pelo seguinte código:

    using System;
    
    namespace SignalR.StockTicker
    {
        public class Stock
        {
            private decimal _price;
    
            public string Symbol { get; set; }
    
            public decimal Price
            {
                get
                {
                    return _price;
                }
                set
                {
                    if (_price == value)
                    {
                        return;
                    }
    
                    _price = value;
    
                    if (DayOpen == 0)
                    {
                        DayOpen = _price;
                    }
                }
            }
    
            public decimal DayOpen { get; private set; }
    
            public decimal Change
            {
                get
                {
                    return Price - DayOpen;
                }
            }
    
            public double PercentChange
            {
                get
                {
                    return (double)Math.Round(Change / Price, 4);
                }
            }
        }
    }
    

    As duas propriedades que você definirá ao criar ações são o Símbolo (por exemplo, MSFT para Microsoft) e o Preço. As outras propriedades dependem de como e quando você define Price. Na primeira vez que você definir Price, o valor será propagado para DayOpen. Nos momentos subsequentes em que você define Price, os valores das propriedades Change e PercentChange são calculados com base na diferença entre Price e DayOpen.

Criar as classes StockTicker e StockTickerHub

Você usará a API do Hub do SignalR para lidar com a interação de servidor para cliente. Uma classe StockTickerHub derivada da classe do Hub SignalR lidará com o recebimento de conexões e chamadas de método de clientes. Você também precisa manter dados de estoque e executar um objeto Timer para disparar periodicamente atualizações de preço, independentemente das conexões de cliente. Você não pode colocar essas funções em uma classe hub, pois as instâncias de Hub são transitórias. Uma instância de classe hub é criada para cada operação no hub, como conexões e chamadas do cliente para o servidor. Portanto, o mecanismo que mantém os dados de ações, atualiza os preços e transmite as atualizações de preços tem que ser executado em uma classe separada, que você nomeará StockTicker.

Difusão do StockTicker

Você só deseja que uma instância da classe StockTicker seja executada no servidor, portanto, você precisará configurar uma referência de cada instância do StockTickerHub para a instância singleton do StockTicker. A classe StockTicker precisa ser capaz de transmitir para clientes porque tem os dados de estoque e dispara atualizações, mas StockTicker não é uma classe Hub. Portanto, a classe StockTicker precisa obter uma referência ao objeto de contexto de conexão do Hub signalR. Em seguida, ele pode usar o objeto de contexto de conexão do SignalR para transmitir aos clientes.

  1. Em Gerenciador de Soluções, clique com o botão direito do mouse no projeto e clique em Adicionar Novo Item.

  2. Se você tiver o Visual Studio 2012 com o ASP.NET and Web Tools Atualização 2012.2, clique em Web em Visual C# e selecione o modelo de item Classe do Hub do SignalR. Caso contrário, selecione o modelo Classe .

  3. Nomeie a nova classe StockTickerHub.cs e clique em Adicionar.

    Adicionar StockTickerHub.cs

  4. Substitua o código do modelo pelo seguinte código:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using Microsoft.AspNet.SignalR;
    using Microsoft.AspNet.SignalR.Hubs;
    
    namespace SignalR.StockTicker
    {
        [HubName("stockTickerMini")]
        public class StockTickerHub : Hub
        {
            private readonly StockTicker _stockTicker;
    
            public StockTickerHub() : this(StockTicker.Instance) { }
    
            public StockTickerHub(StockTicker stockTicker)
            {
                _stockTicker = stockTicker;
            }
    
            public IEnumerable<Stock> GetAllStocks()
            {
                return _stockTicker.GetAllStocks();
            }
        }
    }
    

    A classe Hub é usada para definir métodos que os clientes podem chamar no servidor. Você está definindo um método: GetAllStocks(). Quando um cliente se conectar inicialmente ao servidor, ele chamará esse método para obter uma lista de todas as ações com seus preços atuais. O método pode ser executado de forma síncrona e retornar IEnumerable<Stock> porque está retornando dados da memória. Se o método precisasse obter os dados fazendo algo que envolvesse espera, como uma pesquisa de banco de dados ou uma chamada de serviço Web, você especificaria Task<IEnumerable<Stock>> como o valor retornado para habilitar o processamento assíncrono. Para obter mais informações, consulte ASP.NET Guia de API dos Hubs do SignalR – Servidor – Quando executar de forma assíncrona.

    O atributo HubName especifica como o Hub será referenciado no código JavaScript no cliente. O nome padrão no cliente se você não usar esse atributo é uma versão com maiúsculas e minúsculas do nome da classe, que nesse caso seria stockTickerHub.

    Como você verá posteriormente ao criar a classe StockTicker, uma instância singleton dessa classe é criada em sua propriedade Instance estática. Essa instância singleton do StockTicker permanece na memória, independentemente de quantos clientes se conectam ou se desconectam, e essa instância é o que o método GetAllStocks usa para retornar informações de estoque atuais.

  5. Crie um novo arquivo de classe na pasta do projeto, nomeie-o StockTicker.cs e substitua o código do modelo pelo seguinte código:

    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Threading;
    using Microsoft.AspNet.SignalR;
    using Microsoft.AspNet.SignalR.Hubs;
    
    namespace SignalR.StockTicker
    {
        public class StockTicker
        {
            // Singleton instance
            private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));
    
            private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();
    
            private readonly object _updateStockPricesLock = new object();
    
            //stock can go up or down by a percentage of this factor on each change
            private readonly double _rangePercent = .002;
    
            private readonly TimeSpan _updateInterval = TimeSpan.FromMilliseconds(250);
            private readonly Random _updateOrNotRandom = new Random();
    
            private readonly Timer _timer;
            private volatile bool _updatingStockPrices = false;
    
            private StockTicker(IHubConnectionContext clients)
            {
                Clients = clients;
    
                _stocks.Clear();
                var stocks = new List<Stock>
                {
                    new Stock { Symbol = "MSFT", Price = 30.31m },
                    new Stock { Symbol = "APPL", Price = 578.18m },
                    new Stock { Symbol = "GOOG", Price = 570.30m }
                };
                stocks.ForEach(stock => _stocks.TryAdd(stock.Symbol, stock));
    
                _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);
    
            }
    
            public static StockTicker Instance
            {
                get
                {
                    return _instance.Value;
                }
            }
    
            private IHubConnectionContext Clients
            {
                get;
                set;
            }
    
            public IEnumerable<Stock> GetAllStocks()
            {
                return _stocks.Values;
            }
    
            private void UpdateStockPrices(object state)
            {
                lock (_updateStockPricesLock)
                {
                    if (!_updatingStockPrices)
                    {
                        _updatingStockPrices = true;
    
                        foreach (var stock in _stocks.Values)
                        {
                            if (TryUpdateStockPrice(stock))
                            {
                                BroadcastStockPrice(stock);
                            }
                        }
    
                        _updatingStockPrices = false;
                    }
                }
            }
    
            private bool TryUpdateStockPrice(Stock stock)
            {
                // Randomly choose whether to update this stock or not
                var r = _updateOrNotRandom.NextDouble();
                if (r > .1)
                {
                    return false;
                }
    
                // Update the stock price by a random factor of the range percent
                var random = new Random((int)Math.Floor(stock.Price));
                var percentChange = random.NextDouble() * _rangePercent;
                var pos = random.NextDouble() > .51;
                var change = Math.Round(stock.Price * (decimal)percentChange, 2);
                change = pos ? change : -change;
    
                stock.Price += change;
                return true;
            }
    
            private void BroadcastStockPrice(Stock stock)
            {
                Clients.All.updateStockPrice(stock);
            }
    
        }
    }
    

    Como vários threads estarão executando a mesma instância do código StockTicker, a classe StockTicker deve ser threadsafe.

    Armazenando a instância singleton em um campo estático

    O código inicializa o campo de _instance estático que apoia a propriedade Instance com uma instância da classe e essa é a única instância da classe que pode ser criada, pois o construtor está marcado como privado. A inicialização lenta é usada para o campo _instance, não por motivos de desempenho, mas para garantir que a criação da instância seja threadsafe.

    private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));
    
    public static StockTicker Instance
    {
        get
        {
            return _instance.Value;
        }
    }
    

    Sempre que um cliente se conecta ao servidor, uma nova instância da classe StockTickerHub em execução em um thread separado obtém a instância singleton StockTicker da propriedade estática StockTicker.Instance, como você viu anteriormente na classe StockTickerHub.

    Armazenando dados de ações em um ConcurrentDictionary

    O construtor inicializa a coleção _stocks com alguns dados de estoque de exemplo e GetAllStocks retorna as ações. Como você viu anteriormente, essa coleção de ações, por sua vez, é retornada por StockTickerHub.GetAllStocks, que é um método de servidor na classe Hub que os clientes podem chamar.

    private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();
    
    private StockTicker(IHubConnectionContext clients)
    {
        Clients = clients;
    
        _stocks.Clear();
        var stocks = new List<Stock>
        {
            new Stock { Symbol = "MSFT", Price = 30.31m },
            new Stock { Symbol = "APPL", Price = 578.18m },
            new Stock { Symbol = "GOOG", Price = 570.30m }
        };
        stocks.ForEach(stock => _stocks.TryAdd(stock.Symbol, stock));
    
        _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);
    }
    
    public IEnumerable<Stock> GetAllStocks()
    {
        return _stocks.Values;
    }
    

    A coleção de ações é definida como um tipo ConcurrentDictionary para segurança de thread. Como alternativa, você pode usar um objeto Dictionary e bloquear explicitamente o dicionário quando fizer alterações nele.

    Para este aplicativo de exemplo, não há problema em armazenar dados do aplicativo na memória e perder os dados quando a instância do StockTicker for descartada. Em um aplicativo real, você trabalharia com um armazenamento de dados de back-end, como um banco de dados.

    Atualização periódica dos preços das ações

    O construtor inicia um objeto Timer que chama periodicamente métodos que atualizam os preços das ações aleatoriamente.

    _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);
    
    private void UpdateStockPrices(object state)
    {
        lock (_updateStockPricesLock)
        {
            if (!_updatingStockPrices)
            {
                _updatingStockPrices = true;
    
                foreach (var stock in _stocks.Values)
                {
                    if (TryUpdateStockPrice(stock))
                    {
                        BroadcastStockPrice(stock);
                    }
                }
    
                _updatingStockPrices = false;
            }
        }
    }
    
    private bool TryUpdateStockPrice(Stock stock)
    {
        // Randomly choose whether to update this stock or not
        var r = _updateOrNotRandom.NextDouble();
        if (r > .1)
        {
            return false;
        }
    
        // Update the stock price by a random factor of the range percent
        var random = new Random((int)Math.Floor(stock.Price));
        var percentChange = random.NextDouble() * _rangePercent;
        var pos = random.NextDouble() > .51;
        var change = Math.Round(stock.Price * (decimal)percentChange, 2);
        change = pos ? change : -change;
    
        stock.Price += change;
        return true;
    }
    

    UpdateStockPrices é chamado pelo Timer, que passa nulo no parâmetro state. Antes de atualizar os preços, um bloqueio é feito no objeto _updateStockPricesLock. O código verifica se outro thread já está atualizando os preços e chama TryUpdateStockPrice em cada ação na lista. O método TryUpdateStockPrice decide se deseja alterar o preço das ações e quanto alterá-lo. Se o preço das ações for alterado, BroadcastStockPrice será chamado para transmitir a alteração do preço das ações para todos os clientes conectados.

    O sinalizador _updatingStockPrices é marcado como volátil para garantir que o acesso a ele seja threadsafe.

    private volatile bool _updatingStockPrices = false;
    

    Em um aplicativo real, o método TryUpdateStockPrice chamaria um serviço Web para pesquisar o preço; nesse código, ele usa um gerador de número aleatório para fazer alterações aleatoriamente.

    Obtendo o contexto do SignalR para que a classe StockTicker possa transmitir para clientes

    Como as alterações de preço se originam aqui no objeto StockTicker, esse é o objeto que precisa chamar um método updateStockPrice em todos os clientes conectados. Em uma classe Hub, você tem uma API para chamar métodos de cliente, mas o StockTicker não deriva da classe Hub e não tem uma referência a nenhum objeto Hub. Portanto, para transmitir para clientes conectados, a classe StockTicker precisa obter a instância de contexto do SignalR para a classe StockTickerHub e usá-lo para chamar métodos em clientes.

    O código obtém uma referência ao contexto signalr quando cria a instância de classe singleton, passa essa referência para o construtor e o construtor a coloca na propriedade Clients.

    Há dois motivos pelos quais você deseja obter o contexto apenas uma vez: obter o contexto é uma operação cara e obtê-lo uma vez garante que a ordem pretendida de mensagens enviadas aos clientes seja preservada.

    private readonly static Lazy<StockTicker> _instance =
        new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));
    
    private StockTicker(IHubConnectionContext clients)
    {
        Clients = clients;
    
        // Remainder of constructor ...
    }
    
    private IHubConnectionContext Clients
    {
        get;
        set;
    }
    
    private void BroadcastStockPrice(Stock stock)
    {
        Clients.All.updateStockPrice(stock);
    }
    

    Obter a propriedade Clients do contexto e colocá-la na propriedade StockTickerClient permite que você escreva código para chamar métodos cliente que têm a mesma aparência que em uma classe Hub. Por exemplo, para difundir para todos os clientes, você pode escrever Clients.All.updateStockPrice(stock).

    O método updateStockPrice que você está chamando em BroadcastStockPrice ainda não existe; você o adicionará mais tarde ao escrever um código que é executado no cliente. Você pode consultar updateStockPrice aqui porque Clients.All é dinâmico, o que significa que a expressão será avaliada em runtime. Quando essa chamada de método for executada, o SignalR enviará o nome do método e o valor do parâmetro para o cliente e, se o cliente tiver um método chamado updateStockPrice, esse método será chamado e o valor do parâmetro será passado para ele.

    Clients.All significa enviar para todos os clientes. O SignalR oferece outras opções para especificar para quais clientes ou grupos de clientes enviar. Para obter mais informações, consulte HubConnectionContext.

Registrar a rota do SignalR

O servidor precisa saber qual URL interceptar e direcionar para o SignalR. Para fazer isso, você adicionará algum código ao arquivo Global.asax .

  1. Em Gerenciador de Soluções, clique com o botão direito do mouse no projeto e clique em Adicionar Novo Item.

  2. Selecione o modelo de item Classe de Aplicativo Global e clique em Adicionar.

    Adicionar global.asax

  3. Adicione o código de registro de rota do SignalR ao método Application_Start:

    protected void Application_Start(object sender, EventArgs e)
    {
        RouteTable.Routes.MapHubs();
    }
    

    Por padrão, a URL base para todo o tráfego do SignalR é "/signalr" e "/signalr/hubs" é usada para recuperar um arquivo JavaScript gerado dinamicamente que define proxies para todos os Hubs que você tem em seu aplicativo. O método MapHubs inclui sobrecargas que permitem especificar uma URL base diferente e determinadas opções do SignalR em uma instância da classe HubConfiguration .

  4. Adicione uma instrução using na parte superior do arquivo:

    using System.Web.Routing;
    
  5. Salve e feche o arquivo Global.asax e compile o projeto.

Você concluiu a configuração do código do servidor. Na próxima seção, você configurará o cliente.

Configurar o código do cliente

  1. Crie um novo arquivo HTML na pasta do projeto e nomeie-o StockTicker.html.

  2. Substitua o código do modelo pelo seguinte código:

    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>ASP.NET SignalR Stock Ticker</title>
        <style>
            body {
                font-family: 'Segoe UI', Arial, Helvetica, sans-serif;
                font-size: 16px;
            }
            #stockTable table {
                border-collapse: collapse;
            }
                #stockTable table th, #stockTable table td {
                    padding: 2px 6px;
                }
                #stockTable table td {
                    text-align: right;
                }
            #stockTable .loading td {
                text-align: left;
            }
        </style>
    </head>
    <body>
        <h1>ASP.NET SignalR Stock Ticker Sample</h1>
    
        <h2>Live Stock Table</h2>
        <div id="stockTable">
            <table border="1">
                <thead>
                    <tr><th>Symbol</th><th>Price</th><th>Open</th><th>Change</th><th>%</th></tr>
                </thead>
                <tbody>
                    <tr class="loading"><td colspan="5">loading...</td></tr>
                </tbody>
            </table>
        </div>
    
        <!--Script references. -->
        <!--Reference the jQuery library. -->
        <script src="/Scripts/jquery-1.8.2.min.js" ></script>
        <!--Reference the SignalR library. -->
        <script src="/Scripts/jquery.signalR-1.0.1.js"></script>
        <!--Reference the autogenerated SignalR hub script. -->
        <script src="/signalr/hubs"></script>
        <!--Reference the StockTicker script. -->
        <script src="StockTicker.js"></script>
    </body>
    </html>
    

    O HTML cria uma tabela com 5 colunas, uma linha de cabeçalho e uma linha de dados com uma única célula que abrange todas as cinco colunas. A linha de dados exibe "carregamento..." e só serão mostrados momentaneamente quando o aplicativo for iniciado. O código JavaScript removerá essa linha e adicionará em suas linhas de lugar com dados de estoque recuperados do servidor.

    As marcas de script especificam o arquivo de script jQuery, o arquivo de script principal do SignalR, o arquivo de script de proxies do SignalR e um arquivo de script StockTicker que você criará posteriormente. O arquivo de script de proxies do SignalR, que especifica a URL "/signalr/hubs", é gerado dinamicamente e define métodos de proxy para os métodos na classe Hub, nesse caso para StockTickerHub.GetAllStocks. Se preferir, você pode gerar esse arquivo JavaScript manualmente usando utilitários signalr e desabilitar a criação de arquivo dinâmico na chamada do método MapHubs.

  3. Importante

    Verifique se as referências de arquivo JavaScript em StockTicker.html estão corretas. Ou seja, verifique se a versão jQuery na marca de script (1.8.2 no exemplo) é a mesma da versão jQuery na pasta Scripts do projeto e verifique se a versão do SignalR na marca de script é a mesma da versão do SignalR na pasta Scripts do projeto. Altere os nomes de arquivo nas marcas de script, se necessário.

  4. Em Gerenciador de Soluções, clique com o botão direito do mouse emStockTicker.htmle clique em Definir como Página Inicial.

  5. Crie um novo arquivo JavaScript na pasta do projeto e nomeie-o StockTicker.js..

  6. Substitua o código do modelo pelo seguinte código:

    // A simple templating method for replacing placeholders enclosed in curly braces.
    if (!String.prototype.supplant) {
        String.prototype.supplant = function (o) {
            return this.replace(/{([^{}]*)}/g,
                function (a, b) {
                    var r = o[b];
                    return typeof r === 'string' || typeof r === 'number' ? r : a;
                }
            );
        };
    }
    
    $(function () {
    
        var ticker = $.connection.stockTickerMini, // the generated client-side hub proxy
            up = '▲',
            down = '▼',
            $stockTable = $('#stockTable'),
            $stockTableBody = $stockTable.find('tbody'),
            rowTemplate = '<tr data-symbol="{Symbol}"><td>{Symbol}</td><td>{Price}</td><td>{DayOpen}</td><td>{Direction} {Change}</td><td>{PercentChange}</td></tr>';
    
        function formatStock(stock) {
            return $.extend(stock, {
                Price: stock.Price.toFixed(2),
                PercentChange: (stock.PercentChange * 100).toFixed(2) + '%',
                Direction: stock.Change === 0 ? '' : stock.Change >= 0 ? up : down
            });
        }
    
        function init() {
            ticker.server.getAllStocks().done(function (stocks) {
                $stockTableBody.empty();
                $.each(stocks, function () {
                    var stock = formatStock(this);
                    $stockTableBody.append(rowTemplate.supplant(stock));
                });
            });
        }
    
        // Add a client-side hub method that the server will call
        ticker.client.updateStockPrice = function (stock) {
            var displayStock = formatStock(stock),
                $row = $(rowTemplate.supplant(displayStock));
    
            $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']')
                .replaceWith($row);
            }
    
        // Start the connection
        $.connection.hub.start().done(init);
    
    });
    

    $.connection refere-se aos proxies do SignalR. O código obtém uma referência ao proxy para a classe StockTickerHub e o coloca na variável ticker. O nome do proxy é o nome que foi definido pelo atributo [HubName]:

    var ticker = $.connection.stockTickerMini
    
    [HubName("stockTickerMini")]
    public class StockTickerHub : Hub
    

    Depois que todas as variáveis e funções são definidas, a última linha de código no arquivo inicializa a conexão do SignalR chamando a função de início do SignalR. A função start é executada de forma assíncrona e retorna um objeto jQuery Deferred, o que significa que você pode chamar a função concluída para especificar a função a ser chamada quando a operação assíncrona for concluída..

    $.connection.hub.start().done(init);
    

    A função init chama a função getAllStocks no servidor e usa as informações que o servidor retorna para atualizar a tabela de ações. Observe que, por padrão, você precisa usar maiúsculas e minúsculas no cliente, embora o nome do método seja pascal-cased no servidor. A regra de uso de maiúsculas e minúsculas só se aplica a métodos, não a objetos. Por exemplo, você se refere a estoque. Símbolo e estoque. Preço, não stock.symbol ou stock.price.

    function init() {
        ticker.server.getAllStocks().done(function (stocks) {
            $stockTableBody.empty();
            $.each(stocks, function () {
                var stock = formatStock(this);
                $stockTableBody.append(rowTemplate.supplant(stock));
            });
        });
    }
    
    public IEnumerable<Stock> GetAllStocks()
    {
        return _stockTicker.GetAllStocks();
    }
    

    Se você quisesse usar pascal casing no cliente ou se quisesse usar um nome de método completamente diferente, poderia decorar o método Hub com o atributo HubMethodName da mesma forma que você decorou a própria classe Hub com o atributo HubName.

    No método init, HTML para uma linha de tabela é criado para cada objeto de estoque recebido do servidor chamando formatStock para formatar propriedades do objeto stock e, em seguida, chamando suplant (que é definido na parte superior de StockTicker.js) para substituir espaços reservados na variável rowTemplate pelos valores da propriedade do objeto stock. O HTML resultante é acrescentado à tabela de ações.

    Você chama a inicialização passando-a como uma função de retorno de chamada que é executada após a conclusão da função inicial assíncrona. Se você chamar init como uma instrução JavaScript separada após a chamada iniciar, a função falhará porque ela seria executada imediatamente sem esperar que a função inicial terminasse de estabelecer a conexão. Nesse caso, a função init tentaria chamar a função getAllStocks antes que a conexão do servidor fosse estabelecida.

    Quando o servidor altera o preço de uma ação, ele chama updateStockPrice em clientes conectados. A função é adicionada à propriedade cliente do proxy stockTicker para disponibilizá-la para chamadas do servidor.

    ticker.client.updateStockPrice = function (stock) {
        var displayStock = formatStock(stock),
            $row = $(rowTemplate.supplant(displayStock));
    
        $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']')
            .replaceWith($row);
        }
    

    A função updateStockPrice formata um objeto de estoque recebido do servidor em uma linha de tabela da mesma forma que na função de inicialização. No entanto, em vez de acrescentar a linha à tabela, ela localiza a linha atual do estoque na tabela e substitui essa linha pela nova.

Testar o aplicativo

  1. Pressione F5 para executar o aplicativo em modo de depuração.

    A tabela de ações exibe inicialmente o "carregamento..." linha, depois de um pequeno atraso, os dados iniciais de ações são exibidos e, em seguida, os preços das ações começam a mudar.

    Carregar

    Tabela de ações inicial

    Tabela de ações recebendo alterações do servidor

  2. Copie a URL da barra de endereços do navegador e cole-a em uma ou mais novas janelas do navegador.

    A exibição inicial do estoque é a mesma que o primeiro navegador e as alterações ocorrem simultaneamente.

  3. Feche todos os navegadores e abra um novo navegador e vá para a mesma URL.

    O objeto singleton StockTicker continuou a ser executado no servidor, portanto, a exibição da tabela de ações mostra que as ações continuaram a ser alteradas. (Você não vê a tabela inicial sem números de alteração.)

  4. Feche o navegador.

Habilitar o registro em log

O SignalR tem uma função de log interna que você pode habilitar no cliente para ajudar na solução de problemas. Nesta seção, você habilita o registro em log e vê exemplos que mostram como os logs informam quais dos seguintes métodos de transporte o SignalR está usando:

Para qualquer conexão determinada, o SignalR escolhe o melhor método de transporte que o servidor e o cliente dão suporte.

  1. Abra StockTicker.js e adicione uma linha de código para habilitar o registro em log imediatamente antes do código que inicializa a conexão no final do arquivo:

    // Start the connection
    $.connection.hub.logging = true;
    $.connection.hub.start().done(init);
    
  2. Pressione F5 para executar o projeto.

  3. Abra a janela ferramentas de desenvolvedor do navegador e selecione o Console para ver os logs. Talvez seja necessário atualizar a página para ver os logs do Signalr negociando o método de transporte para uma nova conexão.

    Se você estiver executando a Internet Explorer 10 no Windows 8 (IIS 8), o método de transporte será WebSockets.

    IE 10 IIS 8 Console

    Se você estiver executando o Internet Explorer 10 no Windows 7 (IIS 7.5), o método de transporte será iframe.

    Console do IE 10, IIS 7.5

    No Firefox, instale o suplemento Firebug para obter uma janela console. Se você estiver executando o Firefox 19 no Windows 8 (IIS 8), o método de transporte será WebSockets.

    Firefox 19 IIS 8 Websockets

    Se você estiver executando o Firefox 19 no Windows 7 (IIS 7.5), o método de transporte será eventos enviados pelo servidor.

    Firefox 19 IIS 7.5 Console

Instalar e examinar o exemplo completo do StockTicker

O aplicativo StockTicker instalado pelo pacote NuGet Microsoft.AspNet.SignalR.Sample inclui mais recursos do que a versão simplificada que você acabou de criar do zero. Nesta seção do tutorial, você instalará o pacote NuGet e examinará os novos recursos e o código que os implementa.

Instalar o pacote NuGet SignalR.Sample

  1. Em Gerenciador de Soluções, clique com o botão direito do mouse no projeto e clique em Gerenciar Pacotes NuGet.

  2. Na caixa de diálogo Gerenciar Pacotes NuGet , clique em Online, insira SignalR.Sample na caixa Pesquisar Online e clique em Instalar no pacote SignalR.Sample .

    Instalar o pacote SignalR.Sample

  3. No arquivo Global.asax , comente o RouteTable.Routes.MapHubs(); linha que você adicionou anteriormente no método Application_Start.

    O código em Global.asax não é mais necessário porque o pacote SignalR.Sample registra a rota SignalR no arquivo App_Start/RegisterHubs.cs :

    [assembly: WebActivator.PreApplicationStartMethod(typeof(SignalR.StockTicker.RegisterHubs), "Start")]
    
    namespace SignalR.StockTicker
    {
        public static class RegisterHubs
        {
            public static void Start()
            {
                // Register the default hubs route: ~/signalr/hubs
                RouteTable.Routes.MapHubs();
            }
        }
    }
    

    A classe WebActivator referenciada pelo atributo assembly está incluída no pacote NuGet WebActivatorEx, que é instalado como uma dependência do pacote SignalR.Sample.

  4. Em Gerenciador de Soluções, expanda a pasta SignalR.Sample que foi criada instalando o pacote SignalR.Sample.

  5. Na pasta SignalR.Sample , clique com o botão direito do mouse emStockTicker.htmle clique em Definir como Página Inicial.

    Observação

    A instalação do pacote NuGet SignalR.Sample pode alterar a versão do jQuery que você tem na pasta Scripts . O novo arquivoStockTicker.html que o pacote instala na pasta SignalR.Sample estará sincronizado com a versão jQuery que o pacote instala, mas se você quiser executar seu arquivo deStockTicker.html original novamente, talvez seja necessário atualizar a referência jQuery na marca de script primeiro.

Executar o aplicativo

  1. Pressione F5 para executar o aplicativo.

    Além da grade que você viu anteriormente, o aplicativo de ticker de estoque completo mostra uma janela de rolagem horizontal que exibe os mesmos dados de estoque. Quando você executa o aplicativo pela primeira vez, o "mercado" é "fechado" e você vê uma grade estática e uma janela de ticker que não está rolando.

    Início da tela do StockTicker

    Quando você clica em Abrir Mercado, a caixa Escala de Ações Dinâmicas começa a rolar horizontalmente e o servidor começa a difundir periodicamente as alterações de preço das ações aleatoriamente. Cada vez que um preço das ações muda, tanto a grade tabela de ações dinâmica quanto a caixa Demarque de Ações Dinâmicas são atualizadas. Quando a alteração de preço de uma ação é positiva, a ação é mostrada com um plano de fundo verde e quando a alteração é negativa, a ação é mostrada com um fundo vermelho.

    Aplicativo StockTicker, aberto pelo mercado

    O botão Fechar Mercado interrompe as alterações e interrompe a rolagem do ticker e o botão Redefinir redefine todos os dados de estoque para o estado inicial antes que as alterações de preço sejam iniciadas. Se você abrir mais janelas do navegador e acessar a mesma URL, verá os mesmos dados atualizados dinamicamente ao mesmo tempo em cada navegador. Quando você clica em um dos botões, todos os navegadores respondem da mesma maneira ao mesmo tempo.

Live Stock Ticker display

A exibição live stock ticker é uma lista não ordenada em um elemento div que é formatado em uma única linha por estilos CSS. O ticker é inicializado e atualizado da mesma maneira que a tabela: substituindo espaços reservados em uma <cadeia de caracteres de modelo li> e adicionando dinamicamente os <elementos li> ao <elemento ul> . A rolagem é executada usando a função de animação jQuery para variar a margem esquerda da lista não ordenada dentro do div.

O HTML do ticker de ações:

<h2>Live Stock Ticker</h2>
<div id="stockTicker">
    <div class="inner">
        <ul>
            <li class="loading">loading...</li>
        </ul>
    </div>
</div>

O CSS do ticker de ações:

#stockTicker {
    overflow: hidden;
    width: 450px;
    height: 24px;
    border: 1px solid #999;
    }

    #stockTicker .inner {
        width: 9999px;
    }

    #stockTicker ul {
        display: inline-block;
        list-style-type: none;
        margin: 0;
        padding: 0;
    }

    #stockTicker li {
        display: inline-block;
        margin-right: 8px;   
    }

    /*<li data-symbol="{Symbol}"><span class="symbol">{Symbol}</span><span class="price">{Price}</span><span class="change">{PercentChange}</span></li>*/
    #stockTicker .symbol {
        font-weight: bold;
    }

    #stockTicker .change {
        font-style: italic;
    }

O código jQuery que o faz rolar:

function scrollTicker() {
    var w = $stockTickerUl.width();
    $stockTickerUl.css({ marginLeft: w });
    $stockTickerUl.animate({ marginLeft: -w }, 15000, 'linear', scrollTicker);
}

Métodos adicionais no servidor que o cliente pode chamar

A classe StockTickerHub define quatro métodos adicionais que o cliente pode chamar:

public string GetMarketState()
{
    return _stockTicker.MarketState.ToString();
}

public void OpenMarket()
{
    _stockTicker.OpenMarket();
}

public void CloseMarket()
{
    _stockTicker.CloseMarket();
}

public void Reset()
{
    _stockTicker.Reset();
}

OpenMarket, CloseMarket e Reset são chamados em resposta aos botões na parte superior da página. Eles demonstram o padrão de um cliente disparando uma alteração no estado que é propagada imediatamente para todos os clientes. Cada um desses métodos chama um método na classe StockTicker que afeta a mudança de estado do mercado e, em seguida, transmite o novo estado.

Na classe StockTicker, o estado do mercado é mantido por uma propriedade MarketState que retorna um valor de enumeração MarketState:

public MarketState MarketState
{
    get { return _marketState; }
    private set { _marketState = value; }
}

public enum MarketState
{
    Closed,
    Open
}

Cada um dos métodos que alteram o estado de mercado faz isso dentro de um bloco de bloqueio porque a classe StockTicker precisa ser threadsafe:

public void OpenMarket()
{
    lock (_marketStateLock)
    {
        if (MarketState != MarketState.Open)
        {
            _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);
            MarketState = MarketState.Open;
            BroadcastMarketStateChange(MarketState.Open);
        }
    }
}

public void CloseMarket()
{
    lock (_marketStateLock)
    {
        if (MarketState == MarketState.Open)
        {
            if (_timer != null)
            {
                _timer.Dispose();
            }
            MarketState = MarketState.Closed;
            BroadcastMarketStateChange(MarketState.Closed);
        }
    }
}

public void Reset()
{
    lock (_marketStateLock)
    {
        if (MarketState != MarketState.Closed)
        {
            throw new InvalidOperationException("Market must be closed before it can be reset.");
        }
        LoadDefaultStocks();
        BroadcastMarketReset();
    }
}

Para garantir que esse código seja threadsafe, o campo _marketState que apoia a propriedade MarketState está marcado como volátil,

private volatile MarketState _marketState;

Os métodos BroadcastMarketStateChange e BroadcastMarketReset são semelhantes ao método BroadcastStockPrice que você já viu, exceto que eles chamam diferentes métodos definidos no cliente:

private void BroadcastMarketStateChange(MarketState marketState)
{
    switch (marketState)
    {
        case MarketState.Open:
            Clients.All.marketOpened();
            break;
        case MarketState.Closed:
            Clients.All.marketClosed();
            break;
        default:
            break;
    }
}

private void BroadcastMarketReset()
{
    Clients.All.marketReset();
}

Funções adicionais no cliente que o servidor pode chamar

A função updateStockPrice agora manipula a grade e a tela do ticker e usa jQuery.Color para exibir cores vermelhas e verdes.

As novas funções no SignalR.StockTicker.js habilitam e desabilitam os botões com base no estado do mercado e param ou iniciam a rolagem horizontal da janela do ticker. Como várias funções estão sendo adicionadas ao ticker.client, a função de extensão jQuery é usada para adicioná-las.

$.extend(ticker.client, {
    updateStockPrice: function (stock) {
        var displayStock = formatStock(stock),
            $row = $(rowTemplate.supplant(displayStock)),
            $li = $(liTemplate.supplant(displayStock)),
            bg = stock.LastChange === 0
                ? '255,216,0' // yellow
                : stock.LastChange > 0
                    ? '154,240,117' // green
                    : '255,148,148'; // red

        $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']')
            .replaceWith($row);
        $stockTickerUl.find('li[data-symbol=' + stock.Symbol + ']')
            .replaceWith($li);

        $row.flash(bg, 1000);
        $li.flash(bg, 1000);
    },

    marketOpened: function () {
        $("#open").prop("disabled", true);
        $("#close").prop("disabled", false);
        $("#reset").prop("disabled", true);
        scrollTicker();
    },

    marketClosed: function () {
        $("#open").prop("disabled", false);
        $("#close").prop("disabled", true);
        $("#reset").prop("disabled", false);
        stopTicker();
    },

    marketReset: function () {
        return init();
    }
});

Configuração adicional do cliente após estabelecer a conexão

Depois que o cliente estabelece a conexão, ele tem algum trabalho adicional a ser feito: descubra se o mercado está aberto ou fechado para chamar a função marketOpened ou marketClosed apropriada e anexe as chamadas de método de servidor aos botões.

$.connection.hub.start()
    .pipe(init)
    .pipe(function () {
        return ticker.server.getMarketState();
    })
    .done(function (state) {
        if (state === 'Open') {
            ticker.client.marketOpened();
        } else {
            ticker.client.marketClosed();
        }

        // Wire up the buttons
        $("#open").click(function () {
            ticker.server.openMarket();
        });

        $("#close").click(function () {
            ticker.server.closeMarket();
        });

        $("#reset").click(function () {
            ticker.server.reset();
        });
    });

Os métodos do servidor não são conectados aos botões até que a conexão seja estabelecida, para que o código não possa tentar chamar os métodos do servidor antes que eles estejam disponíveis.

Próximas etapas

Neste tutorial, você aprendeu a programar um aplicativo SignalR que transmite mensagens do servidor para todos os clientes conectados, tanto periodicamente quanto em resposta a notificações de qualquer cliente. O padrão de usar uma instância singleton de vários threads para manter o estado do servidor também pode ser usado em cenários de jogos online de vários jogadores. Para obter um exemplo, consulte o jogo ShootR baseado no SignalR.

Para obter tutoriais que mostram cenários de comunicação ponto a ponto, consulte Introdução com o SignalR e Atualização em tempo real com o SignalR.

Para saber mais sobre os conceitos mais avançados de desenvolvimento do SignalR, visite os seguintes sites para código-fonte e recursos do SignalR: