工欲善其事,必先利其器
通過使用和生成擴展,打造適合你和你的團隊的完美工具。

什么是Visual Studio擴展
https://visualstudio.microsoft.com/zh-hans/vs/features/extend/
Visual Studio擴展(Visual Studio Extend)是可以允許你在Visual Studio中進行自定義並增強在其中的體驗的附加項,通過添加新功能或集成現有工具實現。擴展復雜程度不一,但是其主要用途在於提高工作效率並滿足工作流需求。

Visual Studio擴展商店
Visual Studio擴展商店(Visual Studio Marketplace)提供的數千萬個擴展,可以幫助你查找所需工具,增強你的Visual Studio,還可以通過它發布你的擴展。
創建Visual Studio擴展
https://docs.microsoft.com/visualstudio/extensibility/starting-to-develop-visual-studio-extensions
安裝Visual Studio SDK
Visual Studio軟件開發工具包(Visual Studio SDK)是安裝Visual Studio時一個可選項,我們可以通過Visual Studio Installer安裝器,修改或首次安裝VS對應版本時,選中並勾選其他工具集分類中的Visual Studio 擴展開發工作負荷來安裝它。

或者,如果你本地有擴展的項目,雙擊打開的時候,也會提示你安裝此依賴項。

思考Visual Studio能完成的事情
如果你正好有如下所列方向的需求,那么創建Visual Studio擴展可能是比較好的選項之一:
- 支持未包含在Visual Studio中的語言、語法着色、IntelliSense以及編譯器和調試支持
- 利用附加模板、代碼重構、新對話框或工具窗口擴展核心IDE體驗的生產力工具
- 適用於各種方案(如數據設計或雲支持)的域特定設計器
值得留意的是,許多擴展是開放源代碼的,並且Visual Studio擴展商店(Visual Studio Marketplace)包含指向其GitHub存儲庫的鏈接。

利用Visual Studio擴展能完成的功能
理論上,你可以只擴展VisualStudio的任何部分:菜單、工具欄、命令、窗口、解決方案、項目、編輯器等。
實際上,我們發現大多數人要擴展的功能是命令、菜單和工具欄、Windows、IntelliSense和項目。下面是相關資料鏈接:
- 擴展菜單和命令:將您自己的項添加到VisualStudio菜單和工具欄中。您可以使用它們來啟動新Visual Studio功能或您自己的外部幫助程序應用程序。還可以提供菜單項的自定義快捷方式。
- 擴展和自定義工具Windows:擴展現有工具窗口或創建自己的工具窗口。例如,您可以將新屬性添加到屬性,也可以創建新的工具窗口來添加其他功能。
- 編輯器和語言服務擴展:將你自己的自定義添加到為VisualStudio語言提供的IntelliSense,或創建對新編程語言的支持。你可以創建新的語句完成、建議和新的QuickInfo工具提示。對於輕型電燈泡,可以添加重構建議和代碼修補程序,以支持新的編程語言。
- 擴展項目
- 擴展用戶設置和選項
- 擴展屬性和屬性窗口
- 擴展Visual Studio的其他部分
- Visual Studio獨立Shell
編寫自己的第一個擴展
a. 打開Visual Studio創建項目
這里篩選C#語言、所有平台、擴展項目類型,比如這里找到VSIX Project這種項目模板類型,然后點擊"下一步"按鈕。

b. 輸入項目名稱,創建項目
這里我們就采用demoForVsixExtend作為項目名稱和解決方案的名稱吧,然后點擊"創建"按鈕。

c. 創建成功並打開解決方案

d. 查看並編輯擴展元數據
在demoForVsixExtend項目中找到.vsixmanifest文件,雙擊可以進入配置界面,這里面編輯擴展的描述、語言、協議、圖標、預覽圖、標簽、更新內容、開始指引、擴展鏈接、是否為預覽拓展。

e. 編譯並生成,查看輸出文件
從demoForVsixExtend項目的包引用來看,我們可以清晰看到是依賴Microsoft.VisualStudio.SDK和Microsoft.VSSDK.BuildTools兩個包。
全部編譯之后,我們看看Bin生成目錄,可以看到,它會生成一個DLL,和一個.vsix的包。

向工具欄添加自定義命令
a. 創建擴展的自定義命令類文件
在demoForVsixExtend項目上右鍵,"添加"-"新建項"

在已安裝的Visual C#項中找到Extendsibility分組,選擇Command項,我們取個名字叫FirstCommand,然后點擊"添加"按鈕。

添加成功之后,我們會發現它會自動創建一個FirstCommand.cs分類,並且在Resources文件夾中,創建配套的FirstCommand.png資源文件。

同時,我們還發現自動創建了一個和項目名稱相關的.vsct文件,這個文件是定義我們想添加的自定義命令的地方,用於描述命令位於工具菜單的位置、名稱、圖標等信息。

我們單擊打開這個文件,可以看到里面已經描述了一個Button按鈕和其關聯的圖標資源。

在.vsct這個xml格式文件中,標記了一個Button(FirstCommandId),它的父級指向了一個Group(MyMenuGroup),而這個父級Group的父級指向了頂層的"菜單-工具"(IDM_VS_MENU_TOOLS),並且這個命令的內容文本為Say Hello VsixExtend,且指定了一個圖標資源(bmpPic1)。
<Groups>
<Group guid="guiddemoForVsixExtendPackageCmdSet" id="MyMenuGroup" priority="0x0600">
<Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS"/>
</Group>
</Groups>
<Buttons>
<Button guid="guiddemoForVsixExtendPackageCmdSet" id="FirstCommandId" priority="0x0100" type="Button">
<Parent guid="guiddemoForVsixExtendPackageCmdSet" id="MyMenuGroup" />
<Icon guid="guidImages" id="bmpPic1" />
<Strings>
<ButtonText>Say Hello VsixExtend</ButtonText>
</Strings>
</Button>
</Buttons>
值得注意的是,一個Button必須歸屬到一個Group下面,而一個Group必須歸屬到頂層的"菜單-工具"里面。
b. 調試自定義命令,查看附加效果
我們啟動調試之后,它會再啟動另外一個Visual Studio的實例,並且附加了調試,最終我們能在"工具"菜單找到我們新增的Invove FirstCommand菜單項入口。

c. 修改自定義命令文案信息
我們可以試着修改.vsct文件的內容來自定義按鈕文案,比如我們將按鈕的文案改成Say Hello VsixExtend。
<Buttons>
<!--To define a menu group you have to specify its ID, the parent menu and its display priority.
The command is visible and enabled by default. If you need to change the visibility, status, etc, you can use
the CommandFlag node.
You can add more than one CommandFlag node e.g.:
<CommandFlag>DefaultInvisible</CommandFlag>
<CommandFlag>DynamicVisibility</CommandFlag>
If you do not want an image next to your command, remove the Icon node /> -->
<Button guid="guiddemoForVsixExtendPackageCmdSet" id="FirstCommandId" priority="0x0100" type="Button">
<Parent guid="guiddemoForVsixExtendPackageCmdSet" id="MyMenuGroup" />
<Icon guid="guidImages" id="bmpPic1" />
<Strings>
<ButtonText>Say Hello VsixExtend</ButtonText>
</Strings>
</Button>
</Buttons>
並且我們將FirstCommand.cs文件中的Execute函數,將里面的彈窗提示內容文案做一定的修改。
private void Execute(object sender, EventArgs e)
{
ThreadHelper.ThrowIfNotOnUIThread();
//string message = string.Format(CultureInfo.CurrentCulture, "Inside {0}.MenuItemCallback()", this.GetType().FullName);
string message = "Hello Tip";
string title = "FirstCommand";
// Show a message box to prove we were here
VsShellUtilities.ShowMessageBox(
this.package,
message,
title,
OLEMSGICON.OLEMSGICON_INFO,
OLEMSGBUTTON.OLEMSGBUTTON_OK,
OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
}
我們再運行一次看看。

點擊Say Hello VsixExtend菜單,發現會按預期彈窗和提示。

e. 自定義命令的初始化
在命令的.cs文件FirstCommand.cs中,描述了自定義命令的初始化流程,這里面包含InitializeAsync方法的實現,在它里面有通過FirstCommand的構造函數創建並添加了當前的命令項。
/// <summary>
/// Command handler
/// </summary>
internal sealed class FirstCommand
{
/// <summary>
/// Command ID.
/// </summary>
public const int CommandId = 0x0100;
/// <summary>
/// Command menu group (command set GUID).
/// </summary>
public static readonly Guid CommandSet = new Guid("8229a384-5bd5-4a80-9076-ef2b05901d4c");
/// <summary>
/// VS Package that provides this command, not null.
/// </summary>
private readonly AsyncPackage package;
/// <summary>
/// Gets the instance of the command.
/// </summary>
public static FirstCommand Instance
{
get;
private set;
}
/// <summary>
/// Initializes a new instance of the <see cref="FirstCommand"/> class.
/// Adds our command handlers for menu (commands must exist in the command table file)
/// </summary>
/// <param name="package">Owner package, not null.</param>
/// <param name="commandService">Command service to add command to, not null.</param>
private FirstCommand(AsyncPackage package, OleMenuCommandService commandService)
{
this.package = package ?? throw new ArgumentNullException(nameof(package));
commandService = commandService ?? throw new ArgumentNullException(nameof(commandService));
var menuCommandID = new CommandID(CommandSet, CommandId);
var menuItem = new MenuCommand(this.Execute, menuCommandID);
commandService.AddCommand(menuItem);
}
/// <summary>
/// Initializes the singleton instance of the command.
/// </summary>
/// <param name="package">Owner package, not null.</param>
public static async Task InitializeAsync(AsyncPackage package)
{
// Switch to the main thread - the call to AddCommand in FirstCommand's constructor requires
// the UI thread.
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken);
OleMenuCommandService commandService = await package.GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService;
Instance = new FirstCommand(package, commandService);
}
}
f. 擴展包的初始化
在擴展包的.cs文件demoForVsixExtendPackage.cs的初始化方法InitializeAsync中,正是調用了FirstCommand類的InitializeAsync方法完成了自定義命令的初始化。
[PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
[Guid(demoForVsixExtendPackage.PackageGuidString)]
[ProvideMenuResource("Menus.ctmenu", 1)]
public sealed class demoForVsixExtendPackage : AsyncPackage
{
/// <summary>
/// demoForVsixExtendPackage GUID string.
/// </summary>
public const string PackageGuidString = "6c3bfb2c-f8a4-40ff-99fa-caf75412fc61";
#region Package Members
/// <summary>
/// Initialization of the package; this method is called right after the package is sited, so this is the place
/// where you can put all the initialization code that rely on services provided by VisualStudio.
/// </summary>
/// <param name="cancellationToken">A cancellation token to monitor for initialization cancellation, which can occur when VS is shutting down.</param>
/// <param name="progress">A provider for progress updates.</param>
/// <returns>A task representing the async work of package initialization, or an already completed task if there is none. Do not return null from this method.</returns>
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
{
// When initialized asynchronously, the current thread may be a background thread at this point.
// Do any initialization that requires the UI thread after switching to the UI thread.
await this.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
await FirstCommand.InitializeAsync(this);
}
#endregion
}
而擴展包的初始化方法InitializeAsync,實際上是繼承了統一的Microsoft.VisualStudio.Shell.AsyncPackage類,對原初始化方法進行了重寫來實現的。
namespace Microsoft.VisualStudio.Shell
{
[ComVisible(true)]
public abstract class AsyncPackage
{
//
// 摘要:
// The async initialization portion of the package initialization process. This
// method is invoked from a background thread.
//
// 參數:
// cancellationToken:
// A cancellation token to monitor for initialization cancellation, which can occur
// when VS is shutting down.
//
// 返回結果:
// A task representing the async work of package initialization, or an already completed
// task if there is none. Do not return null from this method.
protected virtual Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
{
return Task.CompletedTask;
}
//
// 摘要:
// When overridden by a derived type, may initiate operations with side effects
// that are not strictly a part of package load that would belong to Microsoft.VisualStudio.Shell.AsyncPackage.InitializeAsync(System.Threading.CancellationToken,System.IProgress{Microsoft.VisualStudio.Shell.ServiceProgressData})
// but that should happen soon after package load (e.g. updating command status).
//
// 參數:
// cancellationToken:
// The package's Microsoft.VisualStudio.Shell.AsyncPackage.DisposalToken.
//
// 返回結果:
// A task that indicates completion of the extra work.
//
// 言論:
// This method is invoked on a threadpool thread.
// The base Microsoft.VisualStudio.Shell.AsyncPackage.OnAfterPackageLoadedAsync(System.Threading.CancellationToken)
// method has an empty implementation, so direct derivations of Microsoft.VisualStudio.Shell.AsyncPackage
// needn't call the base method as part of their override method.
protected virtual Task OnAfterPackageLoadedAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}
g. 自定義命令的響應
在命令的.cs文件FirstCommand.cs中,當我們通過構造函數添加自定義命令項時,我們同時傳入了命令對應的響應委托EventHandler事件方法Execute,所以如果點擊自定義命令的話,會進入Execute這個響應流程。
/// <summary>
/// Command handler
/// </summary>
internal sealed class FirstCommand
{
/// <summary>
/// Initializes a new instance of the <see cref="FirstCommand"/> class.
/// Adds our command handlers for menu (commands must exist in the command table file)
/// </summary>
/// <param name="package">Owner package, not null.</param>
/// <param name="commandService">Command service to add command to, not null.</param>
private FirstCommand(AsyncPackage package, OleMenuCommandService commandService)
{
this.package = package ?? throw new ArgumentNullException(nameof(package));
commandService = commandService ?? throw new ArgumentNullException(nameof(commandService));
var menuCommandID = new CommandID(CommandSet, CommandId);
var menuItem = new MenuCommand(this.Execute, menuCommandID);
commandService.AddCommand(menuItem);
}
/// <summary>
/// This function is the callback used to execute the command when the menu item is clicked.
/// See the constructor to see how the menu item is associated with this function using
/// OleMenuCommandService service and MenuCommand class.
/// </summary>
/// <param name="sender">Event sender.</param>
/// <param name="e">Event args.</param>
private void Execute(object sender, EventArgs e)
{
ThreadHelper.ThrowIfNotOnUIThread();
//string message = string.Format(CultureInfo.CurrentCulture, "Inside {0}.MenuItemCallback()", this.GetType().FullName);
string message = "Hello Tip";
string title = "FirstCommand";
// Show a message box to prove we were here
VsShellUtilities.ShowMessageBox(
this.package,
message,
title,
OLEMSGICON.OLEMSGICON_INFO,
OLEMSGBUTTON.OLEMSGBUTTON_OK,
OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
}
}
向解決方案資源管理器添加自定義命令
a. 首先新建自定義命令項
在demoForVsixExtend項目上右鍵,"添加"-"新建項",在已安裝的Visual C#項中找到Extendsibility分組,選擇Command項,我們取個名字叫SecondCommand,然后點擊"添加"按鈕。

生成后,會幫我們創建SecondCommand.cs、Resources\SecondCommand.png,並且自動將SecondCommand的初始化方法InitializeAsync添加到demoForVsixExtendPackage.cs文件中了。

b. 修改自定義命令項的元數據標記
首先我們前往demoForVsixExtendPackage.vsct文件,查看到Symbols節點下的GuidSymbol組,我們將SecondCommand相關的單獨拆出來。
<!-- This is the guid used to group the menu commands together -->
<GuidSymbol name="guiddemoForToolbarPackageCmdSet" value="{10F2415E-17D1-4FC7-89B6-C5CE9E21A197}">
<IDSymbol name="SecondMenuGroup" value="0x1020" />
<IDSymbol name="SecondCommandId" value="0x0100" />
</GuidSymbol>
<GuidSymbol name="guidImages1" value="{d4f21307-1f2f-40fa-a3fc-2bc83dd71b7a}" >
<IDSymbol name="bmpPic1" value="1" />
<IDSymbol name="bmpPic2" value="2" />
<IDSymbol name="bmpPicSearch" value="3" />
<IDSymbol name="bmpPicX" value="4" />
<IDSymbol name="bmpPicArrows" value="5" />
<IDSymbol name="bmpPicStrikethrough" value="6" />
</GuidSymbol>
這里我們看到兩組GuidSymbol,后面這個是用於描述圖像的,比較好理解,而第一個是整個命令的元數據,這個name要唯一,並且這個value對應的Guid的值,要和SecondCommand.cs里面定義的值一一對應才行,不然無法識別。

接下來我們看到Groups組,該組下面擁有一個新的Group,它的Id也是要額外注意,得唯一,且和GuidSymbol中的定義保持一致:SecondMenuGroup,它的父級指向解決方案資源管理器的標記IDM_VS_TOOL_PROJWIN即可。
<Groups>
<Group guid="guiddemoForVsixExtendPackageCmdSet" id="MyMenuGroup" priority="0x0600">
<Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS" />
</Group>
<Group guid="guiddemoForToolbarPackageCmdSet" id="SecondMenuGroup" priority="0x0600">
<Parent guid="guidSHLMainMenu" id="IDM_VS_TOOL_PROJWIN" />
</Group>
</Groups>
然后我們繼續看Buttons組,該組下面擁有一個新的Button,它的Name的具備唯一性,並且它的Id得唯一,且和GuidSymbol中的定義保持一致:SecondCommandId,並且它的父級需要指向SecondMenuGroup。
<Buttons>
<Button guid="guiddemoForToolbarPackageCmdSet" id="SecondCommandId" priority="0x0100" type="Button">
<Parent guid="guiddemoForToolbarPackageCmdSet" id="SecondMenuGroup" />
<Icon guid="guidImages1" id="bmpPic1" />
<CommandFlag>DefaultInvisible</CommandFlag>
<CommandFlag>DynamicVisibility</CommandFlag>
<Strings>
<ButtonText>Invoke SecondCommand</ButtonText>
</Strings>
</Button>
</Buttons>
c. 給自定義命令添加顯影屬性,根據是否打開解決方案自適應
僅僅完成前面的部分,我們會發現,無論是否打開解決方案,這個新增的命令都會出現在解決方案的資源管理器工具欄中,那么我們是否可以做到顯影控制,讓它根據是否打開解決方案自適應呢?當然顯然是可以。
我們首先需要確保在Button的描述里面,追加命令的Flag,一般來說,就在Icon和String之間即可,我們需要追加DefaultInvisible、DynamicVisibility這兩個命令標記。
<Buttons>
<Button guid="guiddemoForToolbarPackageCmdSet" id="SecondCommandId" priority="0x0100" type="Button">
<Parent guid="guiddemoForToolbarPackageCmdSet" id="SecondMenuGroup" />
<Icon guid="guidImages1" id="bmpPic1" />
<CommandFlag>DefaultInvisible</CommandFlag>
<CommandFlag>DynamicVisibility</CommandFlag>
<Strings>
<ButtonText>Invoke SecondCommand</ButtonText>
</Strings>
</Button>
</Buttons>
然后在.vsct文件中,我們需要在一級標記節點中添加顯影控制描述,一般來說,就在Commands和Symbols之間即可,我們需要追加VisibilityConstraints組,並且含2個VisibilityItem子節點,分別用來處理打開一個或者多個項目的情況。
<Commands>
...
</Commands>
<VisibilityConstraints>
<VisibilityItem guid="guiddemoForToolbarPackageCmdSet"
id="SecondCommandId"
context="UICONTEXT_SolutionHasSingleProject" />
<VisibilityItem guid="guiddemoForToolbarPackageCmdSet"
id="SecondCommandId"
context="UICONTEXT_SolutionHasMultipleProjects" />
</VisibilityConstraints>
<Symbols>
...
</Symbols>
需要注意的是,這里VisibilityItem的guid值和id值都要和GuidSymbol中的定義保持一致才有效。
d. 運行查看效果
- 當未打開任何項目的時候,解決方案資源管理器是不會出現自定義命令的。

- 當打開一個或者多個項目的時候,這時候解決方案資源管理器才會出現我們的定義的命令。

Visual Studio最高一級的組清單
通過前面的案例我們已經知道了,我們可以把我們自定義的Group的父級指向Visual Studio自帶的一級組,這樣我們可以把自定義組掛到已有的菜單項上了,前面我們分別介紹了IDM_VS_MENU_TOOLS和IDM_VS_TOOL_PROJWIN這兩個父級,那么還有哪些組呢?
- 若要向菜單欄添加新的菜單,可將組的父級設置為如下值:
| 組 | ID |
|---|---|
| 文件/編輯/查看 | IDG_VS_MM_FILEEDITVIEW |
| 重構 | IDG_VS_MM_REFACTORING |
| 項目 | IDG_VS_MM_PROJECT |
| 構建 | IDG_VS_MM_BUILDDEBUGRUN |
| 格式/工具 | IDG_VS_MM_TOOLSADDINS |
| Window/Help/Community | IDG_VS_MM_WINDOWHELP |
| 加載項 | IDG_VS_MM_MACROS |
| FullScreenBar | IDG_VS_MM_FULLSCREENBAR |
比如,如果把"項目(IDG_VS_MM_PROJECT)"作為父級,那么效果可以是:
嗯,你想得美~
- 若要向已有菜單添加新的組,可將組的父級設置為如下值:
| 菜單 | ID |
|---|---|
| 文件 | IDM_VS_MENU_FILE |
| 編輯 | IDM_VS_MENU_EDIT |
| 視圖 | IDM_VS_MENU_VIEW |
| 重構 | IDM_VS_MENU_REFACTORING |
| 項目 | IDM_VS_MENU_PROJECT |
| 生成 | IDM_VS_MENU_BUILD |
| 分析 | IDM_VS_MENU_FORMAT |
| 工具 | IDM_VS_MENU_TOOLS |
| 擴展 | IDM_VS_MENU_EXTENSIONS |
| 窗口 | IDM_VS_MENU_WINDOW |
| 加載項 | IDM_VS_MENU_ADDINS |
| 社區 | IDM_VS_MENU_COMMUNITY |
| 幫助 | IDM_VS_MENU_HELP |
比如,如果把"視圖(IDM_VS_MENU_VIEW)"作為父級,那么效果可以是:

- 若要在已有菜單的子菜單中添加組,那么可以將已有菜單的子作為父級:
| 父組 | 子 | 子組 |
|---|---|---|
| IDG_VS_FILE_FILE | IDM_VS_CSCD_NEW | IDG_VS_FILE_NEW_CASCADE |
| IDM_VS_CSCD_OPEN | IDG_VS_FILE_OPENP_CASCADE | |
| IDG_VS_FILE_OPENF_CASCADE | ||
| IDG_VS_FILE_ADD | IDM_VS_CSCD_ADD | IDG_VS_FILE_ADD_PROJECT_NEW |
| IDG_VS_FILE_ADD_PROJECT_EXI | ||
| IDG_VS_FILE_MRU | IDM_VS_CSCD_FILEMRU | IDG_VS_FILE_FMRU_CASCADE |
| IDM_VS_CSCD_PROJMRU | IDG_VS_FILE_PMRU_CASCADE | |
| IDG_VS_FILE_MOVE | IDM_VS_CSCD_MOVETOPRJ | IDG_VS_FILE_MOVE_CASCADE |
| IDG_VS_FILE_MOVE_PICKER | ||
| IDG_VS_VIEW_DEV_WINDOWS | IDM_VS_CSCD_FINDRESULTS | IDG_VS_WNDO_FINDRESULTS |
| IDM_VS_CSCD_WINDOWS | IDG_VS_VIEW_CALLBROWSER | |
| IDG_VS_WNDO_OTRWNDWS1...6 | ||
| IDG_VS_WNDO_WINDOWS2 | ||
| IDG_VS_VIEW_TOOLBARS | IDM_VS_CSCD_COMMANDBARS | |
| IDG_VS_EDIT_GOTO | IDM_VS_EDITOR_FIND_MENU | |
| IDG_VS_EDIT_OBJECTS | IDM_VS_CSCD_MNUDES | IDG_VS_MNUDES_INSERT |
| IDG_VS_MNUDES_EDITNAMES | ||
| IDM_VS_CSCD_OLEVERBS | IDG_VS_EDIT_OLEVERBS | |
| IDG_VS_BUILD_SELECTION | IDM_VS_CSCD_BUILD | IDG_VS_BUILD_CASCADE |
| IDG_VS_BUILD_PROJPICKER | ||
| IDM_VS_CSCD_REBUILD | IDG_VS_REBUILD_CASCADE | |
| IDG_VS_REBUILD_PROJPICKER | ||
| IDM_VS_CSCD_PROJONLY | IDG_VS_PROJONLY_CASCADE | |
| IDM_VS_CSCD_CLEAN | IDG_VS_CLEAN_CASCADE | |
| IDG_VS_CLEAN_PROJPICKER | ||
| IDM_VS_CSCD_DEPLOY | IDG_VS_DEPLOY_CASCADE | |
| IDG_VS_DEPLOY_PROJPICKER | ||
| IDG_VS_PGO_SELECTION | IDM_VS_CSCD_PGO_BUILD | IDG_VS_PGO_BUILD_CASCADE_BUILD |
| IDG_VS_PGO_BUILD_CASCADE_RUN |
比如,如果把"文件-新建(IDM_VS_CSCD_NEW)"作為父級,那么效果可以是:

發布Visual Studio擴展
參考
- 擴展 Visual Studio IDE
- Visual Studio擴展商店
- 演練:發布 Visual Studio 擴展
- 開始開發 Visual Studio 擴展
- 教程 - 編寫自己的第一個擴展:Hello World
- Visual Studio Extensibility Samples
- Community for developers of Visual Studio extensions
- 更新 Visual Studio 2022 Visual Studio擴展
- 從 packages.config 遷移到 PackageReference
- Learn to write Visual Studio extensions
