Dot.Blog

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

Appels synchrones de services. Est-ce possible ou faut-il penser “autrement” ?

[new:30/11/2011]Silverlight ne gère que des appels asynchrones aux Ria Services et autres communications WCF. Le Thread de l’UI ne doit jamais être bloqué assurant la fluidité des applications. Mais comment régler certains problèmes très basiques qui réclament le synchronisme des opérations ? Comme nous allons le voir la solution passe par un inévitable changement de point de vue et une façon nouvelle de penser l’écriture du code.

Ô asynchronisme ennemi, n’ai-je donc tant vécu que pour cette infamie ? ...

Corneille, s’il avait vécu de nos jours et avait été informaticien aurait peut-être écrit ainsi cette célèbre tirade du Cid.

L’asynchronisme s’est immiscé partout dans la programmation et certains environnements comme Silverlight le rendent même obligatoire alors même que les autres plateformes .NET autorisent aussi des communications synchrones.

Avec Silverlight tout appel à une communication externe (web service, Ria services...) est par force asynchrone.

Lutter contre l’asynchronisme c’est comme mettre des sacs de sables devant sa porte quand la rivière toute proche est en crue : beaucoup de sueur, de travail inutile, pour un résultat généralement pas suffisant, l’eau finissant toujours par trouver un passage...

Or on le voit encore tous les jours, il suffit même d’une recherche sous Google ou Bing pour s’en convaincre, nombre d’informaticiens posent encore des questions comme “comment faire des appels synchrones aux Ria services ?”.

La réponse est inlassablement la même : ce n’est pas possible ou le prix à payer est trop cher, mieux vaut changer d’état d’esprit et faire autrement.

Autrement ?

Quand on voit certains bricolages qui utilisent des mutex, des threads joints ou des ManualResetEvents ou autres astuces plus ou moins savantes on comprend aisément que ce n’est pas la solution. D’abord toutes ces solutions, quand elles marchent, finiront pas bloquer le thread principal si l’action à contrôler s’inscrit dans une manipulation utilisateur, et c’est mal. On ne bloque pas le thread principal qui contrôle l’UI, c’est de la programmation de grand-papa qui paralyse l’interface et offre une expérience utilisateur déplorable.

Il faut donc pratiquer d’une autre façon. Mais c’est plus facile à dire qu’à faire.

Pour mieux comprendre le changement d’état d’esprit nécessaire je vais prendre un exemple, celui d’un Login.

Un exemple réducteur mais parlant

Imaginons une application Silverlight qui ne peut être utilisée qu’après s’être authentifié. Nous allons faire très simple dans cette “expérience de pensée” car je ne montrerai pas de code ici. C’est la façon de penser qui compte et que je veux vous expliquer.

Donc imaginons une telle application. Le développeur a créé une ChildWindow de Login qui apparait immédiatement au chargement de l’application. Cette fenêtre modale (pseudo-modale) est très simple : un ID et un mot de passe à saisir et un bouton de validation.

Si le login est correct la fenêtre se ferme, s’il n’est pas valide la fenêtre change son affichage pour indiquer un message d’erreur et proposer des options : retenter sa chance, se faire envoyer ses identifiants par mail ou bien créer un nouveau compte utilisateur.

Je n’entrerai pas dans les détails de ce mécanisme, vous pouvez je pense aisément imaginer comment cela pourrait se présenter.

Comme le développeur de cette application fictive a beaucoup appris de la “componentisation”, de la “réutilisabilité” et autres techniques visant à ne pas réécrire cent fois le même code, il a décidé de créer un UserControl qui va gérer tout le dialogue de login et ses différentes options.

C’est un bon réflexe.

Mais, forcément, le contrôle ne peut pas faire des accès à la base de données puisque celle-ci est spécifique à une application donnée. Toujours en suivant les canons de la réutilisabilité notre développeur se dit “lorsque l’utilisateur cliquera sur la validation de son login, puisque je ne peux pas valider ce dernier dans mon contrôle, il faut que j’émette un évènement.”.

