.NET Documents List Uploader Sample

Jeff Fisher, Google Data APIs team
January 2008

Contents

  1. Introduction: The Scope of the Sample
  2. Making a System Tray App
    1. Balloon Tips
  3. Uploading Documents
    1. Handling Drag and Drop
  4. Listing Documents
    1. Opening Documents in the Browser
  5. Adding a Shell Context Menu
    1. Preventing Multiple Instances
    1. Inter-Process Communication
  6. Conclusion

Introduction: The Scope of the Sample

screenshot of program interface

Download the sample executable.

One of the nice things about the Documents List Data API is that it allows developers to make migration tools for users who are still getting settled into Google Docs. For the purposes of exercising this API, I have used the .NET Client Library to create a .NET 2.0 uploader application, appropriately titled the "DocList Uploader". You can get the source of the uploader from subversion.

This sample is meant to make it easy for a user to migrate their documents from their computer to Google Docs. It allows users to log in to their Google account and then drag and drop supported files which are then uploaded automatically. The sample also provides the option to add a right click menu option to the Windows explorer shell to upload files. This sample is provided under the Apache 2.0 License, so you are free to use it as a starting point for your own programs.

This article is meant to show how some of the behavior of the sample was accomplished using the .NET framework. It mostly consists of annotated code snippets from the relevant sections. This article does not cover how to construct the forms and other UI components of the application itself, as there are many Visual Studio articles out there that go into detail for this. If you are curious about how the UI components were configured, you can load the project file yourself, by downloading the client library and looking inside the "clients\cs\samples\DocListUploader" subdirectory.

Making a System Tray App

tray app example

Migration tools usually are able to run unobtrusively in the operating system, extending what the OS is able to do without too much distraction to the user. One way of structuring such a tool in Windows is to have it run out of the system tray rather than cluttering their taskbar. This makes it much more likely for users to leave the program running continuously instead of only opening the program when they need to perform a specific task. This is a particularly useful idea for this sample as it does not need to store authentication credentials to the disk.

A system tray application is one that runs primarily with just a NotifyIcon in the system tray (the region near the clock on the taskbar). When designing such an application, keep in mind that you don't want the main form of the project to be the one the user interacts with. Instead, create a separate form to display when the application is run. The reason for this will be made clear in a moment.

In my example I have created two forms: HiddenForm, the main form of the application with most of the logic, and OptionsForm a form that lets the user customize some options and sign in to their Google account. I also added a NotifyIcon called DocListNotifyIcon to the HiddenForm and customized it with my own icon. To make sure the HiddenForm is not seen by the user, I set its opacity to 0%, its WindowState to Minimized, and its ShowInTaskbar property to False.

Usually, when a program is running out of the system tray, closing the application isn't supposed to stop the program, but instead hide any active forms and only leave the NotifyIcon visible. To do this we have to override the "FormClosing" event of our form as follows:

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

We also probably want to hide the form when the user minimizes it, since there is no reason to take up space on the taskbar as we already have a notification icon. This can be done with the following bit of code:

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

Now since we don't allow the user to close the OptionsForm, we can just keep one instance around that is tied to our HiddenForm. When we want to display the OptionsForm again, we can simply call its Show method.

Since the main form of this application, the HiddenForm, isn't visible to the user, we have to give them a way to actually exit our application. I elected to add a ContextMenu to the NotifyIcon with a ToolStripMenuItem to close the application. To write the click handler is simple, just call the Close method of the HiddenForm.

Balloon Tips

Many system tray applications communicate with the user by showing a balloon tip, which looks like a rounded bubble stemming from the NotifyIcon. The bubble can be shown as follows:

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

The first argument is the amount of time in milliseconds to display the bubble. Note that there are minimum and maximum amounts of time the OS will allow for this field, which are 10 and 30 seconds, respectively. The second and third arguments specify a title and some content for the bubble. The last argument lets you pick an icon to illustrate the purpose of the bubble.

Uploading Documents

To upload a document is straightforward. Most of the work is done by the UploadDocument method of the DocumentsService object. This process is more clearly explained in the Developer's Guide for the Documents List API.

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

