Executer une action suite à des appels WCF parallèles

7 minutes read

Récemment on m’a 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 MetroSilverlightWPF
Basé sur des TaskOuiNonOui
Basé sur des évènementsOuiOuiOui

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à :</p>

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.

Updated:

Leave a Comment