XAML – Comment connaître la fin d’une transition entre deux VisualStates depuis un view model

Je développe pour WinRT depuis un petit moment en XAML et C#. J’ai récemment fait face à un problème où je devais attendre qu’une animation soit terminée avant de commencer un traitement dans un view model. Le problème était que cette animation est contenue dans une transition entre VisualState dans un de mes Custom Controls.

Mon Custom Control représente un pion de reversi ayant donc deux faces, noire et blanche. Quand sa propriété IsBlack change celà lance une transition entre deux VisualStates. Comme mon application utilise le pattern MVVM j’ai besoin de trouver un moyen de propager l’information qu’une animation a commencée et s’est arretée depuis le custom control au view model.

J’ai tout d’abord ajouté une propriété une propriété a mon contrôle Token :

public bool IsColourChanging
{
    get { return (bool) GetValue(IsColourChangingProperty); }
    set { SetValue(IsColourChangingProperty, value); }
}

public static readonly DependencyProperty IsColourChangingProperty =
    DependencyProperty.Register("IsColourChanging", typeof (bool), typeof(Token), new PropertyMetadata(false));

Ensuite dans la méthode OnApplyTemplate j’appelle la méthode qui est utilisée pour s’abonner aux évènement CurrentStateChanging et CurrentStateChanged du VisualStateGroup contenant les états visuels représentant chaque couleur :

private void HandleColourChanging()
{
    var child = VisualTreeHelper.GetChild(this, 0) as FrameworkElement;

    Debug.Assert(child != null, "Token's template must be defined !");

    var groups = VisualStateManager.GetVisualStateGroups(child);

    Debug.Assert(groups != null, "Token's VisualStateGroups should be defined!");

    // The Cast operator is not necessary in Windows 8 Metro Style Apps
    var colourGroup = groups.Cast<VisualStateGroup>().FirstOrDefault(g => g.Name == ColourStatesGroupName);

    Debug.Assert(colourGroup != null, "Token's VisualState Transition should be defined!");

    colourGroup.CurrentStateChanging += ColourGroupCurrentStateChanging;
    colourGroup.CurrentStateChanged += ColourGroupCurrentStateChanged;
}

Dans les méthodes ColourGroupCurrentStateChang[ing/ed] je n’ai plus qu’à définir la valeur de la propriété IsColourChanging :

private void ColourGroupCurrentStateChanging(object sender, VisualStateChangedEventArgs e)
{
    IsColourChanging = true;
}

private void ColourGroupCurrentStateChanged(object sender, VisualStateChangedEventArgs e)
{
    IsColourChanging = false;
}

 

A cette étape, à chaque fois qu’une transition entre deux couleurs est effectuée la propriété IsColourChanging est mise à True.

Donc maintenant comment propager cette information à mon view model ? En utilisant le binding biensûr et voici comment celà peut-être effectué si on part du principe que l’ItemSource de l’ItemsControl est définie à une collection de TokenViewModels :

<ItemsControl>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <local:Token IsBlack="{Binding IsBlack}"
                            IsColourChanging="{Binding IsAnimating, Mode=TwoWay}"
                            Width="100"
                            Height="100" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

Vous pouvez voir ici que la propriété IsColourChanging du contrôle est bindée à la propriété IsAnimating du view model en mode bi-directionnel. Maintenant c’est au view model de réagir aux changements de la propriété IsAnimating.

Je considère ce trick comme étant important car les applications WinRT se doivent d’être fluides et rapide et donc utilisent beaucoup d’animations. Mais comme fluide et rapide n’est pas synonyme d’inmaintenable c’est un moyen comme un autre de séparer la logique entre la vue et le viewmodel.

Vous pouvez voir un exemple de code en WPF sur mon skydrive.

En espérant que ça aide.

Executer une action suite à des appels WCF parallèles

Récemment on m’as posé la question suivante :

« Comment je peux faire pour exécuter une action après que tout les appels WCF asynchrones se soient exécutés en Silverlight ? »

Ca tombe bien, j’avais une solution sous le coude que j’avais mis en place lors d’un précédent projet. Dans ce projet je devais charger une série de tables de références telles que Pays, Villes etc… Tout ces chargements s’effectuaient de manières asynchrones et étaient lancés en parallèles. Durant le temps de chargement, j’affichais un message indiquant que l’application était en cours de chargement et lorsque toutes les tables de références étaient chargées je faisait disparaitre ce message et rendais active l’application.

L’idée est simple, je vais créer une classe qui va se charger de lancer tout les appels WCF en parallèle. Cette classe prend en paramètre une callback qui sera invoquée lorsque tout les appels auront été terminés.

Pour savoir quand tout les retours ont eu lieu on va faire ça à l’ancienne. A chaque retour d’appels WCF je vais incrémenter un compteur. Lorsque ce compteur aura pour valeur le nombre total d’appels WCF que j’ai lancé j’appellerai la callback passée en paramètre. Simple non ?

Maintenant que le concept est posé passons à l’implémentation

Mon implémentation originale était en Silverlight et pas très générique. J’ai donc retravaillé un peu la chose et choisi de faire un exemple multi-plateforme Windows 8 Metro/Silverlight/WPF. Chaque plateforme ayant ses spécificités je vais d’abord les présenter grâce à un petit tableau.

 

Windows 8 Metro Silverlight WPF
Basé sur des Task Oui Non Oui
Basé sur des évènements Oui Oui Oui

 
Vous remarquez que j’ai classé chaque plateforme dans deux catégories, Task ou Event. Le truc à savoir c’est qu’en fonction de la plateforme que vous ciblez, Visual Studio va générer les proxy WCF de manières différentes. A noter que pour WPF on a le choix de la méthode :

Afin de pouvoir réutiliser ultérieurement un peu de code j’ai créé un Portable Library contenant une classe de base pour les opérations d’appels asynchrones (le code est très commenté et se passe donc de plus de commentaires) :

using System;
using System.Collections.Generic;

