(翻譯)LearnVSXNow!-#3 創建一個帶有簡單命令的Package

  • 2019 年 10 月 5 日
  • 筆記

為了演示如何給我們的package增加功能,本篇將創建一個帶有簡單菜單(命令)的VS Package。和上一篇一樣,我們新建一個Visual Studio Integration Package類型的項目,這一次我們把它命名為SimpleCommand。當項目嚮導出現後,我們選擇C#做為開發語言,並利用嚮導為我們的程式集自動生成一個key文件。在VSPackage Information頁面,我們輸入如下內容:

在下一步,為了創建一個簡單的菜單命令,我們選中Menu Command:

當轉到下一步的時候,嚮導會要求我們填寫菜單的顯示文本和菜單的標識,請參考下圖填寫:

在嚮導的最後一步我們可以建立集成測試項目和單元測試項目,請勾掉這兩個選項並且點擊Finish按鈕。嚮導會在幾秒鐘內幫我們創建項目的源文件。

編譯並運行SimpleCommand項目。當Visual Studio實驗室運行後,你可以在工具菜單下發現我們的package的菜單命令:

點擊菜單My First Command,可以看到一個消息框。證明這個package已經正常運行了。

源文件分析(What is inside?)

關掉VS實驗室,讓我們查看一下源文件。我們可以發現,與上一篇的EmptyPackage相比,這一次多了兩個文件:PkgCmdID.csSimpleCommand.vsct。在文件PkgCmdID.cs中定義了菜單「My First Command」的標識符:

1: //在英文原文中,命名空間是MyCompany.SimpleToolWindow   2: namespace MyCompany.SimpleCommand   3: {   4:   static class PkgCmdIDList   5:   {   6:     public const uint cmdidMyFirstCommand = 0x100;   7:   };   8: }

標識符的名字是我們在項目嚮導那裡填入的,標識符的值0x100是由嚮導自動生成的。

第二個文件SimpleCommand.vsct似乎更「令人興奮」,因為它包含XML內容,這更符合我們進行介面定義的習慣。出於可讀性的考慮,我去掉了源文件中的注釋,該文件內容如下:

1: <?xml version="1.0" encoding="utf-8"?>   2: <CommandTable xmlns="http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable" xmlns:xs="http://www.w3.org/2001/XMLSchema">   3:   <Extern href="stdidcmd.h" mce_href="stdidcmd.h"/>   4:   <Extern href="vsshlids.h" mce_href="vsshlids.h"/>   5:   <Extern href="msobtnid.h" mce_href="msobtnid.h"/>     6:     7:   <Commands package="guidSimpleCommandPkg">   8:     <Groups>   9:       <Group guid="guidSimpleCommandCmdSet" id="MyMenuGroup" priority="0x0600">  10:         <Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS"/>  11:       </Group>  12:     </Groups>    13:     <Buttons>  14:       <Button guid="guidSimpleCommandCmdSet" id="cmdidMyFirstCommand"  15:         priority="0x0100" type="Button">  16:         <Parent guid="guidSimpleCommandCmdSet" id="MyMenuGroup" />  17:         <Icon guid="guidImages" id="bmpPic1" />  18:         <Strings>  19:           <CommandName>cmdidMyFirstCommand</CommandName>  20:           <ButtonText>My First Command</ButtonText>  21:         </Strings>  22:       </Button>  23:     </Buttons>  24:    25:     <Bitmaps>  26:       <Bitmap guid="guidImages" href="ResourcesImages_32bit.bmp" mce_href="ResourcesImages_32bit.bmp" usedList="bmpPic1, bmpPic2, bmpPicSearch, bmpPicX, bmpPicArrows"/>  27:     </Bitmaps>  28:   </Commands>  29:     30:   <Symbols>  31:     <GuidSymbol name="guidSimpleCommandPkg" value="{2291da24-92e5-4ea4-bdb7-72a9b5ac7d59}" />  32:     <GuidSymbol name="guidSimpleCommandCmdSet" value="{a982b107-4ad4-437e-b2bc-cdf2708aa376}">  33:       <IDSymbol name="MyMenuGroup" value="0x1020" />  34:       <IDSymbol name="cmdidMyFirstCommand" value="0x0100" />  35:     </GuidSymbol>  36:     <GuidSymbol name="guidImages" value="{5c3faf04-8190-48c4-a6e9-71f04f1848e5}" >  37:       <IDSymbol name="bmpPic1" value="1" />  38:       <IDSymbol name="bmpPic2" value="2" />  39:       <IDSymbol name="bmpPicSearch" value="3" />  40:       <IDSymbol name="bmpPicX" value="4" />  41:       <IDSymbol name="bmpPicArrows" value="5" />  42:     </GuidSymbol>  43:   </Symbols>  44: </CommandTable>

