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 c # , pero no estoy buscando código, estoy buscando conceptos .
fuente
VBAParser
es generado por ANTLR y me da un árbol de análisis, pero las características no lo consumen. LaRubberduckParser
toma del árbol de análisis, paseos, y emite unaVBProjectParseResult
que contieneDeclaration
los objetos que tienen toda suReferences
resolvieron - que de lo que toman las características para la entrada .. así que sí, es prácticamente una situación de todo o nada. SinRubberduckParser
embargo, 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.Respuestas:
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:
Esto se combinará con la lógica que envuelve el analizador, que puede verse así:
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
ParseStarted
devolució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
fuente
ParseStarted
para 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?ParseStarted
evento, 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.