Muestra del cargador de listas de documentos de .NET

Jeff Fisher, equipo de APIs de datos de Google
Enero de 2008

Introducción: El alcance de la muestra

Captura de pantalla de la interfaz del programa

Descarga el archivo ejecutable de muestra.

Una de las ventajas de la API de Documents List Data es que permite a los desarrolladores crear herramientas de migración para los usuarios que aún se están adaptando a Documentos de Google. Para los fines de ejercitar esta API, usé la biblioteca cliente de.NET para crear una aplicación de carga de .NET 2.0, titulada de forma apropiada "Cargador de DocList". Puedes obtener la fuente del cargador desde Subversion.

El objetivo de este ejemplo es facilitar la migración de documentos de la computadora del usuario a Documentos de Google. Permite que los usuarios accedan a su Cuenta de Google y, luego, arrastren y suelten los archivos compatibles, que se subirán automáticamente. La muestra también proporciona la opción de agregar una opción de menú con el botón derecho al shell del Explorador de Windows para subir archivos. Esta muestra se proporciona bajo la licencia Apache 2.0, por lo que puedes usarla como punto de partida para tus propios programas.

El objetivo de este artículo es mostrar cómo se logró parte del comportamiento de la muestra con el framework de .NET. Consiste principalmente en fragmentos de código anotados de las secciones pertinentes. En este artículo, no se explica cómo crear los formularios y otros componentes de la IU de la aplicación, ya que existen muchos artículos de Visual Studio que explican esto en detalle. Si tienes curiosidad por saber cómo se configuraron los componentes de la IU, puedes cargar el archivo del proyecto por tu cuenta. Para ello, descarga la biblioteca cliente y busca en el subdirectorio "clients\cs\samples\DocListUploader".

Cómo crear una app para la bandeja del sistema

Ejemplo de app de bandeja

Por lo general, las herramientas de migración pueden ejecutarse de forma discreta en el sistema operativo, lo que extiende lo que el SO puede hacer sin distraer demasiado al usuario. Una forma de estructurar una herramienta de este tipo en Windows es hacer que se ejecute desde la bandeja del sistema en lugar de saturar la barra de tareas. Esto hace que sea mucho más probable que los usuarios dejen el programa en ejecución de forma continua en lugar de abrirlo solo cuando necesiten realizar una tarea específica. Esta es una idea particularmente útil para esta muestra, ya que no necesita almacenar credenciales de autenticación en el disco.

Una aplicación de la bandeja del sistema es aquella que se ejecuta principalmente con solo un NotifyIcon en la bandeja del sistema (la región cerca del reloj en la barra de tareas). Cuando diseñes una aplicación de este tipo, ten en cuenta que no quieres que el formulario principal del proyecto sea con el que interactúa el usuario. En su lugar, crea un formulario independiente para mostrarlo cuando se ejecute la aplicación. El motivo de esto se aclarará en un momento.

En mi ejemplo, creé dos formularios: HiddenForm, el formulario principal de la aplicación con la mayor parte de la lógica, y OptionsForm, un formulario que permite al usuario personalizar algunas opciones y acceder a su Cuenta de Google. También agregué un NotifyIcon llamado DocListNotifyIcon al HiddenForm y lo personalicé con mi propio ícono. Para asegurarme de que el usuario no vea el HiddenForm, establecí su opacidad en 0%, su WindowState en Minimized y su propiedad ShowInTaskbar en False.

Por lo general, cuando un programa se ejecuta desde la bandeja del sistema, cerrar la aplicación no debería detener el programa, sino ocultar los formularios activos y dejar solo visible el NotifyIcon. Para ello, debemos anular el evento "FormClosing" de nuestro formulario de la siguiente manera:

private void OptionsForm_FormClosing(object sender, FormClosingEventArgs e)
{
    if(e.CloseReason == CloseReason.UserClosing) {
        this.Hide();
        e.Cancel = true;
    }
}