namespace WcfUtils
{
    /// <summary>
    /// Cette classe de base permet de définir les méthodes et membres de base pour gérer
    /// des appels WCF asyncrhrones devant se synchroniser à la fin.
    /// </summary>
    public abstract class ParallelCallBase<TResult> where TResult : ParallelCallResultBase
    {
        /// <summary>
        /// Nombre de retours d'appels asynchrones.
        /// </summary>
        private int _count;

        /// <summary>
        /// Nombre total d'appels asynchrones.
        /// </summary>
        private readonly int _totalCount;

        /// <summary>
        /// Méthode à appeller lorsque tout les retours auront été reçus.
        /// </summary>
        private Action<TResult> _callback;

        /// <summary>
        /// Contexte de synchronisation.
        /// </summary>
        private System.Threading.SynchronizationContext _context;

        /// <summary>
        /// Objet servant à vérouiller les accès concurrentiels.
        /// </summary>
        protected readonly object SyncRoot = new object();

        /// <summary>
        /// Résultat des appels parallèles.
        /// </summary>
        protected TResult Result { get; set; }

        /// <summary>
        /// Constructeur de l'objet
        /// </summary>
        /// <param name="totalCount">Nombre total de retours à attendre.</param>
        protected ParallelCallBase(int totalCount)
        {
            _totalCount = totalCount;
        }

        /// <summary>
        /// Méthode à invoquer à chaque retour d'appel WCF.
        /// </summary>
        /// <param name="error">Erreur éventuelle survenue.</param>
        protected virtual void OnCallCompleted(Exception error)
        {
            // On lock l'objet de façon à pouvour incrémenter _count et modifier la collection Result.Errors
            // au cas où les deux webservice se terminerai en même temps.
            lock (SyncRoot)
            {
                _count++;

                // Si on a une erreur alors on l'ajoute à la liste des erreurs.
                if (error != null)
                    Result.Errors.Add(error);

                // Si tout les appels ont été effectués on appelle la callback.
                if (_count == _totalCount)
                    MarshallToContext(() => _callback(Result));
            }
        }

        /// <summary>
        /// Execute la méthode <paramref name="a"/> sur le contexte de synchronisation enregistré dans la méthode Run.
        /// </summary>
        /// <param name="a">Action à exécuter.</param>
        protected virtual void MarshallToContext(Action a)
        {
            if (_context != null)
                _context.Post(_ => a(), null);
            else
                a();
        }

        /// <summary>
        /// Execute les appels asynchrones.
        /// </summary>
        /// <param name="context">Contexte de synchronisation.</param>
        /// <param name="callback">Méthode à appeller lorsque tout les retours auront eu lieu.</param>
        public virtual void Run(System.Threading.SynchronizationContext context, Action<TResult> callback)
        {
            if (callback == null)
                return;

            _context = context;
            _callback = callback;
        }
    }

    /// <summary>
    /// Classe de base des résultats d'appels de service WCF en parallèle.
    /// </summary>
    public abstract class ParallelCallResultBase
    {
        private readonly List<Exception> _errors = new List<Exception>();

        /// <summary>
        /// Liste des erreurs survenues.
        /// </summary>
        public ICollection<Exception> Errors { get { return _errors; } }
    }
}

On a donc déjà l’infrastructure nécessaire pour ces appels parallèle. Reste maintenant à rajouter notre propre logique. Dans mon exemple, j’appelle deux services qui me renvoie une liste de pays et une liste de ville. Voyons comment on se sert de la classe précédente pour ce cas là :

 

using System;
using System.Collections.Generic;

#if NETFX_CORE
using MetroApplication.CityServiceReference;
using MetroApplication.CountryServiceReference;
using System.Threading.Tasks;
using System.Collections.ObjectModel;
using Windows.UI.Xaml;
#elif SILVERLIGHT
using SilverlightApplication.CityServiceReference;
using SilverlightApplication.CountryServiceReference;
#elif WPF_EVENT_BASED
using System.Collections.ObjectModel;
using WpfApplicationEventBasedServiceProxy.CityServiceReference;
using WpfApplicationEventBasedServiceProxy.CountryServiceReference;
#elif WPF_TASK_BASED
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using WpfApplicationTaskBasedServiceProxy.CityServiceReference;
using WpfApplicationTaskBasedServiceProxy.CountryServiceReference;
#endif

namespace WcfUtils
{
    /// <summary>
    /// Classe chargeant une liste de pays et de ville de manière asynchrone et parallèle.
    /// </summary>
    public class ReferenceLoader : ParallelCallBase<ReferenceLoaderResult>
    {
        /// <summary>
        /// Méthode appellée lorsque le retour de l'appel à GetAllCities survient.
        /// </summary>
        public Action<Exception, IEnumerable<string>> OnGetAllCitiesCompletedTrigger { get; set; }

        /// <summary>
        /// Méthode appellée lorsque le retour de l'appel à GetAllCountries survient.
        /// </summary>
        public Action<Exception, IEnumerable<string>> OnGetAllCountriesCompletedTrigger { get; set; }

        /// <summary>
        /// Constructeur du loader.
        /// </summary>
        public ReferenceLoader()
            // Nombre de retours d'appels WCF à attendre
            : base(2)
        {
        }

        #region OnCompleted
        private void OnGetAllCitiesCompleted(Exception error, IEnumerable<string> result)
        {
            if (OnGetAllCitiesCompletedTrigger != null)
                MarshallToContext(() => OnGetAllCitiesCompletedTrigger(error, result));
            if (error == null)
                ((ReferenceLoaderResult)Result).Cities = result;
            OnCallCompleted(error);
        }

        private void OnGetAllCountriesCompleted(Exception error, IEnumerable<string> result)
        {
            if (OnGetAllCountriesCompletedTrigger != null)
                MarshallToContext(() => OnGetAllCountriesCompletedTrigger(error, result));
            if (error == null)
                ((ReferenceLoaderResult)Result).Countries = result;
            OnCallCompleted(error);
        }

#if NETFX_CORE || WPF_TASK_BASED
        private void OnGetAllCitiesCompleted(Task<ObservableCollection<string>> antecedent)
        {
            OnGetAllCitiesCompleted(antecedent.Exception, antecedent.Result);
        }

