Exemplo de uploader de lista de documentos do .NET

Jeff Fisher, equipe de APIs de dados do Google
Janeiro de 2008

Introdução: o escopo da amostra

Captura de tela da interface do programa

Faça o download do executável de amostra.

Um dos recursos interessantes da API de dados da lista de documentos é que ela permite que os desenvolvedores criem ferramentas de migração para usuários que ainda estão se adaptando ao Documentos Google. Para fins de exercício desta API, usei a biblioteca de cliente.NET para criar um aplicativo de upload .NET 2.0, chamado de "DocList Uploader". Você pode acessar a origem do uploader no subversion.

Esta amostra foi criada para facilitar a migração de documentos do computador para os Documentos Google. Ele permite que os usuários façam login na Conta do Google e arrastem e soltem arquivos compatíveis, que são enviados automaticamente. A amostra também oferece a opção de adicionar uma opção de menu de clique com o botão direito do mouse ao shell do Windows Explorer para fazer upload de arquivos. Essa amostra é fornecida sob a licença Apache 2.0, então você pode usá-la como ponto de partida para seus próprios programas.

Este artigo mostra como parte do comportamento da amostra foi realizada usando o framework .NET. Ele consiste principalmente em snippets de código anotados das seções relevantes. Este artigo não aborda como criar os formulários e outros componentes da interface do aplicativo, já que há muitos artigos do Visual Studio que detalham isso. Se você quiser saber como os componentes da interface foram configurados, faça o download da biblioteca de cliente e procure no subdiretório "clients\cs\samples\DocListUploader".

Como criar um app para a bandeja do sistema

exemplo de app de bandeja

As ferramentas de migração geralmente podem ser executadas discretamente no sistema operacional, estendendo o que o SO pode fazer sem distrair muito o usuário. Uma maneira de estruturar essa ferramenta no Windows é executá-la na bandeja do sistema em vez de poluir a barra de tarefas. Isso aumenta muito a probabilidade de os usuários deixarem o programa em execução contínua em vez de abrirem apenas quando precisam realizar uma tarefa específica. Essa é uma ideia particularmente útil para este exemplo, já que não é necessário armazenar credenciais de autenticação no disco.

Um aplicativo da bandeja do sistema é aquele que é executado principalmente com apenas um NotifyIcon na bandeja do sistema (a região perto do relógio na barra de tarefas). Ao projetar um aplicativo desse tipo, lembre-se de que você não quer que a forma principal do projeto seja aquela com que o usuário interage. Em vez disso, crie um formulário separado para mostrar quando o aplicativo for executado. O motivo disso será explicado em breve.

No meu exemplo, criei dois formulários: HiddenForm, o formulário principal do aplicativo com a maior parte da lógica, e OptionsForm, um formulário que permite ao usuário personalizar algumas opções e fazer login na Conta do Google. Também adicionei um NotifyIcon chamado DocListNotifyIcon ao HiddenForm e o personalizei com meu próprio ícone. Para garantir que o HiddenForm não seja visto pelo usuário, defino a opacidade como 0%, o WindowState como Minimized e a propriedade ShowInTaskbar como False.

Normalmente, quando um programa está sendo executado fora da bandeja do sistema, fechar o aplicativo não deve interromper o programa, mas sim ocultar todos os formulários ativos e deixar apenas o NotifyIcon visível. Para isso, precisamos substituir o evento "FormClosing" do nosso formulário da seguinte maneira:

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

Também é recomendável ocultar o formulário quando o usuário o minimiza, já que não há motivo para ocupar espaço na barra de tarefas, porque já temos um ícone de notificação. Isso pode ser feito com o seguinte trecho de código:

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

Como não permitimos que o usuário feche o OptionsForm, podemos manter uma instância vinculada ao HiddenForm. Quando quisermos mostrar o OptionsForm novamente, basta chamar o método Show dele.

Como a principal forma deste aplicativo, o HiddenForm, não está visível para o usuário, precisamos dar a ele uma maneira de sair do aplicativo. Optei por adicionar um ContextMenu ao NotifyIcon com um ToolStripMenuItem para fechar o aplicativo. Para gravar o gerenciador de cliques, basta chamar o método Close do HiddenForm.

Dicas sobre balões

Muitos aplicativos da bandeja do sistema se comunicam com o usuário mostrando uma dica em balão, que parece uma bolha arredondada originada do NotifyIcon. A bolha pode ser mostrada da seguinte forma:

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

O primeiro argumento é a quantidade de tempo em milissegundos para mostrar o balão. Há um tempo mínimo e máximo que o SO permite para esse campo, que são 10 e 30 segundos, respectivamente. O segundo e o terceiro argumentos especificam um título e algum conteúdo para o balão. O último argumento permite escolher um ícone para ilustrar a finalidade da bolha.

Como fazer upload de documentos

Fazer upload de um documento é simples. A maior parte do trabalho é feita pelo método UploadDocument do objeto DocumentsService. Esse processo é explicado com mais clareza no Guia para desenvolvedores da API Documents List.

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

Primeiro, o objeto DocumentsService precisa ser inicializado e as credenciais do usuário precisam ser fornecidas. Para evitar alguns problemas ao fazer upload de vários arquivos, o cabeçalho HTTP "keep-alive" foi desativado, já que ele causa problemas com o .NET Framework.

lastUploadEntry = service.UploadDocument(fileName, null);

