Walkthrough: Extending the Solution Explorer Filter

You can extend Solution Explorer filter functionality to show or hide different files. For example, you can create a filter that shows only C# class factory files in the Solution Explorer, as this walkthrough demonstrates.

Prerequisites

To follow this walkthrough, you must install the Visual Studio 2013 SDK. For more information, see Visual Studio Software Development Kit (SDK).

Create a Visual Studio Package Project

  1. Create a VSPackage project in C# named FileFilter, that has a menu command named FileNameFilter and a command ID of cmdidFilterFile

    Warning

    For more information about how to create a VSPackage with a menu command, see Walkthrough: Creating a VSPackage.

  2. Add a reference to System.ComponentModel.Composition.

  3. Make the menu command appear on the Solution Explorer toolbar. Open the FileFilter.vsct file.

  4. Change the <Button> block to the following:

    <Button guid="guidFileFilterCmdSet" id="cmdidFilterFile" priority="0x0400" type="Button">
            <Parent guid="guidSHLMainMenu" id="IDG_VS_TOOLBAR_PROJWIN_FILTERS" />
            <Icon guid="guidImages" id="bmpPic1" />
            <Strings>
              <ButtonText>FileNameFilter</ButtonText>
            </Strings>
          </Button>
    

Update the Manifest File

  1. In the source.extension.vsixmanifest file , add an asset that is a MEF component.

  2. On the Assets tab, choose the New button.

  3. In the Type field, choose Microsoft.VisualStudio.MefComponent.

  4. In the Source field, choose A project in current solution.

  5. In the Project field, choose FileFilter, and then choose the OK button.

Add the Filter Code

  1. Add a class file to the FileFiter project named FileNameFilter.cs.

  2. Replace the empty namespace and the empty class with the code below.

    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.

    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 MSIT.FileFilter
    {
    // Implements ISolutionTreeFilterProvider. The SolutionTreeFilterProvider attribute declares it as a MEF component
        [SolutionTreeFilterProvider(GuidList.guidFileFilterCmdSetString, (uint)(PkgCmdIDList.cmdidFilterFile))]
        public sealed class FileNameFilterProvider : HierarchyTreeFilterProvider
        {
            SVsServiceProvider svcProvider;
            IVsHierarchyItemCollectionProvider hierarchyCollectionProvider;
    
            // Constructor required for MEF composition
            [ImportingConstructor]
            public FileNameFilterProvider(SVsServiceProvider serviceProvider, IVsHierarchyItemCollectionProvider hierarchyCollectionProvider)
            {
                this.svcProvider = serviceProvider;
                this.hierarchyCollectionProvider = hierarchyCollectionProvider;
            }
    
            // Returns an instance of Create filter class.
            protected override HierarchyTreeFilter CreateFilter()
            {
                return new FileNameFilter(this.svcProvider, this.hierarchyCollectionProvider, FileNamePattern);
            }
    
            // Regex pattern for CSharp factory classes
            private const string FileNamePattern = @"\w*factory\w*(.cs$)";
    
            // Implementation of file filtering
            private sealed class FileNameFilter : HierarchyTreeFilter
            {
                private readonly Regex regexp;
                private readonly IServiceProvider svcProvider;
                private readonly IVsHierarchyItemCollectionProvider hierarchyCollectionProvider;
    
                public FileNameFilter(
                    IServiceProvider serviceProvider,
                    IVsHierarchyItemCollectionProvider hierarchyCollectionProvider,
                    string fileNamePattern)
                {
                    this.svcProvider = serviceProvider;
                    this.hierarchyCollectionProvider = hierarchyCollectionProvider;
                    this.regexp = new Regex(fileNamePattern, RegexOptions.IgnoreCase);
                }
    
    
                // Gets the items to be included from this filter provider. 
                // rootItems is a collection that contains the root of your solution
                // Returns a collection of items to be included as part of the filter
                protected override async Task<IReadOnlyObservableSet> GetIncludedItemsAsync(IEnumerable<IVsHierarchyItem> rootItems)
                {
                    IVsHierarchyItem root = HierarchyUtilities.FindCommonAncestor(rootItems);
                    IReadOnlyObservableSet<IVsHierarchyItem> sourceItems;
                    sourceItems = await hierarchyCollectionProvider.GetDescendantsAsync(
                                        root.HierarchyIdentity.NestedHierarchy,
                                        CancellationToken);
    
                    IFilteredHierarchyItemSet includedItems = await hierarchyCollectionProvider.GetFilteredHierarchyItemsAsync(
                        sourceItems,
                        ShouldIncludeInFilter,
                        CancellationToken);
                    return includedItems;
                }
    
                // 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);
                }
            }
        }
    }
    
  3. Add a second class file named Utilities.cs, which contains helper methods that find the top-level common parent object, if any.

  4. In the new file, insert the following code:

    using System;
    using System.Collections.Generic;
    using Microsoft.VisualStudio.Shell;
    
    namespace MSIT.FileFilter
    {
        internal static class Utilities
        {
            // Finds the first common ancestor for items
            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;
            }
    
            // Finds the common ancestor of obj1 and obj2 using the given function to evaluate parent.
            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;
            }
        }
    }
    
  5. Remove the command placement and handling code from FileFilterPackage.cs. Remove the MenuItemCallback method and replace the code in the Initialize() method with the following:

    protected override void Initialize()
    {
        Debug.WriteLine (string.Format(CultureInfo.CurrentCulture, "Entering Initialize() of: {0}", this.ToString()));
        base.Initialize();
    }
    

Test Your Code

  1. Build and run the project. A second instance of Visual Studio appears. This is called the experimental instance.

  2. In the experimental instance of Visual Studio, open a C# project.

  3. Look for the button you added on the Solution Explorer toolbar. It should be the fourth button from the left.

  4. When you click the button, all the files should be filtered out, and you should see “All items have been filtered from view.” in the Solution Explorer.