前言
在之前的兩篇隨筆中,我介紹了Add-In的運行機制,這樣對Add-In的事件、生命周期 、與VS如何交互可以有個基本的了解了。現在是時候看看如何在VS中完成一些操作,這才 是Add-In開發的目的所在。
一般的,Add-In應當提供一些界面元素,這樣用戶可以進行某些操作,比如在主菜單 內添加一個菜單項,或者在編輯器的上下文菜單內添加一個菜單項,在本文中就來看看如 何實現這些。
關於命令(Command)
考慮一個極為常見的場景:在編寫代碼的過程中,選中一段文本,點擊Edit->Copy (或工具欄按鈕)或者按下Ctrl+C,我們可以把選中文本拷貝到剪貼板,這個過程的背後 發生了什麼?
是什麼完成的拷貝操作呢?答案是命令。可以認為命令就是某個特定的功能,如Copy 、Paste、Cut等等。VS本身就內置大量的命令(有數千個之多),而上面說到的菜單項、 工具欄或快捷鍵則執行了這些命令。通過Tools->Options菜單可以查看命令列表:
圖1:VS的命令列表
值得注意的是,執行命令並非必須通過界面元素,比如在命令窗口中:
圖2:在命令窗口執行命令
這樣也可以執行同樣的命令。現在我們知道存在多種方式來執行命令,即菜單、工具 欄、快捷鍵或命令窗口,這裡統稱為觸發者。在VS中,觸發者與命令是分離開來的:用戶 通過觸發者來執行命令,而命令負責檢查自身的狀態(名稱、是否可見、是否可用等等) 並執行。這意味著,命令可以對應一個或多個菜單項,也可以不對應任何菜單項。
另一方面,對於同一個命令,比如Edit.Copy,仍然可能有不同的情況。在文本編輯器 內和在解決方案管理器內的Edit.Copy命令執行內容並不相同。這裡有一個命令目標 (Command Target)的概念,VS將命令轉向給了命令目標,而命令目標按自己的實現來執 行該命令。總結下來就是:
對象 職責 觸發者 提供一種方式供用戶使用 命令 一個邏輯實體,檢查自身的狀態,可以執行命令,也可以轉向一個 命令目標 命令目標 根據傳遞過來的命令,按自己的實現來執行它
關於命令欄(CommandBar)
命令是個“乖孩子”,首先是我們讓它做什麼它就做什麼,第二就是它不會單獨出門 ,它會跟其它同伴待在一塊兒,它們都被放在命令欄中。比如主菜單中File下的那些菜單 項。
既然這樣,如果希望向已經存在的命令欄內添加命令,就得首先找到命令欄才能添加 。現在來看一個例子,向Tools菜單中添加一個菜單項。
添加一個新命令
使用Add-In向導新建一個Add-In,名字定為NEnhancer,意為對VS進行增強:),以後關 於Add-In的例子都會放在這裡面。注意要選中向Tools菜單添加一個菜單項。在Connect類 的OnConnection方法中可以看到添加菜單項的代碼如下:
C# Code - 向Tools菜單添加新項
try
{
// 如果需要向其它菜單欄添加命令,將Tools改為其它名稱(要使用 英文的)
// 關於這些菜單欄的名稱,可以查看CommandBar.resx文件
string resourceName;
ResourceManager resourceManager = new ResourceManager ("NEnhancer.CommandBar", Assembly.GetExecutingAssembly());
CultureInfo cultureInfo = new CultureInfo (_applicationObject.LocaleID);
if(cultureInfo.TwoLetterISOLanguageName == "zh")
{
System.Globalization.CultureInfo parentCultureInfo = cultureInfo.Parent;
resourceName = String.Concat(parentCultureInfo.Name, "Tools");
}
else
{
resourceName = String.Concat (cultureInfo.TwoLetterISOLanguageName, "Tools");
}
toolsMenuName = resourceManager.GetString(resourceName);
}
catch
{
// 如果沒能找到,就使用英文版本的名稱
toolsMenuName = "Tools";
}
// 獲取VS的主菜單
CommandBar menuBarCommandBar =
((CommandBars)_applicationObject.CommandBars) ["MenuBar"];
// 獲取Tools菜單
CommandBarControl toolsControl = menuBarCommandBar.Controls [toolsMenuName];
CommandBarPopup toolsPopup = (CommandBarPopup)toolsControl;
// 如果需要添加多個菜單項,這個try/catch代碼塊可以多次使用
// 但別忘了更新QueryStatus/Exec這兩個方法
try
{
// 向Commands集合添加命令
Command command = commands.AddNamedCommand2(_addInInstance, "NEnhancer", "NEnhancer",
"Executes the command for NEnhancer", true, 59, ref contextGUIDS,
(int)vsCommandStatus.vsCommandStatusSupported+(int) vsCommandStatus.vsCommandStatusEnabled,
(int)vsCommandStyle.vsCommandStylePictAndText, vsCommandControlType.vsCommandControlTypeButton);
// 向菜單中添加新項
if((command != null) && (toolsPopup != null))
{
command.AddControl(toolsPopup.CommandBar, 1);
}
}
catch(System.ArgumentException)
{
// 如果引發異常,可能是由於同名的命令已經存在了,可以忽略該異 常
}
代碼的功能可以查看其中的注釋。首先通過資源文件獲取Tools菜單的名稱(文化相關 的),然後是獲取主菜單,這裡用的就是CommandBar,這裡通過名稱從DET2.CommandBars 集合中進行查找。往下可以看到Tools菜單是主菜單欄下的一個CommandBarPopup控件,獲 取了Tools菜單後就可以給它添加命令了,其關鍵是AddNamedCommand2方法,它的參數信 息為:
C# Code - Method Signature
Command AddNamedCommand2(AddIn AddInInstance, string Name, string ButtonText,
string Tooltip, bool MSOButton, object Bitmap, ref object[] ContextUIGUIDs,
int vsCommandStatusValue, int CommandStyleFlags, vsCommandControlType ControlType);
AddInInstance:用作命令對象的AddIn對象
Name:命令名稱
ButtonText:當命令顯示在菜單或工具欄時的文本
ToolTip:命令的提示信息
MSOButton:如果是true,表示使用預定義的圖標,否則使用自定義的圖標
Bitmap:如果MSOButton值為true,那麼該參數的值將用於所用預定義圖標的索引;否 則用作自定義圖標的Id
ContextUIGUIDs:VS定義了一些GUID值來標識其狀態的改變。比如如果希望進入debug 模式命令可用,可將該參數的值設置為vsContextGuidDebugging
sCommandStatusValue:命令的默認可用狀態
CommandStyleFlags:該參數用於控制命令的外觀,比如只顯示圖標、只顯示文本或者 都顯示
ControlType:所添加命令對應的UI元素的類型,比如菜單項、組合框等
從本文開頭舉的例子可以看到,對於VS內置的菜單項,它們對應的命令名稱是有一定 規律的,即按照菜單的嵌套關系,如Edit.Copy,表示Edit下的Copy項(有空格的話要去 掉)。那麼對於我們的Add-In來說,也是有規律的,即 Namespace.ClassName.CommandName,這裡就是NEnhancer.Connect.NEnhancer。
命令的名稱應當反應它的意圖,所以這裡把AddNamedCommand2的第二個參數改為 CommandViewer。前面提到過,VS中有很多命令,它們按照命令欄來組織,這裡就做一個 Add-In來查看所有的命令和命令欄,方便以後的開發。
命令的執行
現在命令和菜單項是添加了,當用戶點擊菜單時,如何處理它呢?要實現 IDTCommandTarget接口,一旦實現了它,我們的Connect類就成為一個合格的命令目標 (Command Target)了。確切地說,當用戶點擊菜單時,VS要執行它的Exec方法,Exec方 法有如下幾個參數:
C# Code - Method Signature
void Exec(string CmdName, vsCommandExecOption ExecuteOption,
ref object VariantIn, ref object VariantOut, ref bool Handled);
CmdName:命令的全稱,Add-In中命令的命名規則是 Namespace.ClassName.CommandName
vsCommandExecOption:絕大多數情況下,該參數的值是 vsCommandExecOptionDoDefault值,用於通知Add-In按部就班地行事
VariantIn:如果有數據要傳給命令,就使用此參數
VariantOut:與VariantIn相反,此參數用於向調用者傳遞數據
Handler:如果設置為true,VS就知道命令已經執行完畢;如果為false,VS會尋找其 它可以執行命令的方法,對Add-In來說,這意味著出錯——不可能有其它的地方可以執行 我們自定義的命令
命令的狀態
有的命令並不總是可用,比如我們開發了一個Add-In,它專門針對於文本編輯器,如 果沒有任何文件打開,它就不應該是可用的。這裡要使用IDTCommandTarget接口的另一個 方法QueryStatus,它的參數有:
CmdName:命令的全稱
NeededText:當前來說,該參數的值只會是vsCommandStatusTextWantedNone。但是在 開發Add-In的時候,強烈建議要對此進行檢查,因為VS還保留了其它的可能值作將來之用 (可以參考向導生成的代碼)
StatusOption:這是最重要的參數(ref),我們要給它賦值,告訴VS傳入的這個命令 是否支持、是否可用或者是否可見,這幾種情況可以同時存在,此時對它們可以使用“或 ”操作。
CommandText:此參數當前VS還沒有使用,不要給它賦值
好了,了解了命令的概念,也知道如何添加、執行命令了,剩下的就是實現命令的功 能了。這裡要新建一個窗體,添加一個TreeView來顯示命令欄和命令:
圖3:CommandBarViewer窗體
要查看該窗體的具體代碼,可以在文章末尾處下載代碼。
現在只要在Connect類中稍作修改顯示窗體:
C# Code - 執行命令public void QueryStatus(string commandName, vsCommandStatusTextWanted neededText,
ref vsCommandStatus status, ref object commandText)
{
if (neededText == vsCommandStatusTextWanted.vsCommandStatusTextWantedNone)
{
if (commandName == "NEnhancer.Connect.CommandViewer")
{
status = (vsCommandStatus) vsCommandStatus.vsCommandStatusSupported | vsCommandStatus.vsCommandStatusEnabled;
return;
}
}
}
public void Exec(string commandName, vsCommandExecOption executeOption,
ref object varIn, ref object varOut, ref bool handled)
{
handled = false;
if (executeOption == vsCommandExecOption.vsCommandExecOptionDoDefault)
{
if (commandName == "NEnhancer.Connect.CommandViewer")
{
ShowCmdBarViewer();
handled = true;
return;
}
}
}
private void ShowCmdBarViewer()
{
CommandBarViewer viewerForm = new CommandBarViewer();
viewerForm.DTEObject = _applicationObject;
viewerForm.ShowDialog();
}
通過這個例子,我們可以了解如何添加新的命令並執行它,其中的關鍵方法包括 OnConnection、AddNamedCommand2、Exec、QueryStatus。本文前面曾提到過命令的命名 規則,我們可以由此獲知某個菜單項對應的命令,這樣就可以在Add-In中使用該命令而不 需要再重新發明一次輪子。
執行已有命令
先看下圖。
我們可以關閉當前文檔,也可以關閉其它文檔,就是不能關閉所有文檔。有時候還是 需要這個功能的,我想你應該用過Window->Close All Documents菜單項吧?它實現的 正是我們所需要的,現在考慮如何使用這個已有的命令。首先得找到這個命令,按前面的 規則,我們到Tools-Options裡面去找(見圖1),我們可以試著輸入window.close,這時 就可以看到了:Window.CloseAllDocuments。
接下來跟前面例子類似,首先要添加一個命令,前面是把命令添加到了主菜單欄(通 過“MenuBar”獲取命令欄),現在要添加到另外一個命令欄:“Easy MDI Document Window”,這名字有些奇怪(我是在所有的CommandBar中搜了好多次才找到的),在剛才 添加Tools菜單項的代碼下面添加如下代碼:
C# Code - 添加CloseAllDocuments命令
// 通過名稱獲取要使用的命令欄
CommandBar mdiDocCommandBar =
((CommandBars)_applicationObject.CommandBars)["Easy MDI Document Window"];
// 添加命令
Command closeAllDocsCmd = commands.AddNamedCommand2(_addInInstance, "CloseAllDocuments", "Close All Documents",
"Close All Documents", false, 0, ref contextGUIDS,
(int)vsCommandStatus.vsCommandStatusSupported + (int) vsCommandStatus.vsCommandStatusEnabled,
(int)vsCommandStyle.vsCommandStylePictAndText, vsCommandControlType.vsCommandControlTypeButton);
// 要放在Close All But This菜單項的下面
CommandBarControl closeAllButThisCmd = mdiDocCommandBar.Controls["Close All But This"];
// 設定菜單項的索引,注意索引是從1開始的
int closeAllCmdIndex = (closeAllButThisCmd == null) ? 1 : (closeAllButThisCmd.Index + 1);
closeAllDocsCmd.AddControl(mdiDocCommandBar, closeAllCmdIndex);
要使用Window.CloseAllDocuments命令只要一行代碼:
C# Code - Execute Command
_applicationObject.ExecuteCommand("Window.CloseAllDocuments", string.Empty);
現在菜單項看起來應該是這樣的:
趕緊試一試吧!這裡再提一下,在測試、調試完成之後,如果要發布Add-In,最簡單 的方法是將dll和.AddIn放在[My Documents Path]\Visual Studio 2008\Addins,同時把那 個用來測試的.AddIn文件(如NEnhancer - For Testing.AddIn)移掉。
我們身在何處?
本文首先介紹了命令和命令欄的概念,正是通過命令VS才可以與Add-In進行交互。然 後通過兩個例子解釋了如何添加、執行命令,以及如何執行VS內置的命令,接下來我們就 有辦法操作VS的方方面面了:解決方案、項目、文檔、代碼等等,敬請期待:-)
出處:http://anderslly.cnblogs.com
本文配套源碼