目录
介绍
Visual Studio扩展性功能在.NET中并不新鲜。只是它们不是很常用,这对我来说是一个惊喜,因为Visual Studio可扩展性功能非常强大,以至于它们为自定义提供了新的定义。自定义您的IDE,自定义每个开发人员都希望拥有的所需功能,甚至在IDE上进行自定义,最终可能会产生一个全新的产品(例如,具有自己的扩展和功能的自定义Visual Studio)。
当我们谈论可扩展性时,这只不过是我们正在谈论的一个字面术语,可扩展性意味着添加更多功能或自定义任何产品的现有实现以满足您的需求。
在这三篇关于Visual Studio Extensibility的系列文章中,我们将学习如何创建一个新的Visual Studio包,通过持续集成设置将其部署到暂存服务器和GIT上,最后,使用该嵌入式包创建一个Visual Studio隔离的Shell应用程序。这是一个非常罕见的主题,您可能在网上找不到足够的关于这个主题的学习材料来解释如何逐步使用它。MSDN包含良好的内容,但非常通用,而且切中要害。在我的文章中,我将尝试逐步解释每个小部分,以便人们可以在编码时学习。
VSIX软件包
作为Visual Studio软件包的VSIX包使我们作为开发人员能够根据自己的需要和要求灵活地自定义Visual Studio。作为开发人员,人们总是希望他正在使用的IDE除了内置功能之外,还应该具有某些功能。您可以在此处阅读有关理论方面和了解VSIX包详细信息的更多信息。以下是来自同一MSDN链接的小定义:
“VSIX包是一个.vsix文件,其中包含一个或多个Visual Studio扩展,以及Visual Studio用于分类和安装扩展的元数据。该元数据包含在VSIX清单和[Content_Types].xml文件中。VSIX包还可能包含一个或多个Extension.vsixlangpack文件以提供本地化的安装文本,并且可能包含用于安装依赖项的其他VSIX包。
VSIX包格式遵循开放打包约定(OPC)标准。该包包含二进制文件和支持文件,以及[Content_Types].xml文件和.vsix清单文件。一个VSIX包可能包含多个项目的输出,甚至包含具有自己的清单的多个包。"
Visual Studio可扩展性的强大功能使我们有机会创建自己的扩展和包,我们可以在现有的Visual Studio之上构建这些扩展和包,甚至可以在Visual StudioMarketplace Extensions for Visual Studio family of products | Visual Studio Marketplace 分发/销售它们。例如,我在Visual Studio中找不到比较两个文件的选项,所以我创建了自己的Visual Studio扩展来比较Visual Studio中的两个文件。可以从 File Comparer - Visual Studio Marketplace 下载该扩展。以类似的方式,在本文中,我将解释如何在Visual Studio中创建扩展以在Windows资源管理器中打开所选文件。您一定已经看到我们已经具有直接从Visual Studio在Windows资源管理器中打开所选项目/文件夹的功能,但是获得右键单击文件时也会在Windows资源管理器中打开所选文件的功能,这不是很酷吗?所以基本上,我们为自己创建扩展,或者我们可以为我们的团队成员创建一个扩展,或者根据项目的要求,甚至为了娱乐和探索技术。
路线图
让我们更加隔离并定义一个路线图,以实现来自Visual Studio的适当工作的自定义隔离shell应用程序。如下所述,该系列将分为三篇文章,我们将更多地关注实际实现和动手操作,而不是过多地研究理论。
- Visual Studio扩展性(第1天):创建您的第一个Visual Studio VSIX包
- Visual Studio扩展性(第2天):通过持续集成在暂存服务器和GIT上部署 VSI包
- Visual Studio扩展性(第3天):在Visual Studio隔离shell中嵌入VSIX包
先决条件
在处理可扩展性项目时,我们需要注意某些先决条件。如果您安装了Visual Studio 2015,请转到控制面板->程序和功能并搜索Visual Studio 2015并右键单击它以选择“更改”选项:
在这里,我们需要启用Visual Studio扩展性功能来处理此项目类型。在下一个屏幕上,单击“修改”,所有选定/未选定功能的列表现在可用,我们需要做的就是在“功能”->“常用工具”中,选择Visual Studio扩展性工具更新3,如下图所示:
现在按下“更新”按钮,让Visual Studio更新到扩展性功能,之后我们就可以开始了。
在我们真正开始之前,我需要本文的读者从 Extensibility Tools - Visual Studio Marketplace 下载由Mads Kristensen编写的安装可扩展性工具。
本系列文章还受到Mads Kristensen在Build 2016上的演讲以及他在Visual Studio可扩展性方面的工作的启发。
创建VSIX包
现在我们可以在Visual Studio中创建自己的VSIX包。我们将一步一步地进行,因此捕捉每一分钟的步骤并考虑到这一点。正如我之前提到的,我们将尝试创建一个扩展,允许我们在Windows资源管理器中打开选定的Visual Studio文件。基本上,下图中显示的内容:
步骤 1:创建VSIX项目
让我们从最基本的开始。打开您的Visual Studio。我使用的是Visual Studio 2015企业版,并建议至少在本文中使用Visual Studio 2015。
创建一个新项目,就像我们在Visual Studio中创建所有其他项目一样。选择“文件->新建>项目”。
现在,在“模板”中,导航到“扩展性”并选择“VSIX项目”。请注意,此处显示这些模板是因为我们修改了Visual Studio配置以使用Visual Studio扩展性。选择“VSIX项目”并为其命名。例如,我给它起了名字“LocateFolder”。
创建新项目后,将显示一个“入门”页面,其中包含有关Visual Studio扩展性的大量信息和更新。这些是指向MSDN和有用资源的链接,您可以浏览这些资源以了解有关扩展性的更多信息和几乎所有内容。我们的项目有一个默认结构,有一个HTML文件,一个CSS文件和一个vsixmanifest文件。清单文件(顾名思义)保存与VSIX项目相关的所有信息,此文件实际上可以称为项目中创建的扩展的清单。
我们可以清楚地看到,“入门”页面来自使用stylesheet.css的index.html文件。所以在我们的项目中,我们真的不需要这些文件,我们可以删除这些文件。
现在,我们只剩下清单文件。所以从技术上讲,我们的第一步已经完成,我们创建了一个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”,然后按添加,如下图所示:
添加命令后,我们可以看到现有项目发生了很多更改。就像添加一些必需的nugget包、带有图标和图像的资源文件夹、.vsct 文件、.resx文件以及命令和 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和图像选项。
当我们谈论“Groups”时,它是在Visual Studio中显示的一组命令。如下图所示,在Visual Studio中,单击“调试”时,会看到各种命令,如Windows,图形,启动调试等,有些命令也由水平线分隔。这些分隔的水平线是组。因此,组是保存命令的东西,并充当命令之间的逻辑分离。在VSIX项目中,我们可以创建一个新的自定义命令并定义它将关联的组,我们也可以创建新组或扩展现有组,如.vsct XML文件中所示。
步骤 4:配置自定义命令
因此,首先,打开vsct文件,让我们决定命令的放置位置。我们基本上希望当我们右键单击解决方案资源管理器中的任何文件时,我们的命令是可见的。为此,在 .vsct 文件中,您可以指定命令的父级,因为它是一个项目节点,我们可以选择IDM_VS_CTXT_ITEMNODE。
您可以在此链接中查看所有可用位置。
同样,我们也可以创建菜单、子菜单和子项,但现在,我们将坚持我们的目标并将我们的命令放在项节点上。
同样,我们也可以定义命令将显示的位置。在组中设置优先级,默认情况下,它显示为第六位,如下图所示,但您可以随时更改它。例如,我将优先级更改为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进行处理和连接。在这里,我们在下图中看到,是使用新的GUID创建的commandset。
现在,当您向下滚动时,您会在私有构造函数中看到,我们检索从当前服务提供程序获取的命令服务。此服务负责添加命令,前提是该命令有一个有效的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中,如下图所示:
现在是时候再次庆祝了。
步骤 8:优化包
我们的工作即将完成,但还有一些更重要的事情我们需要处理。我们需要使这个包更具吸引力,在扩展中添加一些图像/图标并优化项目结构以使其更具可读性和可理解性。
还记得我们开始本教程时,我提到下载并安装VS扩展性工具吗?VS扩展性工具提供了一些您可以真正利用的很酷的功能。例如,它允许您导出Visual Studio中的所有可用图像。我们可以使用这些图像来制作扩展的图标和默认图像。首先,在编写代码的Visual Studio中,转到“工具->导出图像名字对象..."
将打开一个窗口以搜索您需要选择的图像。搜索“打开”,您将获得与项目上下文菜单中所示相同的图像,以在Windows资源管理器中打开项目。
我们将仅将此图像用于扩展。为其指定大小16*16,然后单击导出,并将其保存在项目的 Resources 文件夹中。替换此文件中已经存在的 LocateFolderCommand.png 文件,并为此新导出的文件指定相同的名称。由于在vsct文件中,定义先前的图像冲刺必须与第一个图标一起使用,所以我们总是在自定义命令文本旁边看到1X,但现在我们需要一个好看的有意义的图像,所以我们导出了这个“在资源管理器中打开”图像。
现在转到.vsct文件,在位图中,首先从usedList中删除列表中除bmpPic1以外的所有图像名称,并在GuidSymbol中删除除bmpPic1以外的所有IDsymbol,如下图所示。我们不需要更改位图节点中的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 StudioMarketplace中为扩展显示。这些图像将代表所有位置的扩展。
因此,请遵循从图像名称导出图像的相同例程。请注意,您还可以使用自己的自定义映像执行所有与映像/图标相关的操作。
如前所述打开图像名字对象并搜索LocateAll,然后导出两个图像,一个用于图标(90X90)。
一个用于预览(175X175)。
在“资源”文件夹中分别导出名为“Icon.png和”Preview.png“的图像。然后在解决方案资源管理器中,将这两个图像包含在项目中,如下图所示:
现在,在 source.extension.vsixmanifest 文件中,将“图标”和“预览”图像设置为相同的导出图像,如下图所示:
步骤 9:测试最终包
同样,是时候使用新的图像和图标测试实现了。因此,编译项目并按 F5,实验实例将启动。添加新项目或现有项目,然后右键单击任何项目文件以查看自定义命令。
所以现在,我们得到了之前从图像名字对象中为此自定义命令选择的图标。由于我们没有接触该功能,因此它应该像以前一样正常工作。
现在转到扩展和更新并搜索已安装的扩展“LocateFolder”。在扩展之前,您会看到一张漂亮的图像,这是尺寸为90X90的相同图像,在右侧面板中,您可以看到放大的175X175预览图像。
现在我们当然可以庆祝任务完全完成。
结论
这篇详细的文章重点介绍了如何创建Visual Studio扩展。在下一篇文章中,我将解释如何优化项目结构以使其更具可读性和可理解性,以及如何通过持续集成和GIT将扩展部署到Visual Studio Market Place。基本思想是优化结构,将代码推送到GIT,通过 AppVeyor 持续集成将扩展推送到Visual Studio库,并将扩展推送到Visual StudioMarketplace。我希望这篇文章能帮助你理解Visual Studio的可扩展性。随时分享反馈、评分和评论。
引用
完整的源代码
Marketplace扩展
https://www.codeproject.com/Articles/1169776/Visual-Studio-Extensibility-Day-1-Creating-Your-Fi