It was almost three weeks ago when I published my latest article in the LearnVSXNow series. I spent my time examining an important area of VSX; I dealt with the Visual Studio project system and hierarchies.
There are a large number of tasks in Visual Studio Extensibility that cannot be done without knowing the how to handle the project hierarchies. Working with custom editors, creating utilities generating source files and code, getting information about the structure of the currently loaded solution are all examples where the knowledge about Visual Studio hierarchies are indispensable.
Generally developers creating this kind of utilities are talking about Visual Studio Project System. However, handling hierarchies is a more generic part of the VS Shell functionality; it is not constrained only to the project system.
The documentation available in this topic is not an easy stuff. I wanted to get something to start with, but did not find a single document explaining the essence. I spend a few days to look after the missing information, examining source code and building up small utilities to understand what is behind the scenes.
In this post and a few future posts are introduce you the most important things I have learnt about hierarchies. This post is a short overview about the basics just to get into the picture.
What Are Hierarchies In Visual Studio?
In general we can say that Visual Studio uses hierarchies to handle and represent tree-like hierarchical information. The Solution Explorer, Class View and Server Explorer tool windows all work on hierarchies, but Team Explorer and Object Explorer are also good examples of living with hierarchical information.
All of these hierarchies are represented by tree view controls in the UI so you might think that Visual Studio hierarchies have similar structures to trees known from the tree view control. Unfortunately, this assumption is not true.
The key type to understand Visual Studio hierarchies is the IVsHierarchy interface. IVsHierarchy is a generic interface to a collection of nodes forming a hierarchy. There is a distinguished node in this collection, called the root node. Nodes in the collection including the root can have arbitrary properties associated with them. Nodes can be one of the following three types:
Leaf node: it has no children (no child node refers to it as its parent).
Container node: it has at least one child node.
Nested hierarchy: the node has a shortcut to a root node of another hierarchy.
Each node within a hierarchy are identified by a 32-bit integer (generally used as unsigned but sometimes used as signed integer) called VSITEMID. This is actually a cookie that may change as we move the node within the hierarchy. There are two special VSITEMID values (they are accessible through the VSConstants type of the Microsoft.VisualStudio.Shell namespace):
VSITEMID_NIL: represents the “null” VSITEMID reference. For example, when a node does not have a child node, its property called FirstChild has this value.
VSITEMID_ROOT: the ID of the root node. In a hierarchy one and only one node can have this ID.
So, an IVsHierarchy instance represents a collection of nodes forming the hierarchy. Nodes (often called “hierarchy items”) within this container can be addressed by their VSITEMID. If we want to refer to the root node, we use the VSITEMID_ROOT ID within the collection. If we want to access node with ID 1234, we must use 1234 as the VSITEMID of to address the node.
To understand how the structure of nodes is represented within the hierarchy, we must have an overview about the IVsHierarchy interface.
This interface has about a dozen of methods defining the behavior of hierarchies. Instead of going through the list of them, I try to explain the role of interface methods.
Siting the Hierarchy Items
As you know, packages are sited in the Visual Studio IDE in order to access services provided by the shell. Similarly, hierarchy items are also sited to access the services. The SetSite method of the interface is called by the environment passing an instance of the Microsoft.VisualStudio.OLE.Interop.IServiceProvider type that can be used for accessing services. If you would like to know which IServiceProvider instance is used to obtain service objects for a specific hierarchy item, the GetSite method can be called to query this information.
Please notice that this IServiceProvider type is not the same than the System.IServiceProvider used when siting packages. The aim of both type is the same, however, their approach is slightly different.
The hierarchy system enables assigning arbitrary properties to any nodes. In order the hierarchy can be represented each node must support a set of properties. The most frequently used method is the GetProperty method of the IVsHierarchy interface, the shell calls it to obtain information about items when working with the item. GetProperty has the following signature:
int GetProperty(uint itemid, int propid, out object pvar);
The itemid parameter represents the VSITEMID of the node queried. The propid parameter tells the identifier of the property we want to query. If the specified property is supported (the node knows what we are talking about) its value is retrieved in pvar and the method return S_OK status. The reference documentation does not tell how this method should behave if the property is not supported. As I examined code samples, I identified two frequently used patterns:
The method returns DISP_E_MEMBERNOTFOUND status value.
A null value is passed back in pvar.
It is also not treated in the reference documents how the method should behave when an unknown itemid is passed (the specified node does not exist in the hierarchy). Most of the code samples I examined returned the DISP_E_MEMBERNOTFOUND status value.
The GetGuidProperty method has similar role as GetProperty:
int GetGuidProperty(uint itemid, int propid, out Guid pguid);
It provides access to properties having GUID values and uses the parameters on the same way; the property value is retrieved in pguid.
Of course, properties also can be set by using the SetProperty and SetGuidProperty methods:
int SetProperty(uint itemid, int propid, object pvar);
int SetGuidProperty(uint itemid, int propid, ref Guid rguid);
Just as in case of property getter methods, itemid is the VSITEMID of the node, propid identifies the property to set. The proposed value is passed to the methods by the pvar or rguid parameters. The reference documentation does not mention what to do with unsupported properties. Generally, the property is simply not set, but methods return without any error code.
Each method uses the propid value that is said to be the identifier of the property to get or to set. There are many property identifiers used by the shell. To avoid using 32-bit literal values, the shell defines enumerations for the property values. Right now there are three enumeration types named __VSHPROPID, __VSHPROPID2 and __VSHPROPID3 all together containing about a hundred identifiers. For example, the __VSHPROPID enumeration contains the __VSHPROPID.VSHPROPID_Parent value that tells the VSITEMID of the specified node’s parent.
The most complex thing in understanding how hierarchies work and what can be done with the project system is about knowing the function and semantics of the node properties. In a later post we are going to examine the properties defining the relation between nodes building up the hierarchy.
The hierarchy system allows logical nesting of one hierarchy into another one. This type of nesting is rather creating a shortcut from a hierarchy node to a node of another hierarchy. It actually means that we do not nest a hierarchy into another: a hierarchy node can point to another node. The IVsHierarchy interface theoretically allows to point from a hierarchy node to an internal node of another hierarchy, but the current implementation allows only shortcuts to the root nodes. The interface provides the GetNestedHierarchy method to query for this shortcut. The method has the following signature:
ref Guid iidHierarchyNested,
out IntPtr ppHierarchyNested,
out uint pitemidNested
The itemid parameter “names” the node we ask the nested hierarchy information for. The iidHierarchyNested GUID is the type identifier of an interface we expect the requested hierarchy to support. As the common root, we can use the GUID of the IVsHierarchy interface (pass the typeof(IVsHierarchy).GUID value). If we know the nested hierarchy node supports other more specific interfaces, we can use the corresponding GUID.
The two output parameters return the information required to access the nested hierarchy node. The ppHierarchyNested is a pointer to the hierarchy. The IntPtr.Zero value means the node specified by itemid does not have a shortcut to another hierarchy. The pitemidNested is the identifier of the node in the nested hierarchy linked to our node. Although IVsHierarchy is set up to support shortcutting to any node in another hierarchy, the environment currently only supports shortcutting to the root node of the nested hierarchy. We can expect the VSITEMID_ROOT value to be retrieved by pitemidNested and we should provide this value if we are implementing this method for our own custom hierarchy.
Using Canonical Names
Even if we have VSITEMID values for nodes, those do not uniquely identify hierarchy items over time. The VSITEMID values are dynamically assigned to the nodes as we build up the hierarchy. If we close a (solution) hierarchy in Visual Studio, the next time we open it again, the structure of the hierarchy remains the same, but there is a high probability that VSITEMID values are altered. If we want to persist some information related to hierarchy nodes we need some identification mechanism that copes with time (remains the same between VS sessions). Because VSITEMIDs cannot play this role we need something else.
This is where canonical names come into the picture. A canonical name is a unique string naming the hierarchy item. IVsHierarchy provides the following methods to work with this name:
int GetCanonicalName(uint itemid, out string pbstrName);
int ParseCanonicalName(string pszName, out uint pitemid);
GetCanonicalName obtains the name for the item specified by itemid (retrieves in pbstrName). The ParseCanonicalName retrieves the current VSITEMID information in pitemid for the node having the name passed in pszName. Unfortunately the documentation does not tell what should be returned for a canonical name that cannot be parsed. I have seen examples passing back VSITEMID_NIL in pitemid as well as passing back literal 0 (which is different from VSITEMID_NIL). You should have your own strategy for handling unexpected canonical names.
Handling Hierarchy Events
I am sure you can imagine how important role is played by the events when managing hierarchies. As we click on an item, move it, open or close it, change its properties, we generally have a lot of things to do. Just think about what kind of complex tasks should be behind the Solution Explorer hierarchy handling our user interactions.
We can subscribe to (and of course unsubscribe from) hierarchy events using the widely-used pattern that is described by the AdviseHierarchyEvents and UnadviseHierarchyEvents methods of the IVsHierarchy interface:
int AdviseHierarchyEvents(IVsHierarchyEvents pEventSink, out uint pdwCookie);
int UnadviseHierarchyEvents(uint dwCookie);
When subscribing to the events (with AdviseHierarchyEvents) we pass an IVsHierarchyEvents-aware object instance in pEventSink and get back a cookie in pdwCookie. This cookie can be used in UnadviseHierarchyEvents when we unsubscribe from event notifications. By using the IVsHierarchyEvents interface we can detect (and respond to) the following events:
An item is added to the hierarchy.
An item is appended to the end of the hierarchy.
An item is deleted from the hierarchy.
One or more properties of an item have changed.
Icon belonging to a hierarchy item is changed.
Changes are made to the child item inventory of a hierarchy.
I suppose it is a bit confusing why these events are supported by the event model and not some others. In a future post I will tell you the details to make the picture clear.
When the environment does not need a hierarchy any more, it closes them. Even if we want to keep a hierarchy live, it may get closed when we exit Visual Studio. The IVsHierarchy interface provides the following methods to handle the persistence and cleanup tasks:
int QueryClose(out int pfCanClose);
Visual Studio automatically calls the Close method on the hierarchies that are the part of a solution (all projects in the solution are hierarchies). If you have packages that define hierarchies outside of the solution, it is the package’s responsibility to call the Close method on those hierarchies. Close must undertake all cleanup and persistence task.
The QueryClose method is called before Visual Studio closes the hierarchy. Zero pfCanClose value prevents closing the hierarchy; any other values enable the close operation. If you have your own hierarchies, you’re responsibility is to call the QueryClose method.
The IVsHierarchy interface defines five methods from Unused0 to Unused4. Let’s say they are there by “historical reasons”. Should you implement IVsHierarchy, please take care these methods simply return the E_NOTIMPL status code.
Where we are
In this post we had an overview of the IVsHierarchy interface that represents a collection of nodes (hierarchy items). The hierarchy abstraction provided by IVsHierarchy is very different from the one we get used to in connection with tree views. The hierarchy has a distinguished node called root node and each node can have a shortcut to other hierarchy. Each node has a collection of properties, a few defining the internal structure of the hierarchy and potentially unlimited number of arbitrary properties that can be accessed and interpreted in the context where the hierarchy is used.
In the next post we look into the properties determining the structure of the hierarchies.
Oct 07 2008, 05:27 PM