Visual Studio 可扩展性(第一天):创建你的第一个 Visual Studio VSIX 包






4.99/5 (35投票s)
本文重点介绍了如何创建 Visual Studio 扩展。
目录
引言
.NET 中的 Visual Studio 可扩展性功能并非新鲜事物。只是它们不太常用,这让我感到惊讶,因为 Visual Studio 可扩展性功能非常强大,它们为自定义提供了新的定义。自定义你的 IDE、自定义每个开发人员都希望拥有的所需功能,甚至可以对 IDE 进行自定义,最终可能催生一个全新的产品(例如,一个带有自身扩展和功能的自定义 Visual Studio)。
当我们谈论可扩展性时,这只不过是我们正在谈论的一个字面意思,可扩展性意味着为满足你的需求而添加更多功能或自定义任何产品的现有实现。
在这个为期三期的 Visual Studio 可扩展性系列文章中,我们将学习如何创建新的 Visual Studio 包,将其部署到暂存服务器,并通过持续集成设置将其部署到 GIT,最后,创建一个嵌入该包的 Visual Studio 独立 Shell 应用程序。这是一个非常罕见的主题,你可能在网上找不到足够多的关于如何分步操作的教学材料。MSDN 包含很好的内容,但非常通用,直奔主题。在我的文章中,我将尝试逐步解释每一个小部分,以便人们可以在编码时学习。
VSIX 包
VSIX 包,也就是 Visual Studio 包,使我们开发人员能够根据我们的需要和要求自定义 Visual Studio。作为开发人员,人们总是希望他们工作的 IDE 除内置功能外,还应具备某些特定功能。你可以在此处阅读更多关于 VSIX 包的理论方面和详细信息。以下是来自同一 MSDN 链接的一个小定义:
“VSIX 包是一个 .vsix 文件,其中包含一个或多个 Visual Studio 扩展,以及 Visual Studio 用于对扩展进行分类和安装的元数据。这些元数据包含在 VSIX 清单和 [Content_Types].xml 文件中。VSIX 包可能还包含一个或多个 Extension.vsixlangpack 文件,以提供本地化的安装文本,并且可能包含其他 VSIX 包来安装依赖项。
VSIX 包格式遵循 OPC(Open Packaging Conventions)标准。包包含二进制文件和支持文件,以及一个 [Content_Types].xml 文件和一个 .vsix 清单文件。一个 VSIX 包可能包含多个项目的输出,甚至包含有自己清单的多个包。”
Visual Studio 可扩展性的强大功能使我们能够创建自己的扩展和包,我们可以在现有的 Visual Studio 之上构建,甚至可以在 Visual Studio 市场https://marketplace.visualstudio.com/上分发/销售这些扩展。例如,我找不到在 Visual Studio 中比较两个文件的选项,所以我创建了自己的 Visual Studio 扩展来在 Visual Studio 中比较两个文件。该扩展可以从https://marketplace.visualstudio.com/items?itemName=vs-publisher-457497.FileComparer下载。类似地,在本文中,我将解释如何创建 Visual Studio 扩展来在 Windows 资源管理器中打开选定的文件。你可能已经看到,我们已经有了一个直接从 Visual Studio 中在 Windows 资源管理器中打开选定的项目/文件夹的功能,但是,如果能够实现右键单击文件也能在 Windows 资源管理器中打开选定文件的功能,那不是很酷吗?所以,基本上,我们为自己创建扩展,或者我们可以为团队成员创建扩展,或者根据项目需求,甚至是为了好玩和探索技术。
路线图
让我们更细致地规划一下,定义一个路线图,以实现一个正常工作的自定义 Visual Studio 独立 Shell 应用程序。该系列将分为以下三篇文章,我们将更多地关注实际操作和动手实践,而不是过多地深入理论。
- Visual Studio 可扩展性(第一天):创建你的第一个 Visual Studio VSIX 包
- Visual Studio 可扩展性(第二天):通过持续集成将 VSIX 包部署到暂存服务器和 GIT
- Visual Studio 可扩展性(第三天):将 VSIX 包嵌入 Visual Studio 独立 Shell
先决条件
在处理可扩展性项目时,我们需要注意一些先决条件。如果你安装了 Visual Studio 2015,请转到“控制面板”->“程序和功能”,搜索“Visual Studio 2015”,右键单击并选择“**更改**”选项。
在这里,我们需要启用 Visual Studio 可扩展性功能来处理此项目类型。在下一个屏幕上,单击“**修改**”,现在将显示所有选定/未选定功能的列表,我们所需要做的就是在“**功能**”->“**通用工具**”下,选择**Visual Studio Extensibility Tools Update 3**,如下图所示。
现在按“**更新**”按钮,让 Visual Studio 更新到可扩展性功能,之后我们就可以开始工作了。
在我们开始之前,我需要本文的读者从https://marketplace.visualstudio.com/items?itemName=MadsKristensen.ExtensibilityTools下载并安装 Mads Kristensen 编写的可扩展性工具。
本文系列还深受 Mads Kristensen 在 Build 2016 上的演讲以及他在 Visual Studio 可扩展性方面工作的影响。
创建 VSIX 包
现在我们可以在 Visual Studio 中创建自己的 VSIX 包了。我们将逐步进行,因此会捕获每一个细微的步骤并予以考虑。如前所述,我们将尝试创建一个允许我们在 Windows 资源管理器中打开选定 Visual Studio 文件的扩展。基本上,就是如下图所示的功能。
步骤 1:创建 VSIX 项目
让我们从最基础的开始。打开你的 Visual Studio。我使用的是 Visual Studio 2015 Enterprise 版,并建议至少使用 Visual Studio 2015 来阅读本文。
像创建其他 Visual Studio 项目一样,创建一个新项目。选择 **文件** -> **新建** -> **项目**。
现在在“**模板**”中,导航到“**可扩展性**”,然后选择“**VSIX 项目**”。请注意,此处显示这些模板是因为我们修改了 Visual Studio 配置以使用 Visual Studio 可扩展性。选择“**VSIX 项目**”并为其命名。例如,我将其命名为“LocateFolder
”。
新项目创建后,将显示一个“**入门**”页面,其中包含大量关于 Visual Studio 可扩展性的信息和更新。这些是 MSDN 和有用资源的链接,你可以探索它们以了解更多关于可扩展性的几乎所有内容。我们得到了一个具有默认结构的起始项目,其中包含一个 HTML 文件、一个 CSS 文件和一个 vsixmanifest 文件。清单文件(顾名思义)保存了与项目中创建的扩展相关的所有信息,并且该文件实际上可以称为项目中创建的扩展的清单。
我们可以清楚地看到,“**入门**”页面来自 index.html 文件,该文件使用了 stylesheet.css。所以在这个项目中,我们实际上不需要这些文件,可以删除它们。
现在,我们只剩下清单文件。所以从技术上讲,我们的第一步已经完成,我们创建了一个 VSIX 项目。
步骤 2:配置清单文件
当我们打开清单文件时,我们会看到我们添加的项目类型的某些相关信息。我们可以根据自己的选择修改此清单文件以满足我们的扩展需求。例如,在 ProductID
中,我们可以删除前缀于 GUID 的文本,只保留 GUID。请注意,GUID 是必需的,因为 VSIX 项目中的所有项目链接都是通过 GUID 完成的。稍后我们将更详细地介绍这一点。
同样,在**描述**框中添加一个有意义的描述,例如“帮助在 Windows 资源管理器中定位文件和文件夹”。此描述是必需的,因为它解释了你的扩展的用途。
如果你通过右键单击并选择“查看代码”或直接按 **F7** 来打开清单文件的代码,你将看到一个在后台创建的 XML 文件,所有这些信息都以一种定义良好的 XML 格式保存。
<?xml version="1.0" encoding="utf-8"?>
<PackageManifest Version="2.0.0"
xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011"
xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
<Metadata>
<Identity Id="106f5189-471d-40ab-9de2-687c0a3d98e4" Version="1.0"
Language="en-US" Publisher="Akhil Mittal" />
<DisplayName>LocateFolder</DisplayName>
<Description xml:space="preserve">
Helps to locate files and folder in windows explorer.Helps </Description>
<Tags>file locator, folder locator, open file in explorer</Tags>ption>
</Metadata>
<Installation>
<InstallationTarget Id="Microsoft.VisualStudio.Community" Version="[14.0]" />
</Installation>
<Dependencies>
<Dependency Id="Microsoft.Framework.NDP" DisplayName="Microsoft .NET Framework"
d:Source="Manual" Version="[4.5,)" />
</Dependencies>
</PackageManifest>
步骤 3:添加自定义命令
我们已成功添加了一个新项目并配置了其清单文件,但真正的工作仍在进行中,那就是编写用于定位文件的扩展。为此,我们需要向项目中添加一个新项,所以只需右键单击项目并从项目模板中选择“添加新项”。
一旦打开项目模板,你就会在“**Visual C# 项**”->“**可扩展性**”下看到添加新自定义命令的选项。自定义命令在 VSIX 扩展中充当按钮。这些按钮帮助我们将操作绑定到其单击事件,因此我们可以将所需的��能添加到此按钮/命令。为你添加的自定义命令命名,例如,我将其命名为“LocateFolderCommand
”,然后按“**添加**”,如下图所示。
添加命令后,我们可以看到我们现有项目中发生了很多变化。例如,添加了一些必需的 NuGet 包,一个包含图标和图像的 Resources 文件夹,一个 .vsct 文件,一个 .resx 文件,以及一个 command 和 CommandPackage.cs 文件。
这里每个文件都有其自身的意义。在本教程中,我们将涵盖所有这些细节。
当我们打开 LocateFolderCommandPackage.vsct 文件时,我们再次看到一个 XML 文件。
当你删除所有注释以使其更易读时,你将得到一个如下所示的文件。
<?xml version="1.0" encoding="utf-8"?>
<CommandTable xmlns="http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<Extern href="stdidcmd.h"/>
<Extern href="vsshlids.h"/>
<Commands package="guidLocateFolderCommandPackage">
<Groups>
<Group guid="guidLocateFolderCommandPackageCmdSet" id="MyMenuGroup" priority="0x0600">
<Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS"/>
</Group>
</Groups>
<Buttons>
<Button guid="guidLocateFolderCommandPackageCmdSet" id="LocateFolderCommandId"
priority="0x0100" type="Button">
<Parent guid="guidLocateFolderCommandPackageCmdSet" id="MyMenuGroup" />
<Icon guid="guidImages" id="bmpPic1" />
<Strings>
<ButtonText>Invoke LocateFolderCommand</ButtonText>
</Strings>
</Button>
</Buttons>
<Bitmaps>
<Bitmap guid="guidImages" href="Resources\LocateFolderCommand.png"
usedList="bmpPic1, bmpPic2, bmpPicSearch, bmpPicX,
bmpPicArrows, bmpPicStrikethrough"/>
</Bitmaps>
</Commands>
<Symbols>
<GuidSymbol name="guidLocateFolderCommandPackage"
value="{a7836cc5-740b-4d5a-8a94-dc9bbc4f7db1}" />
<GuidSymbol name="guidLocateFolderCommandPackageCmdSet"
value="{031046af-15f9-44ab-9b2a-3f6cad1a89e3}">
<IDSymbol name="MyMenuGroup" value="0x1020" />
<IDSymbol name="LocateFolderCommandId" value="0x0100" />
</GuidSymbol>
<GuidSymbol name="guidImages" value="{8ac8d2e1-5ef5-4ad7-8aa6-84da2268566a}" >
<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>
</Symbols>
</CommandTable>
因此,主要文件包含组、按钮(即组中的命令)、按钮文本以及一些 IDSymbol
和图像选项。
当我们谈论“**组**”时,它是 Visual Studio 中显示的命令的分组。就像在下图所示的 Visual Studio 中,当你单击“**调试**”时,你会看到各种命令,如“**窗口**”、“**图形**”、“**开始调试**”等,其中一些由水平线分隔。这些分隔的水平线就是组。所以组是包含命令并充当命令之间逻辑分隔的东西。在 VSIX 项目中,我们可以创建新的自定义命令,还可以定义它将关联的组,我们可以创建新组,也可以扩展现有组,如下图所示的 .vsct XML 文件。
步骤 4:配置自定义命令
所以,首先打开 vsct 文件,让我们决定我们的命令将放在哪里。我们基本上希望我们的命令在右键单击解决方案资源管理器中的任何文件时可见。为此,在 .vsct 文件中,你可以指定命令的父级,因为这是一个项节点,我们可以选择 IDM_VS_CTXT_ITEMNODE
。
你可以在此链接上查看所有可用的位置。
同样,我们也可以创建菜单、子菜单和子项,但现在,我们将坚持我们的目标,并将命令放置在项节点上。
同样,我们也可以定义命令显示的��置。在组中设置优先级,默认情况下,它显示在第 6 位,如下图所示,但你可以随时更改它。例如,我将优先级更改为 0X0200
,以便在顶部第二位看到我的命令。
你还可以将默认按钮文本更改为“**在文件资源管理器中打开**”,最后,在所有修改之后,我们的 XML 将如下所示。
<?xml version="1.0" encoding="utf-8"?>
<CommandTable xmlns="http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<Extern href="stdidcmd.h"/>
<Extern href="vsshlids.h"/>
<Commands package="guidLocateFolderCommandPackage">
<Groups>
<Group guid="guidLocateFolderCommandPackageCmdSet" id="MyMenuGroup" priority="0x0200">
<Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_ITEMNODE"/>
</Group>
</Groups>
<Buttons>
<Button guid="guidLocateFolderCommandPackageCmdSet" id="LocateFolderCommandId"
priority="0x0100" type="Button">
<Parent guid="guidLocateFolderCommandPackageCmdSet" id="MyMenuGroup" />
<Icon guid="guidImages" id="bmpPic1" />
<Strings>
<ButtonText>Open in File Explorer</ButtonText>
</Strings>
</Button>
</Buttons>
<Bitmaps>
<Bitmap guid="guidImages" href="Resources\LocateFolderCommand.png"
usedList="bmpPic1, bmpPic2, bmpPicSearch, bmpPicX,
bmpPicArrows, bmpPicStrikethrough"/>
</Bitmaps>
</Commands>
<Symbols>
<GuidSymbol name="guidLocateFolderCommandPackage"
value="{a7836cc5-740b-4d5a-8a94-dc9bbc4f7db1}" />
<GuidSymbol name="guidLocateFolderCommandPackageCmdSet"
value="{031046af-15f9-44ab-9b2a-3f6cad1a89e3}">
<IDSymbol name="MyMenuGroup" value="0x1020" />
<IDSymbol name="LocateFolderCommandId" value="0x0100" />
</GuidSymbol>
<GuidSymbol name="guidImages" value="{8ac8d2e1-5ef5-4ad7-8aa6-84da2268566a}" >
<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>
</Symbols>
</CommandTable>
当我们打开 LocateFolderCommand.cs 时,这正是我们需要放置逻辑的实际位置。在 VS 可扩展性项目/命令中,所有内容都通过 GUID 进行处理和连接。在这里,我们在下图看到一个 commandset
是用一个新的 GUID 创建的。
现在当你向下滚动时,你会看到在私有构造函数中,我们获取了从当前服务提供商获取的命令服务。该服务负责添加命令,前提是命令具有有效的 menuCommandId
,并定义了 commandSet
和 commandId
。
我们还看到有一个回调方法绑定到命令。这是命令被调用时调用的相同回调方法,这是放置逻辑的最佳位置。默认情况下,此回调方法带有一个默认实现,用于显示一个消息框,证明命令已被实际调用。
现在我们先保留默认实现,然后尝试测试应用程序。稍后我们可以添加业务逻辑来在 Windows 资源管理器中打开文件。
步骤 5:使用默认实现测试自定义命令
有人可能想知道如何测试默认实现。我会说,只需编译并运行应用程序。应用程序通过 **F5** 运行后,将启动一个类似于 Visual Studio 的新窗口,如下图所示。
请注意,我们正在为 Visual Studio 创建一个扩展,因此理想情况下,应该在 Visual Studio 本身中对其进行测试,以了解其外观和工作方式。启动一个新的 Visual Studio 实例来测试命令。请注意,此 Visual Studio 实例称为实验实例。顾名思义,这是用于测试我们的实现,基本上是检查事物将如何工作和外观。
在启动的实验实例中,像在普通 Visual Studio 中一样添加一个新项目。请注意,此实验实例中的所有功能都可以根据需要进行配置和切换开启/关闭。我们可以在我的第三篇文章中讨论这些细节,届时我们将讨论 Visual Studio 独立 Shell。
简单来说,选择一个新的控制台应用程序,并为其命名。我将其命名为“**Sample**”。
将项目添加到解决方案资源管理器后,我们会看到一个常见的项目结构。请记住,我们的功能是向 Visual Studio 解决方案资源管理器中的选定文件添加一个命令。现在我们可以测试我们的实现,只需右键单击任何文件,你就可以在上下文菜单的新组中看到“**在文件资源管理器中打开**”命令,如下图所示。文本来自我们在 VSCT 文件中为命令定义的文本。
在单击命令之前,在命令文件中的 MenuItemCallback
方法上设置断点。因此,当单击命令时,你可以看到 menuItemCallback
方法被调用。
由于此方法包含显示消息框的代码,只需按 **F5**,你就会看到一个带有定义标题的消息框,如下图所示。
这证明了我们的命令有效,我们只需要在这里放入正确的逻辑。此时,我们肯定可以休息一下并庆祝一下。
步骤 6:添加实际实现
现在是时候添加我们的实际实现了。我们已经知道地方了,只需要编写代码。对于实际实现,我在项目中添加了一个新文件夹,并将其命名为 Utilities,在该文件夹中添加了一个名为 LocateFile.cs 的类,其实现如下。
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
namespace LocateFolder.Utilities
{
internal static class LocateFile
{
private static Guid IID_IShellFolder = typeof(IShellFolder).GUID;
private static int pointerSize = Marshal.SizeOf(typeof(IntPtr));
public static void FileOrFolder(string path, bool edit = false)
{
if (path == null)
{
throw new ArgumentNullException("path");
}
IntPtr pidlFolder = PathToAbsolutePIDL(path);
try
{
SHOpenFolderAndSelectItems(pidlFolder, null, edit);
}
finally
{
NativeMethods.ILFree(pidlFolder);
}
}
public static void FilesOrFolders(IEnumerable<FileSystemInfo> paths)
{
if (paths == null)
{
throw new ArgumentNullException("paths");
}
if (paths.Count<FileSystemInfo>() != 0)
{
foreach (
IGrouping<string, FileSystemInfo> grouping in
from p in paths group p by Path.GetDirectoryName(p.FullName))
{
FilesOrFolders(Path.GetDirectoryName
(grouping.First<FileSystemInfo>().FullName),
(from fsi in grouping select fsi.Name).ToList<string>());
}
}
}
public static void FilesOrFolders(IEnumerable<string> paths)
{
FilesOrFolders(PathToFileSystemInfo(paths));
}
public static void FilesOrFolders(params string[] paths)
{
FilesOrFolders((IEnumerable<string>)paths);
}
public static void FilesOrFolders
(string parentDirectory, ICollection<string> filenames)
{
if (filenames == null)
{
throw new ArgumentNullException("filenames");
}
if (filenames.Count != 0)
{
IntPtr pidl = PathToAbsolutePIDL(parentDirectory);
try
{
IShellFolder parentFolder = PIDLToShellFolder(pidl);
List<IntPtr> list = new List<IntPtr>(filenames.Count);
foreach (string str in filenames)
{
list.Add(GetShellFolderChildrenRelativePIDL(parentFolder, str));
}
try
{
SHOpenFolderAndSelectItems(pidl, list.ToArray(), false);
}
finally
{
using (List<IntPtr>.Enumerator enumerator2 = list.GetEnumerator())
{
while (enumerator2.MoveNext())
{
NativeMethods.ILFree(enumerator2.Current);
}
}
}
}
finally
{
NativeMethods.ILFree(pidl);
}
}
}
private static IntPtr GetShellFolderChildrenRelativePIDL
(IShellFolder parentFolder, string displayName)
{
uint num;
IntPtr ptr;
NativeMethods.CreateBindCtx();
parentFolder.ParseDisplayName
(IntPtr.Zero, null, displayName, out num, out ptr, 0);
return ptr;
}
private static IntPtr PathToAbsolutePIDL(string path) =>
GetShellFolderChildrenRelativePIDL(NativeMethods.SHGetDesktopFolder(), path);
private static IEnumerable<FileSystemInfo> PathToFileSystemInfo
(IEnumerable<string> paths)
{
foreach (string iteratorVariable0 in paths)
{
string path = iteratorVariable0;
if (path.EndsWith(Path.DirectorySeparatorChar.ToString()) ||
path.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
{
path = path.Remove(path.Length - 1);
}
if (Directory.Exists(path))
{
yield return new DirectoryInfo(path);
}
else
{
if (!File.Exists(path))
{
throw new FileNotFoundException
("The specified file or folder doesn't exists : " + path, path);
}
yield return new FileInfo(path);
}
}
}
private static IShellFolder PIDLToShellFolder(IntPtr pidl) =>
PIDLToShellFolder(NativeMethods.SHGetDesktopFolder(), pidl);
private static IShellFolder PIDLToShellFolder(IShellFolder parent, IntPtr pidl)
{
IShellFolder folder;
Marshal.ThrowExceptionForHR(parent.BindToObject
(pidl, null, ref IID_IShellFolder, out folder));
return folder;
}
private static void SHOpenFolderAndSelectItems
(IntPtr pidlFolder, IntPtr[] apidl, bool edit)
{
NativeMethods.SHOpenFolderAndSelectItems(pidlFolder, apidl, edit ? 1 : 0);
}
[ComImport, Guid("000214F2-0000-0000-C000-000000000046"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IEnumIDList
{
[PreserveSig, MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType = MethodCodeType.Runtime)]
int Next(uint celt, IntPtr rgelt, out uint pceltFetched);
[PreserveSig, MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType = MethodCodeType.Runtime)]
int Skip([In] uint celt);
[PreserveSig, MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType = MethodCodeType.Runtime)]
int Reset();
[PreserveSig, MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType = MethodCodeType.Runtime)]
int Clone([MarshalAs(UnmanagedType.Interface)] out IEnumIDList ppenum);
}
[ComImport, Guid("000214E6-0000-0000-C000-000000000046"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
ComConversionLoss]
internal interface IShellFolder
{
[MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType = MethodCodeType.Runtime)]
void ParseDisplayName(IntPtr hwnd,
[In, MarshalAs(UnmanagedType.Interface)] IBindCtx pbc,
[In, MarshalAs(UnmanagedType.LPWStr)] string pszDisplayName,
out uint pchEaten, out IntPtr ppidl,
[In, Out] ref uint pdwAttributes);
[PreserveSig, MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType = MethodCodeType.Runtime)]
int EnumObjects([In] IntPtr hwnd, [In] SHCONT grfFlags,
[MarshalAs(UnmanagedType.Interface)] out IEnumIDList ppenumIDList);
[PreserveSig, MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType = MethodCodeType.Runtime)]
int BindToObject([In] IntPtr pidl,
[In, MarshalAs(UnmanagedType.Interface)] IBindCtx pbc, [In] ref Guid riid,
[MarshalAs(UnmanagedType.Interface)] out IShellFolder ppv);
[MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType = MethodCodeType.Runtime)]
void BindToStorage([In] ref IntPtr pidl,
[In, MarshalAs(UnmanagedType.Interface)] IBindCtx pbc,
[In] ref Guid riid,
out IntPtr ppv);
[MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType = MethodCodeType.Runtime)]
void CompareIDs([In] IntPtr lParam,
[In] ref IntPtr pidl1, [In] ref IntPtr pidl2);
[MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType = MethodCodeType.Runtime)]
void CreateViewObject([In] IntPtr hwndOwner,
[In] ref Guid riid, out IntPtr ppv);
[MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType = MethodCodeType.Runtime)]
void GetAttributesOf([In] uint cidl,
[In] IntPtr apidl, [In, Out] ref uint rgfInOut);
[MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType = MethodCodeType.Runtime)]
void GetUIObjectOf([In] IntPtr hwndOwner,
[In] uint cidl, [In] IntPtr apidl, [In] ref Guid riid,
[In, Out] ref uint rgfReserved, out IntPtr ppv);
[MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType = MethodCodeType.Runtime)]
void GetDisplayNameOf([In] ref IntPtr pidl,
[In] uint uFlags, out IntPtr pName);
[MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType = MethodCodeType.Runtime)]
void SetNameOf([In] IntPtr hwnd, [In] ref IntPtr pidl,
[In, MarshalAs(UnmanagedType.LPWStr)] string pszName,
[In] uint uFlags, [Out] IntPtr ppidlOut);
}
private class NativeMethods
{
private static readonly int pointerSize = Marshal.SizeOf(typeof(IntPtr));
public static IBindCtx CreateBindCtx()
{
IBindCtx ctx;
Marshal.ThrowExceptionForHR(CreateBindCtx_(0, out ctx));
return ctx;
}
[DllImport("ole32.dll", EntryPoint = "CreateBindCtx")]
public static extern int CreateBindCtx_(int reserved, out IBindCtx ppbc);
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
public static extern IntPtr ILCreateFromPath
([In, MarshalAs(UnmanagedType.LPWStr)] string pszPath);
[DllImport("shell32.dll")]
public static extern void ILFree([In] IntPtr pidl);
public static IShellFolder SHGetDesktopFolder()
{
IShellFolder folder;
Marshal.ThrowExceptionForHR(SHGetDesktopFolder_(out folder));
return folder;
}
[DllImport("shell32.dll", EntryPoint = "SHGetDesktopFolder",
CharSet = CharSet.Unicode, SetLastError = true)
]
private static extern int SHGetDesktopFolder_(
[MarshalAs(UnmanagedType.Interface)] out IShellFolder ppshf);
public static void SHOpenFolderAndSelectItems
(IntPtr pidlFolder, IntPtr[] apidl, int dwFlags)
{
uint cidl = (apidl != null) ? ((uint)apidl.Length) : 0;
Marshal.ThrowExceptionForHR(SHOpenFolderAndSelectItems_
(pidlFolder, cidl, apidl, dwFlags));
}
[DllImport("shell32.dll", EntryPoint = "SHOpenFolderAndSelectItems")]
private static extern int SHOpenFolderAndSelectItems_([In]
IntPtr pidlFolder, uint cidl,
[In, Optional, MarshalAs(UnmanagedType.LPArray)] IntPtr[] apidl, int dwFlags);
}
[Flags]
internal enum SHCONT : ushort
{
SHCONTF_CHECKING_FOR_CHILDREN = 0x10,
SHCONTF_ENABLE_ASYNC = 0x8000,
SHCONTF_FASTITEMS = 0x2000,
SHCONTF_FLATLIST = 0x4000,
SHCONTF_FOLDERS = 0x20,
SHCONTF_INCLUDEHIDDEN = 0x80,
SHCONTF_INIT_ON_FIRST_NEXT = 0x100,
SHCONTF_NAVIGATION_ENUM = 0x1000,
SHCONTF_NETPRINTERSRCH = 0x200,
SHCONTF_NONFOLDERS = 0x40,
SHCONTF_SHAREABLE = 0x400,
SHCONTF_STORAGE = 0x800
}
}
}
这个类包含业务逻辑,主要是接收文件路径作为参数并与 shell 交互以在资源管理器中打开该文件的方法。我不会详细介绍这个类,而是更关注如何调用这个功能。
现在在 MenuItemCallBack
方法中,放入以下代码来调用我们实用程序类的该方法。
private void MenuItemCallback(object sender, EventArgs e)
{
var selectedItems = ((UIHierarchy)((DTE2)this.ServiceProvider.GetService
(typeof(DTE))).Windows.Item("{3AE79031-E1BC-11D0-8F78-00A0C9110057}").Object).
SelectedItems as object[];
if (selectedItems != null)
{
LocateFile.FilesOrFolders((IEnumerable<string>)(from t in selectedItems
where (t as UIHierarchyItem)?
.Object is ProjectItem
select ((ProjectItem)
((UIHierarchyItem)t).Object).
FileNames[1]));
}
}</string>
此方法现在首先使用 DTE 对象获取所有选定的项。使用 DTE 对象,你可以处理 Visual Studio 组件中的所有事务和操作。在此处阅读更多关于 DTE 对象强大功能的信息。
在获取选定的项后,我们调用实用程序类的 FilesOrFolders
方法,并将文件路径作为参数传递。工作完成。现在再次启动实验实例并检查功能。
步骤 7:测试实际实现
启动实验实例,添加一个新项目或现有项目,右键单击任何文件并调用命令。
调用命令后,你会看到文件夹在 Windows 资源管理器中打开,并选中了该文件,如下图所示。
此功能也适用于 Visual Studio 中的链接文件。让我们来检查一下。在实验实例中打开的项目中添加一个新项,并将一个文件添加为链接,如下图所示。
添加文件时,你只需要选择“**添加为链接**”。然后该文件将在 Visual Studio 中显示,并带有一个不同的图标,表明这是一个链接文件。现在选择实际的 Visual Studio 文件和 Visual Studio 中的链接文件,然后调用命令。
调用命令时,你可以看到两个文件夹被打开,并且两个文件都已在各自的位置被选中。
不仅如此,由于我们创建了这个扩展,在实验实例的“扩展和更新”中,你可以搜索这个扩展,你会在 Visual Studio 中找到它已安装,如下图所示。
现在是再次庆祝的时候了。
步骤 7:优化包
我们的工作已接近尾声,但还有一些更重要的事情需要我们注意。我们需要使这个包更具吸引力,添加一些图像/图标到扩展,并优化项目结构,使其更易于阅读和理解。
还记得我们开始本教程时提到的下载和安装 VS 可扩展性工具吗?VS 可扩展性工具提供了一些很棒的功能,你可以真正利用它们。例如,它允许你导出 Visual Studio 中所有可用的图像。我们可以使用这些图像来制作我们的扩展的图标和默认图像。首先,在 Visual Studio 中编写代码的地方,转到“**工具**”->“**导出图像标识符…**”。
将打开一个窗口来搜索你需要选择的图像。搜索“**打开**”,你将获得与项目中用于在 Windows 资源管理器中打开项目的上下文菜单相同的图像。
我们将仅为我们的扩展使用此图像。将其大小设置为 16*16 并单击“**导出**”,然后将其保存在项目的 Resources 文件夹中。用这个新导出的文件替换已有的 LocateFolderCommand.png 文件,并给它相同的名称。因为在 vsct 文件中,它被定义为第一个图标使用之前的图像精灵,所以我们总是会在自定义命令文本旁边看到 1X,但现在我们需要一个好看的有意义的图像,所以我们导出了这个“在资源管理器中打开”的图像。
现在转到 .vsct 文件,在 Bitmaps 中,从 usedList
中删除 bmpPic1
之外的所有图像名称,并在 GuidSymbol
中删除 bmpPic1
之外的所有 IDsymbol
,如下图所示。我们不需要更改 Bitmap 节点中的 href,因为我们用同名的新导出图像替换了现有的图像。我们这样做是因为我们不再使用那个旧的默认图像精灵,而是使用我们新导出的图像。
在这种情况下,LocateFolderCommandPackage.vsct 文件将如下所示。
<?xml version="1.0" encoding="utf-8"?>
<CommandTable xmlns="http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<Extern href="stdidcmd.h"/>
<Extern href="vsshlids.h"/>
<Commands package="guidLocateFolderCommandPackage">
<Groups>
<Group guid="guidLocateFolderCommandPackageCmdSet" id="MyMenuGroup" priority="0x0200">
<Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_ITEMNODE"/>
</Group>
</Groups>
<Buttons>
<Button guid="guidLocateFolderCommandPackageCmdSet" id="LocateFolderCommandId"
priority="0x0100" type="Button">
<Parent guid="guidLocateFolderCommandPackageCmdSet" id="MyMenuGroup" />
<Icon guid="guidImages" id="bmpPic1" />
<Strings>
<ButtonText>Open in File Explorer</ButtonText>
</Strings>
</Button>
</Buttons>
<Bitmaps>
<Bitmap guid="guidImages"
href="Resources\LocateFolderCommand.png" usedList="bmpPic1"/>
</Bitmaps>
</Commands>
<Symbols>
<GuidSymbol name="guidLocateFolderCommandPackage"
value="{a7836cc5-740b-4d5a-8a94-dc9bbc4f7db1}" />
<GuidSymbol name="guidLocateFolderCommandPackageCmdSet"
value="{031046af-15f9-44ab-9b2a-3f6cad1a89e3}">
<IDSymbol name="MyMenuGroup" value="0x1020" />
<IDSymbol name="LocateFolderCommandId" value="0x0100" />
</GuidSymbol>
<GuidSymbol name="guidImages" value="{8ac8d2e1-5ef5-4ad7-8aa6-84da2268566a}" >
<IDSymbol name="bmpPic1" value="1" />
</GuidSymbol>
</Symbols>
</CommandTable>
下一步是设置扩展图像和预览图像,这些图像将在 Visual Studio 库和 Visual Studio 市场中显示。这些图像将在任何地方代表扩展。
因此,遵循与之前相同的导出图像的常规步骤。请注意,你也可以使用自己的自定义图像进行所有与图像/图标相关的操作。
如前所述打开图像标识符,搜索 LocateAll
,然后导出两个图像,一个用于图标(90*90)。
另一个用于预览(175*175)。
将这两个图像分别导出为 Icon.png 和 Preview.png 并保存在 Resources 文件夹中。然后,在解决方案资源管理器中,将这两个图像包含到项目中,如下图所示。
现在,在 source.extension.vsixmanifest 文件中,将 Icon 和 Preview 图像设置为与导出的图像相同,如下图所示。
步骤 8:测试最终包
现在又到了测试具有新图像和图标的实现的时候了。所以编译项目并按 **F5**,实验实例将会启动。添加一个新项目或现有项目,右键单击任何项目文件以查看你的自定义命令。
所以现在,我们得到了之前从 Image Moniker 为此自定义命令选择的图标。由于我们没有改变功能,它应该像以前一样正常工作。
现在转到“扩展和更新”,搜索已安装的扩展“LocateFolder
”。你会在扩展名前看到一个漂亮的图像,这是尺寸为 90*90 的图像,在右侧面板中,你可以看到放大的 175*175 预览图像。
现在我们可以肯定地庆祝了,因为任务已完全完成。
结论
这篇详细的文章重点介绍了如何创建 Visual Studio 扩展。在下一篇文章中,我将解释如何优化项目结构以使其更易于阅读和理解,以及如何通过持续集成和 GIT 将扩展部署到 Visual Studio 市场。基本思路是优化结构,将代码推送到 GIT,通过 AppVeyor 的持续集成将扩展推送到 Visual Studio 库,并将扩展推送到 Visual Studio 市场。我希望本文能帮助你理解 Visual Studio 可扩展性。请随时分享反馈、评分和评论。
参考资料
完整源代码
在 Marketplace 中的扩展
历史
- 2017 年 2 月 8 日:初始版本