NetTcp avec Silverlight et IIS

Dans cet article j’étendrai le projet de mon article précédent pour ajouter un endpoint de type netTcpBinding au service WCF et l’utiliser depuis le client Silverlight.

 

Configuration du serveur web

C’est probablement la partie la plus ennuyeuse de cet article mais faites attention à ne pas faire de faute ici ou sinon vous aurez de belles surprises. Moi-même j’en ai eu.

Notez bien qu’il faut utiliser IIS pour pouvoir utiliser le binding WCF netTcp car le serveur web de Visual Studio ne le prends pas en charge.

1 – Ajout de « Non-HTTP WCF Activation »

Il faut tout d’abord ajouter cette fonctionnalité de Windows. Fiez-vous au screenshot ci-dessous et cochez la case « Windows Communication Foundation Non-HTTP Activation ».

nonhttpwcfactivation

Une fois l’installation effectuée vérifiez que le « Net.Tcp Listener Adapter » est démarré :

nettcplisteneradapterservice

2 – Hébergement du site web dans IIS

Tout d’abord vous devez lancer Visual Studio en tant qu’administrateur pour lui laisser les droits de modification sur votre instance locale de IIS. Pour cet article j’utiliserai exactement le même projet que pour l’article précédent.

Dans les propriétés du projet web je dis à Visual Studio que je souhaite déployer sur mon serveur IIS local (n’oubliez pas de cliquer de le bouton « Create Virtual Directory »).

vswebappproperties

3 – Configuration des liaisons et des protocoles

Ouvrez le gestionnaire de IIS et cherchez le site web qui héberge votre application web (généralement c’est « Default Web Site »).

Ajouter une liaison de type net.tcp sur le port 4502. C’est ce port que nous utiliserons dans notre application Silverlight (Les applications Silverlight s’exécutant dans le navigateur et n’étant pas reconnues comme applications de confiances sont limitées aux ports 4502 à 4534).

addnettcpbindingiis

Vous aurez aussi besoin d’ajouter le protocole netTcp dans la liste des protocoles actifs pour l’application web. Pour cela, sélectionnez l’application web et dans les propriétés avancés ajoutez net.tcp comme dans le screenshot ci-dessous :

addnettcpprotocoliis

La dernière étape de la configuration consister a installer correctement IIS avec .NET 4. Pour celà ouvrez une instance du « Visual Studio Command Prompt » en tant qu’administrateur et entre la ligne de commande suivante :

aspnet_regiis.exe –iru

 

Configuration des services WCF

Maintenant que IIS est configuré correctement, on a besoin de configurer les services WCF.

Ici je copierai juste mon fichier de configuration qui permet à la fois le netTcp et le pollingDuplex. Notez que korell est le nom de ma machine et que vous devrez mettre le nom de la votre à la place.

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.web>
        <compilation debug="true" targetFramework="4.0" />
    </system.web>

    <system.serviceModel>
      <extensions>
        <bindingExtensions>
          <add name="pollingDuplexHttpBinding" type="System.ServiceModel.Configuration.PollingDuplexHttpBindingCollectionElement, System.ServiceModel.PollingDuplex, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
        </bindingExtensions>
      </extensions>
      <bindings>
        <pollingDuplexHttpBinding>
          <binding name="multipleMessagesPerPollPollingDuplexHttpBinding" duplexMode="MultipleMessagesPerPoll" maxOutputDelay="00:00:00.500" sendTimeout="00:00:02.000" closeTimeout="00:00:02.000" />
        </pollingDuplexHttpBinding>

        <netTcpBinding>
          <binding name="netTcpBindingConfig">
            <security mode="None" />
          </binding>
        </netTcpBinding>

      </bindings>
      <services>
        <service name="SilverlightReactivePushServer.Web.TemperatureService">
          <endpoint address="wsDualHttp" binding="wsDualHttpBinding" contract="SilverlightReactivePushServer.Web.ITemperatureService" />
          <endpoint address="polling" binding="pollingDuplexHttpBinding" bindingConfiguration="multipleMessagesPerPollPollingDuplexHttpBinding" name="pollingDuplex" contract="SilverlightReactivePushServer.Web.ITemperatureService" />
          <endpoint address="netTcp" binding="netTcpBinding" bindingConfiguration="netTcpBindingConfig" contract="SilverlightReactivePushServer.Web.ITemperatureService" />
          <endpoint address="mex" binding="mexHttpBinding" name="mex" contract="IMetadataExchange" />
          <host>
          <baseAddresses>
            <add baseAddress="net.tcp://korell:4502/SilverlightReactivePushServer.Web/TemperatureService.svc" />
            <add baseAddress="http://korell/SilverlightReactivePushServer.Web/TemperatureService.svc" />
          </baseAddresses>
          </host>
        </service>
      </services>
        <behaviors>
            <serviceBehaviors>
                <behavior name="">
                  <serviceMetadata httpGetEnabled="true" />
                    <serviceDebug includeExceptionDetailInFaults="true" />
                </behavior>
            </serviceBehaviors>
        </behaviors>

      <serviceHostingEnvironment multipleSiteBindingsEnabled="false" aspNetCompatibilityEnabled="true" />
    </system.serviceModel>
</configuration>

On doit aussi ajouter le fichier clientaccesspolicy suivant à la racine du site web (ici c’est Default Web Site) :

<?xml version="1.0" encoding="utf-8"?>
<access-policy>
  <cross-domain-access>
    <policy>
      <allow-from http-request-headers="*">
        <domain uri="*" />
      </allow-from>
      <grant-to>
        <resource path="/" include-subpaths="true" />
        <socket-resource port="4502-4530" protocol="tcp" />
      </grant-to>
    </policy>
  </cross-domain-access>
</access-policy>

Notez bien que le fichier ci-dessus est très permissif et qu’il ne devrait être utilisé que dans des environnements de développements.

Modification du client Silverlight

Dans le client Silverlight on doit ajouter une référence à System.ServiceModel.NetTcp :sladdreferencenettcp

On dois aussi modifier le fichier ServiceClient.config pour ajouter le endpoint de type netTcp :

<configuration>
  <system.serviceModel>
    <bindings>
      <customBinding>
        <binding name="httpPolling">
          <binaryMessageEncoding />
          <pollingDuplex duplexMode="MultipleMessagesPerPoll" />
          <httpTransport transferMode="StreamedResponse" maxReceivedMessageSize="2147483647" maxBufferSize="2147483647" />
        </binding>
        <binding name="NetTcpBinding_ITemperatureService">
          <binaryMessageEncoding />
          <tcpTransport maxReceivedMessageSize="2147483647" maxBufferSize="2147483647" />
        </binding>
      </customBinding>
    </bindings>
    <client>
      <endpoint address="http://korell/SilverlightReactivePushServer.Web/TemperatureService.svc/polling"
                binding="customBinding"
                bindingConfiguration="httpPolling"
                contract="TemperatureServer.ITemperatureService"
                name="HttpPollingDuplexBinding_ITemperatureService"/>
      <endpoint address="net.tcp://korell:4502/SilverlightReactivePushServer.Web/TemperatureService.svc/netTcp"
          binding="customBinding" bindingConfiguration="NetTcpBinding_ITemperatureService"
          contract="TemperatureServer.ITemperatureService" name="NetTcpBinding_ITemperatureService" />
    </client>
  </system.serviceModel>
</configuration>

Vous remarquerez que l’on a maintenant deux bindings :

  • NetTcpBinding_ITemperatureService
  • HttpPollingDuplexBinding_ITemperatureService

On peux choisir lequel sera utilisé dans le constructeur de proxy WCF au web service :

_client = new TemperatureServiceClient("HttpPollingDuplexBinding_ITemperatureService");
// OR
_client = new TemperatureServiceClient("NetTcpBinding_ITemperatureService");

Et voilà c’est tout. Le proxy WCF utilisé pour les deux bindings est le même, il n’y a rien d’autre à changer. Choisissez juste votre binding et c’est bon.

J’ai créé un projet qui permet de changer de binding dynamiquement au runtime.

Vous pouvez le voir en action ici. : http://grogru.com/slduplex

Je n’ai pas d’autre moyens pour vous prouver que cela marche qu’en vous disant d’analyser le trafic réseau de votre pc en utilisant un outil tel que Microsoft Network Monitor.

Vous trouver le code source de cette application sur mon skydrive comme toujours.

J’espère que cela vous plaira. Clignement d'œil

 

Utiliser Rx dans une application client-serveur bi-directionnelle avec Silverlight et WCF

Dans l’article précédent je vous avais montré comment Rx pouvait améliorer le processus de chargement de paquets de données depuis un service WCF. C’était un scénario typique où le client Silverlight demandait au serveur des données.

Maintenant nous allons voir comment on peut utiliser Rx dans un scénario où c’est le serveur qui envoie directement des données au client sans que ce dernier en ait fait explicitement la demande.

 

0 – Les pré-requis

Afin de bien suivre cet article vous devez vous assurer d’avoir les dernières versions de Rx et du SDK de Silverlight. Au moment où ces lignes ont été écrites j’utilisais Silverlight 4.0.60310.0 et Rx 1.0.10.621.0.

1 – Le contexte

On veut créer une application Silverlight et un service WCF qui pousse des données de type température au client à intervalles plus ou moins réguliers. L’unité utilisée sera les degrés Celsius. L’application Silverlight (alias client) est divisée en deux modules indépendants, un pour afficher les températures reçues en Celsius et l’autre pour les afficher en Fahrenheit. Même si on a deux modules, on veut qu’ils utilisent la même connexion au service WCF pour recevoir les températures. Ces modules devront aussi n’avoir aucune référence directe sur le proxy WCF généré par Visual Studio de façon à permettre l’injection de dépendance ou encore le test unitaire des view models des modules.

2 – Services duplex

Un service duplex est un service où à la fois le client et le serveur peuvent envoyer des données via la même connexion, ce qui est exactement ce dont nous avons besoin ici. Nous avons choisi de créer un service en utilisant le pattern Subscribe-Publish. Le client créera une connexion au serveur en utilisant la méthode Subscribe de ce dernier et attendra ensuite de manière asynchrone que le serveur lui envoie des données.

En WCF, ce type de scénario est rendu possible par l’utilisation d’une callback de service (service callback en anglais). Cette callback est une interface définie côté serveur et implémentée côté client que le serveur utilisera à chaque fois qu’il souhaite envoyer des données au client.

Pour pouvoir utiliser ces callbacks on doit choisir un binding WCF compatible. Quand on crée une application WPF on peut utiliser tous les bindings fournis par défaut tel que le wsDualHttpBinding mais en Silverlight nous sommes plus limités. En Silverlight on peut utiliser le netTcpBinding ou le pollingDuplexHttpBinding. Le premier est un peu plus compliqué à configurer que le second aussi il fera l’objet d’un article séparé. Pour aujourd’hui on va se concentrer sur le second.

 

3 – PollingHttpDuplexBinding

Ce binding est un peu spécial car c’est un binding duplex fonctionnant via le protocole http or il est bien connu que ce protocole n’est pas bi-directionnel. En http, le client doit faire une demande au serveur pour recevoir des données. Le serveur ne peut pas de lui-même envoyer des données au client comme il est possible de le faire avec des sockets. Si on a besoin que le serveur envoie des données au client on peut tricher en mettant en place ce que l’on appelle du polling. Le polling est le processus où le client appelle régulièrement le serveur (toutes les secondes par exemple) pour savoir si de nouvelles données sont disponibles ou non. C’est un peu un dialogue du genre de l’âne dans Shrek (c’est quand qu’on arrive ?) :

Client : Hey t’as des nouvelles choses pour moi ?

Serveur : Non

Client : Et maintenant ?

Serveur : Non je t’ai déjà dis ça il y a une seconde !

Client : Ouais mais tu me le dis jamais toi-même je dois toujours te le demander. Alors quelque-chose ?

Serveur : Non !

… 30 essais plus tard

Client : Allez et maintenant ?

Serveur : Ouais ça y est j’ai quelque-chose, tiens voilà.

 

Vous remarquez donc que ce processus utilise pas mal d’appels réseaux. Tout réside donc dans la bonne optimisation de ce dernier et dans le bon choix d’intervalle entre les appels au serveur pour éviter de le surcharger. Le pollingHttpDuplexBinding implémente déjà tout ce polling directement au sein de la couche réseau de Silverlight améliorant ainsi nettement les performances de l’ensemble (pas de retours permanents sur le dispatcher). Charge maintenant à nous de choisir le bon intervalle de temps entre les appels en fonction du nombre de clients et de la réactivité attendue par l’utilisateur de l’application Silverlight. En effet, un jeu demandera une grande réactivité alors qu’un logiciel de chat beaucoup moins car recevoir son message au bout de 3 secondes à la place de 1 seconde ne change la vie de personne mais diminue assez nettement la charge à supporter par le serveur.

Le pollingHttpDuplexBinding n’est pas inclus par défaut dans Silverlight ou WCF. On peut trouver les assemblys nécessaires dans le SDK de Silverlight. Sur mon poste elle se trouvent dans C:\Program Files (x86)\Microsoft SDKs\Silverlight\v4.0\Libraries alors pour vous ca devrait être un truc du genre C:\[ProgramFilesArchitecture]\Microsoft SDKs\Silverlight\[SilverlightVersion]\Libraries.

Il y a deux assemblys requises pour faire fonctionner ce binding, une pour le client et une autre pour le serveur. Dans le projet web hébergeant l’application Silverlight et vos services WCF ajoutez une référence à la dll serveur de System.ServiceModel.PollingDuplex.dll et dans le projet Silverlight ajoutez une référence à la dll client.

4 – Création du service

Dans le projet web on commence par créer deux interfaces définissant les contrats WCF pour le service de température et sa callback.

[ServiceContract(CallbackContract = typeof(ITemperatureServiceCallback))]
public interface ITemperatureService {
    [OperationContract(IsOneWay = true)]
    void Subscribe();

    [OperationContract(IsOneWay = true)]
    void Unsubscribe();
}
public interface ITemperatureServiceCallback {
    [OperationContract(IsOneWay = true)]
    void PushTemperature(double temperature);
}

On peut ensuite créer une implémentation du service (l’implémentation ici est simple et ne suffirait bien entendu pas à des services de production). Cette implémentation crée un timer qui appellera la callback de tous les clients connectés au serveur pour envoyer des données de type températures générées aléatoirement à intervalle de temps plus ou moins régulier.

public class TemperatureService : ITemperatureService {
    private static readonly object _locker = new object();
    private static readonly List<OperationContext> _clients = new List<OperationContext>();
    private static readonly Random _random = new Random();

    private static Timer _updateTimer;

    static TemperatureService()
    {
        _updateTimer = null;
    }

    public void Subscribe()
    {
        if (_clients.Contains(OperationContext.Current) == false)
        {
            lock (_locker)
            {
                if (_clients.Count == 0)
                    _updateTimer = new Timer(TimerTick, null, 500, 2000);

                _clients.Add(OperationContext.Current);
            }
        }
    }

    public void Unsubscribe()
    {
        RemoveClient(OperationContext.Current);
    }

    private static void TimerTick(object state)
    {
        Task.Factory.StartNew(() =>
        {
            double temperature = _random.Next(-200, 400) * 0.1d;

            // Copy the clients array because it can be modified while been read var clients = _clients.ToArray();
            foreach (var client in clients)
            {
                try {
                    var channelState = client.Channel.State;
                    if (channelState == CommunicationState.Opened)
                    {
                        var callbackChannel = client.GetCallbackChannel<ITemperatureServiceCallback>();
                        callbackChannel.PushTemperature(temperature);
                    }
                    else {
                        RemoveClient(client);
                    }
                }
                catch (TimeoutException)
                {
                    RemoveClient(client);
                }
                catch (Exception)
                {
                    _updateTimer.Dispose();
                }
            }
        });
    }

    private static void RemoveClient(OperationContext client)
    {
        lock (_locker)
        {
            _clients.Remove(client);
        }
    }
}

Dans cette implémentation vous devez porter votre attention sur deux choses :

  • OperationContext.Current
  • client.GetCallbackChannel<ITemperatureServiceCallback>

OperationContext.Current nous donne des informations sur le client qui a appelé la méthode de service en cours d’exécution.

client.GetCallbackChannel nous donne une instance de la callback de service à utiliser pour envoyer des données au client.

 

Ensuite on expose le service en utilisant un fichier svc. Le mien est le suivant :

<%@ ServiceHost Language="C#" Debug="true" Service="SilverlightReactivePushServer.Web.TemperatureService" CodeBehind="TemperatureService.svc.cs" %>

Finalement on doit configurer le binding dans le fichier web.config, voici la section serviceModel pour le code de ce projet :

<system.serviceModel>
  <extensions>
    <bindingExtensions>
      <add name="pollingDuplexHttpBinding"
           type="System.ServiceModel.Configuration.PollingDuplexHttpBindingCollectionElement, System.ServiceModel.PollingDuplex, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
     </bindingExtensions>
  </extensions>
  <bindings>
  <pollingDuplexHttpBinding>
    <binding name="multipleMessagesPerPollPollingDuplexHttpBinding"
                   duplexMode="MultipleMessagesPerPoll"
                   maxOutputDelay="00:00:00.500"
                   sendTimeout="00:00:02.000"
                   closeTimeout="00:00:02.000"/>
    </pollingDuplexHttpBinding>
  </bindings>
  <services>
    <service name="SilverlightReactivePushServer.Web.TemperatureService">
      <endpoint address="" binding="pollingDuplexHttpBinding"
                bindingConfiguration="multipleMessagesPerPollPollingDuplexHttpBinding"
                name="pollingDuplex"
                contract="SilverlightReactivePushServer.Web.ITemperatureService" />
      <endpoint address="mex" binding="mexHttpBinding" name="mex" contract="IMetadataExchange" />
    </service>
  </services>
  <behaviors>
    <serviceBehaviors>
      <behavior name="">
        <serviceMetadata httpGetEnabled="true" />
        <serviceDebug includeExceptionDetailInFaults="false" />
      </behavior>
    </serviceBehaviors>
  </behaviors>
  <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
</system.serviceModel>

Nous en avons fini pour la partie serveur.

5 – Utilisation du service en Silverlight

Premièrement, on va ajouter une référence sur le service que nous avons créé précédemment. Choisissons comme espace de nom TemperatureServer.

L’outil utilisé par Visual Studio pour générer le proxy de service génère correctement les classes requises mais échoue à générer un fichier ServiceReferences.ClientConfig correct. Par correct, je veux simplement dire que le fichier généré est vide.

On va donc écrire la configuration nous même (n’oubliez pas de remplacer les adresses des endpoint par les vôtres).

<configuration>
  <system.serviceModel>
    <bindings>
      <customBinding>
        <binding name="httpPolling">
          <binaryMessageEncoding />
          <pollingDuplex duplexMode="MultipleMessagesPerPoll" />
          <httpTransport transferMode="StreamedResponse" maxReceivedMessageSize="2147483647" maxBufferSize="2147483647" />
        </binding>
      </customBinding>
    </bindings>
    <client>
      <endpoint address="http://localhost:1614/TemperatureService.svc"
                binding="customBinding"
                bindingConfiguration="httpPolling"
                contract="TemperatureServer.ITemperatureService" />
    </client>
  </system.serviceModel>
</configuration>

On va maintenant créer une nouvelle classe nommée TemperatureService qui englobera le proxy WCF et l’exposera sous forme de collection observable. Cette classe est un singleton (libre à vous d’utiliser de l’injection de dépendance à la place du singleton) qui sera utilisé par tous les modules (Celsius et Fahrenheit) pour appeler le service :

public class TemperatureService {
    private static readonly TemperatureService _temperature = new TemperatureService();

    private TemperatureServiceClient _client;
    private readonly IObservable<double> _temperatures; 

    protected TemperatureService()
    {
        _temperatures = Observable.Create<double>(observer =>
        {
            if (_client == null)
            {
                _client = new TemperatureServiceClient();
                _client.SubscribeAsync();
            }

            _client.PushTemperatureReceived += (s, a) => observer.OnNext(a.temperature);

            return () => { };
        });
    }

    public IObservable<double> Temperatures
    {
        get { return _temperatures; }
    }

    public static TemperatureService Current
    {
        get { return _temperature; }
    }
}

Dans le constructeur, on crée une observable qui appellera la méthode SubscribeAsync du service WCF une seule fois. Ensuite, à chaque fois qu’un abonnement à l’observable à lieu, on s’abonne à l’évènement PushTemperatureReceived du proxy WCF. La lambda utilisée ici pour s’abonner à l’évènement ne fait rien d’autre que de pousser la valeur reçue dans la collection observable en utilisant la méthode OnNext de son observer. Et voilà, c’est a peu près tout, toute la magie a lieu ici. Maintenant, à chaque fois qu’une température est reçue depuis le serveur, elle sera accessible immédiatement à tous les abonnés de la collection observable.

J’ai créé deux vues pour afficher les températures en Celsius et en Fahrenheit. Voici leurs code XAML :

<UserControl x:Class="SilverlightReactivePushServer.Views.CelsiusView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding CelsiusTemperature}" />
        <TextBlock Text=" °C" />
    </StackPanel>
</UserControl>
<UserControl x:Class="SilverlightReactivePushServer.Views.FahrenheitView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding FahrenheitTemperature}" />
        <TextBlock Text=" °F" />
    </StackPanel>
</UserControl>

Voici comment elles ont été utilisées dans la vue principale :