Bonne façon de penser. Cela s’appelle la délégation, un style de programmation rendu célèbre par Visual Basic et Delphi il y a déjà bien longtemps... C’est grâce à la délégation qu’on peut construire des contrôles réutilisable et c’est une avancée majeure.

Par exemple la classe Button propose un évènement Click. Il suffit de fournir l’adresse d’une méthode qui gère ce Click et l’affaire est jouée. Le contrôle ne sait rien du code qui sera utilisé donc il peut être réutilisé dans milles circonstances, il suffira à chaque fois de lui fournir l’adresse de la méthode qui réalisera le travail. Je parle d’adresse de méthode car originellement c’est bien de cela qu’il s’agit. Même aujourd’hui cela fonctionne de cette manière mais .NET cache les adresses et gère une collection permettant à plusieurs bouts de code de venir “s’abonner” au Click d’un seul bouton.

C’est une évolution qui rend la réutilisabilité encore meilleure et le codage encore plus simple mais qui, fondamentalement, reste basée sur la même technique.

De fait, pour en revenir au contrôle de Login, l’idée du développeur est simple : mon contrôle ne sait pas si le login est valide ou non, mais il a besoin de le savoir pour adapter sa réponse (on ferme la fenêtre car le login est ok, on affiche l’écran d’erreur dans le cas contraire). Pour régler ce problème, je décide donc de créer un évènement “CheckLogin” auquel s’abonnera l’application utilisatrice du contrôle. Dans les arguments de l’évènement je prévois une propriété booléenne “IsLoginOk” que le gestionnaire de l’évènement positionnera à true ou false.

Ce développeur pense logiquement et pour l’instant on ne voit pas ce qui cloche...

Le principe qu’il utilise ici est le même que celui qu’on trouve d’ailleurs un peu partout dans le Framework .NET. En suivant un si brillant modèle comment pourrait-il être dans l’erreur ?

Regardons par exemple les évènements de type KeyUp ou KeyDown. Eux aussi ont une propriété transportée dans l’instance des arguments, “Handled” qui permet au gestionnaire de retourner une valeur booléenne indiquant si l’évènement doit suivre son cours (false) ou bien si le contrôle doit considérer que la touche a été gérée et qu’il ne faut pas faire remonter l’information aux autres contrôles (Handled = true).

Jusqu’ici tout semble être écrit dans le respect des bonnes pratiques de notre métier : componentisation pour une meilleure réutilisabilité, utilisation de la délégation, utilisation des arguments pour permettre la remontée d’une information à l’appelant en suivant le modèle du Framework.

Mais ça coince où alors ?

J’y viens... C’est vrai que le décors, pourtant simple, n’est pas si évident que cela à “raconter”. Mais je pense que vous avez saisi l’affaire.

Un contrôle de Login qui expose un évènement “CheckLogin” qui délègue le test d’authentification à l’application et qui récupère la réponse de cette dernière dans les arguments de l’évènement pour savoir ce qu’il doit faire.

Le contrôle est bien développé et pour savoir quelle “page” afficher, il utilise même la gestion des états visuels du VSM de Silverlight. Selon cet état les différents affichages possibles seront montrés à l’utilisateur. L’état “LoginState” montrera la page demandant l’ID et le mot de passe, l’état “LoginErrorState” affichera la page indiquant qu’il y a erreur d’authentification, l’état “CreateNewAccountState” affichera une page autorisant la création d’un nouveau compte client.

Franchement il n’y a rien à dire, ce contrôle est vraiment bien développé !

Hélas non...

image

Regardez le petit schéma ci-dessus.

Le rectangle bleu c’est notre contrôle de Login. Lorsque l’utilisateur valide son authentification l’évènement CheckLogin se déclenche. En face, dans l’application, il faudra bien appeler quelque chose pour répondre si oui ou non l’utilisateur est vraiment reconnu.

Pour cela l’application utilise les Ria Services avec à l’autre bout un modèle Entity Framework et une base de données SQL contenant une table des utilisateurs.

Or, l’appel Ria Service est asynchrone par nature, donc non bloquant.

Le gestionnaire d’évènement accroché à “CheckLogin” va retourner immédiatement les arguments au contrôle. De fait celui-ci ne récupèrera jamais la bonne valeur d’authentification mais la valeur par défaut de “IsLoginOk” contenu dans les arguments. Par sécurité cette valeur est initialisée à false, donc en permanence le contrôle va afficher l’écran d’erreur de login... Même si l’utilisateur est connu car la réponse arrivera bien après que l’argument sera retourné vers le contrôle de Login qui sera déjà passé à la page d’erreur...

Cela ne marche pas car l’appel à une requête Ria Services n’est pas bloquant. Il est donc impossible dans le gestionnaire d’évènement “CheckLogin” de faire l’authentification et de retourner celle-ci au contrôle de Login à temps.

C’est là qu’entrent en scène les fameux bricolages que j’évoque plus haut pour tenter de rendre bloquant l’appel au service externe.

Une autre façon de penser

Nous l’avons vu, en suivant la logique du développeur (fictif) qui a créé ce contrôle on ne détecte aucun défaut, aucune violation des bonnes pratiques. Pourtant cela ne marche pas.

Tenter de rendre synchrone l’appel asynchrone au service externe est peine perdue. Bidouillage sans intérêt ne réglant en rien le problème. Il faut d’emblée s’ôter cette idée de la tête.

Alors ?

Alors, il faut penser autrement. Je l’ai déjà dit. Bravo aux lecteurs qui ont suivi Sourire

L’approche par la ruse

En réfléchissant un peu, tout bon développeur est capable de “ruser”, de trouver une feinte. Ici tout le monde a compris qu’il faudrait découpler la demande d’authentification et le retour de cette information.

Après quelques cogitations notre développeur a imaginé le schéma suivant :

image

 

Il y a toujours un évènement CheckLogin dans le contrôle qui est déclenché lorsque l’utilisateur clique sur le bouton servant à valider son login. Toutefois on note plusieurs changements :

  • L’évènement ne tente plus de récupérer l’information d’authentification, il ne sert qu’à déclencher la séquence d’authentification, ce qui est différent.
  • Le gestionnaire d’évènement de l’application, celui qui va faire l’appel asynchrone, mémorise dans un “custom context” l’adresse du Sender (donc du contrôle de Login).
  • Le contrôle gère maintenant un état, la propriété State. Quand l’évènement est déclenché cet état est à “RequestPending” (une requête est en attente).
  • Dans l’évènement Completed de la requête asynchrone (quel que soit sa nature, ici Ria Services mais ce n’est qu’un exemple), en passant par un Dispatcher permettant d’utiliser le Thread de l’UI, le code de réponse asynchrone va directement modifier l’état du Contrôle de Login (sa propriété State) en réutilisant l’adresse du Sender mémorisée plus haut.

 

Cette approche n’est pas sotte, elle permet en effet de découpler l’évènement Checklogin de la réponse d’authentification qui arrivera plus tard. En utilisant un Dispatcher et en ayant pris soin de mémoriser le Sender de CheckLogin, le code asynchrone peut en effet modifier l’état du contrôle et le faire passer à “LoginOk” ou à “LoginFailed” par exemple, ce qui déclenchera alors le bon comportement du contrôle.

Un grand pas vient d’être sauté : ne plus lutter contre l’asynchronisme et concevoir son code “autrement” pour qu’il s’y adapte sans gêne.

Si cette ruse n’est pas idiote, elle pose tout de même des problèmes. Le plus gros étant la nécessité pour le code asynchrone de l’application de connaitre la classe du Contrôle de Login pour attaquer sa propriété State (et connaitre aussi l’énumération qui permet de modifier State).

Ce n’est pas très propre... Cela viole même la séparation UI / Code de MVVM.

Pas bête, bien tenté, mais ce n’est pas encore la solution...

L’approche par messagerie

Cent fois sur le métier tu remettras ton ouvrage ...

Dans un environnement où l’on suit le pattern MVVM on dispose le plus souvent d’une messagerie. Que cela soit MVVM-Light, Jounce ou d’autres framework, tous proposent un tel mécanisme car il permet de résoudre certains problèmes posés par l’implémentation de MVVM.

