A la fin du dernier épisode, nous avions une application fonctionnelle, mais somme toute rudimentaire. Cette fois, nous allons ajouter de nouvelles fonctionnalités : les messages privés et les notifications. Ces fonctionnalités nous permettront de voir de nouvelles possibilités du hub SignalR : l’envoi d’un message à un utilisateur précis ou à l’expéditeur.

Identifier nos utilisateurs

Pour pouvoir envoyer un message à un utilisateur en particulier, il faut un moyen de l’identifier. La doc SignalR nous fournit un panorama complet des possibilités qui s’offrent à nous. Nous allons choisir la 2e solution : le stockage en mémoire, en la modifiant légèrement.

Pour commencer, il nous faut une structure capable de stocker des connections. Ce sera la classe ConnectionMapping<T> :

using System.Collections.Generic;
using System.Linq;

namespace TwitterWithSignalR.Models
{
  public class ConnectionMapping<T>
  {
    private readonly Dictionary<T> _connections = new Dictionary<T>();

    public int Count
    {
      get
      {
        return _connections.Count;
      }
    }

    public void Add(T key, string connectionId)
    {
      lock (_connections)
      {
        if (!_connections.ContainsKey(key))
        {
          _connections.Add(key, connectionId);
        }
      }
    }

    public T Remove(string connectionId)
    {
      var key = GetConnectionFromValue(connectionId);
      if (!EqualityComparer&lt;T&gt;.Default.Equals(key, default(T)))
      {
        lock (_connections)
        {
          _connections.Remove(key);
        }
      }
     return key;
    }

    public string GetConnectionFromKey(T key)
    {
      if (_connections.ContainsKey(key))
      {
        return _connections[key];
      }
      return null;
    }

    public T GetConnectionFromValue(string value)
    {
      if (_connections.ContainsValue(value))
      {
        return _connections.FirstOrDefault(x =&gt; x.Value == value).Key;
      }
      return default(T);
    }
  }
}

Cette structure threadsafe contient un dictionnaire de connections. Les clés sont génériques : on pourra donc identifier les utilisateurs à l’aide d’entiers, de chaines de caractères, etc. Les valeurs sont les identifiants générés par le hub SignalR. Nous allons donc modifier ce dernier :

private readonly static ConnectionMapping<T> Connections = new ConnectionMapping<T>();

public void NotifyConnection(string userName)
{
  Connections.Add(userName, Context.ConnectionId);
  Clients.All.systemNotification(userName + " vient de se connecter.", "#81F781");
}

public override Task OnDisconnected()
{
  Connections.Remove(Context.ConnectionId);
  return base.OnDisconnected();
}

La première méthode sert, comme son nom l’indique, à notifier le système de la connexion d’un utilisateur. La seconde à sa déconnexion. Enfin, le hub porte une variable statique Connections de type ConnectionMapping<string> contenant les utilisateurs actifs. Nous allons maintenant voir comment communiquer avec eux de façon ciblée.

Impacts sur le hub SignalR

De la même façon que sur Twitter, l’envoi d’un message privé se fera avec la syntaxe suivante :

d [Nom de l’utilisateur] [Message]

Cela demandera à ce que l’on empêche nos utilisateurs de donner un pseudo avec un espace, mais nous verrons cela plus tard. Nous allons maintenant ajouter une nouvelle méthode SendDm au hub :

private void SendDm(string from, string message, int messageId)
{
  string[] words = message.Split(' ');

  if (words.Count() &gt; 2)
  {
    string sendTo = words[1];
    string connectionId = Connections.GetConnectionFromKey(sendTo);

    if (!string.IsNullOrEmpty(connectionId))
    {
      string messageToSend = string.Join(" ", words.Skip(2));
      Clients.Client(connectionId).sendDm(from, messageToSend, messageId);
      if (connectionId != Context.ConnectionId)
      {
        Clients.Caller.sendDm(from, messageToSend, messageId);
      }
    }
    else
    {
      Clients.Caller.systemNotification(sendTo + " n'est pas connecté.", "#FF0000");
    }
  }
}

Nous commençons à découper le message en mots. Le 2e mot est sensé être le destinataire, d’après la grammaire que nous nous imposons. Nous allons donc voir si celui-ci est connecté. Si oui, nous lui envoyons le message à l’aide de la méthode suivante :

Clients.Client(connectionId).sendDm(from, messageToSend, messageId);

SignalR offre en effet plusieurs possibilités pour diffuser un message :

  • A tout le monde : Clients.All
  • A l’appelant : Clients.Caller
  • A un utilisateur en particulier : Clients.Client(id)
  • A un groupe : Clients.Group(groupname). Nous ne verrons pas dans cette série cette possibilité offerte par SignalR.

Nous voulons également que l’expéditeur voit le message qu’il a envoyé. Donc nous lui envoyons également (mais seulement si l’expéditeur et le destinataire sont différents) :

Clients.Caller.sendDm(from, messageToSend, messageId);

Enfin, nous envoyons un message d’erreur à l’expéditeur si nous n’avons pas trouvé le destinataire :

Clients.Caller.systemNotification(sendTo + " n'est pas connecté.", "#FF0000");

Nous allons maintenant modifier la méthode SendToHub de façon à ce qu’elle sache traiter les messages privés :