<UserControl x:Class="SilverlightReactivePushServer.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:views="clr-namespace:SilverlightReactivePushServer.Views">

    <Grid x:Name="LayoutRoot" Background="White">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <ComboBox Grid.ColumnSpan="2" HorizontalAlignment="Center" ItemsSource="{Binding DuplexTypes}" SelectedItem="{Binding DuplexType, Mode=TwoWay}" />
        <views:CelsiusView Grid.Column="0" Grid.Row="1"  HorizontalAlignment="Right" Margin="5" />
        <views:FahrenheitView Grid.Column="1" Grid.Row="1"  HorizontalAlignment="Left" Margin="5" />
    </Grid>
</UserControl>

Chaque vue de température a son propre vue model dont voici leurs implémentations :

public class CelsiusViewModel : INotifyPropertyChanged {
    private double _celsiusTemperature;
    public event PropertyChangedEventHandler PropertyChanged;

    public double CelsiusTemperature
    {
        get { return _celsiusTemperature; }
        private set {
            if (_celsiusTemperature != value)
            {
                _celsiusTemperature = value;
                RaisePropertyChanged("CelsiusTemperature");
            }
        }
    }

    public CelsiusViewModel()
    {
        TemperatureService.Current.Temperatures.Subscribe(t => CelsiusTemperature = t);
    }

    private void RaisePropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}
public class FahrenheitViewModel : INotifyPropertyChanged {
    private double _fahrenheitTemperature;

    public event PropertyChangedEventHandler PropertyChanged;

    public double FahrenheitTemperature
    {
        get { return _fahrenheitTemperature; }
        private set {
            if (_fahrenheitTemperature != value)
            {
                _fahrenheitTemperature = value;
                RaisePropertyChanged("FahrenheitTemperature");
            }
        }
    }

    public FahrenheitViewModel()
    {
        TemperatureService.Current.Temperatures.Subscribe(t => FahrenheitTemperature = (9 / 5) * t + 32);
    }

    private void RaisePropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

 

Et voilà c’est a peu près tout. Tout fonctionne déjà correctement ainsi. Chaque view model s’abonne à l’observable de températures définie dans la classe TemperatureService, que nous avons créée précédemment, et met à jour la température à afficher.

6 -  Conclusion

On a maintenant un service bi-directionnel utilisé par une application Silverlight utilisant le protocole http. On utilise Rx pour simplifier notre code et pour partager la même connexion entre différents view models qui nécessitent tous de recevoir la même information au même moment.

Je trouve ça plutôt sexy et vous ?

 

Comme d’habitude vous trouverez le code de cet article sur mon skydrive.

Reactive Extensions – Dépôt asynchrone simple

 

En Silverlight, tous les appels de webservices sont asynchrones. Dès lors, lorsque l’on implémente un dépôt en Silverlight, on doit faire les choses d’une manière différente de ce que nous aurions fait en Asp.Net ou WPF.

Prenons un exemple. On a un site web exposant une liste de tous les clients d’une société au travers d’un service WCF. On souhaite que notre application Silverlight accède à cette liste afin de l’afficher dans une ListBox. Le service en question peut retourner des dizaines de milliers d’enregistrements. A cause de cela, on ne peux pas récupérer tous les clients au sein d’un même appel.

Voici à quoi ressemble le service :

[ServiceContract(Name = "CustomerService")]
public interface ICustomerService {
    [OperationContract]
    int Count();

    [OperationContract]
    IEnumerable<Customer> Get(int start, int count);
}

On peut voir que pour récupérer tous les clients, on doit d’abord savoir combien il y en a en utilisant la méthode Count et on appelle ensuite la méthode Get pour en récupérer un certain nombre.

Voyons comment un dépôt asynchrone utilisant ce service serait implémenté de manière classique :

public class CustomerAsyncRepository {
    public void GetAll(Action<IEnumerable<Customer>> callback)
    {
        var list = new List<Customer>();
        var client = new CustomerServiceClient();
        client.CountCompleted +=
            (sender, e) =>
            {
                if (e.Result > 1000)
                {
                    var state = new GetState { Count = e.Result, Offset = 0, Step = 500 };
                    ((CustomerServiceClient)sender).GetAsync(state.Offset, state.Step, state);
                }
                else ((CustomerServiceClient)sender).GetAsync(0, e.Result);
            };

        client.GetCompleted += (sender, e) =>
        {
            list.AddRange(e.Result);

            var state = e.UserState as GetState;

            if (state != null && state.Offset + state.Step < state.Count)
            {
                state.Offset += state.Step;
                ((CustomerServiceClient)sender).GetAsync(state.Offset, state.Step, state);
            }
            else {
                ((CustomerServiceClient)sender).CloseAsync();
                callback(list);
            }
        };

        client.CountAsync();
    }

    private class GetState {
        public int Offset { get; set; }
        public int Step { get; set; }
        public int Count { get; set; }
    }
}

 

La méthode GetAll prend un callback en paramètre. Il sera appelé lorsque tous les clients auront été récupérés, charge ensuite à l’appelant de traiter la liste des clients (ici on assigne cette liste à l’ItemsSource d’une ListBox). Cette méthode vérifie aussi le nombre de clients à récupérer. Si ce nombre est supérieur à 1000, alors la méthode passe dans un mode ou elle télécharge la liste des clients par paquet de 500. Pour pouvoir faire cela, on a créé la classe GetState qui représente l’état courant de l’opération de téléchargement de la liste des clients.

 

Voici comment la méthode GetAll est appelée et son retour utilisé :

var repository = new CustomerAsyncRepository();
repository.GetAll(customers => lstBox.ItemsSource = new ObservableCollection<Customer>(customers));

Que cette technique fonctionne et soit complètement asynchrone est une chose, mais il y a, à mon sens, quelques problèmes. Le principal problème est que l’on doit attendre que tous les clients aient été chargés avant de pouvoir invoquer la callback, ce qui peut prendre un temps assez long. Même si on avait déjà téléchargé la moitié des données, l’utilisateur de notre application devra attendre la fin du chargement complet avant de pouvoir commencer à les utiliser. Pour donner une chance à l’utilisateur de travailler avec les données déjà chargées, on pourrait invoquer la callback à chaque retour du webservice ; cependant la méthode appelante serait dans l’incapacité de savoir quand tous les éléments seraient chargés. Il y a toujours des moyens de contourner ou de corriger ce problème mais je vais m’arrêter là et nous allons voir comment utiliser les bases de Reactive Extensions pour faire une implémentation plus souple et puissante de ce dépôt.

Avec Rx, au lieu d’avoir un callback en paramètre, la méthode GetAll revoie un IObservable de Customer. Cet IObservable représente une collection asynchrone où les données nous sont poussées au lieu qu’on ne les récupère comme avec une IEnumerable (on appelle ceci la dualité entre IObservable et IEnumerable). De plus, IObservable fournit un moyen de savoir quand tous les éléments de la collection ont été récupérés ce qui est parfait pour nous.

Après un peu de refactorisation, le dépôt ressemble à ceci :

public class CustomerReactiveRepository {
    public IObservable<Customer> GetAll()
    {
        return Observable.Create<Customer>(observer => OnSubscribe(observer));
    }

    private static Action OnSubscribe(IObserver<Customer> observer)
    {
        try {
            var client = new CustomerServiceClient();
            client.CountCompleted += (sender, e) =>
            {
                if (e.Result > 1000)
                {
                    var state = new GetState { Count = e.Result, Offset = 0, Step = 500 };
                    ((CustomerServiceClient)sender).GetAsync(state.Offset, state.Step, state);
                }
                else ((CustomerServiceClient)sender).GetAsync(0, e.Result);
            };

            client.GetCompleted += (sender, e) =>
            {
                foreach (var c in e.Result)
                    observer.OnNext(c);

                var state = e.UserState as GetState;

                if (state != null && state.Offset + state.Step < state.Count)
                {
                    state.Offset += state.Step;
                    ((CustomerServiceClient)sender).GetAsync(state.Offset, state.Step,
                                                                state);
                }
                else {
                    ((CustomerServiceClient)sender).CloseAsync();
                    observer.OnCompleted();
                }
            };

            client.CountAsync();
        }
        catch (Exception e)
        {
            observer.OnError(e);
        }

        return () => { };
    }

    private class GetState {
        public int Offset { get; set; }
        public int Step { get; set; }
        public int Count { get; set; }
    }
}

 

La plupart du code reste inchangé ici, seules la signature de la méthode et la façon de retourner les clients à la fonction appelante change. La méthode GetAll renvoie maintenant un IObservable de Customer en utilisant la méthode Observable.Create. Cette méthode prend en paramètre un délégué vers une autre méthode,  invoquée lorsqu’une souscription à l’observable aura lieu et prenant en paramètre un IObserver de Customer. L’IObserver est à l’IObservable ce que l’IEnumerator est à l’IEnumerable.

IObserver fournit trois méthodes importantes :

  • OnNext (la méthode à invoquer lorsque l’on souhaite pousser un nouvel élément dans l’observable)
  • OnCompleted (la méthode à invoquer lorsque tous les éléments auront été poussés, ceci termine l’observable)
  • OnError (la méthode à invoquer lorsqu’une erreur s’est produite dans le processus de récupération des données à pousser dans l’observable, ceci termine le processus)

Dès que nous recevons un groupe de customers, on le pousse dans l’observable et ils deviennent immédiatement disponible au souscripteur.

La méthode OnSubscribe du code ci-dessus retourne un délégué vers la méthode à invoquer pour libérer les ressources utilisées par le processus de récupération des données. Cette méthode de libération sera invoquée lorsque l’observable sera terminée. Ici nous n’avons rien à libérer alors on renvoie juste une méthode vide.

Maintenant voyons comment ce dépôt est utilisé :

var customers = new ObservableCollection<Customer>();
lstBox.ItemsSource = customers;
var repository = new CustomerReactiveRepository();
repository.GetAll().Subscribe(customers.Add);

On crée une ObservableCollection et on l’utilise en tant qu’ItemsSource de la ListBox. On souscrit ensuite à l’observable renvoyé par la méthode GetAll en lui passant un délégué sur la méthode Add de l’ObservableCollection précédemment déclarée.

Ce faisant, la listbox sera alimentée dès que de nouveaux customers seront disponibles rendant l’expérience utilisateur bien meilleure.

En utilisant Rx on a aussi accès à tous ses opérateurs Linq, ce qui nous permet d’avoir un contrôle plus fin sur notre observable. Par exemple, on pourrait très facilement dire à Rx d’exécuter la récupération des customers dans un thread du pool de thread et de nous renvoyer les résultats sur le thread dispatcher ainsi :

repository.GetAll()
    .ObserveOn(new DispatcherSynchronizationContext())
    .SubscribeOn(Scheduler.ThreadPool)
    .Subscribe(customers.Add);

Dans cet article je n’ai présenté qu’un nombre limité des nombreux opérateurs facilitant encore plus le traitement des operations asynchrones offerts par les Reactive Extensions.

Je vous invite à télécharger les sources de l’application sur mon skydrive,  afin que vous puissiez vous rendre compte par vous-même comment Rx améliore à la fois le code et l’expérience utilisateur.

 

PS : Benjamin Roux m’a proposé une version plus évoluée du dépôt implémentée avec Rx. Elle utilise un plus grand nombre de fonctions offertes par Rx et est plus en accord avec la philosophie de Rx. Si vous êtes plus à l’aise avec Rx que la cible de cet article cette implémentation peut vous plaire.

 

public static class ObservableExtensions
{
    public static IObservable<T> GetWithPages<T>(this IObservable<T> observable, GetState state, Func<int, int, IObservable<IEnumerable<T>>> getObservable)
    {
        for (int i = 0; i < state.Pages; i++)
        {
            observable = observable.Merge(getObservable(i * state.Step, Math.Min(state.Step, state.Count - i * state.Step)).SelectMany(__ => __));
        }

        return observable;
    }
}

public class CustomerAsyncRepository
{
    public IObservable<int> GetAll()
    {
        var client = (TestService.TestService)new TestServiceClient();

        var observable =
            Observable.Defer(() =>
                Observable.FromAsyncPattern(client.BeginCount, a => client.EndCount(a))()
                            .Select(c => new GetState { Count = c, Offset = 0, Step = 100 })
                            .SelectMany(g => Observable.Empty<int>().GetWithPages(g, Observable.FromAsyncPattern<int, int, IEnumerable<int>>(client.BeginGet, a => client.EndGet(a)))));

        return observable;
    }
}

public class GetState
{
    public int Offset { get; set; }
    public int Step { get; set; }
    public int Count { get; set; }

