Quand les tâches asynchrones font une mauvaise UX

9

J'écris un complément COM qui étend un IDE qui en a désespérément besoin. Il y a beaucoup de fonctionnalités impliquées, mais réduisons-le à 2 pour les besoins de ce post:

  • Il existe une fenêtre d'outils Explorateur de code qui affiche une arborescence qui permet à l'utilisateur de parcourir les modules et leurs membres.
  • Il existe une fenêtre d'outils Inspections de code qui affiche une vue de la grille de données qui permet à l'utilisateur de parcourir les problèmes de code et de les résoudre automatiquement.

Les deux outils ont un bouton "Actualiser" qui démarre une tâche asynchrone qui analyse tout le code dans tous les projets ouverts; l' Explorateur de code utilise les résultats de l'analyse pour créer l' arborescence et les inspections de code utilisent les résultats de l'analyse pour rechercher les problèmes de code et afficher les résultats dans sa vue de datagrid .

Ce que j'essaie de faire ici, c'est de partager les résultats de l'analyse entre les fonctionnalités, de sorte que lorsque l' explorateur de code est actualisé, les inspections de code le savent et puissent se rafraîchir sans avoir à refaire le travail d'analyse que l' explorateur de code vient de faire. .

Donc, ce que j'ai fait, j'ai fait de ma classe d'analyseur un fournisseur d'événements auquel les fonctionnalités peuvent s'inscrire:

    private void _parser_ParseCompleted(object sender, ParseCompletedEventArgs e)
    {
        Control.Invoke((MethodInvoker) delegate
        {
            Control.SolutionTree.Nodes.Clear();
            foreach (var result in e.ParseResults)
            {
                var node = new TreeNode(result.Project.Name);
                node.ImageKey = "Hourglass";
                node.SelectedImageKey = node.ImageKey;

                AddProjectNodes(result, node);
                Control.SolutionTree.Nodes.Add(node);
            }
            Control.EnableRefresh();
        });
    }

    private void _parser_ParseStarted(object sender, ParseStartedEventArgs e)
    {
        Control.Invoke((MethodInvoker) delegate
        {
            Control.EnableRefresh(false);
            Control.SolutionTree.Nodes.Clear();
            foreach (var name in e.ProjectNames)
            {
                var node = new TreeNode(name + " (parsing...)");
                node.ImageKey = "Hourglass";
                node.SelectedImageKey = node.ImageKey;

                Control.SolutionTree.Nodes.Add(node);
            }
        });
    }

Et il fonctionne. Le problème que j'ai, c'est que ... ça marche - je veux dire, quand les inspections de code sont actualisées, l'analyseur dit à l'explorateur de code (et à tout le monde) "mec, analyse de quelqu'un, quoi que ce soit que vous voulez faire à ce sujet? " - et lorsque l'analyse est terminée, l'analyseur dit à ses auditeurs "les gars, j'ai de nouveaux résultats d'analyse pour vous, quoi que ce soit que vous vouliez faire à ce sujet?".

Permettez-moi de vous expliquer un exemple pour illustrer le problème que cela crée:

  • L'utilisateur affiche l'explorateur de code, qui dit à l'utilisateur "attendez, je travaille ici"; l'utilisateur continue de travailler dans l'IDE, l'explorateur de code se redessine, la vie est belle.
  • L'utilisateur fait ensuite apparaître les inspections de code, qui indiquent à l'utilisateur «attendez, je travaille ici»; l'analyseur dit à l'explorateur de code "mec, l'analyse de quelqu'un, qu'est-ce que vous voulez faire à ce sujet?" - l'Explorateur de code dit à l'utilisateur "attendez, je travaille ici"; l'utilisateur peut toujours travailler dans l'EDI, mais ne peut pas naviguer dans l'explorateur de code car il est rafraîchissant. Et il attend également la fin des inspections de code.
  • L'utilisateur voit un problème de code dans les résultats d'inspection qu'il souhaite résoudre; ils double-cliquent dessus pour y accéder, confirment qu'il y a un problème avec le code et cliquent sur le bouton "Corriger". Le module a été modifié et doit être analysé de nouveau, de sorte que les inspections du code se poursuivent; l'explorateur de code dit à l'utilisateur "attendez, je travaille ici", ...

Vous voyez où cela va? Je n'aime pas ça, et je parie que les utilisateurs ne l'aimeront pas non plus. Qu'est-ce que je rate? Comment dois-je procéder pour partager les résultats d'analyse entre les fonctionnalités, tout en laissant à l'utilisateur le contrôle du moment où la fonctionnalité doit faire son travail ?

La raison pour laquelle je demande, c'est parce que je me suis dit que si je reportais le travail réel jusqu'à ce que l'utilisateur décide activement de rafraîchir, et "mis en cache" les résultats de l'analyse à mesure qu'ils entrent ... eh bien, je rafraîchirais un aperçu et localiser les problèmes de code dans un résultat d'analyse éventuellement périmé ... ce qui me ramène littéralement à la case départ, où chaque fonctionnalité fonctionne avec ses propres résultats d'analyse: est-il possible de partager les résultats d'analyse entre les fonctionnalités et d' avoir une belle UX?

Le code est , mais je ne cherche pas de code, je cherche des concepts .