También es probable que queramos ocultar el formulario cuando el usuario lo minimice, ya que no hay motivo para ocupar espacio en la barra de tareas, ya que ya tenemos un ícono de notificación. Esto se puede hacer con el siguiente fragmento de código:

private void OptionsForm_Resize(object sender, EventArgs e)
{
    if (this.WindowState == FormWindowState.Minimized)
    {
        this.Hide();
    }
}

Ahora, como no permitimos que el usuario cierre el OptionsForm, podemos mantener una instancia vinculada a nuestro HiddenForm. Cuando queremos volver a mostrar el OptionsForm, simplemente podemos llamar a su método Show.

Dado que el formulario principal de esta aplicación, HiddenForm, no es visible para el usuario, debemos brindarle una forma de salir de nuestra aplicación. Decidí agregar un ContextMenu al NotifyIcon con un ToolStripMenuItem para cerrar la aplicación. Escribir el controlador de clics es simple, solo llama al método Close de HiddenForm.

Sugerencias sobre globos

Muchas aplicaciones de la bandeja del sistema se comunican con el usuario mostrando una sugerencia en forma de globo, que parece una burbuja redondeada que proviene de NotifyIcon. La burbuja se puede mostrar de la siguiente manera:

DocListNotifyIcon.ShowBalloonTip(10000, "Title", "Example Text", ToolTipIcon.Info);

El primer argumento es la cantidad de tiempo en milisegundos que se mostrará la burbuja. Ten en cuenta que el SO permitirá una cantidad mínima y máxima de tiempo para este campo, que son 10 y 30 segundos, respectivamente. El segundo y el tercer argumento especifican un título y algo de contenido para la burbuja. El último argumento te permite elegir un ícono para ilustrar el propósito de la burbuja.

Cómo subir documentos

Subir un documento es sencillo. La mayor parte del trabajo se realiza con el método UploadDocument del objeto DocumentsService. Este proceso se explica con mayor claridad en la Guía para desarrolladores de la API de Documents List.

service = new DocumentsService("DocListUploader");
((GDataRequestFactory) service.RequestFactory).KeepAlive = false;
service.setUserCredentials(username, password);

Primero, se debe inicializar el objeto DocumentsService y se deben proporcionar las credenciales del usuario. Para evitar algunos problemas al subir varios archivos, se inhabilitó el encabezado HTTP "keep-alive", ya que se sabe que causa algunos problemas con .NET Framework.

lastUploadEntry = service.UploadDocument(fileName, null);

Este fragmento sube el archivo en la ruta de acceso que contiene la cadena fileName. El segundo argumento nulo indica que el nombre del archivo de Documentos de Google debe ser el mismo que el del archivo original.

Cómo controlar la acción de arrastrar y soltar

Para facilitar la carga, tiene sentido permitir que el usuario arrastre y suelte archivos de sus carpetas a la aplicación para subirlos. El primer paso es permitir la operación de soltar desde un archivo. El siguiente código cambiará el cursor para indicar que se permite la operación de soltar:

private void OptionsForm_DragEnter(object sender, DragEventArgs e)
{
    if (e.Data.GetDataPresent(DataFormats.FileDrop, false))
    {
        e.Effect = DragDropEffects.Copy;
    }
}

Una vez que se suelta el archivo o el grupo de archivos, debemos controlar ese evento recorriendo cada archivo que se soltó y subirlo:

