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 in themselves so powerful that it gives a new definition to customization. Customization of your IDE, customization of the desired features that every developer would love to have, and even customization on the IDE that could eventually result in a whole new product altogether i.e. 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 needs.
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. Although this is very rare topic and you could not find enough study material on this topic over the web. This explains how to work with it- step by step. MSDN contains good content but very generic, and to the point. I’ll, in my article, try to explain each and every small part step by step, so that one can learn while coding.
VSIX Packages
VSIX Visual Studio packages give us flexibility to customize Visual Studio as per our need and requirement. As a developer, one always wants that the IDE on which he is working should have certain features apart from the in-built one. You can read more about theoretical aspects and understanding the details of VSIX package here. Following is the 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 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 the 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-click of 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 and need, or even for fun and to explore the technology as well.
Roadmap
Let’ 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. 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 >> Programs and features and search for Visual Studio 2015. Then, right click on it to select “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 to select "Visual Studio Extensibility Tools Update 3" in the Features-> Common Tools, as shown in following image.
Now, press 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 to download and install Extensibility Tools written by Mads Kristensen from here.
This series is 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 minor 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, something like shown in below image.
Step 1 Create a VSIX project
Let’s start from very basic. Open your Visual Studio. I am using Visual Studio 2015 Enterprise edition and would recommend you to use at least Visual Studio 2015 for this article
Create a new project like we create in 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 a name “LoctateFolder”.
As soon as the new project is created, a “Getting Started” page is 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 includes an HTML file, a CSS file, and a VSIXmanifest file. 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 that we see here comes from this 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 left only with the manifest file. So technically speaking, our step one has been accomplished, and we have created a VSIX project.
Step 2 Configure Manifest file
When we open the manifest file, we see certain kind 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 e.g., in the ProductID, we can remove the text that is prefixed to the GUID and only keeps 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 details 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 tells 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. You’ll see an XML file that is created at the background and all this information is saved in a well-defined XML format.
- <?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>
Step 3 Add Custom Command
We have successfully added a new project and configured its manifest file but the real job is still pending - writing an extension to locate the file. For that, we need to add a new item to our project, so just right click on the project and select to 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 e.g. - I gave it a name “LocateFolderCommand” and then press "Add" as shown in the below mentioned 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 along with CommandPackage.cs file.
Each of the files has its own significance here. 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.
- <?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>
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 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 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 like 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 the available locations at the following 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 e.g., I changed the priority to 0X0200 so as 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 looks as shown below.
- “<?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>
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 command set 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 call back method bound to the command. This is the same call back method that's called when the command is invoked, and that is the best place to put our logic. By default, this call back 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 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 the 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 configured and switched to "ON" and "OFF" on need basis. We can cover the details in my third article i.e. when we discuss about Visual Studio Isolated Shell.
To be simple, choose a new console application and name it as per 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 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;
- 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 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]));
- }
- }
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 the 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 followed by invoking the command.
As soon as you invoke the command, you’ll see that 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 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 it installed in your Visual Studio, as shown in the 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 open so as 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". Save that in your "Resources" folder of the project. Replace the already existing "LocateFolderCommand.png" file with 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. In the Bitmaps, first delete all the image names in the list except bmpPic1 from the usedList and in the GuidSymbol, delete all IDsymbol except bmpPic1. We do not need to change the href link in Bitmap node because we replaced existing image with the newly exported image with the same name. We did this because we are now not using that old default image spirit but we are using now our newly exported image.
In that case, the LocateFolderCommandPackage.vsct file would look like this.
- <?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>
The next step is to set extension image and a preview image that would 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 like 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 below.
Now, in the source.extension.vsixmanifest file, set the Icon and Preview images to the same exported images as shown in 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 VS market place. I hope, this article helped you understand the Visual Studio extensibility. Feel free to share your feedback, ratings, and comments.
References
- https://channel9.msdn.com/events/Build/2016/B886
- https://blogs.msdn.microsoft.com/quanto/2009/05/26/what-is-a-vsix
- https://msdn.microsoft.com/en-us/library/dd997148.aspx
Complete Source Code
https://github.com/akhilmittal/LocateFileInWindowsExplorer
Extension at marketplace
https://marketplace.visualstudio.com/items?itemName=vs-publisher-457497.LocateFolder
Read more