Mathieu Guindon
la source
2
Juste un FYI, nous avons également un site UserExperience.SE . Je crois que c'est sur le sujet ici car il s'agit de la conception de code plus que de l'interface utilisateur, mais je voulais vous informer au cas où vos modifications dériveraient davantage vers le côté UI et non pas le côté code / design du problème.
Lorsque vous analysez, est-ce une opération tout ou rien? Par exemple: une modification dans un fichier déclenche-t-elle une analyse complète, ou uniquement pour ce fichier et ceux qui en dépendent?
Morgen
@Morgen il y a deux choses: VBAParserest généré par ANTLR et me donne un arbre d'analyse, mais les fonctionnalités ne consomment pas cela. Le RubberduckParserprend l'arbre d'analyse, le parcourt et émet un VBProjectParseResultqui contient tous les Declarationobjets Referencesrésolus - c'est ce que les fonctionnalités prennent pour la saisie .. alors oui, c'est à peu près une situation tout ou rien. Le RubberduckParserest assez intelligent pour ne pas réanalyser les modules qui n'ont pas été modifiés cependant. Mais s'il y a un goulot d'étranglement, ce n'est pas avec l'analyse, c'est avec les inspections de code.
Mathieu Guindon
4
Je pense que je le ferais comme ceci: lorsque l'utilisateur déclenche un rafraîchissement, cette fenêtre d'outils déclenche l'analyse et montre que cela fonctionne. Les autres fenêtres d'outils ne sont pas encore notifiées, elles continuent d'afficher les anciennes informations. Jusqu'à ce que l'analyseur termine. À ce stade, l'analyseur signalait à toutes les fenêtres d'outils de rafraîchir leur vue avec les nouvelles informations. Si l'utilisateur se rend dans une autre fenêtre d'outils pendant que l'analyseur fonctionne, cette fenêtre entre également dans l'état "de travail ..." et signale une analyse. L'analyseur recommencerait alors à fournir des informations à jour à toutes les fenêtres en même temps.
cmaster - réintègre monica
2
@cmaster Je voterais aussi pour ce commentaire comme réponse.
RubberDuck

Réponses:

7

La façon dont j'aborderais probablement cela serait de se concentrer moins sur la fourniture de résultats parfaits, et plutôt de se concentrer sur une approche au mieux. Cela entraînerait au moins les modifications suivantes:

  • Convertissez la logique qui démarre actuellement une nouvelle analyse pour demander au lieu de lancer.

    La logique pour demander une nouvelle analyse peut finir par ressembler à ceci:

    IF parseIsRunning IS false
      startParsingThread()
    ELSE
      SET shouldParse TO true
    END
    

    Cela sera associé à une logique enveloppant l'analyseur, qui peut ressembler à ceci:

    SET parseIsRunning TO true
    DO 
      SET shouldParse TO false
      doParsing()
    WHILE shouldParse IS true
    SET parseIsRunning TO false
    

    L'important est que l'analyseur s'exécute jusqu'à ce que la dernière demande d'analyse soit honorée, mais pas plus d'un analyseur ne s'exécute à la fois.

  • Supprimez le ParseStartedrappel. Demander une nouvelle analyse est maintenant une opération d'incendie et d'oubli.

    Alternativement, convertissez-le pour ne rien faire d'autre que d'afficher un indicateur rafraîchissant dans une partie de l'interface graphique qui ne bloque pas l'interaction de l'utilisateur.

  • Essayez de fournir une manipulation minimale pour des résultats périmés.

    Dans le cas de l'explorateur de code, cela peut être aussi simple que de rechercher un nombre raisonnable de lignes de haut en bas pour une méthode vers laquelle l'utilisateur souhaite naviguer, ou la méthode la plus proche si un nom exact n'a pas été trouvé.

    Je ne sais pas ce qui serait approprié pour l'inspecteur de code.

Je ne suis pas sûr des détails de l'implémentation, mais dans l'ensemble, cela ressemble beaucoup à la façon dont l'éditeur NetBeans gère ce comportement. Il est toujours très rapide de souligner qu'il est actuellement rafraîchissant, mais ne bloque pas non plus l'accès à la fonctionnalité.

Les résultats périmés sont souvent assez bons - surtout par rapport à l'absence de résultats.

Morgen
la source
1
D'excellents points, mais j'ai une question: j'utilise ParseStartedpour désactiver le bouton [Actualiser] ( Control.EnableRefresh(false)). Si je supprime ce rappel et laisse l'utilisateur cliquer dessus ... alors je me mettrais dans une situation où j'ai deux tâches simultanées effectuant l'analyse ... comment éviter cela sans désactiver l'actualisation de toutes les autres fonctionnalités pendant que quelqu'un est l'analyse?
Mathieu Guindon
@ Mat'sMug J'ai mis à jour ma réponse pour inclure cette facette du problème.
Morgen
Je suis d'accord avec cette approche, sauf que je garderais tout de même un ParseStartedévénement, au cas où vous voudriez permettre à l'interface utilisateur (ou à un autre composant) d'avertir parfois l'utilisateur qu'une analyse se produit. Bien sûr, vous souhaiterez peut-être documenter les appelants qui devraient essayer de ne pas empêcher l'utilisateur d'utiliser les résultats d'analyse actuels (sur le point d'être périmés).
Mark Hurd