Quando le attività asincrone creano una UX errata


9

Sto scrivendo un componente aggiuntivo COM che estende un IDE che ne ha un disperato bisogno. Ci sono molte funzionalità coinvolte, ma riduciamola a 2 per il bene di questo post:

  • C'è una finestra degli strumenti di Code Explorer che mostra una vista ad albero che consente all'utente di navigare tra i moduli e i loro membri.
  • C'è una finestra degli strumenti di Code Inspections che visualizza una datagridview che consente all'utente di navigare tra i problemi del codice e risolverli automaticamente.

Entrambi gli strumenti hanno un pulsante "Aggiorna" che avvia un'attività asincrona che analizza tutto il codice in tutti i progetti aperti; il Code Explorer utilizza i risultati di parsing per costruire il treeview , e il codice ispezioni utilizza i risultati parse per trovare problemi di codice e visualizzare i risultati nella sua datagridview .

Quello che sto cercando di fare qui, è condividere i risultati dell'analisi tra le funzionalità, in modo che quando Code Explorer si aggiorna, allora Code Inspections ne sia a conoscenza e possa aggiornarsi senza dover ripetere il lavoro di analisi eseguito da Code Explorer .

Quindi, quello che ho fatto, ho reso la mia classe parser un provider di eventi a cui le funzionalità possono registrarsi:

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

E funziona Il problema che sto riscontrando è che ... funziona - voglio dire, quando le ispezioni del codice vengono aggiornate, il parser dice all'esploratore del codice (e a tutti gli altri) "amico, qualcuno sta analizzando, cosa vuoi fare al riguardo? " - e al termine dell'analisi, il parser dice ai suoi ascoltatori "ragazzi, ho nuovi risultati di analisi per voi, cosa volete fare al riguardo?".

Lascia che ti spieghi un esempio per illustrare il problema che questo crea:

  • L'utente visualizza Code Explorer, che dice all'utente "aspetta, sto lavorando qui"; l'utente continua a lavorare nell'IDE, il Code Explorer si ridisegna da solo, la vita è bella.
  • L'utente visualizza quindi le ispezioni del codice, che indicano all'utente "aspetta, sto lavorando qui"; il parser dice a Code Explorer "amico, qualcuno sta analizzando, cosa vuoi fare al riguardo?" - Code Explorer dice all'utente "aspetta, sto lavorando qui"; l'utente può ancora lavorare nell'IDE, ma non può navigare in Esplora codici perché è rinfrescante. E sta aspettando anche il completamento delle ispezioni del codice.
  • L'utente vede un problema di codice nei risultati dell'ispezione che desidera affrontare; fanno doppio clic per accedervi, confermano che c'è un problema con il codice e fanno clic sul pulsante "Correggi". Il modulo è stato modificato e deve essere riesaminato, quindi le ispezioni del codice procedono con esso; Code Explorer dice all'utente "aspetta, sto lavorando qui", ...

Vedi dove sta andando? Non mi piace e scommetto che neanche agli utenti piacerà. Cosa mi sto perdendo? Come devo fare per condividere i risultati dell'analisi tra le funzionalità, ma lasciare comunque all'utente il controllo di quando la funzionalità dovrebbe funzionare ?

Il motivo per cui lo sto chiedendo è perché ho pensato che se avessi posticipato il lavoro effettivo fino a quando l'utente non decidesse attivamente di aggiornare, e "memorizzato nella cache" i risultati dell'analisi man mano che entrano ... beh, allora aggiornerei un treeview e localizzare i problemi di codice in un risultato di analisi possibilmente stantio ... che mi riporta letteralmente al punto di partenza, dove ogni funzione funziona con i suoi risultati di analisi: esiste un modo per condividere i risultati di analisi tra funzionalità e avere una UX adorabile?

Il codice è , ma non cerco codice, cerco concetti .


2
Solo una FYI, abbiamo anche un sito UserExperience.SE . Credo che questo sia un argomento qui perché sta discutendo la progettazione del codice più dell'interfaccia utente, ma volevo farti sapere nel caso in cui le tue modifiche si spostino maggiormente verso il lato dell'interfaccia utente e non sul lato del codice / design del problema.

Quando stai analizzando, è un'operazione tutto o niente? Ad esempio: una modifica in un file attiva un reparse completo o solo per quel file e quelli che dipendono da esso?
Morgen,