.vsct文件是Visual Studio 2008 SDK里的一種新的XML格式,vsct代表Visual Studio的命令表(Command Table),Visual Studio利用vsct文件的定義為我們的package的命令創建用戶介面。如果你查看這個文件的屬性的話,你會發現該文件的Build Action是VSCTCompile。在package編譯過程中,vsct文件會被編譯成二進位的資源,並以1000作為資源ID添加到VSPackage.resx資源文件中。當regpkg.exe去註冊我們的package的時候,vsct文件代表的資源也會註冊到Visual Studio中。而當VS實驗室啟動的時候,VS只需要去讀取已註冊的資源以便更新VS的介面(例如顯示菜單或工具欄項),而不需要載入我們的package。

我認為現在還不是深入研究vsct文件的時候,但是我想告訴你一些重要的東西:通過vsct文件,我們可以學習到VS是怎樣架構的,以及它是怎樣組織這麼多的功能和用戶介面的。 - 命令(動作)和觸發命令的用戶介面是分開的。同一個命令可以被不同的菜單或工具欄調用。 - 多個命令可以分組,利用分組,可以簡單的合併到已存在的菜單中。 — 元素是可標識的符號,而不是常量。這樣就不容易出錯:標識符的名字是唯一的,VSCT編譯器會檢測輸入錯誤。

它是如何工作的?

現在讓我們看看我們的菜單項「My First Command」是怎樣顯示在Visual Studio中的。我們必須先弄清楚最重要的問題:當我們點擊我們的命令對應的菜單項時,Visual Studio是怎樣調用相應的動作的?答案就在SimpleCommandPackage.cs文件中,所以讓我們來看一下這個文件。

上一篇中,我們看到EmptyPackage類有很多Attribute的標記,regpkg.exe用這些Attribute來註冊我們的package。在這一篇里,SimpleCommandPackage類比EmptyPackage多了一個ProvideMenuResourceAttribute:

1: ...   2: [ProvideMenuResource(1000, 1)]    3: ...   4: public sealed class SimpleCommandPackage : Package   5: {   6:   ...   7: }

ProvideMenuResourceAttribute構造函數有兩個參數,第一個參數是resourceID,這個參數值必須是1000,因為VSCT編譯器在把vsct文件編譯到VSPackage資源中的時候,默認用1000作為vsct文件對應的資源ID。第二個參數是versionID,它的取值在資源的快取機制中非常重要。現在先不要深入研究它的細節了,只需要記住ProvideMenuResourceAttribute允許我們為package註冊菜單資源。

為了執行一個命令,我們需要做下面的兩步: — 我們需要寫一個Command Handler,裡面放置命令執行時的邏輯。 — 我們必須以某種方式告訴Visual Studio來調用我們的Command Handler。

在這個例子中,我們的Command Handler將顯示一個消息框。Command Handler本身是一個簡單的私有方法,包含眾所周知的EventHandler的參數。但是,在這個方法體內,卻不是僅僅調用Messagebox.Show這麼簡單:

1: private void MenuItemCallback(object sender, EventArgs e)   2: {   3:   IVsUIShell uiShell = (IVsUIShell)GetService(typeof(SVsUIShell));   4:      5:   Guid clsid = Guid.Empty;   6:   int result;   7:     8:   Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(   9:     uiShell.ShowMessageBox(  10:       0,  11:       ref clsid,  12:       "SimpleCommand",  13:       string.Format(CultureInfo.CurrentCulture, "Inside {0}.MenuItemCallback()",   14:         this.ToString()),  15:       string.Empty,  16:       0,  17:       OLEMSGBUTTON.OLEMSGBUTTON_OK,  18:       OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST,  19:       OLEMSGICON.OLEMSGICON_INFO,  20:       0,        // false  21:       out result)  22:     );  23: }

為了使用Visual Studio提供給Add-in和Package的功能,我們必須使用一些service。在我們這個例子中,我們用到了IVsUIShell這個service,它提供了若干方法去實現與介面有關的功能。為了得到這個service的實例,我們必須調用Package基類的GetService方法,並把SVsUIShell類型作為參數傳遞給它。

通過這個service的實例,我們可以調用它的ShowMessageBox方法去彈出一個Visual Studio消息框。這一次我不會解釋ShowMessageBox方法的參數,你只需要知道它會彈出一個帶有「確定」按鈕的消息框就行了。(譯者註:我們也可以直接用System.Windows.Forms.MessageBox.Show方法來彈出一個消息框,讀者可以自己試一下)

Visual Studio的底層用的是COM技術。所有實現的service都可以被COM的interop類訪問。如你所知,COM沒有提供一個運行時的異常處理機制,它僅僅通過函數的返回值去標識異常。當調用一個COM對象的方法時,我們必須手動的判斷返回值,並根據情況拋出異常。Microsoft.VisualStudio.ErrorHandler類的ThrowOnFailure方法幫助我們做了這項工作。

所以,MenuItemCallback方法就是這樣執行我們的命令的。不過,我們還得通過某種方式告訴Visual Studio這個方法的存在,這樣當用戶點擊「My First Command」菜單的時候它才會執行我們的命令。秘訣就在Initialize方法中,當Visual Studio載入我們的Package的時候,會調用Package的Initialize方法:

1: protected override void Initialize()   2: {   3:   Trace.WriteLine (string.Format(CultureInfo.CurrentCulture,    4:     "Entering Initialize() of: {0}", this.ToString()));   5:   base.Initialize();   6:      7:   OleMenuCommandService mcs =    8:     GetService(typeof(IMenuCommandService)) as OleMenuCommandService;   9:   if ( null != mcs )  10:   {  11:     CommandID menuCommandID = new CommandID(GuidList.guidSimpleCommandCmdSet,   12:       (int)PkgCmdIDList.cmdidMyFirstCommand);  13:     MenuCommand menuItem = new MenuCommand(MenuItemCallback, menuCommandID );  14:     mcs.AddCommand( menuItem );  15:   }  16: }  17:

我們重寫了Initialize方法,在做任何動作之前,先調用基類的Initialize方法。一個命令可以通過很多方式調用:通過主菜單、通過工具欄或者通過上下文菜單。所有的菜單項都是通過命令ID和相應的命令綁定起來的。在這裡,我們把命令ID和命令處理方法綁定起來。

通過GetService方法,我們得到了一個OleMenuCommandService的實例,它實現了IMenuCommandService介面,有差不多20個方法和一些屬性。我們利用AddCommand方法把了一個Command handler(在這裡是MenuItemCallback)和一個命令ID(menuCommandId變數)綁定其來。

當用戶點擊「My First Command」菜單項時,它是與以GuidList.guidSimpleCommandCmdSetPkgCmdIDList.cmdidMyFirstCommand為標識的命令綁定其來的,而這個命令ID又是和MenuItemCallback關聯的,所以會彈出消息框。

總結

我們為package添加了一個簡單的菜單命令。為了添加這個命令,我們做了如下的事情:

— 創建了一個vsct文件去描述資源(菜單項、命令和相關的標識符)。這個文件被VSCT編譯器編譯成二進位的資源,併合併到VSPackage資源中。同時,為了註冊這個資源,我們為package添加了一個ProvideMenuResourceAttribute

— 實現了一個方法去彈出消息框。這個方法利用SVsUIShell和Visual Studio交互,以便彈出消息。

— 在package初始化的時候,我們添加了相應程式碼去把命令和命令處理邏輯綁定在一起。在這裡通過OleMenuCommandService和IDE交互,以便將命令和命令處理邏輯關聯起來。

下一次,我們將以工具窗口(Tool Window)的形式,為package添加自己的介面。