private void OptionsForm_DragDrop(object sender, DragEventArgs e)
{
    string[ fileList = (string[) e.Data.GetData(DataFormats.FileDrop);

    foreach (string file in fileList)
    {
      mainForm.UploadFile(file);
    }
}

Documentos de la lista

Obtener una lista de documentos del servidor es una buena manera de recordarle al usuario lo que ya subió. El siguiente fragmento usa el objeto DocumentsService que inicializamos anteriormente para recuperar todos los documentos del servidor.

public DocumentsFeed GetDocs()
{
    DocumentsListQuery query = new DocumentsListQuery();
    DocumentsFeed feed = service.Query(query);
    return feed;
}

Una forma conveniente de visualizar estos datos es usar un ListView. Agregué un ListView llamado DocList al OptionsForm. Para que se vea mejor, también creé una ImageList personalizada de íconos para ilustrar los distintos tipos de documentos. El siguiente código propaga el ListView con la información recuperada del feed anterior:

public void UpdateDocList()
{

    DocList.Items.Clear();


    DocumentsFeed feed = mainForm.GetDocs();


    foreach (DocumentEntry entry in feed.Entries)
    {
        string imageKey = "";
        if (entry.IsDocument)
        {
            imageKey = "Document.gif";
            
        }
        else if (entry.IsSpreadsheet)
        {
            imageKey = "Spreadsheet.gif";
        }
        else
        {
            imageKey = "Presentation.gif";
        }

        ListViewItem item = new ListViewItem(entry.Title.Text, imageKey);
        item.SubItems.Add(entry.Updated.ToString());
        item.Tag = entry;
        DocList.Items.Add(item); 
    }

    foreach (ColumnHeader column in DocList.Columns)
    {
        column.AutoResize(ColumnHeaderAutoResizeStyle.ColumnContent);
    }
}

La variable imageKey selecciona qué imagen de la ImageList asociada se debe usar para cada fila. Aquí se usa la propiedad Tag para almacenar la entrada original, lo que puede ser útil para realizar operaciones en el documento más adelante. Por último, el método AutoResize se usa para dar formato automático al ancho de la columna en ListView.

Cómo abrir documentos en el navegador

Como estos documentos se almacenan en Documentos de Google, es conveniente permitir que el usuario vea el documento en su navegador. Windows tiene una función integrada para hacerlo:

using System.Diagnostics;

private void OpenSelectedDocument()
{
    if (DocList.SelectedItems.Count > 0)
    {
        DocumentEntry entry = (DocumentEntry) DocList.SelectedItems[0].Tag;
        Process.Start(entry.AlternateUri.ToString());
    }
}

Aquí recuperamos la entrada original de la propiedad Tag y, luego, usamos el AlternateUri del documento seleccionado para llamar a Process.Start. El resto se controla con la magia de .NET Framework.

Cómo agregar un menú contextual de Shell

La forma más sencilla de agregar un elemento al menú contextual del shell es modificar el registro. Lo que debemos hacer es crear una entrada en HKEY_CLASSES_ROOT que apunte a nuestra aplicación. Ten en cuenta que se abrirá una nueva instancia de nuestra aplicación cuando el usuario haga clic en el elemento de menú, algo que tendremos que abordar en las siguientes secciones.

using Microsoft.Win32;

public void Register()
{
    RegistryKey key = Registry.ClassesRoot.OpenSubKey("*\\shell\\"+KEY_NAME+"\\command");

    if (key == null)
    {
        key = Registry.ClassesRoot.CreateSubKey("*\\shell\\" + KEY_NAME + "\\command");
    }

    key.SetValue("", Application.ExecutablePath + " \"%1\"");
}

Este código crea la clave de registro en la que se encuentra la aplicación que se está ejecutando actualmente. La notación "%1" se usa para indicar que el archivo seleccionado en la shell se debe pasar dentro de este parámetro. KEY_NAME es una constante de cadena definida que determina el texto de la entrada en el menú contextual.

public void UnRegister()
{
    RegistryKey key = Registry.ClassesRoot.OpenSubKey("*\\shell\\"+KEY_NAME);

    if (key != null)
    {
        Registry.ClassesRoot.DeleteSubKeyTree("*\\shell\\"+KEY_NAME);
    }
}

Este método simplemente quita la clave personalizada que agregamos, si existe.

Cómo evitar varias instancias

Dado que nuestra aplicación se encuentra en la bandeja del sistema, no queremos que se ejecuten varias instancias del programa al mismo tiempo. Podemos usar un Mutex para garantizar que solo una instancia permanezca en ejecución.

using System.Threading;

bool firstInstance;
Mutex mutex = new Mutex(true, "Local\\DocListUploader", out firstInstance);
if (!firstInstance)
{
  return;
}

El código anterior se puede colocar en el método Main de nuestra aplicación para salir antes si nuestro programa ya se está ejecutando. Dado que Mutex se encuentra dentro del espacio de nombres "Local", esto permite que se ejecute una sesión diferente en la máquina para ejecutar nuestra aplicación por separado. Sin embargo, se debe tener un cuidado adicional, ya que estamos modificando el registro global.

Comunicación entre procesos

Cuando un usuario hace clic en el elemento del menú contextual del shell para un archivo que agregamos anteriormente, se inicia una nueva instancia de nuestra aplicación y se le proporciona la ruta de acceso completa a la ubicación del archivo en el disco. Ahora, esta información se debe comunicar a la instancia de la aplicación que ya se está ejecutando. Esto se puede hacer con los mecanismos de IPC de .NET Framework que se introdujeron en la versión 2.0.

using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;

El mensaje que pasamos toma la forma de un objeto personalizado. Aquí creé un objeto que contiene una referencia al HiddenForm que contiene la lógica de esta aplicación. Dado que este objeto se alojará en la instancia original, proporciona a una instancia posterior una forma de comunicarse con el formulario principal de la instancia original.

class RemoteMessage : MarshalByRefObject
{
    private HiddenForm mainForm;

    public RemoteMessage(HiddenForm mainForm)
    {
        this.mainForm = mainForm;
    }

    public void SendMessage(string file)
    {
        mainForm.HandleUpload(file);
    }
}

Cuando se inicializa la primera instancia de la aplicación, el siguiente código permite que escuche las instancias sucesivas:

public void ListenForSuccessor()
{
    IpcServerChannel serverChannel = new IpcServerChannel("DocListUploader");
    ChannelServices.RegisterChannel(serverChannel, false);

    RemoteMessage remoteMessage = new RemoteMessage(this);
    RemotingServices.Marshal(remoteMessage,"FirstInstance");
    
}

Ten en cuenta que, en el ejemplo anterior, se registra un canal de IPC con nombre y se proporciona una copia del objeto RemoteMessage que definimos, inicializándolo con una referencia a sí mismo.

Para las instancias sucesivas del programa, la cadena proporcionada a Main a través del parámetro args debe pasarse a la instancia original. Se puede llamar al siguiente código para conectarse al canal de IPC en espera y recuperar el objeto RemoteMessage de la instancia original. Luego, se usa el método SendMessage para pasar el nombre del archivo a la instancia original.

public static void NotifyPredecessor(string file)
{
    IpcClientChannel clientChannel = new IpcClientChannel();
    ChannelServices.RegisterChannel(clientChannel, false);

    RemoteMessage message = (RemoteMessage) Activator.GetObject(typeof(RemoteMessage), 
      "ipc://DocListUploader/FirstInstance");

    if (!message.Equals(null))
    {
        message.SendMessage(file);
    }
}

El sistema de mensajería remota es muy potente porque, a través de él, podemos hacer que los objetos que pertenecen a una instancia de nuestro programa sean visibles a través de canales de IPC locales para otras instancias.

Conclusión

En este artículo, se explican de forma general algunos de los diversos métodos y trucos que se usan en la muestra del cargador de DocList para proporcionar una utilidad de migración amigable para Documentos de Google. Aún hay muchas funciones que se pueden agregar en tus propias aplicaciones, y puedes extender el ejemplo para que se adapte a tus propios fines.

Estos son algunos recursos útiles para los desarrolladores que desean trabajar con la API de Documents List Data, así como para aquellos que desean usar .NET con otras APIs de Google Data: