This article is available in english.
Récemment, j'ai écrit un article à propos des WinForms, DataBinding et Mises à Jour depuis plusieurs Threads, où j'expliquais comment externaliser l'exécution d'une méthode accrochée à un événement sur la thread de l'interface utilisateur.
J'ai utilisé alors une technique basée sur un Action<Action> qui prend avantage des "Closures", et le fait que l'action va transporter son contexte jusqu'à l'endroit où il doit être exécuté.
Ce concept d'externalisation peut être revisité avec les Reactive
Extensions, et l'interface IScheduler.
L'exemple original
Voyons l'exemple de code original :
1 2 3 4 5 | public MyController(Action<Action> synchronousInvoker) { _synchronousInvoker = synchronousInvoker; ... } |
Ce code est le constructeur du contrôleur du formulaire, et l'action "synchronousInvoker" va être définie comme ceci :
1 | _controller = new MyController(a => Invoke(a)); |
Et utilisée comme ceci :
1 2 3 | _synchronousInvoker( () => PropertyChanged(this, new PropertyChangedEventArgs("Status")) ); |
Ou "Invoke" est en fait
Control.Invoke(), utilisé pour exécuter du code sur la Thread de l'interface graphique, la où les mise à jour de contrôles graphiques peuvent être effectuées sans problèmes.
Bien que la technique Action<Action> fonctionne très bien pour parvenir à isoler les rôles, il n'est pas très évident, juste en regardant le constructeur, de savoir ce que l'on doit donner à ce fameux paramètre.
Utiliser l'interface IScheduler
Pour parvenir à isoler l'exécution du contenu des opérateurs du Reactive Framework, l'équipe du Rx a introduit le concept de Scheduler, avec un tas d'implémentations de schedulers communs.
Cela permet d'exposer simplement un moyen de planifier l'exécution d'une méthode dans le contexte qui est n'est pas à la charge de l'utilisateur. Bien souvent, le code qui utilise le scheduler ne veut pas s'occuper du contexte dans lequel est exécute le code. Dans notre cas, on veut exécuter notre code sur la pompe à message des WinForms, et ça tombe bien, "il y a un Scheduler pour ca".
L'exemple précédent peut être facilement mis à jour en utilisant IScheduler à la place de Action<Action>, et d'utiliser la méthode IScheduler.Schedule().
1 2 3 4 5 | public MyController(ISheduler scheduler) { _scheduler = scheduler; ... } |
Et de remplacer l'appel par :
1 2 3 | _scheduler.Schedule( () => PropertyChanged(this, new PropertyChangedEventArgs("Status")) ); |
Ce n'est pas une modification très complexe, mais cela le code bien plus compréhensible.
On peut utiliser le Scheduler fourni pour les WinForms, le non-documenté System.Concurrency.ControlScheduler (qui n'est pas présent dans la classe Scheduler parce qu'il ne peut pas être créé statiquement car il requiert une instance de Control) :
1 | _controller = new MyController(new ControlScheduler(this)); |
Avec "this" qui est une instance de Control.
Au final, le code est plus lisible, et pour ce qui est de faire des tests unitaires sur le Contrôleur, c'est tout à fait faisable avec le System.Concurrency.CurrentThreadScheduler, puisqu'il n'y a pas besoin de changer de threads dans les tests.
Que se passe-t-il avec les Reactive Extensions et Silverlight pour Windows Phone
7 ?
Dans un refactoring très étrange, l'équipe du WP7 a déplacé le IScheduler depuis System.Concurrency vers le très spécifique namespace Microsoft.Phone.Reactive.
Je ne comprend pas vraiment la raison d'un tel changement, et cela a le désagréable effet de rendre incompatible du code qui compilait facilement sur plusieurs plateformes.
Ils ont peut-être considéré que l'implémentation des Reactive Extensions pour Windows Phone était trop différente de la version Desktop... Mais le Compact Framework a été construit sur cette assomption, et la plupart du code est resté dans les même namespaces.
Si quelqu'un a une explication pour ce refactoring étrange, je suis tout ouïe :)