    public int Pages
    {
        get { return (int)Math.Ceiling(Count / (double)Step); }
    }
}

WP7 – Mango Beta – Base de donnée locale

 

Cet article utilise la version Beta du SDK de WP7 v7.1 alias Mango. Aussi au moment où vous lirez ces lignes, le contenu de cet article pourra être obsolète.

Parmi les nombreuses nouveautés qu’apporte Mango, il en est une qui m’intéresse tout particulièrement et qui est l’ajout d’un base de donnée locale au téléphone. En effet, pour ceux qui ne sont pas au courant, j’ai développé un petit jeu d’Othello sur WP7 du nom de Yao. Ce jeu sauvegarde actuellement les scores des parties dans l’Isolated Storage. Cela fonctionne mais j’aurai aimé avoir quelque chose d’un peu plus structuré. Aussi, à des fins de tests j’ai regardé un peu comment cela fonctionne.

La base de donnée sur WP7 est basée sur SQL CE et on peut y accéder en utilisant du Linq-To-SQL. Oui oui, vous avez bien lu, Linq-To-SQL. Ceci dit ici point de fichier dbml, le modèle ici est généré à la main mais on verra ça plus tard.

0 – Le contexte

Ici le contexte sera : pouvoir ajouter, modifier et supprimer des scores d’Othello dans la base de donnée locale du téléphone.

1 – Préparation du modèle

Afin de pouvoir utiliser la base de donnée interne au téléphone il faut d’abord rajouter une référence à l’assembly System.Data.Linq.

Une fois ceci fait on va pouvoir ajouter commencer à créer le modèle.

Voici la classe Score que nous souhaitons persister en base :

[Table]
public class Score
{
    [Column(
        IsPrimaryKey = true,
        IsDbGenerated = true,
        DbType = "INT NOT NULL Identity",
        CanBeNull = false,
        AutoSync = AutoSync.OnInsert)]
    public int Id { get; set; }

    [Column]
    public int White { get; set; }

    [Column]
    public int Black { get; set; }
}

Vous remarquez deux attributs : Table et Column. Leurs noms sont assez explicites et permettent d’indiquer que la classe Score est une table de la base et qu’elle contient trois colonnes : Id, White et Black.

On noteras cependant que l’attribut Column de la propriété Id est plus détaillé que les autres. Voici les significations des différentes propriétés de l’attribut Column pour la propriété Id :

- IsPrimaryKey à True indique que Id est la clef primaire de la table Score

- IsDbGenerated à True indique que la valeur de la colonne est générée par la base

- DbType indique le type SQL de la colonne tel qu’il serait défini dans une base SQL CE standard, ici entier non-nullable et de type identité

- CanBeNull à False indique que la colonne ne peux avoir de valeur nulle

- AutoSync indique quand doivent être synchronisée les données entre la base SQL et le contexte de donnée actuel, ici la synchronisation se fait à l’insertion

On va ensuite créer un classe que représentera notre contexte de donnée, notre DbContext en fait. Voici la classe en question :

public class Model : System.Data.Linq.DataContext
{
    public Model(string connectionString)
        : base(connectionString)
    {
    }

    public System.Data.Linq.Table<Score> Scores;
}

La classe Model représente la base de donnée dans laquelle on a une table des scores. Ici on ne fait rien d’autre de spécial.

2 – Création de la base de donnée

Lorsque l’application se lance pour la première fois, la base de donnée n’est pas encore créée. Il faudra donc la créer à ce moment là.

Pour ce faire, rendez-vous dans le fichier App.xaml.cs. Dans ce fichier nous allons rajouter la chaine de connexion à utiliser pour notre application :

public static string ConnectionString = "Data Source=isostore:/MangoLocalDatabase.sdf";

Ensuite dans la méthode InitializePhoneApplication on va rajouter le code suivant :

using (Model db = new Model(ConnectionString))
{
    if (db.DatabaseExists() == false)
        db.CreateDatabase();
}

C’est tout ce que l’on a à faire pour créer la base de donnée lorsque celle-ci n’existe pas.

3 – Affichage

Voici le code XAML de la MainPage de l’application :

<StackPanel Background="Transparent">
    <ListBox x:Name="listBox">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal" Margin="15">
                    <TextBlock Text="Black : " />
                    <TextBox Text="{Binding Black, Mode=TwoWay}" Width="100" InputScope="Number" />
                    <TextBlock Text="White : " />
                    <TextBox Text="{Binding White, Mode=TwoWay}" Width="100" InputScope="Number" />
                    <Button x:Name="deleteBtn">Delete</Button>
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
    <StackPanel Orientation="Horizontal" Margin="15">
        <TextBlock Text="Black : " HorizontalAlignment="Center"  />
        <TextBox x:Name="tbBlack" Width="100" InputScope="Number" />
        <TextBlock Text="White : " HorizontalAlignment="Center" />
        <TextBox x:Name="tbWhite" Width="100" InputScope="Number" />
    </StackPanel>
    <Button x:Name="addBtn">Add</Button>
</StackPanel>
4 – Affichage d’éléments venant de la base

Nous allons commencer par ajouter le code nous permettant d’afficher des scores provenant de la base. Pour cela on va aller modifier le code-behind de la page principale pour qu’il ressemble à celui-ci :

public partial class MainPage
{
    private readonly Model _model;
    private readonly ObservableCollection<Score> _scores;

    public MainPage()
    {
        InitializeComponent();

        _model = new Model(App.ConnectionString);
        _scores = new ObservableCollection<Score>(_model.Scores);

        listBox.ItemsSource = _scores;
    }

    protected override void OnNavigatedFrom(NavigationEventArgs e)
    {
        base.OnNavigatedFrom(e);

        _model.Dispose();
    }
}

On a donc créé deux champs, un pour le modèle de donnée et une collection observable de scores. Dans le constructeur de la vue on créer une connexion à la base de donnée et on initialise la collection observable des scores avec la table Score, ensuite on lie la ListBox avec la collection observable. Enfin on n’oublie pas de fermer la connexion à la base lorsque l’on navigue hors de la vue.

Tout cela est bien utile mais pour l’instant on a aucun élément à afficher. On va donc en rajouter.

5 – Ajout d’éléments dans la base

Pour cela on va gérer l’évènement Click sur le bouton addBtn. Voici le code du gestionnaire d’évènement :

private void addBtn_Click(object sender, RoutedEventArgs e)
{
    int black;
    int white;

    if (int.TryParse(tbBlack.Text, out black))
    {
        if (int.TryParse(tbWhite.Text, out white))
        {
            Score score = new Score { Black = black, White = white };

            _model.Scores.InsertOnSubmit(score);
            _scores.Add(score);

            tbBlack.ClearValue(TextBox.TextProperty);
            tbWhite.ClearValue(TextBox.TextProperty);
        }
    }
}

Ici on récupère la valeur des deux TextBox et on construit une instance de la classe Score. Cette instance est d’abord ajouté dans le modèle où elle se verras générer un identifiant automatiquement et ensuite on peux ajouter cette même instance dans la collection observable pour que l’affichage se mette à jour. On nettoie ensuite les valeur des TextBox.

A ce stade il faut noter que l’objet a été ajouté au modèle mais n’a pas encore été persisté en base (méthode InsertOnSubmit). Pour persister les objets il faut aller modifier la méthode OnNavigatedFrom et utiliser SaveChanges sur le modèle :

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
    base.OnNavigatedFrom(e);

    _model.SubmitChanges();
    _model.Dispose();
}

Maintenant on peux ajouter des scores, mais il se peux que l’on veuille les supprimer aussi s’ils n’ont plus de raison d’être.

6 – Suppression d’éléments de la base

Pour cela on va gérer l’évènement Click sur le bouton deleteBtn. Voici le code du gestionnaire d’évènement :

 

private void deleteBtn_Click(object sender, RoutedEventArgs e)
{
    Button btn = sender as Button;

    if (btn == null)
        return;

    Score score = btn.DataContext as Score;
    _model.Scores.DeleteOnSubmit(score);
    _scores.Remove(score);
}

Comme notre bouton est dans un DataTemplate d’une ListBox on récupère le Score associé au bouton via son DataContext. Ensuite, en utilisant la méthode DeleteOnSubmit de la table Score on marque l’objet en suppression et on le supprime de la collection observable par la même occasion.

Dans l’état actuel des choses on est capable de faire un Select, un Insert et un Delete. Mais pour l’instant pas d’Update.

7 – Mise à jour d’enregistrements

Afin que le moteur de base de donnée puisse savoir si une classe a été modifiée ou non il suffit de faire implémenter cette classe de l’interface INotifyPropertyChanged mais il est recommandé de la faire implémenter en plus l’interface INotifyPropertyChanging pour des raisons de performance. Une fois ceci fait, tout à été fait.

Voyons le nouveau code source de la classe Scores :

[Table]
public class Score : INotifyPropertyChanged, INotifyPropertyChanging
{
    private int _id;
    private int _white;
    private int _black;

    public event PropertyChangedEventHandler PropertyChanged;
    public event PropertyChangingEventHandler PropertyChanging;

    [Column(
        IsPrimaryKey = true,
        IsDbGenerated = true,
        DbType = "INT NOT NULL Identity",
        CanBeNull = false,
        AutoSync = AutoSync.OnInsert)]
    public int Id
    {
        get
        {
            return _id;
        }

        set
        {
            if (_id != value)
            {
                NotifyPropertyChanging("Id");
                _id = value;
                NotifyPropertyChanged("Id");
            }
        }
    }