First, the DocumentsService object must be initialized and the user's credentials must be provided. In order to prevent some issues when uploading multiple files, the "keep-alive" HTTP header has been disabled as it is known to cause some issue with the .NET Framework.

lastUploadEntry = service.UploadDocument(fileName, null);

This snippet uploads the file at the path contained in the string fileName. The second argument being null indicates the Google Docs file name is to be the same as the original file name.

Handling Drag and Drop

In order to make uploading easier, it makes sense to let the user drag and drop files from their folders onto the application in order to upload them. The first step is to allow the drop operation from a file, the below code will change the cursor to indicate the drop is allowed:

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

Once the file or group of files has been dropped, we must handle that event by going through each file that was dropped and uploading it:

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

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

Listing Documents

Getting a list of documents from the server is a nice way to remind the user what they have already uploaded. The below snippet uses the DocumentsService object we initialized earlier to retrieve all the documents from the server.

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

A convenient way to visualize this data is by using a ListView. I added a ListView named DocList to the OptionsForm. To make it nicer, I also created a custom ImageList of icons to illustrate the various document types. The following code populates the ListView with the information retrieved from the feed above:

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

The imageKey variable picks out which image in the associated ImageList should be used for each row. The Tag property is used here to store the original entry, which can be useful for performing operations on the document later. Lastly, the AutoResize method is used to auto-format the column width in the ListView.

Opening Documents in the Browser

Since these documents are stored in Google Docs, it is nice to let the user see the document in their browser. There is built-in functionality to do this in Windows:

using System.Diagnostics;

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

Here we retrieve the original entry back out of the Tag property, then use the AlternateUri of the selected document to call Process.Start. The rest is handled by the magic of the .NET Framework.

Adding a Shell Context Menu

The most simple way of adding an item to the shell's context menu is by modifying the registry. What we need to do is create an entry under HKEY_CLASSES_ROOT that points to our application. Note that this will open a new instance of our application when the user clicks on the menu item, which is something we will have to deal with in the following sections.

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

This code creates the registry key to where the currently running application is located. The "%1" notation is used to indicate that the selected file in the shell should be passed along inside of this parameter. The KEY_NAME is a defined string constant that determines the text of the entry in the context menu.

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

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

This method simply removes the custom key we added, if it exists.

Preventing Multiple Instances

Since our application lives in the system tray, we don't want multiple instances of the program to be running at one time. We can use a Mutex to ensure that only one instance stays running.

using System.Threading;

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

The above code can be placed in the Main method of our application to exit early if our program is already running. Since the Mutex is within the "Local" namespace, this allows for a different session on the machine to run our application separately. Some additional care should be taken, however, since we are modifying the global registry.

Inter-Process Communication

When a user clicks the shell context menu item for a file that we added earlier, a new instance of our application is launched and given the full path to where the file is located on the disk. This information now has to be communicated to the already running instance of the application. This can be done using the .NET Framework's IPC mechanisms that were introduced in version 2.0.

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

The message we are passing takes the form of a custom object. Here I have created an object that contains a reference back to the HiddenForm that contains the logic of this application. Since this object will be hosted on the original instance, it provides a later instance a way to communicate with the main form of the original instance.

class RemoteMessage : MarshalByRefObject
{
    private HiddenForm mainForm;

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

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

When the first instance of the application is initialized, the following code enables it to listen for successive instances:

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

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

Note in the above that it registers a named IPC channel, and provides a copy of the RemoteMessage object we defined, initializing it with a reference to itself.

For the successive instances of the program, the string provided to Main via the args parameter needs to be passed along to the original instance. The following code can be called to connect to the listening IPC channel and retrieve the RemoteMessage object from the original instance. The SendMessage method is then used to pass the file name along to the original instance.

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

The remote messaging system is very powerful because through it we can make objects belonging to one instance of our program visible over local IPC channels to other instances.

Conclusion

This article explains at a high level some of the various methods and tricks used in the DocList Uploader sample to provide a friendly migration utility for Google Docs. There is still plenty of functionality that can be added in your own applications, and you are free to extend the sample to suit your own purposes.

Here are some useful resources for developers who are interested in working with the Documents List Data API, as well as those who want to use .NET with other Google Data APIs: