Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

Silverlight: Sérialiser les tâches asynchrones

[new:30/06/2011]L’asynchronisme s’installe durablement dans les applications : multitâche rendu obligatoire pour bénéficier des progrès des nouveaux processeurs et communications asynchrones sont deux ingrédients qu’une application Silverlight doit gérer au minimum. Hélas le développeur raisonne de façon “synchrone” beaucoup plus facilement que de façon asynchrone... Comment simplifier le développement et la maintenance des applications modernes tout en respectant cet asynchronisme qui s’immisce de gré ou de force dans la logique des programmes ?

Une idée, plusieurs implémentations...

L’idée de rendre les tâches asynchrones manipulables de façon synchrone taraude bon nombres de développeurs depuis un moment. Certains y pensent un peu, beaucoup, ou pas du tout, d’autres tentent des implémentations permettant de résoudre le problème.

Et comme souvent en informatique, pour une même idée il existe plusieurs solutions, plusieurs angles de vue qui résultent en plusieurs implémentations.

Il n’y a pas vraiment de package s’attelant uniquement à ce problème. Mais on trouve les solutions dans des frameworks existants. Le plus embêtant étant que bien souvent il semble assez lourd d’utiliser tout un framework pour n’en utiliser qu’une feature. C’est comme cela que certaines applications finissent “hérissées” d’appels à dix librairies de code différentes pesant un poids certain alors qu’en réalité seules quelques lignes de codes ne sont utilisées dans chacune.

Je n’ai pas de solution miracle à ce problème précis mais un début de solution tout pétri de bon sens : quitte à utiliser des frameworks pour ne se servir que de quelques fonctions, autant choisir ceux d’entre eux les plus “light” possible !

C’est pourquoi je préconise MVVM Light pour gérer la problématique spécifique à MVVM, parce que ce framework, à la différence de MEF ou Caliburn par exemple reste limité à la résolution d’un problème précis sans prétendre tout faire. Son code est court, clair, facile à apprendre et facile à utiliser. J’ai parlé de MVVM Light de nombreuses fois ici je laisse le lecteur chercher sur le blog pour retrouver les billets qui en parle ainsi que les gros articles (près de 100 pages) à télécharger qui traitent de MVVM et de la librairie MVVM Light.

Pourquoi parle-je de MVVM light ? D’abord comme exemple de framework super light, mais aussi parce que la solution au problème qui nous intéresse aujourd’hui se trouve dans des frameworks qui s’attachent, eux aussi à résoudre (entre autres) les problèmes de la pattern MVVM. Du coup, se servir de ces frameworks en plus de MVVM Light est un peu stupide (la redondance est l’ennemie du développeur tout autant que le code mort et le méli-mélo de frameworks différents dont je parlais plus haut).

Jounce !

Ce n’est pas une nouvelle expression de cours de récré ni un nouveau truc de web social à la noix à la mode chez les djeuns, c’est juste un autre framework... Un de plus !

Son auteur est bien embêté d’ailleurs... Il le dit lui-même, “nous n’avons pas besoin d’un framework de plus”. Et il a raison. Mais comme dans son blog il traite de solutions à des problèmes de développement qu’il a réuni dans un petit framework il lui a semblé tout aussi idiot de parler d’un framework invisible et non téléchargeable que d’ajouter un framework de plus à la liste de ceux qui existent... Cornélien !

Du coup, il a décidé que, plutôt que de se taire, il était préférable de continuer de discuter de programmation et de publier son petit framework...

Le résultat est un projet CodePlex s’appelant Jounce.

MVVM Light, MEF, Prism, Caliburn, Caliburn.Micro et Jounce

Il devient difficile de faire son marché mais comme dit la sagesse populaire, abondance de bien ne nuit pas... Quoi que... En l’espèce cela ne simplifie pas le travail du développeur. Mais avoir le choix est toujours plus compliqué que la pensée unique ou la dictature, la liberté est un bien paradoxal qu’on confond souvent avec une certaine idée du bonheur, alors que la liberté n’a rien à voir avec la joie ou l’allégresse, c’est une lourde charge qui s’accompagne de responsabilités, mais philosophiquement seule la liberté (de pensée, de mouvement et d’action) fait de la bête que nous sommes originellement des citoyens et non des esclaves...

Bref, en matière de frameworks nous sommes “philosophiquement” comblés !!!

Ce qui n’arrange donc pas nos affaires concernant le “bon choix” à faire...

MVVM Light

