.NET 文档列表上传器示例

Jeff Fisher,Google Data API 团队
2008 年 1 月

简介:示例的范围

程序界面的屏幕截图

下载示例可执行文件

文档列表数据 API 的一个优点是,它允许开发者为仍在适应 Google 文档的用户制作迁移工具。为了演示此 API,我使用 .NET 客户端库创建了一个 .NET 2.0 上传工具应用,并将其命名为“DocList Uploader”。您可以从 Subversion 获取上传者的来源。

此示例旨在让用户轻松地将文档从计算机迁移到 Google 文档。用户可以登录自己的 Google 账号,然后拖放受支持的文件,这些文件随后会自动上传。该示例还提供了一个选项,用于向 Windows 资源管理器外壳添加右键点击菜单选项以上传文件。此示例根据 Apache 2.0 许可提供,因此您可以随意使用它作为自己程序的起点。

本文旨在展示如何使用 .NET 框架实现示例的某些行为。它主要包含相关部分的带注释的代码段。本文不会介绍如何构建应用本身的表单和其他界面组件,因为有很多 Visual Studio 文章对此进行了详细介绍。如果您想了解界面组件的配置方式,可以自行加载项目文件,方法是下载客户端库,然后查看“clients\cs\samples\DocListUploader”子目录中的内容。

开发系统任务栏应用

托盘应用示例

迁移工具通常能够在操作系统中悄无声息地运行,扩展操作系统能够执行的任务,而不会对用户造成太大的干扰。在 Windows 中,一种结构化此类工具的方式是让其在系统托盘中运行,而不是让其占用任务栏空间。这样一来,用户更有可能让该程序持续运行,而不是仅在需要执行特定任务时才打开该程序。对于此示例,这是一个特别有用的想法,因为它不需要将身份验证凭据存储到磁盘。

系统任务栏应用是指主要仅在系统任务栏(任务栏上靠近时钟的区域)中运行 NotifyIcon 的应用。设计此类应用时,请注意不要让用户与项目的主表单进行互动。请改为创建一个单独的表单,以便在应用运行时显示。稍后,您就会明白其中的原因。

在我的示例中,我创建了两个表单:HiddenForm(应用的主要表单,包含大部分逻辑)和 OptionsForm(允许用户自定义一些选项并登录其 Google 账号的表单)。我还向 HiddenForm 添加了一个名为 DocListNotifyIcon 的 NotifyIcon,并使用我自己的图标对其进行了自定义。为了确保用户看不到 HiddenForm,我将其不透明度设置为 0%,将其 WindowState 设置为 Minimized,并将其 ShowInTaskbar 属性设置为 False。

通常,当程序在系统托盘中运行时,关闭应用不应停止程序,而应隐藏所有活动窗体,仅使 NotifyIcon 可见。为此,我们必须按如下方式替换表单的“FormClosing”事件:

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

我们可能还希望在用户最小化表单时隐藏该表单,因为我们已经有通知图标,所以没有理由占用任务栏上的空间。您可以使用以下代码来实现此目的:

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

现在,由于我们不允许用户关闭 OptionsForm,因此只需保留一个与 HiddenForm 关联的实例即可。如果我们想再次显示 OptionsForm,只需调用其 Show 方法即可。

由于此应用的主要表单 HiddenForm 对用户不可见,因此我们必须为用户提供一种实际退出应用的方式。我选择向 NotifyIcon 添加一个 ContextMenu,其中包含一个用于关闭应用的 ToolStripMenuItem。编写点击处理程序很简单,只需调用 HiddenForm 的 Close 方法即可。

气球提示

许多系统托盘应用通过显示气球提示与用户进行通信,气球提示看起来像从 NotifyIcon 延伸出来的圆形气泡。气泡可以显示为:

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

第一个实参是显示气泡的时间量(以毫秒为单位)。请注意,操作系统允许此字段设置的最短和最长时间分别为 10 秒和 30 秒。第二个和第三个实参用于指定气泡的标题和一些内容。最后一个实参用于选择一个图标来表示气泡的用途。

上传文档

上传文档非常简单。大部分工作都是由 DocumentsService 对象的 UploadDocument 方法完成的。如需更清楚地了解此过程,请参阅 Documents List API 开发者指南

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

首先,必须初始化 DocumentsService 对象并提供用户的凭据。为了防止上传多个文件时出现一些问题,我们已停用“keep-alive”HTTP 标头,因为该标头已知会导致 .NET Framework 出现一些问题。

lastUploadEntry = service.UploadDocument(fileName, null);

