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).
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;
Marshal.Release(nestedHierarchyObj);
if (nestedHierarchy != null)
{
FindItemInHierarchy(
ref clew, match, nestedHierarchy, nestedItemId,
recursionLevel, false, visibleNodesOnly);
if (workerManager.IsTermination)
{
return false;
}
}
}
else
{
object pVar;
hr = hierarchy.GetProperty(itemid,
(int)__VSHPROPID.VSHPROPID_Name, out pVar);
clew.Add((string)pVar);
if (match.Length > 0)
{
if (Regex.Match(match, clew[clew.Count - 1],
RegexOptions.IgnoreCase).Value != String.Empty)
{
SelectUIHItemAndWait(clew);
if (workerManager.IsTermination)
{
return false;
}
}
}
else
{
}
++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)
{
uint childId = GetItemId(pVar);
while (childId != VSConstants.VSITEMID_NIL)
{
FindItemInHierarchy(
ref clew, match, hierarchy, childId,
recursionLevel, false, visibleNodesOnly);
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; }
I changed the native working method that performs the matching:
if (Regex.Match(match, clew[clew.Count - 1],
RegexOptions.IgnoreCase).Value != String.Empty)
{
SelectUIHItemAndWait(clew);
if (workerManager.IsTermination)
{
return false;
}
}
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)
{
DTE dte;
DTE2 dte2;
dte = (DTE)GetService(typeof(DTE));
dte2 = dte as DTE2;
if (null == dte2)
return;
UIHierarchy UIH = dte2.ToolWindows.SolutionExplorer;
UIHierarchyItem UIHItem; 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)
{
IVsSolution solution =
(IVsSolution)GetService(typeof(SVsSolution));
if (null != solution)
{
IVsHierarchy solutionHierarchy = solution as IVsHierarchy;
if (null != solutionHierarchy)
{
if (null == dlg)
{
dlg = new FormFindInSlnExplorer();
... }
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;
}
workerManager.Worker.Resume();
workerManager.Worker.Join();
}
StartSearchThread();
}
else if (System.Windows.Forms.DialogResult.Retry == findDlgResult)
{
if (null != workerManager.Worker)
{
workerManager.Worker.Resume();
}
else
{
StartSearchThread();
}
}
}
}
}
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:
private void DoSearch()
{
IVsSolution solution = (IVsSolution)GetService(typeof(SVsSolution));
if (null == solution)
{
return;
}
IVsHierarchy solutionHierarchy = solution as IVsHierarchy;
if (null == solutionHierarchy)
{
return;
}
List<string> clew = new List<string>();
if(!FindItemInHierarchy(
ref clew, dlg.ItemToFind, solutionHierarchy,
VSConstants.VSITEMID_ROOT, 0, true, false)
&& !workerManager.IsTermination)
{
ShowMessageBox("Item not found");
}
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.
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)
{
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.