Ce framework que j’ai déjà évoqué plus haut ne s’intéresse qu’à la pattern MVVM et ne règle pas le problème spécifique posé par les tâches asynchrones. Mais comme il s’agit d’un excellent framework light pour traiter de MVVM, beaucoup l’ont adopté. (http://mvvmlight.codeplex.com/)

MEF

Le Managed Extensibility Framework (MEF donc) est un excellent framework qui gère le chargement dynamique de modules au runtime. Intégré au Framework .NET 4 et à Silverlight 4, c’est aujourd’hui une brique logicielle incontournable. MEF ne règle pas non plus le problème évoqué aujourd’hui, mais étant intégré à Silverlight il vient en concurrence avec d’autres frameworks qui eux résolvent notre problème d’asynchronisme et qui couvrent bien d’autres domaines. Il s’agit donc ici de rappeler que si on utilise MVVM Light et Silverlight 4+ on dispose déjà de 95% des éléments nécessaires pour programmer une application sophistiquée, et que pour régler les 5% qui restent il semble bien lourd d’ajouter à la liste un autre Framework dont on n’utilisera presque rien... Il peut donc y avoir arbitrage et remise en cause des choix initiaux (comme celui d’utiliser MVVM Light si c’est pour intégrer tout Prism par exemple).

Prism

Issu de Microsoft Patterns & Practices, Prism est un “gros morceau”, à la fois un guide, un précis de bonnes pratiques et une librairie de code pour WPF, Silverlight et Windows Phone 7.

Pourquoi Prism ? Tout simplement parce que Jounce est conçu à la base comme un guide et une librairie pour travailler en MVVM avec Prism... Tout se recoupe ! Et on commence à entrevoir les petits problèmes de choix qui risquent de se poser.

Caliburn et Caliburn.Micro

Deux d’un coup ! Cela ira plus vite. Caliburn est un... framework qui implémente des solutions intéressantes aux problèmes posés par MVVM (on le retrouve !) mais aussi certain de ces petits frères comme MVP ou MVC.

Caliburn étant devenu une grosse machinerie, pleine de chose intéressantes mais un peu lourde (et qui va gérer dans le même programme MVVM, MVP et MVC qui sont trois patterns cousines ?), les derniers développements se focalisent sur la version light qui s’appelle Caliburn.Micro.

Et pourquoi donc parler de Caliburn ici ? Tout simplement parce que Jounce est conçu pour s’adapter à Prism et à Caliburn (ce qui laisse un peu plus de marge dans le dilemme posé par le choix d’un framework “de base”). C’est aussi parce que certaines idées de Jounce s’inspirent du code de Caliburn et qu’il est intéressant de le savoir et de s’intéresser à ce framework (je ne compte pas combien de fois j’ai écrit ce mot depuis le début du billet... j’en suis désolé mais il me semble difficile d’éviter cette répétition).

Et Jounce

Je l’ai déjà présenté plus haut, Jounce est un petit framework qui se veut donc light lui aussi, et qui permet de simplifier le travail avec les frameworks Prism et Caliburn. Une sorte de surcouche mais qui reste indépendante malgré tout (ce qui fait qu’on peut utiliser Jounce sans pour autant ajouter le code de Prism ou Caliburn, et qu’on peut ainsi continuer d’utiliser MVVM Light, sauf si on a réellement besoin de tout ce que font ces autres frameworks).

Donc nous en revenons à Jounce, replacé dans son contexte et ses relations avec tous ces autres frameworks. Si cela ne vous dit toujours pas comment régler les problèmes de l’asynchronisme, au moins vous savez plus sur tous ces ensembles de code (j’ai réussi à éviter le mot framework ! .. ah non.. zut !).

Jounce se présente comme un “guide d’application de la pattern MVVM et d’utilisation de MEF sous Silverlight” créé par Jeremy Likness et disponible sous CodePlex.

Régler les problèmes d’asynchronisme n’est donc pas son but premier, juste une de ces features. Mais les autres sont intéressantes aussi, certaines solutions sont originales et peuvent venir compléter ou remplacer celles du framework de base que vous aurez choisi (MVVM Light, Prism ou Caliburn).

L’asynchronisme

L’introduction était peut-être un peu longue, mais il m’a semblé important de replacer Jounce dans le contexte des frameworks existants et de démêler un peu les rapports que ces derniers entretiennent les uns avec les autres ainsi qu’avec Jounce.

Mais il est temps de parler de l’asynchronisme !

La principale difficulté dans l’écriture du code sous Silverlight (qu’elle qu’en soit le mode, donc sous Windows Phone 7 aussi) et WPF c’est de gérer la complexité des opérations asynchrones sachant que de plus en plus d’API sont par nature asynchrones.

Quand on utilise l’APM (Asynchronous Programming Model – voir aussi Guidelines for Asynchronous Programming) or le modèle de programmation event-driven (piloté par les évènements) le développeur doit s’imaginer et bien considérer quelle sera la “chaine des évènements” et devra fournir le code pour gérer convenablement cette chaine.

Microsoft propose les Reactive Extensions (Rx), une librairie intéressante pour ce qui concerne le parallélisme. Ensuivant le lien précédent vous aurez accès à la fois au téléchargement de cette librairie et à de nombreuses explications.

La place de Jounce en la matière se situe à la frontière entre ce qui existe déjà et ce qu’on trouvera dans l’avenir dans d’autres librairies, avec un partie pris : la légèreté.

Le principe des Coroutines

Selon Wikipédia :

Dans un programme, une coroutine est une unité de traitement qui s'apparente à une routine. À ceci près, que la sortie d'une routine met fin à la routine, alors que la sortie de la coroutine peut être le résultat d'une suspension de son traitement jusqu'à ce qu'il lui soit signalé de reprendre son cours. La suspension de la coroutine et la reprise de son cours peuvent s'accompagner d'une transmission de données.

Les coroutines permettent de réaliser des traitements basés sur des algorithmes coopératifs comme par exemple les itérateurs, les générateurs, des canaux de communication, etc.

Ce principe, mis en œuvre dans Jounce a été utilisé dans Caliburn.Micro par Rob Eisenberg, vise à transformer en une séquence précise et facilement traçable des appels asynchrones.

Le besoin est évident : imaginons que je doivent attendre les résultats d’un appel à WCF Ria Services pour obtenir des données, et qu’en fonction de ce résultat j’aille puiser d’autres données, puis encore d’autres dépendantes elles aussi des données reçues. Dans la pratique on voit un code pas très clair qui consiste à enchainer les actions dans les  évènements Completed des appels asynchrones RIA Services. Cela créé une séquence mais il est très difficile de réutiliser les éléments pour les faire participer à une autre séquence un peu différente tellement sont-ils imbriqués. De même gérer les erreurs en plein milieu de la séquence n’est pas aisé.

Il faut donc trouver un moyen de rentre synchrone (au moins en apparence) les appels asynchrones afin de pouvoir ré-exploiter chaque partie dans des séquences différences, de mettre en place une gestion d’erreur plus fiable, et de disposer au final d’un code ordonné mettant en valeur la séquence elle-même (facilité de maintenance) au lieu que celle-ci ne se cache dans des imbrications alambiquées.

Un exemple

Jounce propose une interface que voici :

public interface IWorkflow
{
    void Invoke();
    Action Invoked { get; set; }
}

Elle définit deux méthodes. Invoke qui est appelé pour exécuter l’action, et Invoked qui devra être appelé par l’action quand elle sera terminée (normalement ou suite à une erreur, ce qu’elle pourra indiquer dans une de ses propriétés, puisque cette interface, est, par force, implémentée dans une classe, l’action à exécuter).

IWorkflow est réellement simple et on ne plus directe. Un minimalisme qui en simplifie la compréhension et l’utilisation.

Gérer un workflow

Le but du jeu est de pouvoir gérer une séquence à partir d’actions asynchrones.

Jounce utilise deux ingrédients : l’interface IWorkflow présentée ci-dessus et un détournement : les énumérateurs.

Je parle de détournement car les énumérateurs ne sont pas fait au départ pour synchroniser des actions asynchrones, mais ils peuvent parfaitement être utilisés dans ce but.

Un workflow, une séquence, se présente donc comme un IEnumerable<IWorkflow> et porte un nom, comme toute variable. Cela permet aisément de se référer à un workflow plutôt qu’à un autre.

Chaque partie du workflow est un “yield return” invoquant une action retournant un IWorkflow.

Voici à quoi pourrait ressembler un workflow complet :

private IEnumerable<IWorkflow> Workflow()
{
    Button.Visibility = Visibility.Visible;
    Text.Text = "Initialisation...";
    Button.Content = "Pas encore";
    Button.IsEnabled = false;
 
    yield return new WorkflowDelay(TimeSpan.FromSeconds(2));
 
    Button.IsEnabled = true;
    Text.Text = "Premier clic";
    Button.Content = "Cliquez moi !";
 
    yield return new WorkflowRoutedEvent(() => { }, h => 
                               Button.Click += h, h => Button.Click -= h);
 
    Text.Text = "Compteur démarré !";
    Button.Content = "Cliquez moi !";
 
    yield return new WorkflowRoutedEvent(() => { }, h => 
                               Button.Click += h, h => Button.Click -= h);
 
    Button.IsEnabled = false;
    Button.Content = "En train de compter...";
 
    yield return new WorkflowBackgroundWorker(_BackgroundWork, 
                                                    _BackgroundProgress);
 
    Text.Text = "Workflow terminé.";
    Button.Visibility = Visibility.Collapsed;
}
 

Comme on peut le voir dans ce code plusieurs actions asynchrones s’enchainent dans une séquence contrôlable. Avec plus de finesse on s’aperçoit même que plusieurs types d’actions se mélangent : des actions synchrones (modification du contenu du bouton par exemple), des actions asynchrones de type délai (l’attente de 2 secondes au départ du workflow), des actions asynchrones de type évènement (l’attente des clics dans des expressions lambda).

Le plus grand bénéfice qu’on tire de cette stratégie n’est pas un gain de performance mais bien un gain très important de lisibilité, de contrôle sur le code et de maintenabilité des workflows.

La séquence IEnumerable créée ici pour donner corps à un workflow ne va pas s’exécuter toute seule. Il faudra bien à un moment commencer l’énumération de ses éléments (les actions) et gérer l’interface IWorkflow supportés par ces derniers.

C’est le rôle du WorkflowControler de Jounce qui s’invoquera par exemple ici de la façon suivante :

WorkflowController.Begin(Workflow(),
     ex => JounceHelper.ExecuteOnUI(() => MessageBox.Show(ex.Message)));

Le workflow est démarré et s’il rencontre une erreur un MessageBox sera affiché. On peut bien entendu gérer les erreurs plus finement que dans cet exemple.

Ce petit exemple montre une Textbox ainsi qu’un bouton pour prouver que l’interface n’est pas bloquée et que le travail asynchrone s’exécute bien en tâche de fond. Il n’y a pas de limite de “temps”, c’est à dire que le procédé peut être utilisé pour gérer des actions qui s’étendent sur plusieurs écrans de l’application par exemple ou bien qui gèrent des états de l’application sur l’ensemble de son cycle de vie.

La “feinte” est bien entendu à la fois dans IWorkflow mais surtout dans l’utilisation détournée de l’itérateur qui, in fine, est l’organe qui maintient l’état du workflow.

Les types d’éléments d’un workflow

En soit, Jounce propose un mécanisme simple, celui qui vient d’être montré. C’est au développeur d’implémenter l’interface IWorkflow dans des classes spécifiques, réalisant des “morceaux” du workflow général.

Jounce propose quelques exemples de réalisation de ces classes qui balayent les cas les plus courants. Dans le code proposé plus haut on peut voir un élément s’appelant “WorkflowDelay”. C’est un des exemples les plus simples qu’on puisse créer : il se contente d’attendre un certain temps (passé en paramètre sous la forme d’un TimeSpan).

Pour comprendre le mécanisme de ces classes d’action avec Jounce regardons le code de l’action de délai :

public class WorkflowDelay : IWorkflow 
{
    private readonly DispatcherTimer _timer;
 
    public WorkflowDelay(TimeSpan interval)
    {
        if (interval <= TimeSpan.Zero)
        {
            throw new ArgumentOutOfRangeException("interval");
        }
 
        _timer = new DispatcherTimer {Interval = interval};
    }
 
    void TimerTick(object sender, EventArgs e)
    {
        _timer.Stop();
        _timer.Tick -= TimerTick;
        Invoked();
    }
 
    public void Invoke()
    {
        _timer.Tick += TimerTick;            
        _timer.Start();
    }
 
    public Action Invoked { get; set; }        
}

Cette action ne fait que lancer un Timer. Quand ce dernier émet l’évènement Tick, le timer est stoppé et la méthode IWorkflow.Invoked() est appelée. Faire un yield d’un élément de type action de délai dans un workflow créé une pause paramétrable. Tout simplement.

Selon le même principe on peut construire des éléments de workflow basés sur des évènements. L’implémentation est un peu plus complexe surtout si on désire ajouter la généricité. Mais Jounce est fourni avec des exemples de ce type.

Bien entendu, en utilisant la même logique on créé des éléments d’actions de type Background worker pour exécuter du code en tâche de fond.

De façon encore plus globale on peut créer un élément capable d’exécuter n’importe quelle action.

Conclusion

Arrivé ici, le mieux est de pratiquer un peu et de tester par vous-même ce mécanisme séduisant. Il est évident que rester dans le théorique n’apporte plus grand chose passé une certaine quantité d’explications.

Jounce propose bien d’autres choses comme expliqué dans ce billet. Cette petite librairie contient en tout cas d’excellentes idées et il serait dommage de passer à côté ne serait-ce que pour sa gestion des tâches asynchrones qui sont un véritable casse-tête et de grandes pourvoyeuses de code spaghetti !

C’est tout pour aujourd’hui, vous pouvez éteindre votre PC,

yield return “Stay Tuned” !

Faites des heureux, PARTAGEZ l'article !