Ainsi, il est tout à fait possible de revoir la logique du contrôle de Login en jouant cette fois-ci sur des échanges de messages.

Le nouveau schéma devient alors :

image

 

Bon ! Cette fois-ci on doit y être ! Le découplage entre l’évènement CheckLogin et la réponse asynchrone est préservé et le découplage UI / Code cher à MVVM est aussi respecté !

Dans ce scénario on retrouve bien entendu l’évènement CheckLogin qui sert de déclencheur à la séquence d’authentification.

On retrouve aussi la propriété State dans le Contrôle de Login.

Mais dans l’évènement Completed de l’appel asynchrone, au lieu d’accéder directement au Sender de CheckLogin et de modifier directement son état, le code se contente d’envoyer un message. Par exemple “LoginOk” ou “LoginFailed”.

Tout semble désormais parfait !

Enfin presque...

Ce n’est pas que je veuille à tout prix jouer les rabat-joie, mais ça cloche toujours un peu.

Un exemple ? Prenez le Contrôle de Login lui-même. Il est maintenant obligé de s’abonner à la messagerie pour capter le message qui sera envoyé par le code asynchrone. Côté réutilisabilité personnellement cela me chiffonne. La messagerie ce n’est pas un composant du Framework Silverlight. Il en existe autant que de toolkit MVVM. Cela veut dire que notre contrôle est “marié” désormais à un Framework donné et que même dans une mini application ne nécessitant pas de framework MVVM il faudrait s’en trimbaler un.

De plus les messageries MVVM je m’en méfie comme de la peste. Très vite on arrive à ce que j’appelle du “message spaghetti”, quelque chose de pire que le code spaghetti. Des classes spécialisées de messages qui se baladent de ci de là, des messages portant des noms en chaines de caractères, un ballet incontrôlable, quasi non maintenable de messages qui transitent partout dans un ordre difficile à prédire...

J’ai testé et croyez-moi c’est difficilement acceptable comme solution en pratique. Jounce propose un logger qui autorise le traçage des messages, c’est déjà beaucoup mieux que MVVM light qui ne possède aucun moyen de vérifier les messages transmis.

Donc ici ce qui ne va pas c’est cette dépendance à une messagerie qui dépend elle même d’un code externe. Notre contrôle n’est plus indépendant, il devient ainsi plus difficilement réutilisable, ce qui était la motivation de sa création. On doit pouvoir trouver mieux.

Il n’en reste pas moins vrai que cette solution est la plus acceptable de toutes celles que nous venons de voir.

Si seulement on pouvait éviter cette satanée messagerie... La dépendance à un toolkit MVVM on peut faire avec. Après tout on ne change pas de toolkit à chaque application et on suppose que le développeur restera fidèle à celui qu’il finira par bien maitriser. Cette dépendance à du code externe continue à me chiffonner, mais elle peut s’accepter.

Mais la messagerie. Brrr. Ca me glace le sang. D’autant plus que le Contrôle de Login devra répondre à un message précis qui sera codé en dur. Il faudra bien documenté tout cela pour que les applications qui s’en serviront sachent quel message il faut utiliser, s’il s’agit d’une notification en chaine de caractères il faudra même se rappeler de sa casse exacte. Pas d'IntelliSense ici.

Tout ce qui dépend d’un savoir occulte en programmation me rend méfiant. Non par crainte maladive, mais parce que je sais que cela rend le code inutilisable dans le temps, que cela complique la maintenance et rend l’intégration d’un nouveau développeur dans une équipe particulièrement pénible (lui transmettre toutes ces petites choses non évidentes mais indispensables).

Découplage maximum

Peut-on découpler encore plus les intervenants dans notre scénario et surtout nous passer de la messagerie ?

Regardez le schéma suivant :

 

image

 

Puisque nous suivons le pattern MVVM, autant le faire jusqu’au bout. Et puisqu’il faut choisir un toolkit, j’opte pour Jounce.

