Walkthrough: Extending the Solution Explorer Filter
By extending the filter functionality of Solution Explorer, you can show or hide various kinds of files in the Solution Explorer hierarchy. For example, you can create a filter that shows only C# class factory files in Solution Explorer, as this walkthrough demonstrates.
Note
The steps in this walkthrough are based on a C# project.
Prerequisites
To complete this walkthrough, you must have the following software installed:
Visual Studio 2012
.NET Framework 4.5
Visual Studio 2012 SDK
Create a Visual Studio Package Project
On the menu bar, choose File, New Project.
The New Project dialog box opens.
In the list of project templates, expand Visual C#, and then expand Extensibility.
Choose the Visual Studio Package template, name the project FileFilter, and then choose the OK button.
The Visual Studio Package Wizard opens.
On the Welcome page, choose the Next button.
On the Select a Programming Language page, specify Visual C# as the language, choose Generate a new key file to sign the assembly, and then choose the Next button.
On the Basic VSPackage Information page, retain the default information, and choose the Next button.
On the Select VSPackage Options page, choose Menu Command, and then choose the Next button.
On the Command Options page, specify FileName Filter in the Command name box, specify cmdidFilterFile in the Command ID box, and the choose the Next button.
On the Select Test Project Options page, clear the check boxes for the test project, and then choose the Finish button.
Visual Studio creates a project that’s named "FileFilter" and that contains several files, including a file that’s named FileFilterPackage.cs.
If FileFilterPackage.cs doesn’t open in the code editor automatically, open the shortcut menu for that file, and then choose Open.
In the following line, change Menus to SolutionNavigator:
[ProvideMenuResource("Menus.ctmenu, 1")]
The line should resemble the following example:
[ProvideMenuResource("SolutionNavigator.ctmenu, 1")]
On the shortcut menu for the FileFilter project file (.csproj), choose Unload Project, and then choose Edit FileFilter.csproj.
The file opens in an editor window.
In the VSCTCompile element, change <ResourceName>Menus.ctmenu</ResourceName> to <ResourceName>SolutionNavigator.ctmenu</ResourceName>, save the file, and then reload the project.
In the next procedure, you’ll add the code that provides the functionality for the filter.
Add the Filter Code
In Solution Explorer, open the shortcut menu for the FileFilter project, choose Add, and then choose Class.
Specify a C# class, name it FileNameFilter.cs, and then choose the Add button.
If the file doesn’t open in the code editor automatically, open the shortcut menu for the file, and then choose Open.
Replace the empty namespace and the empty class with the following code:
using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.Internal.VisualStudio.PlatformUI; using Microsoft.VisualStudio.Shell; namespace MicrosoftCorp.FileFilterPackage { /// <summary> /// File name Filter provider class /// </summary> /// <remarks>Implement ISolutionTreeFilterProvider. Use SolutionTreeFilterProvider attribute to declare it as an MEF component</remarks> [SolutionTreeFilterProvider(GuidList.guidFileFilterPackageCmdSetString, (uint)(PkgCmdIDList.cmdidFactoryFilesFilter))] public sealed class FileNameFilterProvider : HierarchyTreeFilterProvider { /// <summary> /// Constructor required for composition /// </summary> [ImportingConstructor] public FileNameFilterProvider(SVsServiceProvider serviceProvider, IVsHierarchyItemCollectionProvider hierarchyCollectionProvider) { this.serviceProvider = serviceProvider; this.hierarchyCollectionProvider = hierarchyCollectionProvider; } /// <summary> /// Returns an instance of Create filter class /// </summary> /// <returns>Returns an instance of Create filter class</returns> protected override HierarchyTreeFilter CreateFilter() { return new FileNameFilter(this.serviceProvider, this.hierarchyCollectionProvider, FileNamePattern); } // Regex pattern for CSharp factory classes private const string FileNamePattern = @"\w*factory\w*(.cs$)"; private readonly IServiceProvider serviceProvider; private readonly IVsHierarchyItemCollectionProvider hierarchyCollectionProvider; /// <summary> /// Implementation for file filtering /// </summary> private sealed class FileNameFilter : HierarchyTreeFilter { private readonly Regex regexp; private readonly IServiceProvider serviceProvider; private readonly IVsHierarchyItemCollectionProvider hierarchyCollectionProvider; public FileNameFilter( IServiceProvider serviceProvider, IVsHierarchyItemCollectionProvider hierarchyCollectionProvider, string fileNamePattern) { this.serviceProvider = serviceProvider; this.hierarchyCollectionProvider = hierarchyCollectionProvider; this.regexp = new Regex(fileNamePattern, RegexOptions.IgnoreCase); } /// <summary> /// Gets the items to be included from this filter provider /// </summary> /// <param name="rootItems">Collection that contains the root of your solution</param> /// <returns>Collection of items to be included as part of the filter</returns> protected override async Task<IReadOnlyObservableSet> GetIncludedItemsAsync(IEnumerable<IVsHierarchyItem> rootItems) { IVsHierarchyItem root = Utilities.FindCommonAncestor(rootItems); IReadOnlyObservableSet<IVsHierarchyItem> sourceItems; sourceItems = await hierarchyCollectionProvider.GetDescendantsAsync( root.HierarchyIdentity.NestedHierarchy, CancellationToken); /* Call to GetFilteredHierarchyItemsAsync below uses the predicate to determine items to be included. For more dynamic filtering, you can create a custom collection that derives from ReadOnlyObservableSet<IVsHierarchyItem> * to handle adding/removing from the collection based on events that user is interested in. * Define an instance of your concrete ReadonlyObservableSet as a member field of this class. * Add event handlers for events (for example, sourceItems.CollectionChanged) that the filter is interested * in handling, and update this collection. * Return your concrete ReadonlyObservableSet instance from this method (GetDescendantsAsync) * Update your concrete ReadonlyObservableSet instance as necessary. Ex. custom collection: class MyCustomIncludedSet : ReadOnlyObservableSet<IVsHierarchyItem> { public bool Add(IVsHierarchyItem hierarchyItem) { return base.AddItem(hierarchyItem); } public bool Remove(IVsHierarchyItem hierarchyItem) { return base.RemoveItem(hierarchyItem); } } */ IFilteredHierarchyItemSet includedItems = await hierarchyCollectionProvider.GetFilteredHierarchyItemsAsync( sourceItems, ShouldIncludeInFilter, CancellationToken); return includedItems; } /// <summary> /// Filters based on file name pattern /// </summary> /// <param name="hierarchyItem">Hierarchy item</param> /// <returns>True if filters hierarchy item name for given filter; otherwise, false</returns> private bool ShouldIncludeInFilter(IVsHierarchyItem hierarchyItem) { if (hierarchyItem == null) { return false; } return this.regexp.IsMatch(hierarchyItem.Text); } } } }
Choose Add, choose Class, and then name the file Utilities.cs.
In the new file, insert the following code:
using System; using System.Collections.Generic; using Microsoft.VisualStudio.Shell; namespace MicrosoftCorp.FileFilterPackage { /// <summary> /// Utilities class /// </summary> internal static class Utilities { /// <summary> /// Finds the first common ancestor for items /// </summary> /// <param name="items">Item collection</param> /// <returns>The first element in the parent tree that is an ancestor of all items</returns> public static IVsHierarchyItem FindCommonAncestor(IEnumerable<IVsHierarchyItem> items) { IVsHierarchyItem commonParent = null; if (items != null) { foreach (IVsHierarchyItem item in items) { commonParent = commonParent == null ? item : FindCommonAncestor(commonParent, item, h => h.Parent); // If we don't find a common parent, stop iterating if (commonParent == null) { break; } } } return commonParent; } /// <summary> /// Finds the common ancestor of obj1 and obj2 using the given function to evaluate parent. /// </summary> /// <param name="obj1">The first object.</param> /// <param name="obj2">The second object.</param> /// <param name="parentEvaluator">The method used to determine the parent of an element.</param> /// <returns>The first element in the parent tree that is both an ancestor of obj1 and obj2, or /// null if no such element exists.</returns> private static IVsHierarchyItem FindCommonAncestor( IVsHierarchyItem obj1, IVsHierarchyItem obj2, Func<IVsHierarchyItem, IVsHierarchyItem> parentEvaluator) { if (obj1 == null || obj2 == null) { return null; } HashSet<IVsHierarchyItem> map = new HashSet<IVsHierarchyItem>(); while (obj1 != null) { map.Add(obj1); obj1 = parentEvaluator(obj1); } while (obj2 != null) { if (map.Contains(obj2)) { return obj2; } obj2 = parentEvaluator(obj2); } return null; } } }
Here are some key points about the sample code.
The FileNameFilterProvider class implements the HierarchyTreeFilterProvider abstract class, which implements the ISolutionTreeFilterProvider interface. The SolutionTreeFilterProvider attribute indicates that this component is an MEF component.
The ImportingConstructor attribute denotes the constructor to be used when the MEF component is created.
FileNameFilterProvider returns an instance of the CreateFilter class.
A Regex expression defines the C# factory class pattern to be matched. For more information, see Regular Expression Language - Quick Reference.
The Task<IReadOnlyObservableSet> GetIncludedItemsAsync(IEnumerable<IVsHierarchyItem rootItems) method passes in the collection that contains the root of the solution (rootItems) and returns the collection of items to be included in the filter.
The ShouldIncludeInFilter method filters the items in the Solution Explorer hierarchy based on the condition that you specify.
The Utilities.cs file contains helper methods that iterate through the hierarchy tree to identify the top-level common parent object, if any.
Update the Manifest File
In Solution Explorer, choose Open from source.extension.vsixmanifest.
On the Assets tab, choose the New button.
In the Type field, choose Microsoft.VisualStudio.MefComponent.
In the Source field, choose A project in current solution.
In the Project field, choose FileFilterPackage, and then choose the OK button.
You can test the filter by choosing the Ctrl + F5 keys to build and run the code in the experimental instance of Visual Studio, opening a solution, and then choosing the FileName Filter command in the list of filters at the top of Solution Explorer.