@Morgen ci sono due cose: VBAParserè generato da ANTLR e mi dà un albero di analisi, ma le funzionalità non lo consumano. La RubberduckParserprende l'albero di analisi, passeggiate, e le questioni di una VBProjectParseResultche contiene Declarationoggetti che hanno tutti i loro Referencesrisolti - che di ciò che le caratteristiche assumono per l'ingresso .. Quindi sì, è più o meno una situazione di tutto-o-niente. È RubberduckParserabbastanza intelligente da non analizzare nuovamente i moduli che non sono stati modificati. Ma se c'è un collo di bottiglia non è con l'analisi, è con le ispezioni del codice.
Mathieu Guindon,

4
Penso che lo farei in questo modo: quando l'utente attiva un aggiornamento, quella finestra degli strumenti avvia l'analisi e mostra che funziona. Le altre finestre degli strumenti non sono ancora state informate, continuano a visualizzare le vecchie informazioni. Fino al termine del parser. A quel punto, il parser segnalerebbe a tutte le finestre degli strumenti di aggiornare la loro vista con le nuove informazioni. Se l'utente dovesse accedere a un'altra finestra degli strumenti mentre il parser sta lavorando, anche quella finestra entrerebbe nello stato "working ..." e segnalerebbe un reparse. Il parser ricomincerebbe quindi a fornire informazioni aggiornate a tutte le finestre contemporaneamente.
cmaster - ripristina monica

2
@cmaster Vorrei votare anche quel commento come risposta.
RubberDuck,

Risposte:


7

Il modo in cui probabilmente mi approccerei a questo sarebbe quello di concentrarmi meno sulla fornitura di risultati perfetti, e invece concentrarsi su un approccio del massimo sforzo. Ciò comporterebbe almeno le seguenti modifiche:

  • Converti la logica che attualmente avvia un re-analisi per richiedere invece di avviare.

    La logica per richiedere un re-analisi potrebbe finire per assomigliare a questa:

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

    Questo sarà associato alla logica che avvolge il parser, che potrebbe assomigliare a questo:

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

    L'importante è che il parser venga eseguito fino a quando la richiesta di re-analisi più recente è stata onorata, ma non più di un parser è in esecuzione in un dato momento.

  • Rimuovi il ParseStartedcallback. Richiedere un re-parse ora è un'operazione antincendio e dimentica.

    In alternativa, convertilo per non fare altro che mostrare un indicatore di aggiornamento in una parte della GUI che non blocca l'interazione dell'utente.

  • Prova a fornire una gestione minima per risultati non aggiornati.

    Nel caso di Code Explorer, ciò può essere semplice come cercare un numero ragionevole di righe su e giù per un metodo a cui l'utente vuole navigare o il metodo più vicino se non viene trovato un nome esatto.

    Non sono sicuro di cosa sarebbe appropriato per Inspector codice.

Non sono sicuro dei dettagli di implementazione, ma nel complesso, questo è molto simile a come l'editor NetBeans gestisce questo comportamento. È sempre molto veloce sottolineare che è attualmente rinfrescante, ma non blocca anche l'accesso alla funzionalità.

I risultati non aggiornati sono spesso abbastanza buoni, soprattutto se confrontati con nessun risultato.


1
Punti eccellenti, ma ho una domanda: sto usando ParseStartedper disabilitare il pulsante [Aggiorna] ( Control.EnableRefresh(false)). Se rimuovo quel callback e lascio che l'utente faccia clic su di esso ... allora mi metterei in una situazione in cui ho due attività simultanee che eseguono l'analisi ... come posso evitare questo senza disabilitare l'aggiornamento su tutte le altre funzionalità mentre qualcuno sta analizzando?
Mathieu Guindon,

@ Mat'sMug Ho aggiornato la mia risposta per includere quella parte del problema.
Morgen,

Sono d'accordo con questo approccio, tranne per il fatto che terrei comunque un ParseStartedevento, nel caso in cui si desideri consentire all'interfaccia utente (o altro componente) di avvisare l'utente che si sta verificando un analisi. Naturalmente, potresti voler documentare che i chiamanti dovrebbero tentare di non impedire all'utente di utilizzare i risultati di analisi correnti (che stanno per essere) obsoleti.
Mark Hurd,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.