Este snippet faz upload do arquivo no caminho contido na string fileName. O segundo argumento sendo nulo indica que o nome do arquivo do Google Docs será o mesmo do arquivo original.

Como processar o recurso de arrastar e soltar

Para facilitar o upload, é recomendável permitir que o usuário arraste e solte arquivos das pastas no aplicativo para fazer o upload. A primeira etapa é permitir a operação de soltar de um arquivo. O código abaixo muda o cursor para indicar que a ação é permitida:

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

Depois que o arquivo ou grupo de arquivos for solto, precisamos processar esse evento passando por cada arquivo solto e fazendo upload dele:

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

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

Como listar documentos

Receber uma lista de documentos do servidor é uma boa maneira de lembrar o usuário do que ele já enviou. O snippet abaixo usa o objeto DocumentsService que inicializamos anteriormente para recuperar todos os documentos do servidor.

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

Uma maneira conveniente de visualizar esses dados é usando uma ListView. Adicionei uma ListView chamada DocList ao OptionsForm. Para deixar mais interessante, também criei uma ImageList personalizada de ícones para ilustrar os vários tipos de documentos. O código a seguir preenche a ListView com as informações recuperadas do feed acima:

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

A variável imageKey escolhe qual imagem na ImageList associada deve ser usada para cada linha. A propriedade Tag é usada aqui para armazenar a entrada original, o que pode ser útil para realizar operações no documento mais tarde. Por fim, o método AutoResize é usado para formatar automaticamente a largura da coluna na ListView.

Abrir documentos no navegador

Como esses documentos são armazenados no Google Docs, é bom permitir que o usuário veja o documento no navegador. Há uma funcionalidade integrada para fazer isso no Windows:

using System.Diagnostics;

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

Aqui, recuperamos a entrada original da propriedade Tag e usamos o AlternateUri do documento selecionado para chamar Process.Start. O restante é processado pela mágica do .NET Framework.

Adicionar um menu de contexto do shell

A maneira mais simples de adicionar um item ao menu de contexto do shell é modificar o registro. Precisamos criar uma entrada em HKEY_CLASSES_ROOT que aponte para nosso aplicativo. Isso vai abrir uma nova instância do nosso aplicativo quando o usuário clicar no item de menu, algo que vamos abordar nas próximas seções.

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

Esse código cria a chave de registro em que o aplicativo em execução está localizado. A notação "%1" é usada para indicar que o arquivo selecionado no shell precisa ser transmitido dentro desse parâmetro. O KEY_NAME é uma constante de string definida que determina o texto da entrada no menu de contexto.

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

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

Esse método simplesmente remove a chave personalizada que adicionamos, se ela existir.

Como evitar várias instâncias

Como nosso aplicativo fica na bandeja do sistema, não queremos que várias instâncias do programa sejam executadas ao mesmo tempo. Podemos usar um Mutex para garantir que apenas uma instância permaneça em execução.

using System.Threading;

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

O código acima pode ser colocado no método Main do aplicativo para sair mais cedo se o programa já estiver em execução. Como o Mutex está no namespace "Local", isso permite que uma sessão diferente na máquina execute nosso aplicativo separadamente. No entanto, é preciso tomar alguns cuidados extras, já que estamos modificando o registro global.

Comunicação entre processos

Quando um usuário clica no item do menu de contexto do shell de um arquivo que adicionamos anteriormente, uma nova instância do nosso aplicativo é iniciada e recebe o caminho completo de onde o arquivo está localizado no disco. Essas informações precisam ser comunicadas à instância do aplicativo que já está em execução. Isso pode ser feito usando os mecanismos de IPC do .NET Framework, que foram introduzidos na versão 2.0.

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

A mensagem que estamos transmitindo tem a forma de um objeto personalizado. Aqui, criei um objeto que contém uma referência de volta ao HiddenForm que contém a lógica deste aplicativo. Como esse objeto será hospedado na instância original, ele oferece a uma instância posterior uma maneira de se comunicar com o formulário principal da instância original.

class RemoteMessage : MarshalByRefObject
{
    private HiddenForm mainForm;

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

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

Quando a primeira instância do aplicativo é inicializada, o código a seguir permite que ela fique atenta a instâncias sucessivas:

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

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

Observe acima que ele registra um canal IPC nomeado e fornece uma cópia do objeto RemoteMessage que definimos, inicializando-o com uma referência a si mesmo.

Para as instâncias sucessivas do programa, a string fornecida a Main pelo parâmetro args precisa ser transmitida à instância original. O código a seguir pode ser chamado para se conectar ao canal IPC de escuta e recuperar o objeto RemoteMessage da instância original. O método SendMessage é usado para transmitir o nome do arquivo à instância 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);
    }
}

O sistema de mensagens remotas é muito eficiente porque, com ele, podemos tornar objetos pertencentes a uma instância do nosso programa visíveis em canais IPC locais para outras instâncias.

Conclusão

Neste artigo, explicamos em um nível geral alguns dos vários métodos e truques usados na amostra do DocList Uploader para fornecer um utilitário de migração amigável para o Google Docs. Ainda há muitas funcionalidades que podem ser adicionadas aos seus próprios aplicativos, e você pode estender a amostra para atender aos seus próprios propósitos.

Confira alguns recursos úteis para desenvolvedores interessados em trabalhar com a API Data da lista de documentos e para quem quer usar o .NET com outras APIs Google Data: