Visual Studio OBA 工具
使用交互操作 API 擴展簡化 OBA 開發
Andrew Whitechapel、Phillip Hoff 和 Vladimir Morozov
本文將介紹以下內容:
-
VSTO Power Tools 和 Office 交互操作 API
-
使用 Outlook、Word 和 Excel 擴展
-
擴充擴展
-
在 Visual Basic 中使用擴展
|
本文使用以下技術:
Office、VSTO Power Tools 和 LINQ
|

目錄
使用 C# 針對 Microsoft Office 用戶端應用程式物件模型開發應用程式可能非常不方便,因為這些物件模型使用的功能(如可選參數和參數化的屬性)在 Visual Basic for Applications (VBA) 和 Visual Basic 內部處理,而不是在 C# 中處理。
此外,Office 大量使用鬆散的類型化參數並返回值,這種方式不適用於 Microsoft .NET Framework 之類的強類型化系統。
Microsoft Visual Studio Tools for the Office System (VSTO) Power Tools 包括一套 Office 交互操作 API 擴展庫,用於為 C# 開發人員提供對這些功能的支援。
使用 Visual Basic 的開發人員還會發現這些庫非常有用,尤其適用于以類型安全的方式查詢 Microsoft Office Outlook 專案。
使用這些擴展有助於提高代碼的可靠行和開發人員的工作效率。
為了向您介紹這些 Visual Studio Power Tools API 擴展,我們首先瞭解使用 Power Tools 自動執行 Office 應用程式的應用程式開發過程,然後看一看擴展採用的 .NET Framework 3.0 功能以及如何擴充擴展以廣泛用於自訂。
我們還將瞭解如何將擴展和您的 Visual Basic 代碼一起使用。
安裝擴展
VSTO Power Tools
在 MSDN 上有詳細介紹並可從
VSTO System Power Tools v1.0.0.0
下載頁獲得。
Office 交互操作 API 擴展全部位於名為 VSTO_PTExtLibs.exe 的下載檔案中。
當您執行此自解壓檔時,它會將擴展文檔安裝在 %PROGRAMFILES%\Microsoft VSTO Power Tools 1.0\Office Interop Extensions 下。
擴展程式集本身將安裝在全域組件快取 (GAC) 中。
這些擴展的初次發行版本本支援 Microsoft Office Excel、InfoPath、Project、Outlook、PowerPoint、Visio 以及 Office 2003 和 2007 Office system 中的 Word。
這些擴展採用一致的命名約定,其模式如下:Microsoft.Office.Interop.<application>.Extensions.dll。
例如,Excel 擴展在命名空間 Microsoft.Office.Interop.Excel.Extensions 中定義並生成到名為 Microsoft.Office.Interop.Excel.Extensions.dll 的程式集中。
請注意,擴展基本上都是對 Office 主交互操作程式集 (PIA) 的精簡包裝,因此,您需要安裝適當的 Office PIA,這些擴展才能正常工作。
預設情況下,PIA 隨 2007 Office system 一起安裝,但也可以隨 Office 2003 一起安裝。
此外,您還可以使用
PIA 可再發行元件
。
雖然這些擴展和 VSTO 是由同一團隊開發的,但您仍然可以在使用 Office 的任何解決方案中採用這些擴展,而不管您是否正在構建 VSTO 解決方案。
使用擴展
為了研究這些擴展,我們要構建調用三個 Office 應用程式(Outlook、Excel 和 Word)的功能的解決方案。
我們的應用程式不是 VSTO 解決方案,而是一個簡單的 Windows Presentation Foundation (WPF) 應用程式,用於自動依次對這些 Office 應用程式執行操作。
首先,它從 Outlook 中提取電子郵件資料並對該資料執行一些簡單的計算;然後,將結果傳入 Excel 生成圖表;最後,將圖表粘貼到 Word 文檔並保存此文檔。
將這三個任務分離,以便使用者可以通過主視窗中的按鈕單獨控制它們(請參見圖 1)。
這顯然是一個虛構的機制,但它卻具有重要的教學意義:分離每個操作以便單獨對其進行檢查。
並且,所有任務都可以使用原始 PIA 物件或擴展庫並以任意循序執行。
圖 1 示例應用程式主視窗
應用程式的最終輸出是 HTML 格式的 Word 文檔,其中包含根據 Outlook 資料生成的 Excel 圖表。
Outlook 資料集需要關注的是所選電子郵件專案集的大小以及發送和接收它們的日期和時間。
圖表根據發送專案和接收專案之間的時間段來確定每個專案的大小。
此文檔如圖 2 所示。
圖 2 應用程式生成的 HTML 文檔(按一下圖像可查看大圖)
執行此解決方案時,操作的順序是先使用最複雜的擴展,最後使用最簡單的擴展。
因此,在按正常順序排列所有步驟之前,我們先按相反的順序研究主要的步驟。
圖 3 總結解決方案要求和用於滿足每個要求的擴展功能。

圖 3 解決方案功能與擴展功能的映射
| 解決方案功能 |
Office 應用程式 |
解決的問題 |
擴展功能 |
|
從電子郵件收件箱中提取所選資料
|
Outlook
|
構建篩選查詢(不使用易出錯的 DASL 查詢字串)。
|
LINQ 到 DASL。
|
|
使用電子郵件資料創建圖表。
|
Excel
|
指定 Excel 儲存格區域(不使用參數化 get_Range 屬性)。
以高效的方式查找所有負儲存格值並將其設置為零。
|
通過參數化的屬性公開參數化的 properties.Collections。
LINQ 到物件。
|
|
將圖表粘貼到文檔並保存此文檔。
|
Word
|
使用強類型化參數調用 Office 功能。
調用 Office 功能但無需傳遞可選參數。
使用 Word 功能但無需由引用顯式傳遞參數。
|
強類型化方法重載。
強類型化可選參數。
可以為空的類型。
物件初始值。
|
Word 擴展
在我們的示例解決方案中,我們使用 Word 執行的操作非常簡單:插入某些文本並設置其樣式,然後移至文檔的結束位置並通過剪貼板粘貼到 Excel 圖表。
Office 物件模型的缺點之一是使用傳址參數,這在 Word 中特別常見。
例如,Word 中的 Document.Range 方法採用兩個參數:您希望提取的區域的起始位置和結束位置。
在 C# 中,這兩個參數必須作為顯式變數由引用傳遞。
下列代碼說明如何使用原始 PIA 方法來提取對應文檔起始位置的區域:
object startPosition = 0;
object endPosition = 0;
Word.Range r = (Word.Range)doc.Range(ref startPosition,
ref endPosition);
擴展提供的最簡單的功能之一是 Range 方法的重載,即採用無需由引用傳遞的兩個簡單參數。
通過使用擴展,提取文檔起始位置的調用就更簡單了。
在此示例中,擴展無需由引用傳遞參數,它們還強制執行傳遞強類型化參數的要求。
起始位置和結束位置始終為整數,但是 Word 物件模型僅指定它們是鬆散類型化物件。
擴展對方法簽名強制實施強類型化,從而帶來了設計時 IntelliSense 和編譯時類型檢查的好處。
同時,還將代碼從三行縮短為一行:
Word.Range r = doc.Range(0, 0);
Office 物件模型中的另一個不便之處是使用參數化的屬性。
在 Office 中,公開利用參數的屬性是很常見的。
請記住,COM 伺服器提供的所有屬性實際上都是方法。
方法當然可以採用多個參數。
在 C# 中,雖然方法可以採用參數,但屬性不能。
因此,Office PIA 將 C# 中的參數化屬性公開為一對 get_ 和 set_ 方法。
例如,設置樣式屬性的 PIA 代碼要求您傳遞一個物件參數(同樣由引用傳遞)。
下列原始 PIA 代碼是上述程式碼片段的延續,但我們在當前 Range 之後插入了某些文本並設置了其樣式:
r.InsertAfter("Email Send Time vs Size");
r.InsertParagraphAfter();
r = (Word.Range)doc.Paragraphs[1].Range;
object style = Word.WdBuiltinStyle.wdStyleTitle;
r.set_Style(ref style);
如果使用擴展執行相同的操作,代碼將更加直觀。
像以前一樣,代碼使用強類型化參數和一個簡單的方法調用,而無需使用臨時變數和 set_ 或 get_ 首碼:
r.InsertAfter("Email Send Time vs Size");
r.InsertParagraphAfter();
r = (Word.Range)doc.Paragraphs[1].Range;
r.Style(Word.WdBuiltinStyle.wdStyleTitle);
Office 物件模型中的許多方法都採用可選參數。
在 Visual Basic 中,您無需提供任何可選參數,因為 Visual Basic 運行時代表您為通用參數提供值,告知 Office 物件對缺失的參數使用預設值。
C# 並不為您執行此操作。
擴展還負責執行基礎操作並為 C# 開發人員提供類似于使用 Visual Basic 的體驗。
例如,若要移到文檔的結束位置並從剪貼板粘貼某些內容,使用原始 PIA 的代碼必須將缺失的參數顯式傳遞為 System.Type.Missing:
object gotoItem = Word.WdGoToItem.wdGoToLine;
object gotoDirection = Word.WdGoToDirection.wdGoToLast;
object missing = Type.Missing;
r = r.GoTo(ref gotoItem, ref gotoDirection,
ref missing, ref missing);
r.Paste();
另一方面,通過使用擴展,我們可以使用 C# 3.0 中引入的物件初始化功能為要指定的值提供強類型化參數並完全省略要使用預設值的任何參數。
為 C# 開發人員提供 Visual Basic 體驗的另一個功能是使用具名引數:
r = r.GoTo(
new DocumentGoToArgs {
What = Word.WdGoToItem.wdGoToLine,
Which = Word.WdGoToDirection.wdGoToLast
});
r.Paste();
通過使用類似于 Word 中的 Document.SaveAs 方法,可選參數功能在擴展中的作用變得更加明顯。
如果使用 PIA 代碼,您需要提供所有參數,共計 16 個。
在我們的示例中,我們要將此文檔另存為 HTML 檔,這需要我們指定檔案名和 Word 中 HTML 檔案格式的枚舉值,二者都是鬆散類型化物件。
雖然我們並不關心其餘 14 參數,但仍要傳遞它們,並且必須由引用傳遞所有參數:
object saveFormat = Word.WdSaveFormat.wdFormatHTML;
doc.SaveAs(ref fileName, ref saveFormat,
ref missing, ref missing, ref missing, ref missing,
ref missing, ref missing, ref missing, ref missing,
ref missing, ref missing, ref missing, ref missing,
ref missing, ref missing);
此擴展方法再次使用強類型化並允許我們完全忽略可選參數:
doc.SaveAs(fileName, Word.WdSaveFormat.wdFormatHTML);
Excel 擴展
在此示例解決方案中,我們使用 Excel 將圖表放入剪貼板,以便隨後可以將其粘貼到 Word 文檔。
首先,我們獲取工作薄中的第一個工作表。
在 Office 物件模型中,返回被類型化為通用物件的特定物件是很普遍的。
例如,Sheets 集合可能包含 Worksheet 物件和 Chart 物件。
因此,您指定了此集合中的特定項後,Excel PIA 就會將該項作為通用物件返回給您。
然後,您需要此物件轉換為您期望的特定 Sheet 類型:
Excel.Worksheet sheet = (Excel.Worksheet)book.Sheets[1];
擴展中往往需要從頭到尾使用強類型化,這也擴展到了從異類集合返回的物件類型,而無需強制開發人員對返回的物件進行推測性轉換。
除了簡化開發人員的代碼外,這種方式還將類型檢查從運行時移動到了編譯時,從而改善了應用程式的可靠性並降低了測試成本。
通過擴展,您可以使用整數或工作表的名稱將此集合編入索引:
//Excel.Worksheet sheet =
// book.Sheets.Item<Excel.Worksheet>("Sheet1");
Excel.Worksheet sheet =
book.Sheets.Item<Excel.Worksheet>(1);
在此示例中,我們要獲取特定的儲存格區域並插入一些資料。
我們有兩列資料:一列資料是發送和接收電子郵件消息之間的相隔時間(以毫秒為單位),另一列資料是電子郵件的大小(以位元組為單位)。
插入列標題後,再將電子郵件資料插入網格儲存格中。
使用 PIA 方法時,我們必須再次使用 Range 和 Offset 的參數化屬性。
請注意,emailData 變數是字串陣列,我們將使用 Outlook 中的電子郵件資料填充完整的解決方案:
sheet.get_Range("A1", missing).Value2 = "mSec";
sheet.get_Range("B1", missing).Value2 = "Bytes";
Excel.Range firstCell = sheet.get_Range("A2", missing);
Excel.Range lastCell;
for (int r = 0; r < this.emailData.Length; r++) {
String[] cellValues = this.emailData[r].Split(',');
for (int c = 0; c < cellValues.Length; c++) {
lastCell = (Excel.Range)firstCell.get_Offset(r, c);
lastCell.Value2 = cellValues[c];
}
}
使用擴展時,我們可以使用強類型化 Range 方法。
請注意,擴展庫包括 Office 物件模型的大部分而不是全部。
因此,在此示例中,我們可以使用 Range 屬性的擴展。
不過,除非我們擴充了這些擴展(稍後詳細討論),否則必須使用 PIA Offset 屬性:
sheet.Range("A1").Value2 = "mSec";
sheet.Range("B1").Value2 = "Bytes";
firstCell = sheet.Range("A2");
接下來,我們要查找工作表中的負值並將其設置為零。
可以使用 PIA 以簡單方法執行此操作:
Excel.Range chartData = sheet.get_Range("A2", lastCell);
foreach (Excel.Range cell in chartData) {
if (((double)cell.Value2) < 0) {
cell.Value2 = 0;
}
}
或者,使用擴展以類似于 SQL 的查詢執行此操作。
這將使用擴展庫中的 LINQ 到物件功能。
請注意,LINQ 針對性能進行了優化並使用延遲執行模型。
在此模型中,直到您開始枚舉結果集,查詢才真正開始執行。
在某些情況下,這可以顯著改善應用程式的性能。
也就是說,通過調用 ToList 或 ToArray 強制執行查詢,然後對結果執行操作,有時效率更高(例如,希望多次反覆運算結果集時)。
在此示例中,搜索條件非常簡單(儲存格值小於零),但您會看到,類似于 SQL 的查詢在搜索條件更複雜時將變得更加有用:
Excel.Range chartData = sheet.Range("A2", lastCell);
var cells = (from c in chartData.Items()
where ((double)c.Value2) < 0
select c).ToArray();
foreach (Excel.Range cell in cells) {
cell.Value2 = 0;
}
接下來,我們從 mSec 和 Bytes 列中創建簡單的線圖表,然後將其複製到剪貼板。
使用 PIA 方法時,我們必須使用鬆散的類型化、顯式可選參數和參數化屬性:
Excel.Shape chart = sheet.Shapes.AddChart(
Excel.XlChartType.xlLine,
missing, missing, missing, missing);
chart.Select(missing);
excel.ActiveChart.SetSourceData(
sheet.get_Range("A1", lastCell), missing);
excel.ActiveChart.ChartArea.Copy();
擴展僅在此處通過參數化的 Range 屬性為我們提供了現成的説明,它們並不為 Shapes.AddChart 或 Chart.SetSourceData 提供重載。
下麵是我們可能希望擴充擴展的又一種情況。
Outlook 擴展
使用 DAV 搜索和查找 (DASL) 查詢語言,您可以搜索、篩選和查詢 Outlook 專案。
此語言類似于 SQL 並且功能相當強大。
然而,它有一個嚴重的缺點:您需要編寫字串並將其作為篩選器傳遞給 Outlook Restrict 或 Find 方法。
由於此查詢是一個字串,所以可能沒有設計時和編譯時支援。
這意味著,除非您在運行時執行查詢,否則您無法確定此字串是否真實表示此查詢。
您甚至無法確定此字串是否為正確編寫的查詢。
DASL 查詢使用不直觀的屬性 URN,通常包括隱藏的 hex 值,而且說明不夠詳盡,很容易出現輸入錯誤。
如果您的查詢使用字串模式、日期或時間條件,您必須謹慎使用萬用字元和格式規則,因為它們更容易出現輸入錯誤。
顯然,這些查詢字串可能會迅速變得非常複雜,難於構建和分析,從而會降低工作效率、增加測試成本並使代碼維護變得困難。
在我們的示例中,我們設置了 DASL 篩選器(如圖 4 所示),以便查找作為電子郵件消息的所有收件箱專案(也就是說,它們的 MessageClass 基於 IPM.Note),其中的主題要以“RE:”開頭,並且是在過去的 30 天內收到的。
如果 Windows 桌面搜索 (WDS) 可用,我們可以用它來提高搜索速度。
預設情況下,WDS 安裝在 Windows Vista 上,但也可以選擇安裝在 Windows XP 和 Windows Server 2003 上。
請注意,DASL 查詢總是按協調世界時(正式縮寫為 UTC)執行日期-時間比較,您必須在比較時使用它的 UTC 值。

圖 4 用於查找電子郵件消息的 DASL 篩選器
string filter;
if (outlook.Session.DefaultStore.IsInstantSearchEnabled) {
filter =
@"@SQL=(((""http://schemas.microsoft.com/mapi/proptag/0x001a001e"""
+ "CI_STARTSWITH 'IPM.Note')"
+ @" AND (""urn:schemas:httpmail:subject"" CI_STARTSWITH 'RE:'))"
+ @" AND (""urn:schemas:httpmail:datereceived"" >= '"
+ (DateTime.Now.ToUniversalTime()
- new TimeSpan(30, 0, 0, 0)).ToString("g") + @"'))";
}
else {
filter =
@"@SQL=(((""http://schemas.microsoft.com/mapi/proptag/0x001a001e"""
+ "LIKE 'IPM.Note%')"
+ @" AND (""urn:schemas:httpmail:subject"" LIKE 'RE:%'))"
+ @" AND (""urn:schemas:httpmail:datereceived"" >= '"
+ (DateTime.Now.ToUniversalTime()
- new TimeSpan(30, 0, 0, 0)).ToString("g") + @"'))";
}
Outlook.Items folderItems = folder.Items;
Outlook.Items filteredItems = folderItems.Restrict(filter);
StringBuilder builder = new StringBuilder();
foreach (Outlook.MailItem item in filteredItems) {
builder.AppendLine(String.Format("{0},{1},{2}",
item.SentOn, item.ReceivedTime, item.Size));
}
CalculateElapsedTime(builder.ToString());
檢索所有需要的專案後,我們將為每個專案構建一個字串,其中包含發送電子郵件花費的時間和電子郵件的大小(使用自訂方法 CalculateElapsedTime,只需執行字串和 DateTime 操作而不使用 Office 物件模型。
此方法未在此處列出,但是您可以從隨附的原始程式碼下載中找到它)。
使用擴展時,我們可以摒棄字串查詢而改用 LINQ 查詢來執行相同的搜索。
與 DASL 字串查詢相比,LINQ 查詢可以帶來設計時 IntelliSense、編譯時類型和語法檢查的好處。
請注意一個限制:擴展並不公開 Outlook 專案的 Size 屬性,因此,我們可以改用 Body 屬性的 Length(這是需要擴充擴展的另一種情況):
var filteredItems = (
from item in folder.Items.AsQueryable<Mail>()
where item.MessageClass.StartsWith("IPM.Note")
&& item.Subject.StartsWith("RE:")
&& item.DateReceived >=
DateTime.Now.ToUniversalTime() - new TimeSpan(30, 0, 0, 0)
select item).ToList();
StringBuilder builder = new StringBuilder();
foreach (Mail item in filteredItems) {
builder.AppendLine(String.Format("{0},{1},{2}",
item.Date, item.DateReceived, item.Body.Length));
}
CalculateElapsedTime(builder.ToString());
擴展內幕
現在,我們已經瞭解到擴展能夠使開發人員的編碼體驗更輕鬆、代碼品質更高,現在就讓我們瞭解一下這種簡易性和可靠性是如何實現的吧。
首先考慮 Word 中的 Document.SaveAs 方法。
此方法採用 16 個可選參數,但大多數情況下,您只需指定其中的一小部分參數(在某些情況下不需要指定任何參數)。
要編寫擴展方法,只需編寫靜態方法,該方法將要擴展的物件作為第一個(或唯一)參數。
所以,要為 Document 物件提供強類型化的 SaveAs 方法,您可以編寫方法 SaveAs,將 Document 物件作為其第一個參數。
您也可以為擴展方法提供多個重載,這樣每個重載可用於簡化開發人員的編碼體驗,但是真正的功能最終在基本 PIA Document 物件本身上調用。
這就是擴展方法的實現原理,如圖 5 中的 SaveAs 方法的代碼清單所示。
在 Visual Studio 中進行設計時,開發人員看到的擴展方法是擴展物件的方法,這些擴展方法可通過 IntelliSense 獲得,是自動完成的。
IntelliSense 將使用“(extension)”標記擴展方法,如圖 6 所示。

圖 5 重載的 SaveAs 方法
public static void SaveAs(this Document doc) {
SaveAs(doc, (string) null);
}
public static void SaveAs(this Document doc, string fileName) {
SaveAs(doc, fileName, null);
}
public static void SaveAs(this Document doc, string fileName,
WdSaveFormat? fileFormat) {
SaveAs(doc, new DocumentSaveAsArgs {
FileName = fileName, FileFormat = fileFormat });
}
public static void SaveAs(this Document doc, DocumentSaveAsArgs args) {
doc.SaveAs(
ref args.FileNameInternal,
ref args.FileFormatInternal,
ref args.LockCommentsInternal,
ref args.PasswordInternal,
ref args.AddToRecentFilesInternal,
ref args.WritePasswordInternal,
ref args.ReadOnlyRecommendedInternal,
ref args.EmbedTrueTypeFontsInternal,
ref args.SaveNativePictureFormatInternal,
ref args.SaveFormsDataInternal,
ref args.SaveAsAOCELetterInternal,
ref args.EncodingInternal,
ref args.InsertLineBreaksInternal,
ref args.AllowSubstitutionsInternal,
ref args.LineEndingInternal,
ref args.AddBiDiMarksInternal);
}
圖 6 在 Visual Studio 中用於擴展方法的 IntelliSense(按一下圖像可查看大圖)
您也可以利用 C# 中類型可以為空的功能,即強類型化的參數可以作為空值傳遞(請注意,這不同于使用 System.Type.Missing 值傳遞鬆散類型物件)。
在圖 5 中顯示的第三個重載 SaveAs 方法中,WdSaveFormat 參數指定為可以為空(使用 ?語法聲明),這就允許我們調用此方法並為此參數傳遞空值。
保存文檔的工作最終是在使用 DocumentSaveAsArgs 物件的重載中完成的。
正如我們所見的,這種方式使用物件初始值設定項,允許開發人員按照名稱為所需參數提供值,並且可以忽略具有預設值的任何參數。
DocumentSaveAsArgs 類從 SaveAsArgs 擴展類派生而來。
參數值本身存儲在類的內部欄位(如 FileNameInternal)中,並且這些欄位直接由擴展方法使用。
開發人員使用 FileName 等公共屬性,而不是這些內部欄位。
之所以這樣設計,是因為 Word 中大多數方法中的要求反對模型由引用傳遞參數。
其他 Office 應用程式的擴展庫中的參數類不使用此附加層:
public abstract class SaveAsArgs : ExtensionArgs {
internal object FileNameInternal = Type.Missing;
public string FileName {
get {
return ToReference<string>(FileNameInternal);
}
set {
FileNameInternal = ToObject(value);
}
}
// code omitted for brevity.
}
而 SaveAsArgs 派生自 ExtensionArgs,包括在真實類型間轉換的代碼和這些類型的裝箱的等效項,還包括處理 System.Type.Missing 的邏輯:
protected T ToReference<T>(object obj) where T : class {
return (obj != null && obj != Type.Missing) ? (T) obj : null;
}
參數化屬性也由擴展庫中的重載的擴展方法處理 — 像以前一樣,這些屬性使用基本 PIA 物件實現最終功能。
例如,Excel.Range 擴展提供了三種重載,它們在內部都使用 get_Range 屬性方法:
public static Range Range(this Worksheet worksheet, string name) {
return worksheet.get_Range(name, Type.Missing);
}
public static Range Range(this Worksheet worksheet,
string name1, string name2) {
return worksheet.get_Range(name1, name2);
}
public static Range Range(this Worksheet worksheet, string range1, Range range2) {
return worksheet.get_Range(range1, range2);
}
為支援 Outlook 中的 LINQ 到 DASL 查詢,擴展庫提供了一個抽象基類,以提供查詢資料來源:OutlookItemSource。
該類實現了 IQueryable<t> 和 LINQ 使用的 IQueryProvider 介面。
擴展還提供了派生的類 ItemsSource 和 ApplicationSource,分別用於針對單個資料夾中的 Items 集合和跨多個資料夾的查詢:
public abstract class OutlookItemSource<T> : IQueryProvider, IQueryable<T> {
public IQueryable<TElement> CreateQuery<TElement> (Expression expression) {
MethodCallExpression call = expression as MethodCallExpression;
switch (call.Method.Name) {
case "Where": return new
OutlookItemSourceWhereHandler<TElement>(
this, expression, _builder, _search);
case "Select": return new
OutlookItemSourceSelectHandler<TElement>(
this, expression);
}
}
// code omitted for brevity.
}
OutlookItemSource 類使用其他內部擴展類根據 LINQ 查詢構建 DASL 查詢字串。
例如,ParseMethodCallExpression 方法將 LINQ 運算式(如 Contains)轉換為 DASL 查詢子字串,如 (LIKE '%'):
private static void ParseMethodCallExpression(MethodCallExpression call) {
if (call.Method.DeclaringType == typeof(string)) {
string format = null;
switch (call.Method.Name) {
case "Contains": format = @"({0} LIKE '%{1}%')"; break;
case "StartsWith": format = @"({0} LIKE '{1}%')"; break;
case "EndsWith": format = @"({0} LIKE '%{1}')"; break;
}
// code omitted for brevity.
}
}
OutlookItemProperty 屬性提供了 DASL 屬性和 Outlook 專案介面上屬性之間的映射。
該屬性在從 OutlookItem 類公開的公共屬性上使用,這樣,擴展就允許開發人員避免使用晦澀的 URN/hex 標記字串指定目標屬性:
public class OutlookItem : IOutlookItemWrapper {
[OutlookItemProperty(
"http://schemas.microsoft.com/mapi/proptag/0x001a001e")]
public virtual string MessageClass
{ get { return GetProperty("MessageClass") as string; } }
// code omitted for brevity.
}
擴充擴展
瞭解擴展在內部是如何實現的,即可輕鬆對它們進行擴充。
Office 物件模型中的一些物件和方法不在擴展範圍內,在某些情況下,您可能甚至要提供使用者定義的擴展。
在提供的示例應用程式中,最明顯的事例為 Excel 方法 Range.get_Offset、Shapes.AddChart、Shape.Select 和 Chart.SetSourceData。
對於這些事例,提供擴展方法很簡單。
再次重申,它們只需要是靜態方法,將要擴展的類型的某個物件作為方法的第一個參數,如圖 7 中所示。

圖 7 擴展 Excel 方法
public static class RangeExtensions {
public static Excel.Range Offset(
this Excel.Range cells, int rowOffset, int columnOffset) {
return cells.get_Offset(rowOffset, columnOffset) as Excel.Range;
}
}
public static class ShapesExtensions {
public static Excel.Shape AddChart(
this Excel.Shapes shapes, Excel.XlChartType chartType) {
return shapes.AddChart(chartType,
Type.Missing, Type.Missing, Type.Missing, Type.Missing);
}
}
public static class ShapeExtensions {
public static void Select(this Excel.Shape shape) {
shape.Select(Type.Missing);
}
}
public static class ChartExtensions {
public static void SetSourceData(this Excel.Chart chart,
Excel.Range range) {
chart.SetSourceData(range, Type.Missing);
}
}
注意,以 Shapes.AddChart 方法為例,該擴展方法之所以可行,就在於與 PIA AddChart 方法(或者任一擴展重載)具有不同數目的參數。
如果您嘗試提供的擴展方法具有相同的參數,但已直接強類型化,那麼編譯器將不會調用它,因為它支援本機方法,不管參數類型的作用如何。
我們可以使用 Word 處理這種情況,因為它使用 by-reference 參數;通過提供 Word 方法的 by-value 擴展,編譯器始終能夠區分二者。
Excel 擴展庫實際上包括許多方法,而編譯器通過這種方式無法獲得這些方法。
為編譯器提供它不使用的方法看起來有些奇怪,但這些方法仍要在 IntelliSense 中顯示,以説明使用者識別參數的真實類型。
此外,如果需要,開發人員可以通過普通靜態方法語法使用這些方法。
使用這些自訂擴展,我們可以簡化應用程式碼:
//lastCell = (Excel.Range)firstCell.get_Offset(r, c);
lastCell = (Excel.Range)firstCell.Offset(r, c);
//chart = sheet.Shapes.AddChart(
// Excel.XlChartType.xlLine, missing, missing, missing, missing);
chart = sheet.Shapes.AddChart(Excel.XlChartType.xlLine);
//chart.Select(missing);
chart.Select();
//excel.ActiveChart.SetSourceData(sheet.Range("A1", lastCell), missing);
excel.ActiveChart.SetSourceData(sheet.Range("A1", lastCell));
除直接允許開發人員提供其他擴展方法外,擴展庫本身支援一定程度的可擴展性。
例如,LINQ 到 DASL 功能支援可插入日誌記錄(請參見圖 8)。
ItemsSource 類提供了 Log 屬性,您可以將該屬性設置為從 System.IO.TextWriter 派生的任一物件。
在此示例中,我們可以編寫輸出到調試視窗的簡單類。
當擴展代碼根據 LINQ 查詢完成構建 DASL 查詢字串後,該類將在內部使用,並且該字串將輸出到調試視窗。

圖 8 使用 LINQ 到 DASL 的日誌記錄
// Provide a logging class for our LINQ-to-DASL queries.
internal class DebuggerWriter : TextWriter {
public override Encoding Encoding {
get { throw new NotImplementedException(); }
}
public override void WriteLine(string value) {
Debug.WriteLine(value);
}
}
// Subclass the extensions Mail class so that we can expose a Size
// property that maps to the real Outlook size property.
internal class MailEx : Mail {
[OutlookItemProperty("http://schemas.microsoft.com/mapi/proptag/0x0E080003")]
public int Size { get { return Item.Size; } }
}
// Create the ItemsSource manually (this is
// what Items.AsQueryable() does implicitly).
var source = new ItemsSource<MailEx>(folder.Items);
// Set the Log property of the ItemsSource to a TextWriter.
// It will be given the DASL query string immediately before
// the query is executed in Outlook.
source.Log = new DebuggerWriter();
//var filteredItems = (from item in folder.Items.AsQueryable<Mail>()
var filteredItems = (from item in source
where item.MessageClass.StartsWith("IPM.Note")
&& item.Subject.StartsWith("RE:")
&& item.DateReceived >=
DateTime.Now.ToUniversalTime() - new TimeSpan(30, 0, 0, 0)
select item).ToList();
//foreach (Mail item in filteredItems)
foreach (MailEx item in filteredItems) {
builder.AppendLine(String.Format("{0},{1},{2}",
//item.Date, item.DateReceived, item.Body.Length));
item.Date, item.DateReceived, item.Size));
}
同時,我們可以為擴展 Mail 類劃分子類,以公開基本 Outlook Size 屬性的公共屬性,並使用 OutlookItemProperty 屬性建立映射。
再次重申,以前我們必須使用 Body.Length 作為一種解決方法,因為預設情況下 Size 不由擴展公開。
具有了這些額外的擴展增強,我們的示例應用程式才算完整。
我們使用 LINQ 到 DASL 提取篩選出的 Outlook 郵件項資料,將該資料傳遞給 Excel 以製作圖表,然後將圖表複製到 Word 文檔,最後將其另存為 HTML 檔。
注意,Outlook 2007 引入了 Table 物件,該物件是用於枚舉資料夾項的性能優化的機制。
它允許您指定屬性為表列,篩選並排序專案以填充表行。
在 Outlook 2007 及更高版本中,您應該首先選擇 Table 機制,而不是使用專案集合的傳統方法。
要獲得表,您使用與 Restrict 或 Find 方法配合使用的相同的篩選器字串,但此篩選器字串與 GetTable 方法配合使用。
在圖 9 中顯示的代碼中,可以清楚地看到,將現有版本的 LINQ 到 DASL 擴展與 Outlook Table 配合使用沒有優勢,因為您仍需要使用查詢字串才可以獲得 Table,並且 Table 篩選通過指定 Table 列才可完成。
擴展庫中的 LINQ 到 DASL 方法可用於 Outlook 2003 或 Outlook 2007,但是如果您僅在 Outlook 2007 中使用,Table 方法可能會提供更高的性能,尤其是對於大型資料集。
圖 9 顯示了此方法的示例代碼。

圖 9 在 Outlook 2007 中使用 Table
//Outlook.Items folderItems = folder.Items;
//Outlook.Items filteredItems = folderItems.Restrict(filter);
//foreach (Outlook.MailItem item in filteredItems) {
// builder.AppendLine(String.Format("{0},{1},{2}",
// item.SentOn, item.ReceivedTime, item.Size));
//}
// Get a table of mail items, remove the default column set,
// and add the specific columns we're interested in instead.
table = folder.GetTable(filter, Outlook.OlTableContents.olUserItems);
table.Columns.RemoveAll();
table.Columns.Add("SentOn");
table.Columns.Add("ReceivedTime");
table.Columns.Add("Size");
// Iterate the table rows.
while (!table.EndOfTable) {
Outlook.Row nextRow = table.GetNextRow();
builder.AppendLine(String.Format("{0},{1},{2}",
nextRow["SentOn"], nextRow["ReceivedTime"], nextRow["Size"]));
}
在 Visual Basic 中使用擴展
擴展的主要優點是為 C# 開發人員提供類似于 Visual Basic 的編碼體驗。
也就是說,其中的一些擴展(尤其是 LINQ 到 DASL 擴展)也會為使用 Visual Basic(實際上包括任何其他託管語言)的開發人員帶來好處。
例如,在任何語言中使用強類型的 LINQ 查詢而不使用基於字串的 DASL 查詢都是有好處的。
圖 10 顯示了 Visual Basic 版本的 Outlook 查詢操作。