    [Column]
    public int White
    {
        get
        {
            return _white;
        }

        set
        {
            if (_white != value)
            {
                NotifyPropertyChanging("White");
                _white = value;
                NotifyPropertyChanged("White");
            }
        }
    }

    [Column]
    public int Black
    {
        get
        {
            return _black;
        }

        set
        {
            if (_black != value)
            {
                NotifyPropertyChanging("Black");
                _black = value;
                NotifyPropertyChanging("Black");
            }
        }
    }

    private void NotifyPropertyChanging(string propertyName)
    {
        if (PropertyChanging != null)
            PropertyChanging(this, new PropertyChangingEventArgs(propertyName));
    }

    private void NotifyPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}
8 – Conclusion

La gestion de la base de donnée dans WP7 est somme toute assez simple. Il est cependant étrange que Linq-To-SQL ait été choisi alors que celui-ci est remplacé par Entity Framework. De plus, l’approche ici s’apparente plus a de l’Entity Framework code-first qu’à du Linq-To-Sql puisque la configuration se fait entièrement par code.

A noter que le framework est encore en version bêta et que donc les choses peuvent être amenée à évoluer.

Comme d’habitude vous retrouverez les sources de l’application sur mon skydrive.

nRoute – RelayConverters – Création de Converters locaux à une vue

Lorsque l’on développe des applications Silverlight ou WPF il arrive que l’on ait besoin de créer des converters qui n’auront d’utilité que dans une vue particulière. On doit alors créer une classe et donc généralement un fichier supplémentaire qui n’aura aucune utilité pour le reste de l’application ce qui, sur de gros projets, peux rapidement devenir assez volumineux.

Suite à l’article précédent concernant les RelayCommands, on va voir comment nRoute propose de résoudre le problème ci-dessus en utilisant les RelayConverters.

Les RelayConverters suivent le même principe que les RelayCommands avec une déclaration d’un relay en ressource et une initialisation grâce à un behavior. Mais assez parlé, place au code.

On va utiliser une version légèrement modifié des ViewModels de l’article sur le RelayCommands.

public class Book
{
    public long ID { get; set; }
    public string Author { get; set; }
    public string Title { get; set; }
    public int Likes { get; set; }
}
public class MainPageViewModel : ViewModelBase
{
    private readonly IEnumerable<Book> _books;

    public MainPageViewModel()
    {
        var authors = new[] {
            "Terry Pratchett",
            "Molière",
            "Shakespeare",
            "Frank Herbert",
            "Isaac Asimov"
        };

        var list = new List<Book>();
        for (int i = 0; i < 10; ++i)
            list.Add(new Book
            {
                ID = i,
                Author = authors[i % authors.Length],
                Title = "Title " + i,
                Likes = i * i * 10
            });

        _books = list;
    }

    public IEnumerable<Book> Books
    {
        get { return _books; }
    }
}

On veux afficher dans la vue principale la liste des livres d’une manière un peu différente en fonctions du nombre de Likes sur le livre.

Si on a plus de 100 Likes on va dire que beaucoup de gens aiment ce livre, en dessous de 100 on dira que peu de gens l’aiment et à 0 on précisera que ce livre n’est aimé par personne.

Les converters permettent de faire celà, cependant créer une classe juste pour ça ne représente aucun intérêt particulier aussi on va utiliser un RelayConverter. Voici comment on va déclarer notre Xaml :

<UserControl x:Class="nRoute_RelayConverters.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    xmlns:n="http://nRoute/schemas/2010/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">

    <UserControl.Resources>
        <n:ValueConverterRelay x:Name="BookValueConverterRelay" />
    </UserControl.Resources>

    <i:Interaction.Behaviors>
        <n:BridgeViewModelBehavior />
        <n:BridgeValueConverterBehavior ValueConverterRelay="{StaticResource BookValueConverterRelay}" />
    </i:Interaction.Behaviors>

    <StackPanel x:Name="LayoutRoot" Background="White">
        <ItemsControl ItemsSource="{Binding Books}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch">
                        <TextBlock Text="{Binding Converter={StaticResource BookValueConverterRelay}}" />
                    </StackPanel>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </StackPanel>
</UserControl>

Comme on peux le constater, la façon de procéder ici est quasi identique qu’avec les RelayCommand. A la différence près que pour l’instant on a juste déclaré un converter et pas la fonction de conversion en elle même. Pour celà direction le code-behind de la vue.

On va premièrement rajouter un using :

using nRoute.Components;

Ce using permet d’accèder aux méthodes d’extensions de nRoute, l’une d’entre elle, SetRelayConverter, permet de définir la fonction de conversion du RelayConverter et voici comment celà s’utilise :

[MapView(typeof(ViewModels.MainPageViewModel))]
public partial class MainPage
{
    public MainPage()
    {
        InitializeComponent();

        this.SetRelayConverter<Book, string>("BookValueConverterRelay", BookValueConverterIn);
    }

    private static string BookValueConverterIn(Book book)
    {
        if (book.Likes > 100)
            return string.Format("A lot of people likes {0} by {1} !", book.Title, book.Author);
        else if (book.Likes > 1)
            return string.Format("Not a lot of people likes {0} by {1}.", book.Title, book.Author);
        return string.Format("No one likes {0} by {1}...", book.Title, book.Author);
    }
}

SetRelayConverter est une fonction générique prenant paramètre de type les types d’entrée et de retour du converter et en paramètre le nom du RelayConverter défini dans la vue suivi de la méthode de conversion à utilisée.

Verdict, un peu de code-behind pour un traitement purement visuel et une classe économisée et oui on fait toujours du MVVM.

 

Comme d’habitude, les sources de l’articles sont disponibles sur mon Skydrive.

nRoute – CommandRelay – Accéder au contexte parent depuis un DataTemplate

Un problème récurrent lorsqu’on fait du MVVM en Silverlight 4 est d’avoir accès à certains éléments du DataContext courant dans des DataTemplate de nos ItemsControl.

Prenons l’exemple de l’application ci-dessous :

clip_image001[4]

Celle-ci est composée d’une DataGrid bindée sur une collection de livres. Dans le view model qui contient cette collection de livre, on trouve aussi une commande qui sert à afficher les livres.

Voici donc le code de cette page, de son view model et de la classe livre :

public class Book
{
    public long ID { get; set; }
    public string Author { get; set; }
    public string Title { get; set; }
}
public class MainPageViewModel : ViewModelBase
{
    private readonly IEnumerable<Book> _books;
    private readonly ICommand _showAuthorCommand;

    public MainPageViewModel()
    {
        var list = new List<Book>();
        for (int i = 0; i < 10; ++i)
            list.Add(new Book
            {
                ID = i,
                Author = "Author " + i,
                Title = "Title " + i
            });

        _books = list;
        _showAuthorCommand = new ActionCommand<Book>(ShowAuthor);
    }

    public IEnumerable<Book> Books
    {
        get { return _books; }
    }

    public ICommand ShowAuthorCommand
    {
        get { return _showAuthorCommand; }
    }

    private static void ShowAuthor(Book book)
    {
        MessageBox.Show(book.Author);
    }
}
<Grid x:Name="LayoutRoot" Background="White">
    <sdk:DataGrid ItemsSource="{Binding Books}">
        <sdk:DataGrid.Columns>
            <sdk:DataGridTemplateColumn>
                <sdk:DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <Button Content="Show Author"
                                Command="{Binding ShowAuthorCommand}"
                                CommandParameter="{Binding}" />
                    </DataTemplate>
                </sdk:DataGridTemplateColumn.CellTemplate>
            </sdk:DataGridTemplateColumn>
        </sdk:DataGrid.Columns>
    </sdk:DataGrid>
</Grid>

Ce que nous souhaitons faire ici c’est appeler la commande ShowAuthorCommand du view model de la page courante en lui passant en paramètre le livre courant afin qu’il en fasse un traitement (ici affichage de l’auteur du livre dans une MessageBox mais cela pourrait être autre chose). Cependant si on exécute le code ci-dessus la commande n’est pas invoquée. Le Button servant à invoquer la commande se trouve ici dans un DataTemplate son DataContext est donc ici un livre et non le view model de la page. On ne peux donc pas utiliser la commande directement.

Si le DataTemplate n’a pas accès au DataContexte de la page il a néanmoins accès aux StaticResources définies dans la page.

Ce que l’on va faire maintenant avec nRoute va être d’enregistrer la commande à invoquer dans une ressource statique au niveau de la page et ensuite de l’invoquer. Le même concept n’est pas spécifique à nRoute cependant mais je trouve qu’il l’implémente intelligemment.

Tout d’abord nous allons créer dans les ressources statiques de la page une instance de CommandRelay (ne pas confondre avec RelayCommand du MVVMLightToolkit). Vous pouvez traduire la ligne ci-dessous en français en disant : « Je déclare une variable du nom de ShowAuthorCommandRelay » :

<UserControl.Resources>
    <n:CommandRelay x:Key="ShowAuthorCommandRelay" />
</UserControl.Resources>

Ensuite dans les Behavior de la page, en plus du BridgeViewModelBehavior qui sert à mapper la vue et son view model on va rajouter un BridgeCommandBehavior qui va initialiser l’instance de CommandRelay précédemment déclarée :

<i:Interaction.Behaviors>
    <n:BridgeViewModelBehavior />
    <n:BridgeCommandBehavior CommandRelay="{StaticResource ShowAuthorCommandRelay}"
                                CommandSource="{Binding ShowAuthorCommand}" />
</i:Interaction.Behaviors>

La propriété CommandRelay est mise à sur le CommandRelay à initialiser et la propriété CommandSource sur la valeur de la commande (ici c’est la commande ShowAuthorCommand de notre view model).

Vous pouvez traduire les lignes de code ci-dessus en français par : « Initialise-moi ShowAuthorCommandRelay avec la commande ShowAuthorCommand ».

Maintenant il ne reste plus qu’à invoquer la commande dans notre Button en utilisant le code suivant :

<DataTemplate>
    <Button Content="Show Author"
            Command="{Binding Command, Source={StaticResource ShowAuthorCommandRelay}}"
            CommandParameter="{Binding}" />
</DataTemplate>

On voit que la syntaxe pour l’invocation de la commande a changé. Ici on pointe sur la propriété Command de la ressource statique ShowAuthorCommandRelay. Et comme dis précédemment, on peut sans problème accéder aux ressources statiques de la page dans un DataTemplate.

Si on exécute le code ainsi corrigé on peut voir que la MessageBox s’affiche correctement.

NB : En Silverlight 5 dans un tel scénario on aurait probablement utilisé une nouvelle option de Binding du nom de RelativeSource AncestorType mais la technique des CommandRelay sera de toute façon toujours valable.

Vous pouvez comme d’habitude télécharger le code source d’application sur mon Skydrive.

