Desenvolvimento - C#

Sistema de troca de mensagens com AngularJS e SignalR

Veja nesse artigo como trabalhar com SignalR e AngularJS através de um exemplo clássico de aplicação em tempo real, que é o desenvolvimento de um chat.

por Edison Oliveira



SignalR é um framework para desenvolvedores que utilizam a plataforma ASP.NET e que simplifica muito o desenvolvimento de aplicações web em tempo real (Real Time), que permite ao servidor enviar conteúdo instantaneamente aos clientes conectados, sem que o mesmo receba uma requisição desses clientes. Como exemplos de aplicações web em tempo real podemos citar chats, feed de notícias, caixas de entrada de e-mail, dashboards e apuração de votos. Todas essas aplicações têm em comum que, uma vez que o cliente abre o browser e se conecta ao servidor, ele não precisa ficar atualizando a página toda vez que ele quiser ver se há um novo e-mail em sua caixa de entrada, ou se há uma nova notícia no seu feed do Facebook. Se um novo e-mail é enviado a ele, sua caixa de entrada é imediatamente atualizada, sem que ele precise fazer nada, já que o trabalho é feito pelo servidor. É o servidor que, ao receber a mensagem de e-mail, dispara a ação de atualizar a caixa de entrada do cliente, se ele estiver conectado, ou seja, para esse exemplo, estar com o browser aberto na página de e-mail.

SignalR torna essa tarefa muito simples, fornecendo uma API que encapsula a comunicação entre cliente e servidor via chamadas remotas de procedimentos (RPC), expondo ao desenvolvedor apenas a preocupação do que será transmitido (broadcast) e não como.

Arquitetura do SignalR

Os pacotes enviados pelo SignalR são transmitidos geralmente via WebSocket, que é um protocolo baseado no TCP, que permite a comunicação bidirecional. Por que geralmente? Porque websocket requer que servidor e cliente tenham suporte a HTML5 e que o servidor esteja usando Windows Server 2012 ou Windows 8 e .NET Framework 4.5. Se o servidor não atender a esses requisitos, SignalR irá utilizar outros protocolos (foreverFrame, serverSentEvents ou longPolling) para efetuar a transmissão dos pacotes.

As conexões entre cliente e servidor podem ser de dois tipos: Persistent Connections e Hubs.

As persistent connections formam um modelo de mais baixo nível de programação, mas em contrapartida, oferecem mais flexibilidade ao desenvolvedor, como por exemplo, definir o protocolo de comunicação.

Os Hubs, por sua vez, são de alto nível de programação, deixando o desenvolvedor se preocupar apenas com o que e quando transmitir mensagens.

A Figura 1 ilustra a arquitetura do SignalR.

Arquitetura do SignalR

Figura 1. Arquitetura do SignalR

Nossa Aplicação Demo

Nossa demo será uma aplicação web com um único form em que o usuário escreve e posta uma mensagem e também visualiza todas as mensagens postadas. O primeiro passo é adicionar os pacotes AngularJS e SignalR via Nuget no nosso projeto no Visual Studio, conforme mostram as Figuras 2 e 3.

Adicionando AngularJS ao projeto

Figura 2. Adicionando AngularJS ao projeto.

Adicionando SignalR ao projeto

Figura 3. Adicionando SignalR ao projeto.

Para iniciar o desenvolvimento propriamente, incluiremos nosso HUB (conforme descrito previamente, HUB é um tipo de conexão no SignalR), conforme a Figura 4.

Figura 4. Adicionando hub

No código do Hub incluiremos o método SendMessage, que será invocado cada vez que um usuário enviar a mensagem. Fazendo uso da propriedade All, do objeto Clients, incluímos o método que fará a propagação dessa mensagem aos clientes conectados. O objeto Clients já é inerente à classe Hub e, portanto, não é necessário instanciá-lo. Na Listagem 1 vemos o código da nossa classe MyHub.

Listagem 1. Classe MyHub

  [HubName("HubMessage")]
  public class MyHub : Hub
  {
    public void SendMessage(string remetente, string destinatario, string message)
          {
              Clients.All.messageAdded(remetente, destinatario, message);
          }      
  }

Através do IntelliSense pode-se perceber que a propriedade All não implementa, nativamente, o método messageAdded. Isso é porque estamos dizendo ao SignalR que as aplicações clientes (no caso, AngularJS) vão estar aguardando a chamada desse método, como veremos a seguir.

O próximo passo é adicionar uma classe startup OWIN, que irá configurar as rotas do SignalR quando a aplicação iniciar. A Figura 5 exibe como adicionar a classe.

Adicionando a OWIN Startup Class

Figura 5. Adicionando a OWIN Startup Class

Dentro da classe OWIN apenas incluiremos a chamada ao método MapSignalR no método Configuration. E a Listagem 2 exibe a classe OWIN.

Listagem 2. Classe OWIN

[assembly: OwinStartup(typeof(SignalR.Startup))] namespace SignalR { public class Startup { public void Configuration(IAppBuilder app) { app.MapSignalR(); } } }

Iniciaremos agora a implementação da aplicação cliente no AngularJS. Após incluir no projeto um arquivo JavaScript, nomeado App.js, definiremos nossa aplicação como messageBox, conforme a Listagem 3.

Listagem 3. Definindo MessageBOX

         (function () {
      var app = angular.module('messageBox', []);
  })()
  

Dentro do nosso app definiremos os dois elementos que construirão a aplicação cliente: a controller e a factory.