圖 10 Visual Basic 中的 Outlook 查詢
Dim source As New ItemsSource(Of MailEx)(folder.Items)
Dim builder As New StringBuilder()
Dim filteredItems = ( _
From item In source _
Where item.MessageClass.StartsWith("IPM.Note") _
AndAlso item.Subject.StartsWith("RE:") _
AndAlso item.DateReceived >= _
(DateTime.Now.ToUniversalTime() - New TimeSpan(30, 0, 0, 0)) _
Select item).ToList()
For Each filteredItem In filteredItems
builder.AppendLine(String.Format("{0},{1},{2}", _
filteredItem.Date, filteredItem.DateReceived, filteredItem.Size))
Next
MessageBox.Show(builder.ToString())
請注意 C# 和 Visual Basic 生成表達樹的方式的一個不同點。
C# 使用基類型 OutlookItem,在我們的示例中,這一類型定義了 MessageClass 的映射屬性。
而 Visual Basic 使用通常派生的類型 Mail,這一類型定義對 MessageClass 的覆蓋但並不被賦予屬性映射。
因此,我們必須在賦予了屬性映射的 MailEx 類中定義 MessageClass 的另一覆蓋(請參見圖 11),這可能是在未來版本得以體現的一項改進。

圖 11 適用于 Visual Basic 的 MailEx
Friend Class MailEx
Inherits Mail
<OutlookItemProperty( "http://schemas.microsoft.com/mapi/proptag/0x0E080003")> _
Public ReadOnly Property Size() As Integer
Get
Return MyBase.Item.Size
End Get
End Property
<OutlookItemProperty( "http://schemas.microsoft.com/mapi/proptag/0x001a001e")> _
Public Overrides ReadOnly Property MessageClass() As String
Get
Return MyBase.Item.MessageClass
End Get
End Property
End Class
總結
在本文,我們介紹了 C# 開發人員如何使用 Office 交互操作 API 擴展簡化 Office 開發。
這些擴展針對鬆散的類型化 Office 物件模型提供了一個簡易、強類型化的層,可以使您的代碼不容易出錯並更加可靠,還可以顯著降低測試和維護的成本。
我們還介紹了如何為自訂方案擴充擴展以及如何在 Visual Basic 中使用這些擴展。
Andrew Whitechapel
是 Visual Studio 業務應用程式團隊的專案經理,負責在 Visual Studio 中構建新的 Office 開發功能。
Phillip Hoff 是 Visual Studio 業務應用程式團隊的軟體發展工程師,還是 Office 交互操作 API 擴展庫的原創者之一。
目前,他正在研究 SharePoint 工具和 Visual Studio 的集成。
Vladimir Morozov 是 Visual Studio 業務應用程式團隊的開發人員。
他曾與 Phillip Hoff 一起預想和創建了 Office 交互操作 API 擴展庫。
目前,他正在研究適用于 Visual Studio 的 SharePoint 工具。