        private void OnGetAllCountriesCompleted(Task<ObservableCollection<string>> antecedent)
        {
            OnGetAllCountriesCompleted(antecedent.Exception, antecedent.Result);
        }
#elif SILVERLIGHT || WPF_EVENT_BASED
        private void OnGetAllCitiesCompleted(object s, GetAllCitiesCompletedEventArgs e)
        {
            OnGetAllCitiesCompleted(e.Error, e.Result);
        }

        private void OnGetAllCountriesCompleted(object s, GetAllCountriesCompletedEventArgs e)
        {
            OnGetAllCountriesCompleted(e.Error, e.Result);
        }
#endif
        #endregion

        /// <summary>
        /// Execute les appels asynchrones.
        /// </summary>
        /// <param name="context">Contexte de synchronisation.</param>
        /// <param name="callback">Méthode à appeller lorsque tout les retours auront eu lieu.</param>
        public override void Run(System.Threading.SynchronizationContext context, Action<ReferenceLoaderResult> callback)
        {
            base.Run(context, callback);
            Result = new ReferenceLoaderResult();

            // On instancie tout les webservices.
            var cityServiceClient = new CityServiceClient();
            var countryServiceClient = new CountryServiceClient();

#if NETFX_CORE || WPF_TASK_BASED
            var task1 = cityServiceClient.GetAllCitiesAsync();
            var task2 = countryServiceClient.GetAllCountriesAsync();

            // On ajoute des continuation à appeler lorsque nos tasks auront terminées leurs executions.
            task1.ContinueWith(t => OnGetAllCitiesCompleted(t.Exception, t.Result));
            task2.ContinueWith(t => OnGetAllCountriesCompleted(t.Exception, t.Result));
#elif SILVERLIGHT || WPF_EVENT_BASED
            // On s'abonne aux évènement Completed de chacune des méthodes que l'on veux appeller.
            cityServiceClient.GetAllCitiesCompleted += OnGetAllCitiesCompleted;
            countryServiceClient.GetAllCountriesCompleted += OnGetAllCountriesCompleted;
#endif

            // On appelle tout les webservice en parallèle
            cityServiceClient.GetAllCitiesAsync();
            countryServiceClient.GetAllCountriesAsync();
        }
    }

    /// <summary>
    /// Classe représentant englobant les résultats de tout les webservices
    /// et de leurs erreurs possibles.
    /// </summary>
    public class ReferenceLoaderResult : ParallelCallResultBase
    {
        /// <summary>
        /// Liste des pays.
        /// </summary>
        public IEnumerable<string> Countries { get; set; }

        /// <summary>
        /// Liste des villes.
        /// </summary>
        public IEnumerable<string> Cities { get; set; }
    }
}

Vous remarquerez quelques directives de compilations :
- NETFX_CORE, directive standard pour la compilation d’applications Metro
- SILVERLIGHT, directive standard pour la compilation d’applications Silverlight
- WPF_EVENT_BASED, directive personnalisée pour le projet WPF utilisant les proxy wcf basés sur des évènements.
- WPF_TASK_BASED, directive personnalisée pour le projet WPF utilisant les proxy wcf basés sur des Task.

Celà nous permet d’avoir une majorité du code indépendant du type de proxy wcf.

Maintenant au niveau client graphique voici un exemple d’utilisation avec une application Metro :

<Grid Background="{StaticResource ApplicationPageBackgroundBrush}">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    <ListBox x:Name="lstCountries" />
    <ListBox x:Name="lstCities" Grid.Column="1" />
    <ProgressRing x:Name="progressRing" Grid.ColumnSpan="2" />
</Grid>

 

/// <summary>
/// Invoked when this page is about to be displayed in a Frame.
/// </summary>
/// <param name="e">Event data that describes how this page was reached.  The Parameter
/// property is typically used to configure the page.</param>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
    progressRing.IsActive = true;

    // On instancie le loader et on appelle la méthode Run en lui passant en paramètre la méthode Ã
    // appeller lorsque tout les appels de webservices auront été effectués.
    var loader = new ReferenceLoader();

    // Pensez à traiter les éventuelles erreurs !!!
    loader.OnGetAllCitiesCompletedTrigger = (error, result) => lstCities.ItemsSource = result;
    loader.OnGetAllCountriesCompletedTrigger = (error, result) => lstCountries.ItemsSource = result;
    loader.Run(System.Threading.SynchronizationContext.Current, OnLoadCompleted);
}

private void OnLoadCompleted(ReferenceLoaderResult result)
{
    // Pensez à traiter les éventuelles erreurs !!!
    if (result.Errors.Count > 0)
    {
        var sb = new StringBuilder();
        sb.AppendLine("Errors :");
        foreach (var err in result.Errors)
            sb.AppendLine(err.ToString());

        MessageDialog d = new MessageDialog(sb.ToString(), "Errors");
        d.ShowAsync();
    }

    // On peux aussi traiter les retours ici
    //lstCities.ItemsSource = result.Cities;
    //lstCountries.ItemsSource = result.Countries;

    progressRing.IsActive = false;
}

Voici une manière simple, réutilisable et surtout adaptable pour répondre à une problématique qui se rencontre de plus en plus souvent. Car il n’y a pas qu’async et await dans la vie, il y a aussi le parallèlisme :-)

 

Comme d’habitude une solution de sample (Visual Studio 11, WPF 4.5, Silverlight 4, Windows 8 Metro Style) est disponible sur mon skydrive.

WinRT – CacheMode et VisualStatesTransition