nRoute – Les messages – Ouverture d’une ChildWindow

Un des aspects récurrent dans le développement MVVM est l’utilisation des messages. Un message peut être considéré comme une sorte d’évènement global à l’application et donc accessible à n’importe quel niveau de celle-ci.

L’idée derrière ce pattern est d’avoir un composant applicatif auquel fait référence toute partie de l’application cherchant à écouter ou à envoyer des messages. Ce composant (qui a donné son nom au pattern) est en général connu sous le nom de médiateur et nous permet de mettre en place une architecture dans laquelle les destinataires et les destinateurs des évènements ne se connaissent pas et donc d’obtenir une meilleure modularité.

Le framework nRoute intègre un framework d’envoi/réception de messages de manière synchrone ou asynchrone. Cette partie de nRoute est basée sur le framework Rx pour son implémentation. Ceux ayant déjà eu l’occasion d’utiliser ce dernier se sentiront donc en territoire connu.

Une problématique revenant souvent lorsque l’on développe une application en utilisant le pattern MVVM est de commander l’ouverture d’une ChildWindow et de récupérer le résultat de son traitement à sa fermeture depuis un view model. On va donc voir comment on peux atteindre cet objectif en utilisant des messages.

0 – Le contexte

Le but de l’application que l’on va développer est d’avoir une liste de contacts que l’on pourra éditer en appuyant sur un bouton. Ce bouton déclenchera l’ouverture d’une ChildWindow dans laquelle on pourra éditer le contact et où on aura le choix de valider ou d’annuler nos modifications.

Voici les captures d’écrans du résultat attendu.

mvvm-messages-1

Figure 1

mvvm-message-2

Figure 2

1 – Mise en place de la vue

Notre contrôle principal est le contrôle Home.xaml. C’est celui représenté sur la figure 1.
Le code Xaml de ce contrôle contient une Listbox templatée pour afficher les contacts et un bouton bindé sur une commande pour lancer l’édition du contact séléctionné dans la Listbox.

Voici le code Xaml de ce contrôle :

<UserControl x:Class="nRoute_MVVM_ChildWindow.Views.Home"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    xmlns:n="http://nRoute/schemas/2010/xaml"
    mc:Ignorable="d">

    <i:Interaction.Behaviors>
        <n:BridgeViewModelBehavior />
    </i:Interaction.Behaviors>

    <Grid x:Name="LayoutRoot" Background="White">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="350" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <ListBox x:Name="lstBox" ItemsSource="{Binding Contacts}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding FirstName}" />
                        <TextBlock Text=" " />
                        <TextBlock Text="{Binding LastName}" />
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

        <Button
            Grid.Row="1"
            Content="Edit"
            Command="{Binding EditContactCommand}"
            CommandParameter="{Binding Path=SelectedItem, ElementName=lstBox}" />
    </Grid>
</UserControl>


Et son code-behind :

[nRoute.ViewModels.MapView(typeof(ViewModels.HomeViewModel))]
public partial class Home
{
    public Home()
    {
        InitializeComponent();
    }
}

2 – Définition des view model

Vous voyez grâce à l’attribut MapView que le contrôle Home est lié au view model HomeViewModel mais avant de définir celui-ci définissons d’abord le view model représentant les utilisateurs :

public class ContactViewModel : ViewModelBase, IEditableObject
{
    private string _firstName;
    private string _lastName;
    private ContactViewModel _originalValue;

    public long Id { get; set; }

    public string FirstName
    {
        get
        {
            return _firstName;
        }

        set
        {
            if (_firstName != value)
            {
                _firstName = value;
                NotifyPropertyChanged(() => FirstName);
            }
        }
    }

    public string LastName
    {
        get
        {
            return _lastName;
        }

        set
        {
            if (_lastName != value)
            {
                _lastName = value;
                NotifyPropertyChanged(() => LastName);
            }
        }
    }

    public void BeginEdit()
    {
        _originalValue = new ContactViewModel
        {
            FirstName = FirstName,
            LastName = LastName
        };
    }

    public void CancelEdit()
    {
        if (_originalValue != null)
        {
            LastName = _originalValue.LastName;
            FirstName = _originalValue.FirstName;
            _originalValue = null;
        }
    }

    public void EndEdit()
    {
        _originalValue = null;
    }
}

La principale particularité de ce view model est qu’il implémente l’interface IEditableObject se trouvant dans l’espace de nom System.ComponentModel. Cette dernière nous permet de gérer l’édition du contact grâce à trois méthodes qui nous seront très utiles pour faire le formulaire de la ChildWindow :

- BeginEdit (passe l’objet en édition)

- CancelEdit (annule les modifications de l’objet et termine l’édition)

- EndEdit (valide les modifications de l’objet et termine l’édition)

Maintenant on va définir le view model du contrôle Home :

public class HomeViewModel : ViewModelBase
{
    private readonly ICommand _editCommand;
    private readonly ObservableCollection<ContactViewModel> _contacts;

    public HomeViewModel()
    {
        _contacts = new ObservableCollection<ContactViewModel>();
        _editCommand = new ActionCommand<ContactViewModel>(BeginContactEdition);

        for (int i = 0; i < 10; i++)
            _contacts.Add(new ContactViewModel
            {
                Id = i,
                FirstName = "FirstName" + i,
                LastName = "LastName" + i
            });
    }

    public IEnumerable<ContactViewModel> Contacts
    {
        get
        {
            return _contacts;
        }
    }

    public ICommand EditContactCommand
    {
        get
        {
            return _editCommand;
        }
    }

    private void BeginContactEdition(ContactViewModel contact)
    {
    }
}

Dans son état actuel le view model génère une liste arbitraire de 10 éléments et défini la commande EditContactCommand qui est appellée lors de clic sur le bouton Edit du contrôle Home. C’est donc dans la méthode BeginContactEdition qu’on veux demander à la vue d’ouvrir une ChildWindow ayant pour DataContext le notre contact.

3 – Création de la ChildWindow

Dans l’interface de Visual Studio on va ajouter une nouvelle ChildWindow à notre projet et la modifier pour que son Xaml soit le suivante :

<controls:ChildWindow x:Class="nRoute_MVVM_ChildWindow.ChildWindows.ContactChildWindow"
           xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
           xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
           xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"
           Width="400" Height="300"
           Title="ContactChildWindow">

    <Grid x:Name="LayoutRoot" Margin="2">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="First name : " />
                <TextBox Width="100" Text="{Binding FirstName, Mode=TwoWay}" />
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="Last name : " />
                <TextBox Width="100" Text="{Binding LastName, Mode=TwoWay}" />
            </StackPanel>
        </StackPanel>

        <Button x:Name="CancelButton" Content="Cancel" Click="CancelButton_Click" Width="75" Height="23" HorizontalAlignment="Right" Margin="0,12,0,0" Grid.Row="1" />
        <Button x:Name="OKButton" Content="OK" Click="OKButton_Click" Width="75" Height="23" HorizontalAlignment="Right" Margin="0,12,79,0" Grid.Row="1" />
    </Grid>
</controls:ChildWindow>

Le code behind lui ne change pas par rapport à sa version d’origine.

Par contre nous allons modifier le code-behind de la vue Home afin de créer une instance de la ChildWindow :

[nRoute.ViewModels.MapView(typeof(ViewModels.HomeViewModel))]
public partial class Home
{
    private ContactChildWindow _contactChildWindow;

    public Home()
    {
        InitializeComponent();

        _contactChildWindow = new ContactChildWindow();
    }
}

 

4 – Création des messages

Le concept même du médiateur induit trois choses :

- Nous avons besoin d’un message à envoyer

- Nous avons besoin qu’un contrôle envoie le message

- Nous avons besoin qu’au moins un contrôle écoute le message

Le contrôle envoyant le message doit donc fournir suffisemment d’informations dans le message pour que le ou les contrôles qui écoutent ce message puissent en faire quelque chose.

Dans notre application on aura deux messages, un pour demander l’édition d’un contact et un pour informer de la fin de l’édition d’un contact.

public class BeginContactEditionMessage
{
    public ContactViewModel Contact { get; set; }
}

 

public class EndContactEditionMessage
{
    public ContactViewModel Contact { get; set; }

    public bool Success { get; set; }
}

5 – Envoi et réception des messages

Maintenant qu’on a nos deux messages il faut les envoyer. Et en toute logique on va commencer par celui qui concerne l’ouverture de la ChildWindow.

Pour ce faire on va retourner dans le HomeViewModel et rajouter le champs suivant :

private readonly IChannel<BeginContactEditionMessage> _beginContactEditionChannel;


Dans le constructeur on va l’initialiser avec le canal de communication par défaut associé au message BeginContactEditionMessage (Il existe aussi des canaux privés mais ils ne seront pas utilisés ou abordés dans cet article) :

_beginContactEditionChannel = Channel<BeginContactEditionMessage>.Public;


Enfin il nous faut modifier la méthode BeginContactEdition avec le code suivant :

private void BeginContactEdition(ContactViewModel contact)
{
    var message = new BeginContactEditionMessage
    {
        Contact = contact
    };

    contact.BeginEdit();
    _beginContactEditionChannel.OnNext(message);
}

La méthode OnNext est la méthode qui sert à envoyer un message à tout ceux que ça intéresse (ici les gens qui ont un peu utilisé Rx se sentiront en terrain connu). Et justement pour l’instant le message est envoyé mais celà n’interesse personne dans l’application.

La principale interessée par ce message est bien entendu la ChildWindow. On va la modifier un peu pour quelle réagisse aux messages envoyés par le view model et pour ça direction son code-behind.

On va rajouter deux lignes à son constructeur :

var channel = Channel<BeginContactEditionMessage>.Public;
_beginContactEditionChannelDisposable = channel.Subscribe(BeginContactEdition);

Comme dans le view model, on récupère tout d’abord l’instance publique du canal de communication mais cette fois-ci on ne va pas appeller OnNext (qui signifie envoi) mais Subscribe (qui signifie écoute). La méthode Subscribe prend en paramètre un Action<T> qui sera appellé à chaque réception d’un nouveau message et renvoi un objet IDisposable que l’on devra supprimer au moment où on voudra se désabonner du canal.

Pour l’instant, on va sauvegarder cette valeur dans un champs dont la déclaration sera la suivante :

private readonly IDisposable _beginContactEditionChannelDisposable;


Ensuite on va implémenter la méthode BeginContactEdition :

private void BeginContactEdition(BeginContactEditionMessage m)
{
    DataContext = m.Contact;
    Show();
}


Afin de faire tout proprement on va modifier légèrement la déclaration de la ChildWindow et lui faire implémenter IDisposable :

public void Dispose()
{
    _beginContactEditionChannelDisposable.Dispose();
}

Et voilà ! Maintenant lorsque l’on sélectionne un contact et que l’on clique sur le bouton éditer la ChildWindow s’affiche avec comme contexte de donnée le contact sélectionné ! Mais le view model n’est jamais notifié lorsque l’édition de l’utilisateur est terminée.