public void SendToHub(string name, string message)
{
  _nbMessages++;
  if (message.StartsWith("d "))
  {
    SendDm(name, message, _nbMessages);
  }
  else
  {
    var filteredMessage = message.Replace(',', ' ').Replace('.', ' ').Replace('!', ' ').Replace('?', ' ');
    var destinataires = filteredMessage.Split(' ').Where(w =&gt; w.StartsWith("@")).ToArray();
    Clients.All.sendToClients(name, message, _nbMessages, destinataires);
  }
}

Et voilà pour le hub. Passons au client Javascript.

Impacts sur le client Javascript

Commençons par ajouter deux nouvelles colonnes sur notre page html :


<div style="width: 22%; float: left; border: solid 1px #dad7d7; margin-right: 20px; padding: 5px; " id="discussion" onclick="vider('#h3Messages', 'Fil de discussion', '#nbMessages')">
 <input type="hidden" id="nbMessages" value="0" />
 <h3 id="h3Messages">Fil de discussion</h3>
 </div>
 <div style="width: 22%; float: left; border: solid 1px #dad7d7; margin-right: 20px; padding: 5px; " id="mentions" onclick="vider('#h3Mentions', 'Mentions', '#nbMentions')">
 <input type="hidden" id="nbMentions" value="0" />
 <h3 id="h3Mentions">Mentions</h3>
 </div>
 <div style="width: 22%; float: left; border: solid 1px #dad7d7; margin-right: 20px; padding: 5px; " id="messagesprives" onclick="vider('#h3MessagesPrives', 'Messages privés', '#nbMessagesPrives')">
 <input type="hidden" id="nbMessagesPrives" value="0" />
 <h3 id="h3MessagesPrives">Messages privés</h3>
 </div>
 <div style="width: 22%; float: left; border: solid 1px #dad7d7; margin-right: 20px; padding: 5px; " id="system" onclick="vider('#h3System', 'Notifications système', '#nbNotificationsSysteme')">
 <input type="hidden" id="nbSystem" value="0" />
 <h3 id="h3System">Notifications</h3>
 </div>

Nous ajoutons les deux méthodes Javascript appelées par le hub :

chat.client.sendDm = function(from, message, messageId) {
 var nbMessagesPrives = parseInt($('#nbMessagesPrives').val());
 var borderColor = '#dad7d7';
 nbMessagesPrives++;
 $('#nbMessagesPrives').val(nbMessagesPrives);
 $('#h3MessagesPrives').text('Messages privés (' + nbMessagesPrives + ')');
 $('#h3MessagesPrives').after(getDmToAppend(from, message, messageId, borderColor));
 }

chat.client.systemNotification = function (message, color) {
 var nbMessagesPrives = parseInt($('#nbMessagesPrives').val());
 nbMessagesPrives++;
 $('#nbSystem').val(nbMessagesPrives);
 $('#h3System').text('Notifications (' + nbMessagesPrives + ')');
 $('#h3System').after(getSystemTextToAppend(message, color));
 }

Enfin, il nous manque quelques fonctions javascript pour manipuler les messages :

function dm(idMessage) {
 var from = $('#from' + idMessage).val();
 $('#message').val('d ' + from + ' ');
 $('#message').focus();
 }

function getTextToAppend(name, message, messageId, borderColor) {
 return '<div style="border: solid 1px '+ borderColor + '; margin-top:5px; padding:5px"><strong>' + htmlEncode(messageId) + ' - ' + htmlEncode(name)
 + '</strong>> ' + htmlEncode(message)
 + ' | <a onclick="repondre(' + messageId + ')">Répondre</a><input type="hidden" id=from' + messageId + ' value="' + name + '" />'
 + ' | <a onclick="dm(' + messageId + ')">Message privé</a>'
 + '</div>';
 }

function getDmToAppend(name, message, messageId, borderColor) {
 return '<div style="border: solid 1px ' + borderColor + '; margin-top:5px; padding:5px"><strong>' + htmlEncode(messageId) + ' - ' + htmlEncode(name)
 + '</strong>> ' + htmlEncode(message)
 + ' | <a onclick="dm(' + messageId + ')">Message privé</a>'
 + '</div>';
 }

function getSystemTextToAppend(message, borderColor) {
 return '<div style="border: solid 1px ' + borderColor + '; margin-top:5px; padding:5px">' + htmlEncode(message) + '</div>';
 }

Conclusion

Voilà. Nous avons maintenant une version minimale de notre application. Nous pouvons envoyer et recevoir des messages publics, privés et voir nos mentions. Le système nous envoie également des notifications en fonction des événements de connexion des autres utilisateurs et en cas d’erreur. L’application a été mise à jour sur Appharbor, et le code sur Github. C’était le dernier article de cette série sur ASP.NET SignalR 2. Nous avons vu les bases du framework. A partir d’ici, nous pourrions imaginer quelques fonctionnalités supplémentaires :

  • La possibilité de créer des groupes d’utilisateurs, comme un chan privé sur IRC, en utilisant les groupes SignalR
  • Persister les données
  • Mieux gérer les événements de déconnexion/reconnexion
  • Afficher la liste des utilisateurs à jour

En revanche, ce que nous ferons dans un futur proche, c’est de donner un gros coup de pinceau à la partie cliente, pour en faire une vraie application SPA avec AngularJS et Bootstrap.