Bei asynchronen Aufgaben entsteht eine schlechte UX


9

Ich schreibe ein COM-Add-In, das eine IDE erweitert, die es dringend benötigt. Es gibt viele Funktionen, aber lassen Sie es uns für diesen Beitrag auf 2 eingrenzen:

  • Es gibt einen Code - Explorer Toolwindow , dass zeigt eine Baumansicht , die die Benutzer navigieren Module und ihre Mitglieder können.
  • Es gibt ein Code - Inspektionen Toolwindow , dass zeigt eine Datagridview , die die Benutzer navigieren Code Probleme können und automatisch beheben.

Beide Tools verfügen über eine Schaltfläche "Aktualisieren", mit der eine asynchrone Aufgabe gestartet wird, mit der der gesamte Code in allen geöffneten Projekten analysiert wird. Der Code-Explorer verwendet die Analyseergebnisse, um die Baumansicht zu erstellen , und die Code-Inspektionen verwenden die Analyseergebnisse, um Codeprobleme zu finden und die Ergebnisse in der Datagrid-Ansicht anzuzeigen .

Was ich versuche , hier zu tun, ist die Parse - Ergebnisse zwischen den Funktionen zu teilen, dass so , wenn die Code - Explorer Auffrischungen, dann ist der Code - Inspektionen weiß es und kann sich erneuern , ohne die Analyse der Arbeit wiederholen zu müssen , dass der Code - Explorer habe gerade .

Also habe ich meine Parser-Klasse zu einem Ereignisanbieter gemacht, bei dem sich die Funktionen registrieren können:

    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);
            }
        });
    }

Und es funktioniert. Das Problem, das ich habe, ist, dass ... es funktioniert - ich meine, wenn die Codeinspektionen aktualisiert werden, sagt der Parser dem Code-Explorer (und allen anderen): "Alter, jemand analysiert, was möchten Sie dagegen tun?" "" - und wenn das Parsen abgeschlossen ist, sagt der Parser seinen Zuhörern: "Leute, ich habe neue Analyseergebnisse für Sie, was möchten Sie dagegen tun?".

Lassen Sie mich Sie durch ein Beispiel führen, um das dadurch entstehende Problem zu veranschaulichen:

  • Der Benutzer ruft den Code-Explorer auf, der dem Benutzer mitteilt, dass "Moment, ich arbeite hier". Benutzer arbeitet weiterhin in der IDE, der Code Explorer zeichnet sich neu, das Leben ist schön.
  • Der Benutzer ruft dann die Code-Inspektionen auf, die dem Benutzer mitteilen, dass "Warte, ich arbeite hier". Der Parser teilt dem Code Explorer mit: "Alter, jemand analysiert, was möchten Sie dagegen tun?" - Der Code Explorer teilt dem Benutzer mit, "Moment, ich arbeite hier". Der Benutzer kann weiterhin in der IDE arbeiten, jedoch nicht im Code-Explorer navigieren, da dieser aktualisiert wird. Und er wartet auch darauf, dass die Code-Inspektionen abgeschlossen sind.
  • Der Benutzer sieht ein Codeproblem in den Prüfergebnissen, die er behandeln möchte. Sie doppelklicken, um dorthin zu navigieren, bestätigen, dass ein Problem mit dem Code vorliegt, und klicken auf die Schaltfläche "Beheben". Das Modul wurde geändert und muss erneut analysiert werden, damit die Code-Inspektionen fortgesetzt werden können. Der Code Explorer teilt dem Benutzer mit, "Moment, ich arbeite hier", ...

Sehen Sie, wohin das führt? Ich mag es nicht und ich wette, Benutzer werden es auch nicht mögen. Was vermisse ich? Wie soll ich vorgehen, um Analyseergebnisse zwischen Features zu teilen, aber dem Benutzer trotzdem die Kontrolle darüber zu überlassen, wann das Feature seine Arbeit erledigen soll ?

Der Grund, den ich frage, ist, dass ich dachte, wenn ich die eigentliche Arbeit verschieben würde, bis der Benutzer sich aktiv für eine Aktualisierung entscheidet, und die Analyseergebnisse "zwischengespeichert" habe, sobald sie eingehen ... nun, dann würde ich eine Baumansicht aktualisieren und Suchen von Codeproblemen in einem möglicherweise veralteten Analyseergebnis ... was mich buchstäblich zu Punkt eins zurückbringt, wo jedes Feature mit seinen eigenen Analyseergebnissen arbeitet: Gibt es eine Möglichkeit, Analyseergebnisse zwischen Features auszutauschen und eine schöne UX zu haben?

Der Code ist , aber ich suche keinen Code, ich suche nach Konzepten .


2
Nur zu Ihrer Information , wir haben auch eine UserExperience.SE- Site. Ich glaube, dass dies hier ein Thema ist, da hier mehr über das Code-Design als über die Benutzeroberfläche gesprochen wird, aber ich wollte Sie wissen lassen, falls Ihre Änderungen eher auf die Benutzeroberfläche und nicht auf die Code- / Design-Seite des Problems gerichtet sind.

Ist dies beim Parsen eine Alles-oder-Nichts-Operation? Beispiel: Löst eine Änderung in einer Datei eine vollständige Analyse aus, oder nur für diese und die davon abhängigen Dateien?
Morgen