Alors on va faire le même travail pour le message EndContactEdition que pour le message BeginContactEdition mais en inversant les rôles de destinateur et de destinataire.

Toujours dans le code behind de la ChildWindow on va modifier les évènements liés au clics sur les boutons OK et Cancel par le code suivant :

private void OKButton_Click(object sender, RoutedEventArgs e)
{
    DialogResult = true;
    PublishResult();
}

private void CancelButton_Click(object sender, RoutedEventArgs e)
{
    DialogResult = false;
    PublishResult();
}

private void PublishResult()
{
    var contact = (ContactViewModel)DataContext;
    var channel = Channel<EndContactEditionMessage>.Public;
    var message = new EndContactEditionMessage
    {
        Contact = contact,
        Success = DialogResult.HasValue && DialogResult.Value
    };

    channel.OnNext(message);
}

Et maintenant on retourne dans le view model et on applique la même méthode pour la réception du message EndContactEditionMessage que celle utilisée dans le code-behind de la ChildWindow pour recevoir le message BeginContactEditionMessage à savoir.

Création du champs IDisposable :

private readonly IDisposable _endContactEditionChannelDisposable;


Modification du constructeur du view model :

_endContactEditionChannel = Channel<EndContactEditionMessage>.Public;
_endContactEditionChannelDisposable = _endContactEditionChannel.Subscribe(
    m =>
    {
        if (m.Success)
            m.Contact.EndEdit();
        else
            m.Contact.CancelEdit();
    });


Ajout de l’interface IDisposable au view model et ajout de son implémentation

public void Dispose()
{
    _endContactEditionChannelDisposable.Dispose();
}

En espérant que cet article vous sera utile.

Comme d’habitude vous pourrez trouver les sources de l’article sur mon skydrive.

nRoute – Les commandes inversées

 

Introduction

Dans un article précédent, je vous avais parlé de l’utilisation des commandes avec nRoute. A titre de rappel, les commandes permettent à la vue d’envoyer un message au view model pour qu’il exécute une action particulière. Mais comment faire dès lors que, depuis le view model, nous avons besoin d’envoyer un message à la vue pour, par exemple, changer l’état visuel d’un contrôle avec son VisualStateManager ?

Les commandes inversées alias ReverseCommands ont été créées précisément pour remplir cette fonction.

Contexte

Afin d’illustrer cet article, on va d’abord créer une application Silverlight classique. Voici le Xaml de la MainPage :

<UserControl x:Class="nRoute_ReverseCommands.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:ei="clr-namespace:Microsoft.Expression.Interactivity.Core;assembly=Microsoft.Expression.Interactions"
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    xmlns:n="http://nRoute/schemas/2010/xaml">

    <i:Interaction.Behaviors>
        <n:BridgeViewModelBehavior />
    </i:Interaction.Behaviors>

    <Grid x:Name="LayoutRoot" Background="White">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="VisualStateGroup">
                <VisualState x:Name="RedState">
                    <Storyboard>
                        <ColorAnimation Duration="0" To="Red"
                                        Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
                                        Storyboard.TargetName="rectangle" />
                    </Storyboard>
                </VisualState>
                <VisualState x:Name="BlueState">
                    <Storyboard>
                        <ColorAnimation Duration="0" To="Blue"
                                        Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
                                        Storyboard.TargetName="rectangle" />
                    </Storyboard>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <Rectangle x:Name="rectangle" Width="50" Height="50" Fill="Transparent" />
    </Grid>
</UserControl>
  

La grille possède deux états visuels, l’un qui peindra le rectangle en bleu et l’autre qui le peindra en rouge.

Le but de l’exemple va être de changer la couleur du rectangle toute les secondes grâce à ses états visuels et à l’utilisation de commandes inversées.

Voici le view model de départ :

public class MainPageViewModel : ViewModelBase
{
    private bool _ok;
    private readonly DispatcherTimer _timer;

    public MainPageViewModel()
    {
        _timer = new DispatcherTimer();
        _timer.Interval = TimeSpan.FromSeconds(1);
        _timer.Tick += (s, a) => OnTimerTick();
        _timer.Start();
    }

    private void OnTimerTick()
    {
        // Changer l'état visuel
        _ok = !_ok;
    }
}

 

Création et initialisation des commandes

Dans le code ci-dessus, on initialise un timer qui appelle la méthode OnTimerTick toutes les secondes. Dans cette méthode nous devons donner l’ordre à la vue de changer d’état visuel. Pour ce faire nous allons créer deux commandes inversées :

private readonly IReverseCommand _okCommand;
private readonly IReverseCommand _koCommand;

 

Et ne pas oublier de les initialiser avec une action vide dans le constructeur du view model :

_okCommand = new ActionCommand(() => { });
_koCommand = new ActionCommand(() => { });

 

Ici, on peut voir que les ActionCommand qu’on a déjà utilisé auparavant implémentent IReverseCommand. Cependant, l’initialisation d’une commande inversée n’a rien de différent de celle d’une commande normale. Ce qui diffère, c’est son utilisation : on a initialisé la commande avec une action vide car on n’a pas d’actions en provenance de la vue à traiter.

Si on regarde la définition de IReverseCommand, on voit qu’elle n’expose qu’un seul évènement nommé CommandExecuted. Nous verrons en quoi cela permettra d’exécuter une action dans la vue un peu plus loin.

 

Maintenant que nos commandes sont initialisées, on va pouvoir les utiliser. Pour cela, on va modifier le corps de la méthode OnTimerTick afin d’exécuter les commandes inversées Ok et Ko une fois sur deux alternativement :

private void OnTimerTick()
{
    if (_ok)
        _okCommand.Execute(null);
    else
        _koCommand.Execute(null);
    _ok = !_ok;
}

 

On doit aussi l’exposer à la vue, on rajoute donc deux propriétés :

public ICommand OkCommand { get { return _okCommand; } }
public ICommand KoCommand { get { return _koCommand; } }

 

Liaison des commandes inversées à la vue

Le view model est maintenant terminé et il est temps de retourner dans la vue pour utiliser les commandes inversées exposées. nRoute fournit à cette fin un trigger qui va s’abonner à l’évènement CommandExecuted exposé par l’interface IReverseCommand et nous permettre d’exécuter une action dans la vue. Ce trigger est le ReverseCommandTrigger et on va voir tout de suite comment il s’utilise.

Dans le Xaml de la MainPage, on va rajouter des triggers sous la déclaration des états visuels (balise VisualStateManager.VisualStateGroups).

<i:Interaction.Triggers>
    <n:ReverseCommandTrigger ReverseCommand="{Binding OkCommand}">
        <ei:GoToStateAction StateName="BlueState" />
    </n:ReverseCommandTrigger>
    <n:ReverseCommandTrigger ReverseCommand="{Binding KoCommand}">
        <ei:GoToStateAction StateName="RedState" />
    </n:ReverseCommandTrigger>
</i:Interaction.Triggers>

 

La déclaration est assez claire et parle d’elle même. Lorsque la commande OkCommand est invoquée, on charge l’état visuel “BlueState”. Lorsque la commande KoCommand est invoquée, on charge l’état visuel “RedState”. Au lieu de changer l’état visuel on aurait pu faire d’autres actions comme lancer un storyboard, effectuer une navigation etc… Le choix en revient désormais à la vue.

Maintenant, si on lance l’application on a un joli rectangle qui change de couleur toutes les secondes.

 

Vous pourrez trouver le code d’exemple de cet article sur mon skydrive.

 

A vous de jouer maintenant !

Silverlight – Multiples projets de tests unitaires

Introduction

Les projets de tests unitaires en Silverlight tels qu’ils existent dans le Silverlight Toolkit sont des applications s’exécutant de manière autonome.

Lorsque l’on teste un projet Silverlight seul, ça ne pose généralement pas de problèmes :

testmyapp-smallest

Notre application de test (ici TestMyApp.Tests) fait référence à notre application Silverlight à tester (TestMyApp). On n’a donc alors plus qu’à exécuter le projet de tests pour tester l’application.

Prenons maintenant l’exemple d’une application Silverlight composée de plusieurs projets :

testmyapp-all-projects

Dans ce cas, deux possibilités : soit on garde un seul projet de test qui référencera tous les projets Silverlight et les testera (ce que je vous déconseille de faire), soit on crée un projet de test par projet Silverlight à tester.

testmyapp-full

L’inconvénient, c’est qu’on doit lancer chaque projet de tests manuellement.

Le but de cet article va donc être de montrer comment on peut exécuter tous les tests via le projet TestMyApp.Tests.

Un prérequis cependant, nous devrons nommer tous nos projets de tests avec la forme *.Tests. Cette convention permet de faire la différence entre les projets contenant des tests unitaires et les autres.

Décomposition d’une application de test

Une application de tests unitaires Silverlight est avant tout une application Silverlight. La principale différence entre une application Silverlight classique et une application de test se situe dans le gestionnaire de l’évènement Startup :

testmyapp-testdecomposition

private void Application_Startup(object sender, StartupEventArgs e)
{
    RootVisual = UnitTestSystem.CreateTestPage();
}

On peut voir ci-dessus que l’on affecte à la propriété RootVisual de notre application une page de projet de test. Le framework de test fait de la reflection sur l’assembly courant afin de trouver et d’exécuter les tests unitaires et génère une page qui ressemble à celle-ci :

testmyapp-simpletest

Afin de pouvoir exécuter plusieurs tests, nous allons modifier l’évènement startup.

Modification de l’initialisation de l’application

Dans un premier temps il faut choisir l’application de test principale. Ici, on va choisir l’application TestMyApp.Tests. Dans cette application, il faut référencer les autres applications de tests.

testmyapp-testreferences

Une fois les références correctement ajoutées, on peut modifier le gestionnaire de l’évènement startup.

private void Application_Startup(object sender, StartupEventArgs e)
{
    UnitTestSettings settings = UnitTestSystem.CreateDefaultSettings();

    var testAssemblies = Deployment.Current.Parts.Where(p => p.Source.EndsWith(".Tests.dll")).ToList();
    foreach (AssemblyPart part in testAssemblies)
    {
        Uri assemblyUri = new Uri(part.Source, UriKind.Relative);
        Stream ressourceStream = GetResourceStream(assemblyUri).Stream;
        AssemblyPart assemblyPart = new AssemblyPart();

        Assembly assembly = assemblyPart.Load(ressourceStream);

        settings.TestAssemblies.Add(assembly);
    }

    RootVisual = UnitTestSystem.CreateTestPage(settings);
}

Le code ci-dessus permet d’ajouter au projet de test principal la liste des assembly dont le nom se termine par .Tests.dll. Problème de ceci : notre assembly courant répond aussi à ce critère et dans les propriétés par défaut d’un projet de test, l’assembly courant est déjà chargé. Conséquence : le projet sera chargé deux fois. Pour éviter ça, on va donc améliorer la requête Linq de la manière suivante :

