Solutions pour la rentrée asynchrone C # 5

16

Donc, quelque chose me dérange sur le nouveau support asynchrone en C # 5:

L'utilisateur appuie sur un bouton qui démarre une opération asynchrone. L'appel revient immédiatement et la pompe à messages recommence à fonctionner - c'est tout.

Ainsi, l'utilisateur peut appuyer à nouveau sur le bouton - provoquant une nouvelle entrée. Et si c'est un problème?

Dans les démos que j'ai vues, ils désactivent le bouton avant l' awaitappel et le réactivent ensuite. Cela me semble être une solution très fragile dans une application réelle.

Faut-il coder une sorte de machine à états qui spécifie quels contrôles doivent être désactivés pour un ensemble donné d'opérations en cours? Ou existe-t-il une meilleure façon?

Je suis tenté de simplement afficher une boîte de dialogue modale pendant la durée de l'opération, mais cela ressemble un peu à l'utilisation d'un marteau.

Quelqu'un a des idées brillantes, s'il vous plaît?


ÉDITER:

Je pense que la désactivation des contrôles qui ne devraient pas être utilisés pendant l'exécution d'une opération est fragile car je pense que cela deviendra rapidement complexe lorsque vous aurez une fenêtre avec de nombreux contrôles dessus. J'aime garder les choses simples car cela réduit les risques de bugs, à la fois lors du codage initial et de la maintenance ultérieure.

Que se passe-t-il s'il existe une collection de contrôles qui doivent être désactivés pour une opération particulière? Et si plusieurs opérations s'exécutent simultanément?

Nicholas Butler
la source
Qu'est-ce qui est fragile? Il donne à l'utilisateur une indication que quelque chose est en cours de traitement. Ajouter un travail d'indication dynamique en cours de traitement et cela me semble être une très bonne interface utilisateur.
Steven Jeuris
PS: Le C # de .NET 4.5 est-il appelé C # 5?
Steven Jeuris
1
Juste curieux de savoir pourquoi cette question est ici et non pas SO? Il s'agit définitivement de code, il devrait donc y appartenir, je pense ...
C. Ross
@ C.Ross, c'est plus une question de conception / d'interface utilisateur. Il n'y a pas vraiment de point technique à expliquer ici.
Morgan Herlocker
Nous avons un problème similaire dans les formulaires HTML ajax où un utilisateur clique sur le bouton d'envoi, une demande ajax est envoyée et nous voulons ensuite empêcher l'utilisateur d'appuyer à nouveau sur soumettre, la désactivation du bouton est une solution très courante dans cette situation
Raynos

Réponses:

14

Donc, quelque chose me dérange sur le nouveau support asynchrone en C # 5: L'utilisateur appuie sur un bouton qui démarre une opération asynchrone. L'appel revient immédiatement et la pompe à messages recommence à fonctionner - c'est tout. Ainsi, l'utilisateur peut appuyer à nouveau sur le bouton - provoquant une nouvelle entrée. Et si c'est un problème?

Commençons par noter que c'est déjà un problème, même sans support asynchrone dans la langue. Beaucoup de choses peuvent entraîner la suppression des messages pendant que vous gérez un événement. Vous devez déjà coder défensivement pour cette situation; la nouvelle fonctionnalité de langue rend cela plus évident , ce qui est la bonté.

La source la plus courante d'une boucle de message s'exécutant pendant un événement provoquant une rentrée indésirable est DoEvents. La bonne chose à propos de, awaitpar opposition à, DoEventsest que awaitcela rend plus probable que les nouvelles tâches ne vont pas "affamer" les tâches en cours d'exécution. DoEventspompe la boucle de message, puis appelle de manière synchrone le gestionnaire d'événements rentrants, alors que la nouvelle tâche est awaitgénéralement mise en file d'attente afin qu'elle s'exécute à un moment donné dans le futur.

Dans les démos que j'ai vues, ils désactivent le bouton avant l'appel en attente et le réactivent ensuite. Je pense que la désactivation des contrôles qui ne devraient pas être utilisés pendant l'exécution d'une opération est fragile car je pense que cela deviendra rapidement complexe lorsque vous aurez une fenêtre avec de nombreux contrôles. Que se passe-t-il s'il existe une collection de contrôles qui doivent être désactivés pour une opération particulière? Et si plusieurs opérations s'exécutent simultanément?

Vous pouvez gérer cette complexité de la même manière que vous géreriez toute autre complexité dans un langage OO: vous enlevez les mécanismes dans une classe et rendez la classe responsable de la mise en œuvre correcte d'une stratégie . (Essayez de garder les mécanismes logiquement distincts de la politique; il devrait être possible de modifier la politique sans trop bricoler les mécanismes.)

Si vous avez un formulaire avec beaucoup de contrôles qui interagissent de manière intéressante, vous vivez dans un monde de complexité de votre propre fabrication . Vous devez écrire du code pour gérer cette complexité; si vous n'aimez pas cela, simplifiez le formulaire afin qu'il ne soit pas si complexe.

Je suis tenté de simplement afficher une boîte de dialogue modale pendant la durée de l'opération, mais cela ressemble un peu à l'utilisation d'un marteau.

Les utilisateurs détesteront cela.

Eric Lippert
la source
Merci Eric, je suppose que c'est la réponse définitive - c'est au développeur de l'application.
Nicholas Butler
6