Je suis récemment tombé sur un bug en portant mon jeu de reversi fonctionnant sur Wp7, Silverlight et WPF vers WinRT. En effet les animations qui permettaient aux pions de se retourner ne se lançaient pas sur WinRT. Ces animations sont en fait des transitions entre deux états visuels dont le code était strictement identique sur toutes les plateformes. Après quelques jours de galères et de recherche j’ai fini par comprendre que le problème résidait d’un cache que j’avais mis sur le contrôle Board contenant mes Tokens. Ce contrôle Board n’est qu’un contrôle personnalisé héritant de ItemsControl. Dans le template de ma board j’avais placé un CacheMode à BitmapCache.

A des fins d’exemple j’ai reproduit le comportement dans un petit projet contenant une application WPF et une application WinRT.

Le contrôle Token est le suivant. Lorsque sa propriété IsBlack change, une animation est lancée :

#if NETFX_CORE
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Shapes;
#else
using System.Windows;
using System.Windows.Controls;
using System.Windows.Shapes;
#endif

namespace CacheBug
{
    [TemplatePart(Name = PartWhite, Type = typeof(Ellipse))]
    [TemplatePart(Name = PartBlack, Type = typeof(Ellipse))]
    public sealed class TokenControl : Control
    {
        private const string PartWhite = "PART_White";
        private const string PartBlack = "PART_Black";

        private Ellipse _black = null;
        private Ellipse _white = null;

        public TokenControl()
        {
            this.DefaultStyleKey = typeof(TokenControl);
        }

        public bool IsBlack
        {
            get { return (bool)GetValue(IsBlackProperty); }
            set { SetValue(IsBlackProperty, value); }
        }

        public static readonly DependencyProperty IsBlackProperty =
            DependencyProperty.Register("IsBlack", typeof(bool), typeof(TokenControl), new PropertyMetadata(true, new PropertyChangedCallback((s, a) =>
                {
                    ((TokenControl)s).SetColor();
                })));

#if NETFX_CORE
        protected override void OnApplyTemplate()
#else
        public override void OnApplyTemplate()
#endif
        {
            base.OnApplyTemplate();

            _white = (Ellipse)GetTemplateChild(PartWhite);
            _black = (Ellipse)GetTemplateChild(PartBlack);

            SetColor();
        }

        private void SetColor()
        {
            if (_black == null || _white == null)
                return;

            if (IsBlack)
                VisualStateManager.GoToState(this, "BlackState", true);
            else
                VisualStateManager.GoToState(this, "WhiteState", true);
        }
    }
}

Le template du contôle est le suivant :

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:CacheBug">

    <Style TargetType="local:TokenControl">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:TokenControl">
                    <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="VisualStateGroup">
                                <VisualStateGroup.Transitions>
                                    <VisualTransition From="BlackState" To="WhiteState">
                                        <Storyboard Duration="0:0:0.5">
                                            <DoubleAnimationUsingKeyFrames BeginTime="0:0:0" Duration="0:0:1" Storyboard.TargetName="PART_White" Storyboard.TargetProperty="(UIElement.RenderTransform).ScaleX">
                                                <EasingDoubleKeyFrame KeyTime="0:0:0.0" Value="0" />
                                                <EasingDoubleKeyFrame KeyTime="0:0:0.25" Value="0">
                                                    <EasingDoubleKeyFrame.EasingFunction>
                                                        <CubicEase EasingMode="EaseIn"/>
                                                    </EasingDoubleKeyFrame.EasingFunction>
                                                </EasingDoubleKeyFrame>
                                                <EasingDoubleKeyFrame KeyTime="0:0:0.5" Value="1">
                                                    <EasingDoubleKeyFrame.EasingFunction>
                                                        <CubicEase EasingMode="EaseOut"/>
                                                    </EasingDoubleKeyFrame.EasingFunction>
                                                </EasingDoubleKeyFrame>
                                            </DoubleAnimationUsingKeyFrames>
                                            <DoubleAnimationUsingKeyFrames BeginTime="0:0:0" Storyboard.TargetName="PART_Black" Storyboard.TargetProperty="(UIElement.RenderTransform).ScaleX">
                                                <EasingDoubleKeyFrame KeyTime="0:0:0" Value="1">
                                                    <EasingDoubleKeyFrame.EasingFunction>
                                                        <CubicEase EasingMode="EaseIn"/>
                                                    </EasingDoubleKeyFrame.EasingFunction>
                                                </EasingDoubleKeyFrame>
                                                <EasingDoubleKeyFrame KeyTime="0:0:0.25" Value="0">
                                                    <EasingDoubleKeyFrame.EasingFunction>
                                                        <CubicEase EasingMode="EaseIn"/>
                                                    </EasingDoubleKeyFrame.EasingFunction>
                                                </EasingDoubleKeyFrame>
                                            </DoubleAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualTransition>
                                    <VisualTransition From="WhiteState" To="BlackState">
                                        <Storyboard Duration="0:0:0.5">
                                            <DoubleAnimationUsingKeyFrames BeginTime="0:0:0" Duration="0:0:1" Storyboard.TargetName="PART_White" Storyboard.TargetProperty="(UIElement.RenderTransform).ScaleX">
                                                <EasingDoubleKeyFrame KeyTime="0:0:0" Value="1">
                                                    <EasingDoubleKeyFrame.EasingFunction>
                                                        <CubicEase EasingMode="EaseIn"/>
                                                    </EasingDoubleKeyFrame.EasingFunction>
                                                </EasingDoubleKeyFrame>
                                                <EasingDoubleKeyFrame KeyTime="0:0:0.25" Value="0">
                                                    <EasingDoubleKeyFrame.EasingFunction>
                                                        <CubicEase EasingMode="EaseIn"/>
                                                    </EasingDoubleKeyFrame.EasingFunction>
                                                </EasingDoubleKeyFrame>
                                            </DoubleAnimationUsingKeyFrames>
                                            <DoubleAnimationUsingKeyFrames BeginTime="0:0:0" Storyboard.TargetName="PART_Black" Storyboard.TargetProperty="(UIElement.RenderTransform).ScaleX">
                                                <EasingDoubleKeyFrame KeyTime="0:0:0.25" Value="0" />
                                                <EasingDoubleKeyFrame KeyTime="0:0:0.25" Value="0">
                                                    <EasingDoubleKeyFrame.EasingFunction>
                                                        <CubicEase EasingMode="EaseIn"/>
                                                    </EasingDoubleKeyFrame.EasingFunction>
                                                </EasingDoubleKeyFrame>
                                                <EasingDoubleKeyFrame KeyTime="0:0:0.5" Value="1">
                                                    <EasingDoubleKeyFrame.EasingFunction>
                                                        <CubicEase EasingMode="EaseOut"/>
                                                    </EasingDoubleKeyFrame.EasingFunction>
                                                </EasingDoubleKeyFrame>
                                            </DoubleAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualTransition>
                                </VisualStateGroup.Transitions>
                                <VisualState x:Name="BlackState">
                                    <Storyboard>
                                        <DoubleAnimationUsingKeyFrames BeginTime="0:0:0" Storyboard.TargetName="PART_Black" Storyboard.TargetProperty="(UIElement.RenderTransform).ScaleX">
                                            <EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
                                        </DoubleAnimationUsingKeyFrames>
                                        <DoubleAnimationUsingKeyFrames BeginTime="0:0:0" Storyboard.TargetName="PART_White" Storyboard.TargetProperty="(UIElement.RenderTransform).ScaleX">
                                            <EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
                                        </DoubleAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="WhiteState">
                                    <Storyboard>
                                        <DoubleAnimationUsingKeyFrames BeginTime="0:0:0" Storyboard.TargetName="PART_Black" Storyboard.TargetProperty="(UIElement.RenderTransform).ScaleX">
                                            <EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
                                        </DoubleAnimationUsingKeyFrames>
                                        <DoubleAnimationUsingKeyFrames BeginTime="0:0:0" Storyboard.TargetName="PART_White" Storyboard.TargetProperty="(UIElement.RenderTransform).ScaleX">
                                            <EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
                                        </DoubleAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                        <Ellipse x:Name="PART_Black" RenderTransformOrigin="0.5,0.5" Fill="Black" CacheMode="BitmapCache">
                            <Ellipse.RenderTransform>
                                <ScaleTransform />
                            </Ellipse.RenderTransform>
                        </Ellipse>
                        <Ellipse x:Name="PART_White" RenderTransformOrigin="0.5,0.5" Fill="#FFFAFAFA" CacheMode="BitmapCache">
                            <Ellipse.RenderTransform>
                                <ScaleTransform />
                            </Ellipse.RenderTransform>
                        </Ellipse>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