Na factory define-se a conexão ao Hub e atribui-se um delegate ao método que será chamado pelo servidor. Na controller temos o método que inclui a mensagem e envia ao servidor, além do método que recebe a resposta do servidor, que pode ter sido chamado por qualquer cliente. O código completo do arquivo App.js é definido na Listagem 4.

Listagem 4. Arquivo App.js

(function () {
    var app = angular.module('messageBox', []);
    app.value('$', $);

    app.factory('noteService', ['$', '$rootScope',
    function ($, $rootScope) {
        var proxy;
        var connection;
        return {
            connect: function () {
                connection = $.hubConnection();
                proxy = connection.createHubProxy('HubMessage');
                connection.start();
                proxy.on('messageAdded', function (remetente, destinatario, message) {
                    $rootScope.$broadcast('messageAdded', remetente, destinatario, message);
                });
            },
            isConnecting: function () {
                return connection.state === 0;
            },
            isConnected: function () {
                return connection.state === 1;
            },
            connectionState: function () {
                return connection.state;
            },
            sendMessage: function (remetente, destinatario, message) {
                proxy.invoke('SendMessage', remetente, destinatario, message);
            },
        }
    }]);

    app.controller('messageController', function ($scope, noteService) {
                
        noteService.connect();
        $scope.messages = [];

        $scope.$on('messageAdded', function (event, remetente, destinatario, message) {
            var mensagem = { de: remetente, para: destinatario, mensagem: message };
            $scope.messages.push(mensagem);
            $scope.$apply();
        });

        $scope.sendMessage = function () {
            noteService.sendMessage($scope.remetente, $scope.destinatario, $scope.mensagem);
        };
    });
})()

O método sendMessage da factory noteService fará a chamada ao servidor e enviará a mensagem postada pelo usuário. No servidor, por sua vez, a mensagem é enviada aos clientes, invocando o método messageAdded, presente na controller que estará em todos os clientes.

Por fim, adicionaremos nosso form terá três campos: um para o remetente, um para o destinatário e um para o texto da mensagem. Todos os campos do form estarão conectados à controller através do atributo ng-Model. O botão que posta a mensagem invoca o método sendMessage, também implementado na controller. Por fim, há uma div que será repetida para cada mensagem enviada, contendo o destinatário, o remetente e o texto da mensagem.

No form faremos referência aos arquivos JavaScript utilizados: AngularJS, SignalR, jQuery e nosso App. O código completo do form é descrito na Listagem 5.

Listagem 5. Form principal

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" ng-app="messageBox">
<head>
    <title></title>
    <link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css" />
    <script src="Scripts/jquery-1.6.4.js"></script>
    <script src="Scripts/jquery.signalR-2.2.0.js"></script>
    <script src="Scripts/angular.js"></script>
    <script src="App.js"></script>
</head>
<body ng-controller="messageController">
    <form>
        <div class="form-group">
            <label class="col-sm-3 control-label">Remetente</label>
            <div class="col-sm-9">
                <input type="text" class="form-control" ng-model="remetente">
            </div>
        </div>
        <div class="form-group">
            <label class="col-sm-3 control-label">Destinatário</label>
            <div class="col-sm-9">
                <input type="text" class="form-control" ng-model="destinatario">
            </div>
        </div>
        <div class="form-group">
            <label class="col-sm-3 control-label">Mensagem</label>
            <div class="col-sm-9">
                <textarea class="form-control" rows="3" ng-model="mensagem"></textarea>
            </div>
        </div>
        <div class="col-sm-8">
            <button type="submit" class="btn btn-default" ng-click="sendMessage()">Enviar</button>
        </div>
    </form>
    <br />
    <div ng-repeat="item in messages">
        <div class="col-sm-10 table table-bordered">
            <h3>{{item.mensagem}}</h3>
            <br />
            De: {{item.de}}
            <br />
            Para: {{item.para}}
        </div>
    </div>
</body>
</html>

A Figura 6 mostra a aplicação funcionando. No lado esquerdo temos uma instância do Internet Explorer, onde a usuária Márcia escreve ao usuário Paulo, que está conectado na instância do Google Chrome, à direita. A cada vez que um usuário posta uma mensagem e clica em enviar, a mensagem é automaticamente exibida ao destinatário, sem que ele tenha que atualizar seu browser.


Figura 6. Aplicação funcionando

O fluxo da informação é o seguinte:

  • o usuário clica no botão Enviar e dispara o método sendMessage, presente na controller messageController;
  • o Controller invoca o método sendMessage da factory noteService;
  • a Factory noteService invoca o método sendMessage no servidor, presente no HUB. É nesse ponto que o SignalR é invocado;
  • o HUB invoca o método messageAdded da propriedade All do objeto Clients. Esse é o momento exato em que acontece a propagação do pacote para todos os clientes conectados;
  • o método $scope.$on da controller messageController terá sido disparado remotamente pelo SignalR e incluirá a mensagem no array $scope.messages;
  • o objeto $scope.messages é o objeto que alimenta a lista de mensagens do formulário. Assim que a nova mensagem é incluída no array, através do método push, uma nova linha é incluída na lista e o browser é automaticamente atualizado.

O resultado dessa integração entre AngularJS e SignalR é uma experiência de uso muito rica aos usuários. E isso pode ser alcançado com poucas linhas de código.

Caso seu projeto não funcione, recomendamos o download original.

Edison Oliveira

Edison Oliveira - Bacharel em Sistemas de Informação pela UCDB. MBA em Governança de TI (Mauá). Tem MCSA – MCSD – MCP – MCDBA – PMP – ITIL – COBIT. Atualmente é desenvolvedor sênior na startup BeXs, do ramo de sistemas web de terceira geração.