Click here to Skip to main content
Click here to Skip to main content

VS SDK/VS Package Find Item in Solution Explorer Window

, 20 Jan 2010 CPOL
Rate this:
Please Sign up or sign in to vote.
Visual Studio Integration Package Menu command. Find Item by using a Regular Expression in Solution Explorer window.

Find item in Solution Dialog

Introduction

Modern solutions contain tens or even far more than hundreds of projects, where each project consists of hundreds of files. To orient yourself in such space is difficult. It is normal if you were there from the beginning of the project - you know the history of each one and you are privy to a huge part of the files contained. But it is another thing if you are newbie in a project with a ten-year history. For some reasons, Microsoft has not provided the facilty to search in a Solution Explorer tree.

There are some ways to handle such a situation. One is to use the Visual Studio native feature: 'Track active item in Solution Explorer' under Tools/Options.../Projects and Solutions/General, and type in the CPP-code, something like: #include "file.cpp", and then click the right mouse button and select the 'Open "file.cpp" document' item from the context menu. But what to do if you are .NET developer, and the VS auto item tracking buzzes sometimes (no! - unfortunately, the proper word is 'often' in the case of huge solutions with tens of thousands of items)?

This package allows to search for items in the Solution Explorer. When an item is found, the default action executes (the same if you double-click an item in the Solution Explorer window). Also, the package builds-in an additional menu item to the contextual menu of the opened document (source file only) that positions at the respective UI item in the Solution Explorer tree.

Visual Studio SDK provides a number of objects to extend the basic functionality. This articles is devoted to the approach of how to create a Visual Studio Integration Package (C#/Menu command). This is not a walkthrough and it has in mind that you have a basic knowledge about VS packages. Here, I just provide the working code (with installation) with short comments about things that took a lot of my time to be resolved.

You could find a lot of documentation and tutorials in the MSDN using the 'Visual Studio Software Development Kit (SDK)' search clause.

Background

To compile and run the provided code, you have to have the Visual Studio SDK installed in your computer (I use VsSDKFebruary2007 in my computer, it works on VS2005 and VS2008 as well). In case you want to just get the described functionality, you can download the MS installer.

As a basis for the package, I utilized the sample provided by the Visual Studio SDK: Example.SolutionHierarchyTraversal. It (and many other samples) is located at $(Program Files)\Visual Studio SDK\(version)\VisualStudioIntegration\Samples.

Solution

MSDN tutorials help to create the VS package. All was clear, but I spent a lot of time to get know where I can get the names of the constants for the menu and the submenu, and what is more important - where I can get the names of the constants for the menu groups (all commands must belong to some group). VS stores the menu hierarchy in .ctc files:

  • ShellCmdDef.ctc
  • ShellCmdPlace.ctc
  • SharedCmdDef.ctc
  • SharedCmdPlace.ctc

They are located at $(Program Files)\Visual Studio SDK\(version)\VisualStudioIntegration\Common\Inc. If you want to put a command at a certain place in VS, you should find a command which is already there in the files above. And then, bind your created menu group to the same group. Be careful, the menu commands appear as solid strings, but the real text code consists of a '&' in the middle, as in button captions (but they are not underlined).

'Find in Solution' command in Solution Explorer Window contextual menu

The first confusion that I encountered was the existence of two type of solution hierarchies: IVsHierarchy and UIHierarchy. The first is the 'natural' hierarchy that enumerates the solution objects. The second is the user interface hierarchy that represents the tree in the Solution Explorer window. The good news is that guys from Microsoft optimized the user interface, and if you do not see a node in the Solution Explorer tree (it is located inside a collapsed parent node), VS does not allocate memory for it). I.e., if you have 8K of files, only the 'selected' ones have their respective items in the UI hierarchy. The bad news is that you can not travel over UIHierarchyItem with the intent to find an interested item, because the fact that you have not found the item in the UI hierarchy does not mean that it does not exist at all; it means that might just not be loaded into memory. So you have to travel over 'real' objects in the solution hierarchy, but a method to get the respective UI item using the real item is absent. To do this this is the first trick.

I utilized a method from the example above to travel over the hierarchy items:

private bool FindItemInHierarchy(ref List<string> clew, string match, 
        IVsHierarchy hierarchy, uint itemid, int recursionLevel, 
        bool hierIsSolution, bool visibleNodesOnly)
{
    Boolean isTermination;
    lock(workerManager)
    {
        isTermination = workerManager.IsTermination;
    }
    if (isTermination)
    {
        return false;
    }

    int hr;
    IntPtr nestedHierarchyObj;
    uint nestedItemId;
    Guid hierGuid = typeof(IVsHierarchy).GUID;

    hr = hierarchy.GetNestedHierarchy(itemid, ref hierGuid, 
                   out nestedHierarchyObj, out nestedItemId);
    if (VSConstants.S_OK == hr && IntPtr.Zero != nestedHierarchyObj)
    {
        IVsHierarchy nestedHierarchy = 
          Marshal.GetObjectForIUnknown(nestedHierarchyObj) 
          as IVsHierarchy;
        // we are responsible to release
        // the refcount on the out IntPtr parameter
        Marshal.Release(nestedHierarchyObj);
        if (nestedHierarchy != null)
        {
            // Display name and type of the node in the Output Window
            //if (
            FindItemInHierarchy(
                ref clew, match, nestedHierarchy, nestedItemId, 
                    recursionLevel, false, visibleNodesOnly);
            //)
            //{
                //return true;
            //}

            if (workerManager.IsTermination)
            {
                return false;
            }
        }
    }
    else
    {
        object pVar;

        //Get the name of the root node in question
        //here and push its value to clew
        hr = hierarchy.GetProperty(itemid, 
                (int)__VSHPROPID.VSHPROPID_Name, out pVar);

        clew.Add((string)pVar);

        if (match.Length > 0)
        {
            //If we find match, terminate node enumerating
            if (Regex.Match(match, clew[clew.Count - 1], 
                  RegexOptions.IgnoreCase).Value != String.Empty)
            {
                SelectUIHItemAndWait(clew);
                
                if (workerManager.IsTermination)
                {
                    return false;
                }
                
                //return true;
            }
        }
        else
        {
            // suppose this is just request for all items in solution
        }

        ++recursionLevel;

        hr = hierarchy.GetProperty(itemid,
            ((visibleNodesOnly || (hierIsSolution && recursionLevel == 1) ?
                (int)__VSHPROPID.VSHPROPID_FirstVisibleChild : 
                (int)__VSHPROPID.VSHPROPID_FirstChild)), out pVar);
        Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(hr);
        if (VSConstants.S_OK == hr)
        {
            //We are using Depth first search so at each level
            //we recurse to check if the node has any children
            //and then look for siblings.
            uint childId = GetItemId(pVar);
            while (childId != VSConstants.VSITEMID_NIL)
            {
                //if (
                FindItemInHierarchy(
                    ref clew, match, hierarchy, childId, 
                    recursionLevel, false, visibleNodesOnly);
                //)
                //{
                    //return true
                //}

                if (workerManager.IsTermination)
                {
                    return false;
                }

                hr = hierarchy.GetProperty(childId,
                    ((visibleNodesOnly || (hierIsSolution && recursionLevel == 1)) ?
                        (int)__VSHPROPID.VSHPROPID_NextVisibleSibling : 
                        (int)__VSHPROPID.VSHPROPID_NextSibling),
                    out pVar);
                if (VSConstants.S_OK == hr)
                {
                    childId = GetItemId(pVar);
                }
                else
                {
                    Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(hr);
                    break;
                }
            }
        }

        if (match.Length > 0)
        {
            clew.RemoveAt(clew.Count - 1);
        }
    }

    return false; //node is not found in current hierarchy
}

I changed the native working method that performs the matching:

//If we find match, terminate node enumerating
if (Regex.Match(match, clew[clew.Count - 1], 
    RegexOptions.IgnoreCase).Value != String.Empty)
{
    SelectUIHItemAndWait(clew);
    
    if (workerManager.IsTermination)
    {
        return false;
    }
    
    //return true;
}

Thus, we can type any Regular Expression to find something in the Solution Explorer tree. The 'something' could be a project, folder, source file, or any item that appears in the Solution Explorer tree.

Both the real item and the UI item will have a name and an equal ancestors hierarchy. And, it is possible to select the UI item by using the following method:

UIHierarchyItem GetItem (
    [InAttribute] string Names
)

where Names contains the names in order from the root leading to the subsequent subnodes. The last name in the array is the node returned as a UIHierarchyItem object.

And again, guys from Microsoft have a surprise for you. If the determined path node does not exist in the UI hierarchy by optimization, it won't. You must force its existence in the UI hierarchy via parent node expansion. There are hidden dangers again in that it first looks like a simple task.

UIHierarchyItems.Expanded Property