La page de test est la suivante (vous remarquerez la ligne en commentaire pour qui provoque le bug si décommentée) :

<Page
    x:Class="CacheBug.BlankPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:CacheBug"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
    <Page.Resources>
        <Style x:Key="ItemsControlStyle" TargetType="ItemsControl">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="ItemsControl">
                        <Border Background="{TemplateBinding Background}"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}"
                                Padding="{TemplateBinding Padding}">
                            <!-- Uncomment below to make tokens stop animating -->
                            <!--<Border.CacheMode>
                                <BitmapCache />
                            </Border.CacheMode>-->
                            <ItemsPresenter />
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Page.Resources>

    <Grid Background="{StaticResource ApplicationPageBackgroundBrush}">
        <ItemsControl x:Name="lstBox" Style="{StaticResource ItemsControlStyle}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <local:TokenControl Width="100" Height="100" IsBlack="{Binding IsBlack}" />
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</Page>

Et son codebehind se charge d’initialiser une collection de pions et de changer la valeur de la propriété IsBlack avec un timer :

using System;
using System.Collections.Generic;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;

namespace CacheBug
{
    public sealed partial class BlankPage : Page
    {
        private readonly DispatcherTimer _timer = new DispatcherTimer();
        private readonly Random _rnd = new Random();
        private readonly List<TokenModel> list = new List<TokenModel>();

        public BlankPage()
        {
            this.InitializeComponent();

            _timer.Tick += TimerTick;
            _timer.Interval = TimeSpan.FromSeconds(0.5d);
        }

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            list.Add(new TokenModel { IsBlack = true });
            list.Add(new TokenModel { IsBlack = false });
            list.Add(new TokenModel { IsBlack = true });
            list.Add(new TokenModel { IsBlack = false });
            list.Add(new TokenModel { IsBlack = true });
            list.Add(new TokenModel { IsBlack = false });
            list.Add(new TokenModel { IsBlack = true });

            lstBox.ItemsSource = list;

            _timer.Start();
        }

        private void TimerTick(object sender, object e)
        {
            int index = _rnd.Next(0, list.Count - 1);
            var token = list[index];
            token.IsBlack ^= true;
        }
    }
}

 

Ce bug est spécifique à WinRT car le même code fonctionne correctement en WPF, Silverlight ou même WP7. J’ai donc désactivé le CacheMode sur ce contrôle pour WinRT.

 

Bug ou feature je ne sais pas trop mais en tout cas c’est posté sur Connect.

 

Et comme souvent, vous pouvez télécharger le code de sample sur mon Skydrive.

 

Edit :

J’ai eu une réponse du pourquoi celà ne marchait pas correctement sous WinRT avec le CacheMode. C’est dû au fait que certains types d’animations (celles impliquant un recalcul du layout ou celles que le moteur estime comme étant lourde) ne sont plus lancées automatiquement. Pour ce faire il faut rajouter la propriété EnableDependentAnimation à True sur chacune des animations. Après tout fonctionne comme prévu.

 

 

Préparez vos RaisePropertyChanged pour une mise en production

Lorsque l’on développe des applications XAML on se met assez rapidement à utiliser l’interface INotifyPropertyChanged. Chaque développeur a en général une méthode levant l’évènement PropertyChanged. Dans le cadre de cet article elle sera RaisePropertyChanged.
L’implémentation classique du RaisePropertyChanged est la suivante :