Pourquoi pensez-vous que la désactivation du bouton avant le awaitpuis la réactivation à la fin de l'appel est fragile? Est-ce parce que vous craignez que le bouton ne soit jamais réactivé?

Eh bien, si l'appel ne revient pas, vous ne pouvez pas refaire l'appel, il semble donc que c'est le comportement que vous souhaitez. Cela indique un état d'erreur que vous devez corriger. Si votre appel asynchrone arrive à expiration, cela peut quand même laisser votre application dans un état indéterminé - là encore, un bouton activé pourrait être dangereux.

La seule fois où cela pourrait devenir compliqué, c'est s'il y a plusieurs opérations qui affectent l'état d'un bouton, mais je pense que ces situations devraient être très rares.

ChrisF
la source
Bien sûr, vous devez gérer les erreurs - J'ai mis à jour ma question
Nicholas Butler
5

Je ne sais pas si cela s'applique à vous, car j'utilise généralement WPF, mais je vois que vous faites référence à un ViewModelpour qu'il le puisse.

Au lieu de désactiver le bouton, définissez un IsLoadingindicateur sur true. Ensuite, tout élément d'interface utilisateur que vous souhaitez désactiver peut être lié au drapeau. Le résultat final est qu'il appartient à l'interface utilisateur de trier son propre état activé, et non votre logique métier.

En plus de cela, la plupart des boutons dans WPF sont liés à un ICommand, et généralement je mets ICommand.CanExecuteégal à !IsLoading, donc il empêche automatiquement l'exécution de la commande lorsque IsLoading est égal à true (désactive également le bouton pour moi)

Rachel
la source
3

Dans les démos que j'ai vues, ils désactivent le bouton avant l'appel en attente et le réactivent ensuite. Cela me semble être une solution très fragile dans une application réelle.

Il n'y a rien de fragile à cela, et c'est ce que l'utilisateur attend. Si l'utilisateur doit attendre avant d'appuyer à nouveau sur le bouton, faites-le attendre.

Je peux voir comment certains scénarios de contrôle pourraient rendre assez complexe la décision de désactiver / activer les contrôles, mais est-il surprenant que la gestion d'une interface utilisateur complexe soit complexe? Si vous avez trop d'effets secondaires effrayants à partir d'un contrôle particulier, vous pouvez toujours simplement désactiver le formulaire entier et lancer un gigantesque "chargement" sur le tout. Si c'est le cas sur chaque contrôle, votre interface utilisateur n'est probablement pas très bien conçue en premier lieu (couplage serré, mauvais regroupement des contrôles, etc.).

Morgan Herlocker
la source
Merci - mais comment gérer la complexité?
Nicholas Butler
Une option serait de placer les contrôles associés dans un conteneur. Désactiver / activer par conteneur de contrôle, pas par contrôle. ie: EditorControls.Foreach (c => c.Enabled = false);
Morgan Herlocker
2

La désactivation du widget est l'option la moins complexe et la moins sujette aux erreurs. Vous le désactivez dans le gestionnaire avant de démarrer l'opération asynchrone et vous le réactivez à la fin de l'opération. Ainsi, le désactiver + activer pour chaque contrôle sont conservés localement à la gestion de ce contrôle.

Vous pouvez même créer un wrapper, qui prendra un délégué et un contrôle (ou plusieurs d'entre eux), désactiver les contrôles et exécuter quelque chose de manière asynchrone, qui fera le délégué et que réactiver les contrôles. Il doit prendre en charge les exceptions (effectuez l'activation dans enfin), afin qu'il fonctionne toujours de manière fiable si l'opération échoue. Et vous pouvez le combiner avec l'ajout d'un message dans la barre d'état, afin que l'utilisateur sache ce qu'il attend.

Vous voudrez peut-être ajouter un peu de logique pour compter les désactivations, afin que le système continue de fonctionner si vous avez deux opérations, qui devraient toutes les deux en désactiver une troisième, mais ne s'excluent pas mutuellement. Vous désactivez le contrôle deux fois, il ne sera donc réactivé que si vous l'activez à nouveau deux fois. Cela devrait couvrir la plupart des cas tout en gardant les cas séparés autant que possible.

D'un autre côté, quelque chose comme la machine à états va devenir complexe. D'après mon expérience, toutes les machines d'état que j'ai jamais vues étaient difficiles à entretenir et votre machine d'état devrait englober tout le dialogue, reliant tout à tout.

Jan Hudec
la source
Merci - j'aime cette idée. Auriez-vous donc un objet gestionnaire pour votre fenêtre qui compte le nombre de demandes de désactivation et d'activation pour chaque contrôle?
Nicholas Butler
@NickButler: Eh bien, vous avez déjà un objet gestionnaire pour chaque fenêtre: le formulaire. J'aurais donc une classe implémentant les demandes et comptant et ajouterais simplement des instances aux formulaires pertinents.
Jan Hudec
1

Bien que Jan Hudec et Rachel aient exprimé des idées connexes, je suggérerais d'utiliser quelque chose comme une architecture Model-View - exprimer l'état de la forme dans un objet et lier les éléments de forme à cet état. Cela désactiverait le bouton d'une manière qui peut évoluer pour des scénarios plus complexes.

psr
la source