Dans un tel contexte je règle le problème de la façon suivante :

  • Le Contrôle de Login possède lui aussi un ViewModel.
  • C’est ce VM qui porte et expose la propriété State.
  • L’évènement CheckLogin est bien entendu géré par le VM. S’agissant d’un UserControl il sera malgré tout “repiqué” dans ce dernier pour être exposé à l’extérieur (l’application utilisatrice), ce qui n’est pas montré ici.
  • L’état visuel de la Vue est bindé à la propriété State de son VM. Cela pour rappeler que maintenant nous avons une Vue et un ViewModel et que la première va communiquer avec le dernier par le biais du binding et des ICommand.
  • Le VM implémente l’interface ILoginVM selon un mode favorisé par Jounce (non obligatoire mais comme on le voit dans cet exemple qui permet un découplage fort entre les différents codes).
  • L’appel au déclenchement de la séquence d’authentification par l’évènement CheckLogin reste identique.
  • L’évènement Completed du code asynchrone utilise le Router de Jounce pour obtenir le VM du contrôle de Login, mais uniquement au travers de son interface ILoginVM, donc sans rien connaitre de la classe réelle qui l’implémente.
  • Grâce à cette indirection, le code asynchrone Modifie directement la propriété State du VM, ce qui déclenchera automatiquement la mise en conformité visuelle de la Vue.

 

Plus de messagerie ! Jounce permet ce genre de choses car il est un peu plus sophistiqué que MVVM Light. Le Router enregistre la liste de toutes les “routes”, c’est-à-dire de tous les couples possibles "Vue / ViewModel”. Puisque le VM implémente une interface il est possible d’obtenir celle-ci plutôt que le VM réel. On conserve ainsi un découplage fort entre le code de l’application qui ne sait rien de l’implémentation réelle du VM du Contrôle de Login.

Le schéma le montre bien visuellement, on a bien deux partie très différentes et bien différenciées surtout : en haut le Contrôle de Login qui ne sait rien de l’application, en bas l’application et son code asynchrone qui ne sait rien du Contrôle de Login et qui ne connait qu’une Interface (et l’évènement CheckLogin).

Pensez-vous “autrement” ?

C’est la question piège : arrivez-vous maintenant à penser “autrement” que la logique du premier exemple ? Car bien entendu tout ce billet porte sur cette question et non pas la mise en œuvre d’un Contrôle de Login...

Avez-vous capté le glissement progressif entre le premier et le dernier schéma ? Cette transformation qui fait que désormais l’asynchronisme n’est plus un ennemi contre lequel on cherche l’arme absolue mais un mécanisme naturellement intégré à la conception de toute l’application ?

Voyez-vous pourquoi je parlais de bricolages en évoquant toutes les solutions qui permettraient de rendre bloquant les appels asynchrones ?

Conclusion

On pourrait se dire que la dernière solution substitue à la connaissance d’un message celle d’une Interface et qu’il existe donc toujours un lien entre l’application et le Contrôle de Login. C’est un peu vrai.

En fait, mon expérience me prouve qu’il faut limiter l’usage des messageries sous MVVM sous peine de se retrouver avec un fatras non maintenable. Je préfère un code qui implémente une Interface qu’un autre qui utilisera la messagerie.

La solution de l’Interface est aussi plus facilement portable. C’est un procédé légitime du langage C#, la messagerie est un mécanisme “propriétaire” dépendant d’un toolkit précis.

Mais le plus important n’est pas d’ergoter sur les deux derniers schémas, le plus essentiel c’est bien entendu que vous puissiez vous rendre compte comment “penser autrement” face à l’asynchronisme et comment passer d’une logique dépassée et inopérante à une logique de codage en harmonie avec les nouvelles contraintes.

Ne vous posez plus jamais la question de savoir comment rendre bloquant un appel asynchrone sous Silverlight.

Demandez-vous systématiquement comment concevoir autrement votre code pour qu’il s’adapte sans peine ni bricolage à l’asynchronisme...

(en passant, une autre chose à ne pas oublier : Stay Tuned !)

NB: les schémas ont été réalisés sont Expression Design qui n’est pas fait pour cela, mais c’était une façon de parler de ce soft que j’aime beaucoup et qui n’est pas assez connu !