此代码段会上传字符串 fileName 中包含的路径所对应的文件。第二个实参为 null 表示 Google 文档文件名与原始文件名相同。

处理拖放操作

为了方便用户上传文件,最好允许用户将文件夹中的文件拖放到应用中,以便上传这些文件。第一步是允许从文件进行放置操作,以下代码会将光标更改为指示允许放置:

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

放置文件或文件组后,我们必须通过遍历放置的每个文件并上传它们来处理该事件:

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

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

列出文档

从服务器获取文档列表是一种很好的方式,可以提醒用户他们已上传的内容。以下代码段使用我们之前初始化的 DocumentsService 对象从服务器检索所有文档。

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

直观呈现这些数据的一种便捷方式是使用 ListView。我已将名为 DocList 的 ListView 添加到 OptionsForm。为了让界面更美观,我还创建了一个自定义图标 ImageList 来展示各种文档类型。以下代码使用从上述 Feed 中检索到的信息填充 ListView:

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

imageKey 变量用于选择关联的 ImageList 中应为每一行使用哪个图片。此处的 Tag 属性用于存储原始条目,这对于稍后对文档执行操作非常有用。最后,AutoResize 方法用于自动设置 ListView 中的列宽。

在浏览器中打开文档

由于这些文档存储在 Google 文档中,因此最好让用户在浏览器中查看文档。Windows 中内置了相应的功能:

using System.Diagnostics;

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

在此示例中,我们从 Tag 属性中检索原始条目,然后使用所选文档的 AlternateUri 调用 Process.Start。其余部分由 .NET Framework 的神奇功能处理。

添加 Shell 上下文菜单

向 Shell 的上下文菜单添加项的最简单方法是修改注册表。我们需要做的是在 HKEY_CLASSES_ROOT 下创建一个指向我们应用的条目。请注意,当用户点击相应菜单项时,系统会打开应用的新实例,这是我们将在后续部分中处理的问题。

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

此代码会创建注册表键,以指向当前正在运行的应用所在的位置。“%1”表示法用于指示应在此参数内传递 shell 中所选的文件。KEY_NAME 是一个已定义的字符串常量,用于确定上下文菜单中条目的文本。

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

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

此方法只是移除我们添加的自定义键(如果存在)。

防止出现多个实例

由于我们的应用位于系统托盘中,因此我们不希望同时运行多个程序实例。我们可以使用 Mutex 来确保只有一个实例保持运行。

using System.Threading;

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

上述代码可以放置在应用的 Main 方法中,以便在程序已运行时提前退出。由于 Mutex 位于“本地”命名空间内,因此机器上的不同会话可以单独运行我们的应用。不过,由于我们要修改全局注册表,因此应格外小心。

进程间通信

当用户点击我们之前添加的文件的 shell 上下文菜单项时,系统会启动应用的新实例,并提供该文件在磁盘上的完整路径。现在,必须将此信息传递给已在运行的应用实例。这可以使用 .NET Framework 在版本 2.0 中引入的 IPC 机制来实现。

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

我们传递的消息采用自定义对象的形式。这里,我创建了一个包含对 HiddenForm 的反向引用的对象,该 HiddenForm 包含此应用的逻辑。由于此对象将托管在原始实例上,因此它为后续实例提供了一种与原始实例的主要表单进行通信的方式。

class RemoteMessage : MarshalByRefObject
{
    private HiddenForm mainForm;

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

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

当应用的第一个实例初始化时,以下代码会使其能够监听后续实例:

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

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

请注意,上述代码会注册一个已命名的 IPC 通道,并提供我们定义的 RemoteMessage 对象的副本,同时使用对自身的引用来初始化该副本。

对于程序的后续实例,通过 args 参数提供给 Main 的字符串需要传递给原始实例。可以调用以下代码来连接到监听 IPC 通道,并从原始实例检索 RemoteMessage 对象。然后使用 SendMessage 方法将文件名传递给原始实例。

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

远程消息传递系统非常强大,因为通过它,我们可以通过本地 IPC 渠道将属于程序的一个实例的对象显示给其他实例。

总结

本文从较高层面介绍了 DocList Uploader 示例中用于为 Google 文档提供友好迁移实用程序的一些方法和技巧。您仍然可以在自己的应用中添加许多功能,并且可以根据自己的用途随意扩展此示例。

以下是一些实用资源,可供有意使用 Documents List Data API 的开发者以及希望将 .NET 与其他 Google Data API 搭配使用的开发者参考: