.NET 文件清單上傳工具範例

Google Data API 團隊 Jeff Fisher
2008 年 1 月

簡介:樣本範圍

程式介面螢幕截圖

下載範例可執行檔

文件清單資料 API 的優點之一,是可讓開發人員為仍在使用 Google 文件的使用者製作遷移工具。為練習使用這個 API,我已使用 .NET 用戶端程式庫建立 .NET 2.0 上傳器應用程式,並適當命名為「DocList Uploader」。您可以從 Subversion 取得上傳者的來源

這個範例旨在協助使用者輕鬆將電腦中的文件遷移至 Google 文件。使用者登入 Google 帳戶後,只要將支援的檔案拖曳到這個頁面,系統就會自動上傳。這個範例也提供選項,可將上傳檔案的右鍵選單選項新增至 Windows 檔案總管 Shell。這個範例是依據 Apache 2.0 授權提供,因此您可以自由使用,做為自己程式的起點。

本文旨在說明如何使用 .NET 架構,達成範例的某些行為。其中大多是相關章節的註解程式碼片段。本文不會說明如何建構應用程式本身的表單和其他 UI 元件,因為有許多 Visual Studio 文章詳細介紹了這項內容。如要瞭解 UI 元件的設定方式,您可以下載用戶端程式庫,並查看「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 字串中路徑的檔案。第二個引數為空值,表示 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 即可輕鬆以視覺化方式呈現這項資料。我在 OptionsForm 中新增了名為 DocList 的 ListView。為了讓介面更美觀,我也建立了自訂的圖示 ImageList,用來說明各種文件類型。下列程式碼會使用從上述動態饋給擷取的資訊,填入 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 內容選單

如要將項目新增至殼層的內容選單,最簡單的方法是修改登錄檔。我們需要做的是在 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」標記法用於表示應將殼層中選取的檔案傳遞至這個參數內。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 位於「Local」命名空間內,因此機器上的不同工作階段可以分別執行應用程式。不過,由於我們要修改全域登錄檔,因此應格外小心。

程序間通訊

當使用者點選我們稍早新增的檔案殼層內容選單項目時,系統會啟動應用程式的新例項,並提供檔案在磁碟上的完整路徑。現在必須將這項資訊傳達給已執行的應用程式例項。您可以使用 2.0 版導入的 .NET Framework IPC 機制完成這項操作。

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

我們傳遞的訊息會採用自訂物件的形式。我在這裡建立的物件包含對 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,以下是一些實用資源: