jueves, 28 de septiembre de 2006

GeneXus Extension: Open Command Prompt

Algo que siempre pensé que le falta a GeneXus, es una opción para abrir una línea de comandos en el directorio de la base de conocimientos.

Ahora con GeneXus Rocha, tenemos la posibilidad de hacer nuestras propias extensiones, así que decidí implementarlo...

Las extensiones se desarrollan en Visual Studio 2005 (o con Visual C# 2005 Express a partir del build que subieron hoy), y se desarrolla en C#. El SDK cuenta con un wizard que crea una primer instancia del proyecto, lo cual facilita bastante el proceso.

En la versión actual del wizard hay una forma de agregar una opción de menú, que no estaba en la primer versión, por lo que ahora debería ser más fácil hacer esto. De todas formas, siempre es bueno saber como hacer las cosas "a mano".

El proyecto para VS2005 se puede descargar de GXOpen. Si alguien quiere solo incorporar la extensión a GeneXus, lo puede hacer copiando el archivo GXCmdPrompt.dll (que está en GXCmdPrompt\GXCmdPrompt\bin\Debug) a la carpeta Packages debajo de la instalación de GX Rocha, y reiniciar GeneXus.

Para agregar esta funcionalidad, fue necesario modificar varios archivos:

  • un archivo XML donde se definen los componentes de la extensión (GXCmdPrompt.xml),
  • un archivo .resx de recursos (GXCmdPrompt.resx),
  • dos clases para que la opción de menú pueda realizar acciones (CommandKey y CommandManager),
  • y una clase para el paquete (Package).

Veamos en más detalle como implementar esta extensión.

GXCmdPrompt.xml

Lo primero que debemos hacer es crear el archivo GXCmdPrompt.xml, que tiene lo siguiente:

<?xml version="1.0" encoding="utf-8" ?>
<Package xmlns:gx='http://schemas.genexus.com/addin/v1.0'
id="f0c16daa-a732-43a9-b3eb-2a40824975cf"
name="GXCmdPrompt" >
<Resources>
<Resource type="strings" name="MCrispino.Packages.GXCmdPrompt.GXCmdPrompt"/>
</Resources>

<Commands>
<CommandDefinition id="OpenCommandPrompt" />
</Commands>
<Menues>
<Menu type="menubar">
<Popup name="Extensions" insertBefore="Help" >
<Command refid="OpenCommandPrompt" insertBefore="CopyModel" />
</Popup>
</Menu>
</Menues>
</Package>

El tag Package tiene un atributo id, que debe ser (obligatoriamente) el GUID de la clase Package, que se genera automáticamente cuando creamos la extensión con el wizard. Además tiene un atributo name, que indica el nombre del paquete.

Dentro de Resources, deben ir todos los archivos de recursos, en este caso el único era GXCmdPrompt.resx, que está en el namespace MCrispino.Packages.GXCmdPrompt.

Dentro de Commands deben ir los comandos, en este caso solo tenemos OpenCommandPrompt.

Dentro de Menues se define la estructura de los menues que vamos a tener, en este caso en la barra de menues aparece la opción Extensions (<Popup name="Extensions") antes de la opción Help (insertBefore="Help"). Dentro de este menú aparece el comando OpenCommandPrompt.

Dos comentarios importantes con respecto a este archivo:

  • La propiedad Build Action debe decir Embedded Resource, para que el archivo quede dentro del assembly.
  • Es conveniente configurar el Schema, poniendo la ruta completa al archivo GXPackageDeclaration.xsd, para tener intellitips en la edición.

GXCmdPrompt.resx

En el archivo de recursos lo único que se definió fue el texto que debe aparecer en el menú. Es un archivo que tiene pares de (nombre, valor), en este caso la única entrada es name=OpenCommandPrompt (igual que el comando), value=Open Command Prompt.

Package.cs

La clase Package se crea por defecto cuando se genera el paquete con el wizard. No voy a mostrar el código completo, para que funcione es necesario crear dos métodos:

private void LoadCommandTargets()
{
AddCommandTarget(new CommandManager());
}

public override Artech.Packages.Definition.Package GetPackageDeclarations()
{
try
{
return GetPackageDeclarationsFromResource("MCrispino.Packages.GXCmdPrompt.GXCmdPrompt.xml");
}
catch (System.Exception e)
{
System.Windows.Forms.MessageBox.Show("Error loading package GXCmdPrompt:\n" + e.ToString());
return null;
}
}

Además hay que invocar al método LoadCommandTargets en el método Initialize. Este método es el que agrega la funcionalidad al paquete, creando la clase CommandManager y agregándola como Command target.

El método GetPackageDeclarations, carga las definiciones del archivo XML que se describió más arriba.

CommandKeys.cs

Aún no me queda muy claro para que se necesita esta clase, pero así funciona :). Lo que se definen acá son los comandos, que luego se usan en la clase CommandManager. Pienso que no es necesaria, y que se podría usar new CommandKey(Package.guid, "OpenCommandPrompt") donde se usa CommandKeys.OpenCommandPrompt...

En todo caso, el código es:

public class CommandKeys

{

private static CommandKey cmdPrompt = new CommandKey(Package.guid, "OpenCommandPrompt");

public static CommandKey OpenCommandPrompt { get { return cmdPrompt; } }

}

CommandManager.cs

Esta es la clase que hace la mágia, es la que le da vida a la opción de menú que agregamos.

El nombre de la clase en sí no es importante, lo que debemos tener en cuenta es que debe heredar de la clase CommandDelegator.

En el constructor tenemos:

public CommandManager()
{
AddCommand(CommandKeys.OpenCommandPrompt, new ExecHandler(ExecOpenCommandPrompt), new QueryHandler(QueryOpenCommandPrompt));
}

donde:

  • tenemos una llamada al método AddCommand, que es el que registra el comando
  • el primer parámetro es CommandKeys.OpenCommandPrompt, que es el comando que definimos en la clase CommandKeys
  • el segundo parámetro es el método que se va a invocar cuando se haga click en la opción, en este caso ExecOpenCommandPrompt
  • el tercer parámetro es el método que se va a invocar para determinar si la opción debe estar habilitada o no, en este caso QueryOpenCommandPrompt

El método ExecOpenCommandPrompt está definido como

public bool ExecOpenCommandPrompt(object[] parameters)
{
IKBService kbserv = UIServices.KB;
if (kbserv != null && kbserv.CurrentKB != null)
{
string kbDirectory = kbserv.CurrentKB.Location;

Process p = new Process();
p.StartInfo.FileName = System.Environment.GetEnvironmentVariable("comspec");
p.StartInfo.Arguments = "/K cd /D " + kbDirectory;
p.Start();
}

return true;
}

Lo que hace es fijarse el directorio de la KB (primero verifica que haya una KB abierta), y luego invocar al cmd.exe (obtiene la ruta con System.Environment.GetEnvironmentVariable("comspec");) pasandole el comando necesario para posicionarse en el directorio de la KB. Por último devuelve true, para indicar que el evento fue procesado.

El método QueryOpenCommandPrompt está definido como

private bool QueryOpenCommandPrompt(ICommandAdapter adap, ref CommandStatus status)
{
status.State = CommandState.Disabled;

IKBService kbserv = UIServices.KB;
if (kbserv != null && kbserv.CurrentKB != null)
{
status.State = CommandState.Enabled;
}

return true;
}

Es decir, por defecto el estado (variable por referencia status) lo pone como deshabilitado, y solo si la KB está abierta lo habilita.

Conclusiones

La experiencia de construir esta extensión fue muy positiva, no solo por haber logrado desarrollar la extensión, sino también por el aprendizaje que queda.

Creo que es muy bueno tener un mecanismo para extender GeneXus. Si bien desarrollar estas extensiones no es trivial, es algo que se puede hacer sin demasiado esfuerzo (después que uno averigua donde tocar).

De aquí en más queda mucho por hacer. Para Artech, escribir lo más que puedan de documentación, implementar más wizards con los escenarios más comunes, y proveer ejemplos. Para la comunidad, desarrollar las extensiones que consideremos necesarias, que sin duda van a ser muchas.

1 comentario:

  1. Bien metida!.
    Esperemos que sea la primera de varias extensiones por venir!.

    ResponderEliminar