Cuando las tareas asincrónicas hacen una mala experiencia de usuario


9

Estoy escribiendo un complemento COM que extiende un IDE que lo necesita desesperadamente. Hay muchas características involucradas, pero reduzcamos a 2 por el bien de esta publicación:

  • Hay una ventana de herramientas de Code Explorer que muestra una vista de árbol que permite al usuario navegar por los módulos y sus miembros.
  • Hay una ventana de herramientas de Inspecciones de código que muestra una vista de cuadrícula de datos que permite al usuario navegar por los problemas de código y corregirlos automáticamente.

Ambas herramientas tienen un botón "Actualizar" que inicia una tarea asincrónica que analiza todo el código en todos los proyectos abiertos; el explorador de código utiliza los resultados de análisis sintáctico para construir la vista de árbol , y la inspecciones de código utiliza los resultados de análisis sintáctico para encontrar problemas de código y mostrar los resultados en su DataGridView .

Lo que estoy tratando de hacer aquí es compartir los resultados de análisis entre las características, de modo que cuando el Explorador de códigos se actualice, las Inspecciones de código lo sepan y puedan actualizarse sin tener que rehacer el trabajo de análisis que acaba de hacer el Explorador de códigos .

Entonces, lo que hice, hice de mi clase de analizador un proveedor de eventos en el que las características pueden registrarse:

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

Y funciona. El problema que tengo es que ... funciona. Quiero decir, cuando se actualizan las inspecciones de código, el analizador le dice al explorador de código (y a todos los demás) "amigo, alguien está analizando, ¿hay algo que quieras hacer al respecto? " - y cuando finaliza el análisis, el analizador le dice a sus oyentes "chicos, tengo nuevos resultados de análisis para ustedes, ¿qué quieren hacer al respecto?".

Permíteme mostrarte un ejemplo para ilustrar el problema que esto crea:

  • El usuario muestra el Explorador de códigos, que le dice al usuario "espera, estoy trabajando aquí"; el usuario continúa trabajando en el IDE, el Code Explorer se vuelve a dibujar, la vida es hermosa.
  • El usuario luego muestra las inspecciones de código, que le dicen al usuario "espera, estoy trabajando aquí"; el analizador le dice al Code Explorer "amigo, alguien está analizando, ¿hay algo que quieras hacer al respecto?" - el Explorador de códigos le dice al usuario "espera, estoy trabajando aquí"; el usuario aún puede trabajar en el IDE, pero no puede navegar por el Explorador de códigos porque es refrescante. Y también está esperando que se completen las inspecciones del código.
  • El usuario ve un problema de código en los resultados de la inspección que desea abordar; hacen doble clic para navegar hasta él, confirman que hay un problema con el código y hacen clic en el botón "Reparar". El módulo se modificó y debe volver a analizarse, por lo que las inspecciones de código continúan con él; Code Explorer le dice al usuario "espera, estoy trabajando aquí", ...

¿Ves a dónde va esto? No me gusta, y apuesto a que a los usuarios tampoco les gustará. ¿Qué me estoy perdiendo? ¿Cómo debo compartir los resultados de análisis entre las funciones, pero aún así dejar al usuario en control de cuándo la función debe hacer su trabajo ?

La razón por la que pregunto es porque pensé que si posponía el trabajo real hasta que el usuario decidiera actualizar activamente, y "almacenaba en caché" los resultados del análisis a medida que entraban ... bueno, entonces estaría actualizando una vista de árbol y localizar problemas de código en un resultado de análisis posiblemente obsoleto ... que literalmente me lleva de vuelta al punto de partida, donde cada función funciona con sus propios resultados de análisis: ¿hay alguna manera de compartir los resultados de análisis entre funciones y tener un UX encantador?

El código es , pero no estoy buscando código, estoy buscando conceptos .


2
Solo para su información, también tenemos un sitio UserExperience.SE . Creo que esto es sobre el tema aquí porque está discutiendo el diseño del código más que la interfaz de usuario, pero quería informarle en caso de que sus cambios se desvíen más hacia el lado de la interfaz de usuario y no hacia el lado del código / diseño del problema.

Cuando está analizando, ¿es esta una operación de todo o nada? Por ejemplo: ¿un cambio en un archivo desencadena un análisis completo, o solo para ese archivo y los que dependen de él?
Morgen

@ Morgen hay dos cosas: VBAParseres generado por ANTLR y me da un árbol de análisis, pero las características no lo consumen. La RubberduckParsertoma del árbol de análisis, paseos, y emite una VBProjectParseResultque contiene Declarationlos objetos que tienen toda su Referencesresolvieron - que de lo que toman las características para la entrada .. así que sí, es prácticamente una situación de todo o nada. Sin RubberduckParserembargo, es lo suficientemente inteligente como para no volver a analizar módulos que no se han modificado. Pero si hay un cuello de botella no es con el análisis, sino con las inspecciones del código.
Mathieu Guindon

44
Creo que lo haría así: cuando el usuario activa una actualización, esa ventana de herramientas activa el análisis y muestra que está funcionando. Las otras ventanas de herramientas aún no se notifican, siguen mostrando la información anterior. Hasta que el analizador termine. En ese punto, el analizador indicaría a todas las ventanas de herramientas que actualicen su vista con la nueva información. En caso de que el usuario vaya a otra ventana de herramientas mientras el analizador está funcionando, esa ventana también entrará en el estado "trabajando ..." y señalará un análisis. El analizador comenzaría nuevamente para entregar información actualizada a todas las ventanas al mismo tiempo.
cmaster - reinstalar a monica

2
@cmaster Yo también votaría ese comentario como respuesta.
RubberDuck

Respuestas:


7

La forma en que probablemente abordaría esto sería centrarme menos en proporcionar resultados perfectos y, en cambio, enfocarme en un enfoque de mejor esfuerzo. Esto resultaría en al menos los siguientes cambios:

  • Convierta la lógica que actualmente inicia un nuevo análisis para solicitar en lugar de iniciar.

    La lógica para solicitar un nuevo análisis puede terminar pareciéndose a esto:

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

    Esto se combinará con la lógica que envuelve el analizador, que puede verse así:

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

    Lo importante es que el analizador se ejecute hasta que se haya cumplido la solicitud de análisis más reciente, pero no se está ejecutando más de un analizador en un momento dado.

  • Eliminar la ParseStarteddevolución de llamada. Solicitar un nuevo análisis ahora es una operación de disparo y olvido.

    Alternativamente, conviértalo para que no haga nada más que mostrar un indicador refrescante en una parte de la GUI que no bloquea la interacción del usuario.

  • Intente proporcionar un manejo mínimo para obtener resultados obsoletos.

    En el caso del Code Explorer, eso puede ser tan simple como buscar un número razonable de líneas hacia arriba y hacia abajo para un método al que el usuario desea navegar, o el método más cercano si no se encuentra un nombre exacto.

    No estoy seguro de lo que sería apropiado para el Inspector de Código.

No estoy seguro de los detalles de implementación, pero en general, esto es muy similar a cómo el editor de NetBeans maneja este comportamiento. Siempre es muy rápido señalar que actualmente es refrescante, pero tampoco bloquea el acceso a la funcionalidad.

Los resultados obsoletos a menudo son lo suficientemente buenos, especialmente cuando se comparan con ningún resultado


1
Excelentes puntos, pero tengo una pregunta: estoy usando ParseStartedpara deshabilitar el botón [Actualizar] ( Control.EnableRefresh(false)). Si elimino esa devolución de llamada y dejo que el usuario haga clic en ella ... me pondría en una situación en la que tengo dos tareas simultáneas haciendo el análisis ... ¿cómo evito esto sin deshabilitar la actualización en todas las demás funcionalidades mientras alguien está analizando?
Mathieu Guindon

@ Mat'sMug Actualicé mi respuesta para incluir esa faceta del problema.
Morgen

Estoy de acuerdo con este enfoque, excepto que aún mantendría un ParseStartedevento, en caso de que desee permitir que la interfaz de usuario (u otro componente) a veces advierta al usuario que se está produciendo un análisis. Por supuesto, es posible que desee documentar las personas que llaman deben intentar no impedir que el usuario use los resultados de análisis actuales (que están por ser) obsoletos.
Mark Hurd
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.