As MSDN says, it sets or gets whether a node in the hierarchy is expanded. Gets... maybe, but does not set. The code is compiled and run. But the value does not change after you have assigned some value it. It does not expand and all. Also, it does not change its value to 'true'. I spent a lot of time to ascertain 'why'. It seems just a bug that MS does not want to fix in VS version to version.

The solution is to simulate a user click at each node of the route to the interesting node. You can perform this using this pair: UIHierarchyItem.Select(vsUISelectionType.vsUISelectionTypeSelect) - select the node, and UIHierarchy.DoDefaultAction() - expand the currently selected node in the hierarchy of the Solution Explorer window. The complete code is shown below:

private void SelectUIHItem(List<string> nodesTree)
{
    // Get an instance of the currently running Visual Studio IDE.
    DTE dte;
    DTE2 dte2;

    dte = (DTE)GetService(typeof(DTE));
    dte2 = dte as DTE2;

    if (null == dte2)
        return;

    UIHierarchy UIH = dte2.ToolWindows.SolutionExplorer;
    UIHierarchyItem UIHItem;// = UIH.GetItem(path);
    StringBuilder path = new StringBuilder();

    for (int i = 0; i < nodesTree.Count; ++i)
    {
        if (i > 0)
        {
            path.Append('\\');
        }
        path.Append(nodesTree[i]);

        UIHItem = UIH.GetItem(path.ToString());
        UIHItem.Select(vsUISelectionType.vsUISelectionTypeSelect);

        if (!UIHItem.UIHierarchyItems.Expanded)
        {
            UIH.DoDefaultAction();
        }

    }
}

That was a little trick.

During nodes traveling, I trace the path to each one using a list. When I come one level down, I add one item to the list. When I go one level up, I drop the last item from the list. This is like a clue for traveling through a maze. For that reason, the List<string> was named clew.

At the point we have matched the node, we have the full path to it, so we can select it by our 'clicking' method which I described above. That was the first trick.

At this point, we have got an approach to find the first match. In the earlier version, to find the second, third, fourth, and so on, matches, I just call the same traveling method with an integer parameter that tells how many matches it is necessary to skip. So each time the user initiates the 'find next' task, the system has to start all the work from the beginning. I did so because I had no idea of how to restore the state of the recursive calls (the search performs recursively, iterating over each child of the current node, over each child of each child, and so on). I do not know how to do this even in C++, none the more in Managed C#.

I got a new idea when I solved an absolutely unrelated task. I used threads there. So I decided to swap the restoration by waiting. The idea is: to perform the search in a separate thread, and when an item is found, the working thread falls asleep; if the user resumes a search, the UI thread resumes the working thread.

Here is the method that handles finding something in the tree of the Solution Explorer:

private void MenuItemFindItemInSlnExplorer(object sender, EventArgs e)
{
    //Get the solution service so we can traverse 
    //each project hierarchy contained within.
    IVsSolution solution = 
       (IVsSolution)GetService(typeof(SVsSolution));
    if (null != solution)
    {
        IVsHierarchy solutionHierarchy = solution as IVsHierarchy;
        if (null != solutionHierarchy)
        {
            if (null == dlg)
            {
                dlg = new FormFindInSlnExplorer();

                ... //init dialog
            }
            //dead worker is the same as worker is absent
            if (null != workerManager.Worker && 
                 false == workerManager.Worker.IsAlive)
            {
                workerManager.Worker = null;
            }
            
            System.Windows.Forms.DialogResult findDlgResult = dlg.ShowDialog();
            if (System.Windows.Forms.DialogResult.OK == 
                 findDlgResult && dlg.ItemToFind.Length > 0)
            {
                if (null != workerManager.Worker)
                {
                    lock (workerManager)
                    {
                        workerManager.IsTermination = true;
                    }
                    //resume working thread
                    workerManager.Worker.Resume();
                    //wait until working thread terminated
                    workerManager.Worker.Join();
                }

                StartSearchThread();
            }
            else if (System.Windows.Forms.DialogResult.Retry == findDlgResult)
            {
                if (null != workerManager.Worker)
                {
                    workerManager.Worker.Resume();
                    //resume working thread
                }
                else
                {
                    //if there was no searching before 
                    //we consider that 'find' button was pushed
                    StartSearchThread();
                }
            }
        }
    }
}

/// Starts search thread
private void StartSearchThread()
{
    lock (workerManager)
    {
        workerManager.Worker = new Std.Thread(new Std.ThreadStart(DoSearch));
        workerManager.Worker.Start();
    }
}

The method below executes using a working thread and it starts the search:

/// Search method
private void DoSearch()
{
    //Get objects again
    IVsSolution solution = (IVsSolution)GetService(typeof(SVsSolution));
    if (null == solution)
    {
        return;
    }
    IVsHierarchy solutionHierarchy = solution as IVsHierarchy;
    if (null == solutionHierarchy)
    {
        return;
    }

    //Search recursively
    List<string> clew = new List<string>();
    // used for path tracking

    if(!FindItemInHierarchy(
            ref clew, dlg.ItemToFind, solutionHierarchy, 
            VSConstants.VSITEMID_ROOT, 0, true, false)
        && !workerManager.IsTermination)
    {
        ShowMessageBox("Item not found");
        //see implementation in source codes
    }
    
    //terminate termination
    lock (workerManager)
    {
        workerManager.IsTermination = false;
    }
}

private bool FindItemInHierarchy(ref List<string> clew, string match, IVsHierarchy hierarchy, uint itemid, int recursionLevel, bool hierIsSolution, bool visibleNodesOnly) appeared above.

Here is the method that performs the UI item selection and forces the current thread to sleep:

private void SelectUIHItemAndWait(List<string> nodesTree)
{
    SelectUIHItem(nodesTree);
    Std.Thread.CurrentThread.Suspend();
}

To put the search code in a separate thread instead of restoring the state when a match is found is the second trick. Actually, I utilized a pair: Suspend/Restore, instead of some other Microsoft recommended approach, because they only say 'bad code', or 'obsolete', but do not provide any understandable substitute.

'Synchronize' command in Current document contextual menu

The next task is selecting the currently opened document in Solution Explorer. As I told earlier, you can achieve the same effect by checking on the 'Track active item in Solution Explorer' option nder Tools/Options.../Projects and Solutions/General. And as I told earlier, it can cause a buzz.

The solution is coming up to the parent from the relative ProjectItem and assembling the names in the full node path. Then, we just select the node, using this familiar function:

private void MenuItemSyncItemWithSlnExplorer(object sender, EventArgs e)
{
    // Get an instance of the currently running Visual Studio IDE.
    DTE dte;
    DTE2 dte2;

    dte = (DTE)GetService(typeof(DTE));
    dte2 = dte as DTE2;

    if (null == dte2)
    {
        return;
    }

    Document activeDocument = dte2.ActiveDocument;
    if (null == activeDocument)
    {
        return;
    }

    dte.SuppressUI = false;

    List<string> clew = new List<string>();
    object item = activeDocument.ProjectItem; ;

    while (null != item)
    {
        clew.Insert(0, GetNameProperty(item));
        item = GetNextParent(item);
    }

    clew.Insert(0, 
     dte2.ToolWindows.SolutionExplorer.UIHierarchyItems.Item(1).Name);

    SelectUIHItem(clew);

    dte.SuppressUI = true;
}

This trick is another form of the first trick.

The code was tested in XP and Vista with Visual Studio 2005 and Visual Studio 2008. I hope Microsoft adds this functionality to the VS version TEN.

History

The source code is three years old. Actually, the plug-ins for VS2005 are there because I started to code in C#. So be tolerant about the code quality. Through this article, I have tried to bring ideas to the readers. The article was written in 24th of December, 2009.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

db_developer
Database Developer Freelancer
Ukraine Ukraine
MS SQL Server Database Developer with 7+ years experience
 
Technologies/languages: Business Intelligence, SQL, MDX, VBA, SQL Server, Analysis Services (SSAS), Reporting services (SSRS), Integration Services (SSIS), DataWarehouse.
Also: economic background.
 
Feel free to contact me for rates and details.

Comments and Discussions

 
GeneralTHIS PACKAGE DOES NOT WORK PinmemberMember 391552511-Aug-10 8:15 
GeneralMy vote of 1 PinmemberMember 391552511-Aug-10 8:11 
This package DOES NOT WORK
GeneralInstall Failure Pinmemberjake07220-Jan-10 10:40 
GeneralRe: Install Failure PinmemberRoman_K20-Jan-10 12:15 
GeneralRe: Install Failure Pinmemberjake07226-Jan-10 4:20 
GeneralRe: Install Failure Pinmemberjake07227-Jan-10 6:41 
GeneralRe: Install Failure PinmemberRoman_K28-Jan-10 12:25 
GeneralExcellent [modified] Setup is not workin Pinmembersuresh suthar24-Dec-09 18:15 
GeneralRe: Excellent [modified] Setup is not workin PinmemberRoman_K25-Dec-09 3:52 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.141015.1 | Last Updated 20 Jan 2010
Article Copyright 2009 by db_developer
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid