In this three article series of Visual Studio Extensibility, we’ll learn how to create a new Visual Studio package, deploy that on staging server and GIT via continuous integration setup and at the end, create a Visual Studio isolated Shell application with that embedded package. This article explains how to create an extension in Visual Studio to open the selected file in Windows Explorer.
Table of Contents
Introduction
Visual Studio Extensibility features are not new in .NET. It’s just that they are not very commonly used, which to me is a surprise because Visual Studio extensibility features are so powerful that they give a new definition to customization. Customization of your IDE, customization of the desired features that every developer would love to have and even customizations on the IDE that could eventually result in a whole new product altogether (for example, a custom Visual Studio with one’s own extensions and features).
When we talk about extensibility, this is nothing but a literal term that we are talking about, extensibility means adding some more features or customizing the existing implementation of any product to fulfill your need.
In this three article series of Visual Studio Extensibility, we’ll learn how to create a new Visual Studio package, deploy that on staging server and GIT via continuous integration setup and at the end, create a Visual Studio isolated Shell application with that embedded package. This is a very rare topic and you might not find enough study material on this topic over the web that explains how to work with it step by step. MSDN contains good content but is very generic, and to the point. In my article, I'll try to explain each and every small part step by step, so that one can learn while coding.
VSIX Packages
VSIX packages that are Visual Studio packages give us as a developer the flexibility to customize Visual Studio as per our need and requirements. As a developer, one always wants that the IDE on which he is working should have certain features apart from the inbuilt one. You can read more about theoretical aspects and understanding the details of VSIX package here. The following is a small definition from the same MSDN link:
"A VSIX package is a .vsix file that contains one or more Visual Studio extensions, together with the metadata Visual Studio uses to classify and install the extensions. That metadata is contained in the VSIX manifest and the [Content_Types].xml file. A VSIX package may also contain one or more Extension.vsixlangpack files to provide localized setup text, and may contain additional VSIX packages to install dependencies.
The VSIX package format follows the Open Packaging Conventions (OPC) standard. The package contains binaries and supporting files, together with a [Content_Types].xml file and a .vsix manifest file. One VSIX package may contain the output of multiple projects, or even multiple packages that have their own manifests. "
The power of Visual Studio extensibility gives us that opportunity to create our own extensions and packages that we can build on top of existing Visual Studio and even distribute/sell those over the Visual Studio market place https://marketplace.visualstudio.com/. For example, I could not find an option in Visual Studio to compare two files, so I created my own Visual Studio extension to compare two files within Visual Studio. The extension could be downloaded from https://marketplace.visualstudio.com/items?itemName=vs-publisher-457497.FileComparer. In a similar way, in this article, I will explain how we can create an extension in Visual Studio to open the selected file in Windows Explorer. You must have seen that we already have a feature to open the selected project/folder in Windows Explorer directly from Visual Studio, but won’t it be cool to get the feature that, on right-clicking a file opens the selected file in Windows Explorer as well? So basically, we create the extensions for ourselves, or we can create an extension for our team members, or as per project’s requirement, or even for fun and to explore the technology.
Roadmap
Let's get more segregated and define a roadmap to achieve a proper working customized Isolated shell application from Visual Studio. The series will be divided into three articles as mentioned below, and we’ll focus more on practical implementations and hands-on rather than going much into theory.
- Visual Studio Extensibility (Day 1): Creating Your First Visual Studio VSIX Package
- Visual Studio Extensibility (Day 2): Deploying the VSIX Package on Staging Server and GIT via Continuous Integration
- Visual Studio Extensibility (Day 3): Embedding VSIX Package in Visual Studio Isolated Shell
Prerequisites
There are certain prerequisites that we need to take care of while working on extensibility projects. If you have Visual Studio 2015 installed, go to Control panel -> Program and features and search for Visual Studio 2015 and right click on it to select the "change" option:
Here, we need to enable Visual Studio extensibility feature to work on this project type. On the next screen, click on "Modify", a list of all selected/unselected features would be available now and all we need to do is in the Features -> Common Tools, select Visual Studio Extensibility Tools Update 3 as shown in the following image:
Now press the Update button and let Visual Studio update to extensibility features after which we are good to go.
Before we actually start, I need the readers of this article to download install Extensibility tools written by Mads Kristensen from https://marketplace.visualstudio.com/items?itemName=MadsKristensen.ExtensibilityTools.
This article series is also highly inspired with Mads Kristensen’s speech at Build 2016 and his work on Visual Studio extensibility.
Create VSIX Package
Now we can create our own VSIX package inside Visual Studio. We’ll go step by step, therefore capturing every minute step and taking that into account. As I mentioned earlier, we’ll try to create an extension that allows us to open the selected Visual Studio file in Windows Explorer. Basically, something that is shown in the below image:
Step 1: Create a VSIX Project
Let’s start from the very basic. Open your Visual Studio. I am using Visual Studio 2015 Enterprise edition and would recommend at least using Visual Studio 2015 for this article.
Create a new project like we create every other project in Visual Studio. Select File->New->Project.
Now in the Templates, navigate to Extensibility and select VSIX project. Note that these templates are shown here because we modified Visual Studio configuration to use Visual Studio Extensibility. Select VSIX project and give it a name. For example, I gave it the name "LocateFolder
".
As soon as the new project is created, a "Getting Started" page would be displayed with a lot of information and updates on Visual Studio extensibility. These are links to MSDN and useful resources that you can explore to learn more and almost everything about extensibility. We got our project with a default structure to start with which has an HTML file, a CSS file and a vsixmanifest file. A manifest file (as the name suggests) keeps all the information related to the VSIX project and this file actually can be called a manifest to the extension created in the project.
We can clearly see that the "Getting Started" page comes from the index.html file which uses stylesheet.css. So in our project, we really don’t need these files and we can remove these files.
And now, we are only left with the manifest file. So technically speaking, our step one has been accomplished, and we created a VSIX project.
Step 2: Configure Manifest file
When we open the manifest file, we see certain kinds of related information for the type of project that we added. We can modify this manifest file as per our choice for our extension. For example, in the ProductID
, we can remove the text that is prefixed to the GUID and only keep the GUID. Note that GUID is necessary as all the linking of items is done via GUID in VSIX projects. We’ll see this in more detail later.
Similarly, add a meaningful description in the Description box like "Helps to locate files and folder in windows explorer." This description is necessary as it explains what your extension is for.
And if you look at the code of the manifest file by selecting the file, right-click and view code or just press F7 on the designer opened to view code, and you’ll see an XML file that is created in the background and all this information is saved in a well-defined XML format.
="1.0"="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>
Step 3: Add Custom Command
We successfully added a new project and configured its manifest file, but the real job is still pending and that is writing an extension to locate file. For that, we need to add a new item to our project, so just right-click on the project and select add a new item from the items template.
As soon as you open the item templates, you’ll see an option to add a new Custom Command under Visual C# items - > Extensibility. The custom commands act as a button in VSIX extensions. These buttons help us to bind an action to its click event, so we can add our desired functionality to this button/command. Name the custom command you added, for example, I gave it a name "LocateFolderCommand
" and then press Add as shown in the below image:
Once the command is added, we can see a lot of changes happening to our existing project. Like adding of some required nugget packages, a Resources folder with an icon and an image a .vsct file, a .resx file and a command and CommandPackage.cs file.
Each of the files has its own significance here. In the tutorial, we'll cover all these details.
When we open the LocateFolderCommandPackage.vsct file, we again see an XML file:
And when you remove all the comments to make it more readable, you’ll get a file something like shown below:
="1.0"="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>
So, primarily the file contains groups, buttons (that are commands lying in that group), button text and some IDSymbol
and image options.
When we talk about "Groups," it is a grouping of commands that are shown in Visual Studio. Like in the below image, when in Visual Studio, you click on Debug, you see various commands like Windows, Graphics, Start Debugging, etc., some are separated by horizontal lines as well. These separated horizontal lines are groups. So a group is something that holds commands, and acts as a logical separation between commands. In VSIX project, we can create a new custom command and also define the groups to which it will associate, we can create new groups as well or extend existing groups as shown in the .vsct XML file.
Step 4: Configure Custom Command
So first, open the vsct file and let us decide where our command will be placed. We basically want our command to be visible when we right-click on any file in solution explorer. For that, in the .vsct file, you can specify the parent of your command, since it is an item node, we can choose IDM_VS_CTXT_ITEMNODE
.
You can check all available locations at this link.
Similarly, we can also create menus, sub menus and sub items, but for now, we’ll stick to our objective and place our command to item node.
Similarly, we can also define the position at which our command will be shown. Set the priority in the group, by default, it is shown as 6th position as shown in the below image, but you can always change it. For example, I changed the priority to 0X0200
, to see my command at top level second position.
You can also change the default button text to "Open in File Explorer" and finally, after all the modifications, our XML will look as shown below:
="1.0"="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>
When we open the LocateFolderCommand.cs, that’s the actual place where we need to put our logic. In VS extensibility project/command everything is handled and connected via GUIDs. Here, we see in the below image that a commandset
is created with a new GUID.
Now when you scroll down, you see in the private constructor, we retrieve the command service that is fetched from the current service provider. This service is responsible for adding the command, provided that the command has a valid menuCommandId
with defined commandSet
and commandId
:
We also see that there is a callback method bound to the command. This is the same callback method that is called when the command is invoked, and that is the best place to put our logic. By default, this callback method comes with a default implementation of showing a message box that proves the command is actually invoked.
Let’s keep the default implementation for now and try to test the application. We can later on add business logic to open the file in Windows Explorer.
Step 5: Test Custom Command with Default implementation
One may wonder how to test the default implementation. I would say, just compile and run the application. As soon as the application is run via F5, a new window will be launched that is similar to Visual Studio as shown below:
Note that we are creating an extension for Visual Studio, so ideally it should be tested in Visual Studio itself, on how it should look and how it should work. A new Visual Studio instance is launched to test the command. Note that this instance of Visual Studio is called Experimental Instance. As the name suggests, this is for testing our implementation, basically checking how things will work and look like.
In the launched experimental instance, add a new project like we add in normal Visual Studio. Note that all the features in this experimental instance can be configurable and switched to On and Off on need basis. We can cover the details in my third article when we discuss Visual Studio Isolated Shell.
To be simple, choose a new console application, and name it of your choice. I named it "Sample".
When the project is added to solution explorer, we see a common project structure. Remember our functionality was to add a command to the selected file in Visual Studio solution explorer. Now we can test our implementation, just right-click on any file and you can see the "Open in File Explorer" command in a new group in the context menu as shown in following image. The text comes from the text that we defined for our command in VSCT file.
Before you click on the command, place a breakpoint on MenuItemCallback
method in the command file. So when the command is clicked, you can see the menuItemCallback
method is invoked.
Since this method contains the code to show a message box, just press F5 and you see a message box with a defined title as shown in the following image:
This proves that our command works, and we just need to put right logic here. We can certainly take a break and celebrate at this point.
Step 6: Add Actual Implementation
So now, this is the time to add our actual implementation. We already know the place, just need to code. For actual implementation, I have added a new folder to the project and named it Utilities and added a class to that folder and named it LocateFile.cs with the following implementation:
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
}
}
}
This class contains the business logic, primarily methods that take file path as a parameter and work with shell to open this file in explorer. I’ll not go into the details of this class, but focus more on how we can invoke this functionality.
Now in the MenuItemCallBack
method, put the following code to invoke the method of our utility class:
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>
This method now first fetches all the selected items using DTE object. With DTE objects, you can do all the transactions and manipulations in Visual Studio components. Read more about the power of DTE objects here.
After getting the selected items, we invoke the FilesOrFolders
method of the utility class and pass file path as a parameter. Job done. Now again, launch the experimental instance and check the functionality.
Step 7: Test Actual Implementation
Launch experimental instance, add a new or existing project and right click on any file and invoke the command.
As soon as you invoke the command, you’ll see the folder is opened in Windows Explorer with that file selected as shown below:
This functionality also works for the linked files in Visual Studio. Let’s check that. Add a new item in the project opened in experimental instance and add a file as a link as shown in the following image:
You only need to select "Add as Link" while adding the file. This file would be then be shown in Visual Studio with a different icon showing that this is a linked file. Now select the actual Visual Studio file and the linked file in Visual Studio and invoke the command now.
When the command is invoked, you can see two folders opened with both the files selected at their own location.
Not only this, since we have created this extension, in the Extensions and Updates in this experimental instance, you can search for this extension and you’ll get that installed in your Visual Studio as shown in following image:
Now it’s time to celebrate again.
Step 7: Optimizing the Package
Our job is nearly done, but there are some more important things that we need to take care of. We need to make this package more appealing, add some image/icons to the extension and optimize the project structure to make it more readable and understandable.
Remember when we started this tutorial, I mentioned to download and install VS Extensibility Tools? VS Extensibility Tools provide some cool features that you can really leverage. For example, it allows you to export all the available images in Visual Studio. We can use these images to make our icon and default image for the extension. To start with, in Visual Studio, where your code was written, go to "Tools->Export Image Moniker…"
A window will be opened to search for the image you need to choose. Search for "Open," and you’ll get the same image as shown in the context menu of project to open the project in Windows Explorer.
We’ll use this image only for our extension. Give it a size 16*16 and click Export, and save that in your Resources folder of the project. Replace the already existing LocateFolderCommand.png file from this file and give this new exported file the same name. Since in the vsct file, it was defined that the prior image sprint has to be used with first icon, so we always got to see 1X beside the custom command text, but we need a good looking meaningful image now, so we exported this "open in explorer" image.
Now go to .vsct file and in the Bitmaps, first delete all images name in the list except bmpPic1
from the usedList
and in the GuidSymbol
, delete all IDsymbol
except bmpPic1
as shown in the below image. We do not need to change the href in Bitmap node because we replaced existing image with the newly exported image with the same name. We did this because we are not using that old default image sprite, but we are using now our newly exported image.
In that case, the LocateFolderCommandPackage.vsct file would look as shown below:
="1.0"="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>
The next step is to set extension image and a preview image that will be shown for the extension in Visual Studio gallery and Visual Studio market place. These images will represent the extension everywhere.
So follow the same routine of exporting image from Image Monikor. Note that you can also use your own custom images for all the image/icon related operations.
Open the image moniker as explained earlier and search for LocateAll
, then export two images, one for icon (90 X 90).
and one for preview (175 X 175).
Export both the images with the name Icon.png and Preview.png respectively in the Resources folder. Then in the solution explorer, include those two images in the project as shown in the below image:
Now in the source.extension.vsixmanifest file, set the Icon and Preview images to the same exported images as shown in the following image:
Step 8: Test Final Package
Again, it’s time to test the implementation with new Images and icons. So compile the project and press F5, experimental instance would launch. Add a new or existing project and right click on any project file to see your custom command.
So now, we got the icon that was earlier selected from Image Moniker for this custom command. Since we have not touched the functionality, it should work fine as before.
Now go to extensions and updates and search for the installed extension "LocateFolder
". You’ll see a beautiful image before your extension, this is the same image with dimensions 90X90 and in the right side panel, you can see the enlarged 175X175 preview image.
Now we can certainly celebrate as the task is completely accomplished.
Conclusion
This detailed article focused on how a Visual Studio extension could be created. In the next article, I’ll explain how the project structure could be optimized to make it more readable and understandable and how to deploy the extension to Visual Studio Market Place via continuous integration and GIT. The basic idea would be to optimize the structure, push the code to GIT, push the extension to Visual Studio gallery via continuous integration through AppVeyor and push the extension to Visual Studio market place. I hope this article helped you to understand Visual Studio extensibility. Feel free to share feedback, ratings and comments.
References
Complete Source Code
Extension at Marketplace
History
- 8th February, 2017: Initial version