@Morgen gibt es zwei Dinge: Wird VBAParservon ANTLR generiert und gibt mir einen Analysebaum, aber die Funktionen verbrauchen das nicht. Der RubberduckParsernimmt den Analysebaum, geht ihn durch und gibt ein aus VBProjectParseResult, das DeclarationObjekte enthält , deren ReferencesAuflösung vollständig ist - das ist es, was die Funktionen für die Eingabe benötigen. Also ja, es ist so ziemlich eine Alles-oder-Nichts-Situation. Das RubberduckParserist klug genug, um Module, die nicht geändert wurden, nicht erneut zu analysieren. Aber wenn es einen Engpass gibt, liegt das nicht am Parsen, sondern an den Code-Inspektionen.
Mathieu Guindon

4
Ich denke, ich würde es so machen: Wenn der Benutzer eine Aktualisierung auslöst, löst dieses Toolfenster die Analyse aus und zeigt, dass es funktioniert. Die anderen Toolfenster werden noch nicht benachrichtigt, sie zeigen weiterhin die alten Informationen an. Bis der Parser fertig ist. Zu diesem Zeitpunkt würde der Parser allen Werkzeugfenstern signalisieren, ihre Ansicht mit den neuen Informationen zu aktualisieren. Sollte der Benutzer während der Arbeit des Parsers zu einem anderen Toolfenster wechseln, wechselt dieses Fenster ebenfalls in den Status "Working ..." und signalisiert eine Analyse. Der Parser würde dann von vorne beginnen, um allen Fenstern gleichzeitig aktuelle Informationen zu liefern.
cmaster

2
@cmaster Ich würde diesen Kommentar auch als Antwort positiv bewerten.
RubberDuck

Antworten:


7

Die Art und Weise, wie ich dies wahrscheinlich angehen würde, wäre, mich weniger auf perfekte Ergebnisse zu konzentrieren, sondern mich auf einen Best-Effort-Ansatz zu konzentrieren. Dies würde zumindest zu folgenden Änderungen führen:

  • Konvertieren Sie die Logik, die derzeit eine erneute Analyse startet, in eine Anforderung anstelle einer Initiierung.

    Die Logik zum Anfordern einer erneuten Analyse sieht möglicherweise folgendermaßen aus:

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

    Dies wird mit einer Logik gepaart, die den Parser umschließt. Dies könnte ungefähr so ​​aussehen:

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

    Wichtig ist, dass der Parser ausgeführt wird, bis die letzte Anforderung zum erneuten Analysieren berücksichtigt wurde, jedoch nicht mehr als ein Parser gleichzeitig ausgeführt wird.

  • Entfernen Sie den ParseStartedRückruf. Das Anfordern einer erneuten Analyse ist jetzt eine Feuer-und-Vergessen-Operation.

    Alternativ können Sie es konvertieren, um nichts anderes zu tun, als einen Aktualisierungsindikator in einem abgelegenen Teil der GUI anzuzeigen, der die Benutzerinteraktion nicht blockiert.

  • Versuchen Sie, minimale Handhabung für veraltete Ergebnisse bereitzustellen.

    Im Fall des Code-Explorers kann dies so einfach sein, dass eine angemessene Anzahl von Zeilen nach oben und unten nach einer Methode gesucht wird, zu der der Benutzer navigieren möchte, oder nach der nächsten Methode, wenn kein genauer Name gefunden wurde.

    Ich bin mir nicht sicher, was für den Code Inspector angemessen wäre.

Ich bin mir der Implementierungsdetails nicht sicher, aber insgesamt ist dies sehr ähnlich wie der NetBeans-Editor mit diesem Verhalten umgeht. Es ist immer sehr schnell darauf hinzuweisen, dass es derzeit aktualisiert wird, aber auch den Zugriff auf die Funktionalität nicht blockiert.

Veraltete Ergebnisse sind oft gut genug - insbesondere im Vergleich zu keinen Ergebnissen.


1
Hervorragende Punkte, aber ich habe eine Frage: Ich ParseStarteddeaktiviere damit die Schaltfläche [Aktualisieren] ( Control.EnableRefresh(false)). Wenn ich diesen Rückruf entferne und den Benutzer darauf klicken lasse, würde ich mich in eine Situation versetzen, in der zwei Aufgaben gleichzeitig ausgeführt werden. Wie vermeide ich dies, ohne die Aktualisierung aller anderen Funktionen zu deaktivieren, während jemand analysiert?
Mathieu Guindon

@ Mat'sMug Ich habe meine Antwort aktualisiert, um diese Facette des Problems aufzunehmen.
Morgen

Ich bin mit diesem Ansatz einverstanden, mit der Ausnahme, dass ich weiterhin ein ParseStartedEreignis behalten würde , falls Sie der Benutzeroberfläche (oder einer anderen Komponente) erlauben möchten, den Benutzer manchmal vor einer Analyse zu warnen. Natürlich möchten Sie möglicherweise dokumentieren, dass Anrufer versuchen sollten, den Benutzer nicht daran zu hindern, die (kurz vor dem) veralteten aktuellen Analyseergebnisse zu verwenden.
Mark Hurd
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.