var currentAssembly = GetCurrentAssemblyName();
var testAssemblies = Deployment.Current.Parts.Where(p => p.Source != currentAssembly && p.Source.EndsWith(".Tests.dll")).ToList();

La méthode GetCurrentAssemblyName est définie de cette façon :

private static string GetCurrentAssemblyName()
{
    string currentAssembly = Assembly.GetExecutingAssembly().FullName;
    currentAssembly = currentAssembly.Substring(0, currentAssembly.IndexOf(','));
    return currentAssembly + ".dll";
}

Lancement de l’application

Notre projet de test est maintenant prêt et si on l’exécute, voilà ce qu’on obtient :

testmyapp-full-testexec

Maintenant vous n’avez plus aucune excuses pour ne pas tester vos applications Silverlight.

C’est tout pour aujourd’hui !

nRoute – Commands – Passage de paramètres

Pré-requis

Afin de mieux appréhender le contenu de cet article, il est conseillé d’avoir lu l’article précédent concernant l’utilisation et la création de commandes.

Cet article vous fera découvrir différentes manières de passer des paramètres à vos commandes avec nRoute.

Le contexte

Nous souhaitons développer une application simple qui affiche une liste d’utilisateur et qui, lorsqu’un élément de cette liste est sélectionné, permet d’en afficher les détails à droite de la liste.

Cette application ressemblera à la capture ci-dessous :

nRoute-Command-Parameters

Le modèle de données

Notre modèle de donnée dispose de deux classes représentant les utilisateurs.
La première est UserLight et ne contient que les informations minimale de notre utilisateur (Id et Login). C’est de cette classe dont nous nous servirons pour peupler la liste à gauche.
La seconde est User qui est la classe contenant toutes les informations de notre utilisateur.

Voici les définitions de ces deux classes :

public class UserLight
{
    public long Id { get; set; }
    public string Login { get; set; }
}
public class User
{
    public long Id { get; set; }
    public string Login { get; set; }
    public string Email { get; set; }
    public DateTime CreationDate { get; set; }
}

L’accès aux données

Afin de faciliter l’accès à nos données, nous avons créé un dépôt des utilisateurs. Ce dépôt est un dépôt de test qui contient dix utilisateurs et qui nous renvoie nos résultats via une callback tel que le ferait une implémentation qui ferait un appel à un service web. Il contient deux méthodes, une nous renvoyant la liste de tout les utilisateurs, et une nous renvoyant les détails d’un utilisateur donné.

Voici le code du dépôt :

public class UserRepository
{
    private static readonly List<User> _users;

    static UserRepository()
    {
        _users = new List<User>();

        for (int i = 0; i < 10; ++i)
        {
            _users.Add(new User
            {
                Id = i,
                Login = "login_" + i,
                Email = "email" + i + "@company.com",
                CreationDate = DateTime.Now.AddDays(-i)
            });
        }
    }

    public void GetAll(Action<IEnumerable<UserLight>> callback)
    {
        callback(_users.Select(u => new UserLight { Id = u.Id, Login = u.Login }));
    }

    public void GetById(long userId, Action<User> callback)
    {
        callback(_users.FirstOrDefault(u => u.Id == userId));
    }
}

Le view model et la vue

Maintenant nous allons créer notre view model. Celui-ci contient deux propriétés. La première est Users qui est la liste de tout les utilisateurs. La seconde est CurrentUser qui représentera l’utilisateur sélectionné dans la liste. Le view model chargera dans son constructeur, la liste de tout les utilisateurs du dépôt afin de les afficher dans la liste à gauche.

Voici le code du view model :

[MapViewModel(typeof(MainPage))]
public class MainPageViewModel : ViewModelBase
{
    private readonly UserRepository _userRepository;

    private ObservableCollection<UserLight> _users;
    private User _currentUser;

    public MainPageViewModel()
    {
        _userRepository = new UserRepository();

        _userRepository.GetAll(users => Users = new ObservableCollection<UserLight>(users));
    }

    public ObservableCollection<UserLight> Users
    {
        get
        {
            return _users;
        }

        set
        {
            if (_users != value)
            {
                _users = value;
                NotifyPropertyChanged(() => Users);
            }
        }
    }

    public User CurrentUser
    {
        get
        {
            return _currentUser;
        }

        set
        {
            if (_currentUser != value)
            {
                _currentUser = value;
                NotifyPropertyChanged(() => CurrentUser);
            }
        }
    }
}

Et bien entendu voici le code XAML de notre MainPage :

<UserControl
    x:Class="nRoute_Command_Parameters.Views.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    xmlns:n="http://nRoute/schemas/2010/xaml">

    <i:Interaction.Behaviors>
        <n:BridgeViewModelBehavior />
    </i:Interaction.Behaviors>

    <Grid x:Name="LayoutRoot" Background="White">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="2*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <ListBox x:Name="listBox"
                 Grid.Column="0"
                 Grid.RowSpan="4"
                 ItemsSource="{Binding Users}"
                 DisplayMemberPath="Login"
                 Margin="20"
                 SelectedValuePath="Id">
        </ListBox>

        <TextBlock Grid.Column="1" Grid.Row="0" Text="Id : " />
        <TextBlock Grid.Column="1" Grid.Row="1" Text="Login : " />
        <TextBlock Grid.Column="1" Grid.Row="2" Text="Email : " />
        <TextBlock Grid.Column="1" Grid.Row="3" Text="Date : " />

        <TextBox Grid.Column="2" Grid.Row="0" Text="{Binding CurrentUser.Id}" IsReadOnly="True" />
        <TextBox Grid.Column="2" Grid.Row="1" Text="{Binding CurrentUser.Login}" />
        <TextBox Grid.Column="2" Grid.Row="2" Text="{Binding CurrentUser.Email}" />
        <TextBox Grid.Column="2" Grid.Row="3" Text="{Binding CurrentUser.CreationDate}" IsReadOnly="True" />
    </Grid>
</UserControl>

Déclaration et utilisation d’une commande paramétrée

Tout ceci est très bien mais lorsque l’on clique sur un utilisateur chargé dans notre liste rien de ne s’affiche à droite, ce qui est normal car nous n’avons rien codé.

Afin de remédier à celà nous allons créer une commande qui sera invoquée lorsque la sélection de notre listbox changera.

Nous allons donc modifier notre view model pour lui ajouter le code suivant :

private ICommand _showDetailCommand;
public ICommand ShowDetailCommand
{
    get
    {
        return _showDetailCommand;
    }
}
private void ShowDetail(long id)
{
    _userRepository.GetById(id, u => CurrentUser = u);
}

N’oublions pas d’initialiser cette commande dans notre constructeur.

_showDetailCommand = new ActionCommand<long>(ShowDetail);

Vous remarquerez que contrairement à l’article précédent, ici nous utilisons la version générique de ActionCommand. Ici nous indiquons que cette commande prend un paramètre de type long et qu’elle doit utiliser la méthode ShowDetail. Cette dernière utilisera notre dépôt pour récupérer les détails de l’utilisateur sélectionné et de l’affecter à la propriété CurrentUser du view model.

Et voilà ! La boucle est bouclée ! Oui mais enfait non. Nous n’avons toujours pas lié notre commande dans le xaml. Il nous faut donc remplacer le code de notre listbox par le suivant :

<ListBox x:Name="listBox"
    Grid.Column="0"
    Grid.RowSpan="4"
    ItemsSource="{Binding Users}"
    DisplayMemberPath="Login"
    Margin="20"
    SelectedValuePath="Id">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="SelectionChanged">
            <n:ExecuteCommandAction
                Command="{Binding ShowDetailCommand}"
                Parameter="{Binding ElementName=listBox, Path=SelectedValue}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</ListBox>

Maintenant si nous analysons le XAML ci-dessus nous voyons que la valeur sélectionnée par notre listbox correspond à la propriété Id de notre utilisateur (SelectedValuePath=”Id”).

Nous déclarons un trigger qui se déclenche lorsque la sélection de notre lisbox est changé (EventName=”SelectionChanged”).

Lorsque ce trigger est déclenché, nous executons la commande ShowDetailCommand de notre view model (Command=”{Binding ShowDetailCommand}”) en lui passant la valeur sélectionnée par notre listbox qui est ici l’Id de l’utilisateur (Parameter=”{Binding ElementName=listBox, Path=SelectedValue}”).

Cette fois-ci c’est vraiment fini. Si nous executons notre application vous pourrez constater que tout se passe comme prévu.

Vous savez maintenant comment passer un paramètre à une commande en XAML.

Variantes et paramètres multiples.

Pour la suite je vais vous proposer une petite variante qui va vous permettre d’apprendre à passer plusieurs paramètres à vos commande.

nRoute fournit deux couples de classes permettant de passer plusieurs paramètres :

n:Parameter / n:ParametersCollection

n:DependencyParameter / n:DependencyParametersCollection

Le groupe Parameter n’accepte que des valeurs statiques alors que le groupe DependencyParameter permet le binding ce qui est très souvent le fonctionnement recherché.

Modifions donc notre XAML pour passer un paramètre en utilisant une instance de DependencyParameter.

<n:ExecuteCommandAction Command="{Binding ShowDetailCommand}">
    <n:ExecuteCommandAction.Parameter>
        <n:DependencyParameter Value="{Binding ElementName=listBox, Path=SelectedValue}" />
    </n:ExecuteCommandAction.Parameter>
</n:ExecuteCommandAction>

Maintenant l’initialisation de notre commande dans le ViewModel :

_showDetailCommand = new ActionCommand<DependencyParameter>(
    p =>
    {
        var id = System.Convert.ToInt64(p.Value);
        ShowDetail(id);
    });

En lancant l’application vous constaterez que le fonctionnement est identique à précédement.

Si nous décidions, à titre d’exemple, de passer le login de l’identifiant et le login de l’utilisateur en paramètre à notre commande nous pourrions procéder de la manière suivante.

Modification du XAML :

<n:ExecuteCommandAction Command="{Binding ShowDetailCommand}">
    <n:ExecuteCommandAction.Parameter>
        <n:DependencyParametersCollection>
            <n:DependencyParameter Key="Id" Value="{Binding ElementName=listBox, Path=SelectedValue}" />
            <n:DependencyParameter Key="Login" Value="{Binding ElementName=listBox, Path=SelectedItem.Login}" />
        </n:DependencyParametersCollection>
    </n:ExecuteCommandAction.Parameter>
</n:ExecuteCommandAction>

Modification de l’initialisation de notre commande dans le ViewModel :

_showDetailCommand = new ActionCommand<DependencyParametersCollection>(
    p =>
    {
        var id = System.Convert.ToInt64(p["Id"]);
        var login = p["Login"].ToString();
        System.Windows.MessageBox.Show(login);
        ShowDetail(id);
    });

Je pense que vous avez compris le principe maintenant ;-)

Vous pourrez retrouver le code de cet article sur mon skydrive.