protected virtual void RaisePropertyChanged(string propertyName)
{
    PropertyChangedEventHandler handler = PropertyChanged;

    if (handler != null)
        handler(this, new PropertyChangedEventArgs(propertyName);
}

L’utilisation de cette méthode se fait donc de cette façon :

private bool _isBusy;
public bool IsBusy
{
    get { return _isBusy; }
    set
    {
        if (value != _isBusy)
        {
            _isBusy = value;
            RaisePropertyChanged("IsBusy");
        }
    }
}

 

Il arrive aussi très régulièrement que l’on adopte une syntaxe basée sur des expressions lambdas  :

private bool _isBusy;
public bool IsBusy
{
    get { return _isBusy; }
    set
    {
        if (value != _isBusy)
        {
            _isBusy = value;
            RaisePropertyChanged(() => IsBusy);
        }
    }
}

L’utilisation de cette syntaxe est très pratique en développement et permet de refactoriser le code sans craindre d’oublier de renommer les noms des propriétés. Elle a cependant un coup non négligeable en performance et en mémoire. Effectivement elle génère des arbres d’expressions qui doivent être analysés pour extraire le nom de la propriété et le tout à l’exécution.

Mon avis sur la question est donc d’utiliser la méthode avec des expressions lambas lorsque le projet débute et qu’il y a encore beaucoup de refactorisation. Ensuite je préfère utiliser la première méthode qui est beaucoup moins coûteuse à l’execution.

La question est donc de savoir comment changer toutes ces expressions lambdas en chaînes de caractères ? Pour un projet simple avec 3 classes c’est rapide à faire à la main, mais sur un projet avec des centaines de classes, faire tout cela à la main est juste inenvisageable.

C’est ici que rentre en scène Visual Studio car lui il sais le faire ! Faut juste savoir lui dire comment.
Si vous allez dans le menu « Edition » puis « Rechercher et remplacer » vous trouverez une action « Remplacer dans les fichiers » (ou alors faites Ctrl+Shift+H c’est plus rapide) vous aurez la possibilité de remplacer du texte en masse. Se cache dans les options de recherche une option très pratique nommée « Expression régulière ». C’est elle qui va nous permettre de remplacer en masse par le bon texte. Cochez donc la case « Expression régulière » et appliquez ensuite la méthode pour votre version de Visual Studio :

Pour Visual Studio 2010 :
Dans le champ de recherche entrez : RaisePropertyChanged\(\(\) +=\> +{:i}\)
Dans le champ de remplacement entrez : RaisePropertyChanged(« \1″)

Si vous utilisez Visual Studio 2011 Beta il faut utiliser d’autres expressions :
Dans le champ de recherche entrez : RaisePropertyChanged\(\(\) +=\> +(\b(_\w+|[\w-[0-9_]]\w*)\b)\);
Dans le champ de remplacement entrez : RaisePropertyChanged(« $1″);

 

Plus aucune excuse pour générer des arbres d’expressions inutilement à chaque mise à jour de propriétés maintenant !

Windows 8 – Metro – WCF multi-endpoint pour C# et JS.

Ajourd’hui j’ai voulu m’amuser à regarder comment fonctionnait l’accès aux services WCF depuis JS et C# dans une application Metro.

Pour cela j’ai voulu n’utiliser qu’un seul et même service consommé par ces deux plateformes via divers protocoles.

Rapidement j’ai constaté la nécessité d’adopter le protocole HTTP avec Javascript. Plusieurs options s’offrent alors, exposer les données en JSON ou en XML. Pour des raisons de pure discrimination envers un format aussi verbeux que le XML, j’ai choisi JSON.

En ce qui concerne le C#, ceux ayant lu quelques-un de mes précédents articles se sont surement rendu compte de mon point faible envers les protocoles de bas niveau et tout particulièrement le netTcpBinding. Pour moi il est le juste milieu entre un protocole typé et léger sur le réseau. Il a des inconvénients tout particulièrement son non-support de la sécurité, mais pour des données non sensibles c’est la Rolls (ou Porsche pour certains expatriés français dans la bay qui se reconnaitront) des protocoles WCF. Pour la route j’ai aussi voulu exposer les données dans un protocole un peu plus haut niveau qui est le NetHttpBinding. Il offre l’avantage d’encoder les données en binaire ce qui offre une volumétrie plus faible que du XML mais malgré tout assez importante (voir détails à la fin).

Donc pour résumer on a :

  • NetHttpBinding (C#)
  • NetTcpBinding (C#)
  • WebHttpBinding (JS)

 

Maintenant un peu de code !

J’ai généré une solution de base avec un client C#/XAML de base, un client JS/HTML de base et un service WCF aussi de base (original non ?). J’ai pris soin d’héberger ce dernier dans mon IIS local et non dans IIS Express pour le netTcpBinding.

Voici à quoi ressemble le service WCF :

[ServiceContract]
public interface IService1
{
    [OperationContract]
    string GetData(int value);

    [OperationContract]
    CompositeType GetDataUsingDataContract(CompositeType composite);

    [OperationContract]
    [WebGet(UriTemplate = "/GetDataJSON/{value}", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
    string GetDataJSON(string value);

    [OperationContract]
    [WebInvoke(Method="POST", UriTemplate = "/GetDataUsingDataContractJSON", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json, BodyStyle=WebMessageBodyStyle.Bare)]
    CompositeType GetDataUsingDataContractJSON(CompositeType value);
}

[DataContract]
public class CompositeType
{
    bool boolValue = true;
    string stringValue = "Hello ";

    [DataMember]
    public bool BoolValue
    {
        get { return boolValue; }
        set { boolValue = value; }
    }

    [DataMember]
    public string StringValue
    {
        get { return stringValue; }
        set { stringValue = value; }
    }
}

Et son implémentation :

[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class Service1 : IService1
{
    public string GetData(int value)
    {
        System.Threading.Thread.Sleep(2000);
        return string.Format("You entered: {0}", value);
    }

    public string GetDataJSON(string value)
    {
        return GetData(Convert.ToInt32(value));
    }

    public CompositeType GetDataUsingDataContract(CompositeType composite)
    {
        System.Threading.Thread.Sleep(2000);
        if (composite == null)
        {
            throw new ArgumentNullException("composite");
        }
        if (composite.BoolValue)
        {
            composite.StringValue += "Suffix";
        }
        return composite;
    }

    public CompositeType GetDataUsingDataContractJSON(CompositeType composite)
    {
        return GetDataUsingDataContract(composite);
    }
}

 

Le seul truc un peu original a été d’ajouter les attributs WebGet et WebInvoke pour rendre les méthodes concernées utilisable par Javascript en REST.

Maintenant le fichier de configuration. Il ressemble beaucoup à celui que j’avais fait pour la série sur le netTcpBinding et le httpPollingDuplex en Silverlight :

<system.serviceModel>
  <behaviors>
    <serviceBehaviors>
      <behavior>
        <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" />
        <serviceDebug includeExceptionDetailInFaults="false"/>
      </behavior>
    </serviceBehaviors>
    <endpointBehaviors>
      <behavior name="restbehavior">
        <webHttp />
      </behavior>
    </endpointBehaviors>
  </behaviors>
  <bindings>
    <netTcpBinding>
      <binding name="netTcpBindingConfig">
        <security mode="None" />
      </binding>
    </netTcpBinding>
  </bindings>
  <services>
    <service name="WcfService1.Service1">
      <endpoint address="" contract="WcfService1.IService1" binding="netHttpBinding" name="HttpBinding" />
      <endpoint address="rest" contract="WcfService1.IService1" binding="webHttpBinding" behaviorConfiguration="restbehavior" name="rest"/>
      <endpoint address="netTcp" contract="WcfService1.IService1" binding="netTcpBinding" bindingConfiguration="netTcpBindingConfig" name="netTcpBinding" />
      <endpoint address="mex" binding="mexHttpBinding" name="mex" contract="IMetadataExchange" />
      <host>
        <baseAddresses>
          <add baseAddress="net.tcp://xiaoba:808/WcfService1/Service1.svc" />
          <add baseAddress="http://xiaoba/WcfService1/Service1.svc" />
        </baseAddresses>
      </host>
    </service>
  </services>
  <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="false" />
</system.serviceModel>

Maintenant qu’en est-t-il des clients ?
Je n’ai pas été bien loin dans l’investigation, le but étant juste de le faire marcher des deux côtés.
Dans chaque client j’appelle d’abord la méthode GetData puis la méthode GetDataUsingDataContract et met le résultat dans un champs texte à l’écran.

Commençons par la star du moment le dénommé Javascript :

(function () {
    "use strict";

    var app = WinJS.Application;

    app.onactivated = function (eventObject) {
        if (eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch) {
            if (eventObject.detail.previousExecutionState !== Windows.ApplicationModel.Activation.ApplicationExecutionState.terminated) {
                GetData();
            }
            WinJS.UI.processAll();
        }
    };

    function GetData() {
        var value = 1;

        var ResultSpan = document.getElementById('SpanResult');

        var baseUrl = "http://xiaoba/WcfService1/Service1.svc/rest/GetDataJSON/";
        var urlPost = "http://xiaoba/WcfService1/Service1.svc/rest/GetDataUsingDataContractJSON";

        var url = baseUrl + value;

       WinJS.xhr({url:url}).then(function (r) {
            var result = JSON.parse(r.responseText);
            ResultSpan.innerHTML = result;
        });

        var sData = JSON.stringify({
            BoolValue: true,
            StringValue: "Ô rage ! Ô désespoir ! Ô C# ennemi !"
        });

        WinJS.xhr({
            url: urlPost,
            type: "POST",
            headers: { "Content-Type": "application/json; charset=utf-8" },
            data: sData
        }).then(function (r) {
            var result = JSON.parse(r.responseText);
            ResultSpan.innerHTML = result.StringValue;
        });
    }

    app.oncheckpoint = function (eventObject) {
    };

    app.start();
})();

L’appel au service WCF se fait ici en JSON en utilisant les objets WinJS et JSON. Attention aux deux petits fourbes ici : j’ai nommé JSON.stringify et les headers de l’objet xhr pour le post. Si vous oubliez d’encoder votre objet Javascript en une chaîne de caractère JSON avant de l’affecter au champ data de l’objet xhr, bah ça marche pas. De même pour les headers, il faut les préciser. C’est d’autant plus étrange lorsqu’on a l’habitude de JQuery mais c’est comme ça.

Je ne reviens pas sur le concept de promise utilisé ici, de très bon articles existent sur le sujet.

 

Et maintenant comment ça marche en C# ?

public sealed partial class BlankPage : Page
{
    public BlankPage()
    {
        this.InitializeComponent();
    }

    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        GetData();
    }

    private async void GetData()
    {
        var client = new Service1Client(Service1Client.EndpointConfiguration.HttpBinding);
        //var client = new Service1Client(Service1Client.EndpointConfiguration.netTcpBinding);

        string result = await client.GetDataAsync(1);

        resultTextBlock.Text = result;

        CompositeType c = await client.GetDataUsingDataContractAsync(new CompositeType { BoolValue = true, StringValue = "Ô rage ! Ô désespoir ! Ô Javascript ennemi !" });

        resultTextBlock.Text = c.StringValue;

        await client.CloseAsync();
    }
}

Ici j’ai déclaré la méthode GetData en async pour pouvoir profiter du pattern async await. Le fonctionnement ici n’est pas exactement identique à la version JS car ici j’attend le retour du premier appel avant de lancer le second, ça ne change fondamentalement pas les choses. On voit dans le code ci-dessus une ligne commentée. Elle permet de changer de protocole de communication pour le netTcpBinding.

 

 

 

Qu’est-ce que j’ai appris de ce petit exercice.

  1. Il n’est pas facile de trouver les erreurs que l’on fait lorsqu’on configure mal WCF pour le JSON. Au final j’ai réussi à débuguer lorsque j’ai installé Fiddler et que j’ai forgé mes requêtes HTTP à la main.
  2. C’est quand même beaucoup plus simple de consommer des services WCF en C#
  3. On fait très vite des erreurs en Javascript sans s’en rendre compte. Genre une majuscule à la place d’une minuscule dans une variable. Aucune erreur de « compilation » ne nous prévient de ça.
  4. En jouant un peu avec Fiddler2 on se rend compte que JSON est beaucoup plus concis que le NetHttpBinding. Il faut jouer un peu des coudes avec des extensions IIS pour faire baisser la taille des données renvoyées par ce dernier.
  5. Les différentes valeurs de EndPointConfiguration sont générées par le générateur de proxy de Visual Studio 11 en fonction des endpoints disponibles sur le service et compatibles avec C#/XAML.
  6. Le changement de binding wcf côté client en C# ne se passe pas du tout comme en Silverlight.
  7. Enfin rien à voir mais c’est juste l’enfer de faire un VerticalAlignement= »Center » en HTML…

 

A vot’ bon cÅ“ur m’sieurs dames et bonne soirée.

Comme d’habitude le code source se trouve dans les nuages.

 

Asynchronisme – Simuler de la latence

Un des problèmes récurrents lorsque l’on développe une application contenant des opérations asynchrones est de tester cet asynchronisme. On souhaite vérifier le comportement qu’aurait l’application si telle ou telle opération mettait 10 secondes ou 1 minute à s’exécuter.

Prenons un exemple simple
On a une méthode MyAsyncMethod qui notifie la fin de son exécution avec un évènement MyAsyncMethodCompleted. On souhaite simuler une latence de 5 secondes sur cette méthode.

Voici le code permettant de faire cela :

public void MyAsyncMethod()
{
    Timer timer = null;

    var context = SynchronizationContext.Current;

    TimerCallback c = state =>
    {
        if (timer != null)
            timer.Dispose();

        context.Post(o => MyAsyncMethodCompleted(this, null));
    };
    timer = new Timer(c, null, 5000, int.MaxValue);
}

L’idée est que l’on va créer un timer qui attendra le temps voulu avant d’invoquer un callback.
Ce callback se chargera de disposer le timer et d’invoquer l’évènement Completed.
Le problème du timer est qu’il va invoquer son callback dans un autre thread que celui qui a initialisé l’appel.
Afin de rebasculer sur le bon thread on a sauvegardé le contexte de synchronisation dans la variable context. Ensuite, on a envoyé l’appel à l’évènement completed au contexte de synchronisation grâce à sa méthode Post. C’est un peu l’équivalent à Dispatcher.BeginInvoke pour Silverlight ou WPF mais là ça marche pour tout les threads.

Donc pour finir, pour la méthode apellante tout est transparent et on a simulé une latence.

Simple et pratique :-)

Back to Basics – C# – Cast d’un entier en énumération

 

Récemment je suis tombé sur un cast qui a éveillé ma curiosité.

La ligne tentait de convertir un entier en une énumération d’une manière que je n’avais jamais vu.

Soit le type d’énumération suivant :

public enum MyEnum
{
    Zero = 0,
    One = 1
}

Soit le cast suivant :

MyEnum res;
int val = 1;

res = (MyEnum)Enum.Parse(typeof(MyEnum), val.ToString());

La première réaction que j’ai eu a été de me demander pourquoi diable convertir un entier en string pour le caster en énumération juste après quand un cast direct suffit ?

res = (MyEnum)val;

J’ai donc posé la question à un collègue en me demandant si j’avais pas raté un truc et on m’a répondu que Enum.Parse renvoie une exception si la variable val a une valeur ne correspondant pas aux valeurs possibles déclarées de l’énumération (par exemple 2).

Deuxième moment de doute. Je me décide donc à faire un petit test :

// Cast 1
res = (MyEnum)Enum.ToObject(typeof(MyEnum), val);
Console.Out.WriteLine("{0}", res);

// Cast 2
res = (MyEnum)Enum.Parse(typeof(MyEnum), val.ToString(CultureInfo.InvariantCulture));
Console.Out.WriteLine("{0}", res);

// Cast 3
res = (MyEnum)val;
Console.Out.WriteLine("{0}", res);

J’ai pu constater qu’avec ces tests toutes les conversions passaient avec val valant 2. Aucune exceptions ou erreurs de lancée. Celà veux donc dire que la valeur numérique de res est ici de deux alors que la valeur maximum déclarée est de 1 ! Ce comportement ressemble étrangement au C.

Maintenant que se passe-t-il avec le test suivant :

// Cast 4
string sVal = "Two";
res = (MyEnum)Enum.Parse(typeof(MyEnum), sVal);
Console.Out.WriteLine("{0}", res);

Cette fois-ci nous avons bien une exception de lancée indiquant que la valeur Two n’existe pas dans l’énumération ce qui est un comportement attendu.

 

Si on reviens à la ligne qui m’as posé problème au début de l’article on se rend donc compte de l’erreur du développeur pensant que Enum.Parse fonctionne de la manière avec des chaînes qu’avec des chaînes contenant une valeur numérique entière.

 

Dès lors comment s’assurer que la valeur que nous souhaitons convertir dans l’énumération existe belle et bien ?

Plusieurs choix s’offrent à nous :

if (typeof(MyEnum).IsEnumDefined(val))
{
    res = (MyEnum)val;
    Console.Out.WriteLine("{0}", res);
}
else
    Console.Out.WriteLine("Cannot convert {0} to MyEnum", val);

if (Enum.IsDefined(typeof(MyEnum), val))
{
    res = (MyEnum) val;
    Console.Out.WriteLine("{0}", res);
}
else
    Console.Out.WriteLine("Cannot convert {0} to MyEnum", val);

La première méthode n’existe qu’à partir de .NET 4 alors que la seconde existe dans les précédentes versions du framework.

 

Comme quoi on peux en apprendre tout les jours sur notre langage préféré.

Et vous ? Le saviez-vous ?

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); }
    }
}