.NET Zone is brought to you in partnership with:

Andrey Karpov is technical manager of the OOO "Program Verification Systems" (Co Ltd) company developing the PVS-Studio tool which is a package of static code analyzers integrating into the Visual Studio development environment. Site: http://www.viva64.com/ My page on LinkedIn site: http://www.linkedin.com/pub/4/585/6a3 Andrey has posted 10 posts at DZone. View Full User Profile

The Structure of the Visual Studio Project Model

09.18.2013
| 3974 views |
  • submit to reddit
About a year ago in our blog a series, we published several articles on the development of Visual Studio plugins in C#. We have recently revised those materials and added new sections and now invite you to have a look at the updated version of the manual as a series of articles here on DZone.

The other articles in the series can be found here:

This article (the sixth and final one in the series) covers the structure of the Visual Studio project model and its implementation on the example of Visual C++ (VCProject). Also included are the cases of using the project model for enumeration of project elements and obtaining their compilation properties through the corresponding configurations. Isolated Shell based Atmel Studio environment will be examined as an example of a third-party implementation of the project model.

Introduction

Visual Studio project model is a collection of interfaces describing the properties of a compiler, linker and other build tools, as well as the structure of MSVS-compatible projects themselves, and it is connected with the Visual Studio Automation Object Model through the VCProjects late-bound properties. Visual C++ project model extends the standard Visual Studio project model, providing access to the specific functionality of Visual C++ (vcproj/vcxproj) project types. Visual C++ project model is a stand-alone COM component available through the VCProjectEngine.dll assembly, which could also be used independently outside of Visual Studio development environment. It is possible for third-party developers to create custom implementations of the project model, adding the support of new languages and compilers into Visual Studio.

Project Model Structure

Visual Studio provides an extendable project-neutral object model that represents solutions, projects, code objects, documents, etc. Every MSVS project type has a corresponding project automation interface. Every tool in the environment that has a project also has an object of the 'Project' type associated with it. Standard Visual C++ project model also complies with this general automation project model scheme:

Projects
  |- Project -- Object(unique for the project type)
      |- ProjectItems (a collection of ProjectItem)
          |- ProjectItem (single object) -- ProjectItems (another
                                                          collection)
              |- Object(unique for the project type)

The “Project’s” interface provides an ensemble of abstract objects of the 'Project' type. The 'Project' interface defines an abstract project, i.e. it can reference an object from any project model that complies with the standard scheme. Any peculiar properties of a specific model should be defined through a special interface, which is unique only to this model alone. A reference for such an object could be acquired through the Project.Object property. For instance, specific properties of Visual C++ project could be obtained through the VCProject interface, and for the Atmel Studio model it will be the AvrGCCNode interface:

Project proj;
...
VCProject vcproj = proj.Object as VCProject;
AvrGCCNode AvrGccProject = proj.Object as AvrGCCNode;

It is possible to obtain a list of all projects loaded in IDE and belonging to any project model type through the dte.Solution.Projects field; projects belonging to a particular model can be acquired through the DTE.GetObject method (see the example below for Visual C++ model):

Projects vcprojs = m_dte.GetObject("VCProjects") as Projects;

To obtain projects of all types, the following code could be used:

Projects AllProjs = PVSStudio.DTE.Solution.Projects;

The ProjectItems interface represents an ensemble of abstract solution tree elements of 'ProjectItem' type. Similar to the 'Project' interface, the 'ProjectItem' can define any kind of element; it can even contain the same 'ProjectItems' collection inside itself (accessible through the ProjectItem.ProjectItems) or it can be a Project altogether. An object unique for a specific project model can be obtained through the ProjectItem.Object field. For instance, a Visual C++ source code file is represented by a VCFile type, and Atmel Studio source file be the AvrGccFileNode interface:

ProjectItem projectItem;
...
VCFile file = projectItem.Object as VCFile;
AvrGccFileNode file = projectItem.Object as AvrGccFileNode;

An embedded project can be obtained in a similar manner when such an element of the hierarchy represents a project:

Project proj = projectItem.Object as Project;

Recursively Walking All Elements of a Solution Tree's Branch

The interface for controlling hierarchies IVsHierarchy can be used to perform a passing of Solution tree's branch. This interface provides an access to abstract nodes of a tree, each one of which in turn could be a leaf, a container of elements or a link to another hierarchy. Each tree node is uniquely identified through the DWORD identifier VSITEMID. Such identifiers are unique within the scope of a single hierarchy and possess a limited lifetime within it.

A hierarchy object can be obtained for a tree branch of a single project through the VsShellUtilities.GetHierarchy method:

public static IVsHierarchy ToHierarchy(EnvDTE.Project project)
{
  System.IServiceProvider serviceProvider = 
    new ServiceProvider(project.DTE as
  Microsoft.VisualStudio.OLE.Interop.IServiceProvider);
  Guid guid = GetProjectGuid(serviceProvider, project);
  if (guid == Guid.Empty)
    return null;
  return VsShellUtilities.GetHierarchy(serviceProvider, guid);
}

In the example above, the hierarchy was obtained for a project through its GUID identifier. Consider the example of obtaining this GUID identifier for a project:

private static Guid GetProjectGuid(System.IServiceProvider 
  serviceProvider, Project project)
{
  if (ProjectUnloaded(project))
    return Guid.Empty;

  IVsSolution solution = 
   (IVsSolution)serviceProvider.GetService(typeof(SVsSolution)) as
     IVsSolution;
  IVsHierarchy hierarchy;
  solution.GetProjectOfUniqueName(project.FullName, out hierarchy);
  if (hierarchy != null)
  {
    Guid projectGuid;

    ErrorHandler.ThrowOnFailure(
      hierarchy.GetGuidProperty(
      VSConstants.VSITEMID_ROOT,
    (int)__VSHPROPID.VSHPROPID_ProjectIDGuid,
      out projectGuid));

    if (projectGuid != null)
    {
      return projectGuid;
    }
  }

  return Guid.Empty;
}

The IEnumHierarchies interface permits obtaining all of the hierarchies for projects of a particular type through the solution. GetProjectEnum method. Here is an example of obtaining the hierarchies for every Visual C++ project in a solution tree:

IVsSolution solution = PVSStudio._IVsSolution;
if (null != solution)
{
  IEnumHierarchies penum;
  Guid nullGuid = Guid.Empty;
  Guid vsppProjectGuid = 
    new Guid("8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942");

  //You can ask the solution to enumerate projects based on the
  //__VSENUMPROJFLAGS flags passed in. For
  //example if you want to only enumerate C# projects use
  //EPF_MATCHTYPE and pass C# project guid. See
  //Common\IDL\vsshell.idl for more details.
  int hr = solution.GetProjectEnum(
    (uint)(__VSENUMPROJFLAGS.EPF_LOADEDINSOLUTION |   
    __VSENUMPROJFLAGS.EPF_MATCHTYPE),
    ref vsppProjectGuid, out penum);
  ErrorHandler.ThrowOnFailure(hr);
  if ((VSConstants.S_OK == hr) && (penum != null))
  {
    uint fetched;
    IVsHierarchy[] rgelt = new IVsHierarchy[1];
    PatternsForActiveConfigurations.Clear();
    while (penum.Next(1, rgelt, out fetched) == 0 && fetched == 1)
    {
      ...
    }
  }
}

As evident by the example above, the GetProjectEnum method provides hierarchies for projects based on a project kind specified by the GUID identifier. GUID identifiers for regular Visual Studio/MSBuild project types can be obtained here. The penum.Next() method allows us to enumerate all project hierarchies we've acquired (the rgelt array). It should be remembered that user-created project models could possess their own unique identifiers in case they define a new project type for themselves. Such custom user identifiers can be obtained from XML project files of the corresponding models.

But our own experience in developing PVS-Studio IDE plug-in demonstrates that an opposite situation is quite possible as well, that is, when a user-created project type uses a GUID from one of the stock project types, usually the one from which it was derived. In particular, we've encountered a VCProject type that was extended to provide development for the Android platform. As a result, this project model extension had caused crashes in our plug-in because it did not provide several properties that are otherwise present in VCProject model (OpenMP for example) through the automation API. An intricacy of this situation is that such an extended project model type cannot be differentiated from a regular one, and thus, it is quite hard to correctly process it as well. Therefore, when you are extending a project model through your custom types, to avoid such conflicts with various IDE components (including other third-party extensions as well), it is always important to remember the necessity of providing the means to uniquely identify your types.

Possessing an IVsHierarchy for the project, we are able to recursively enumerate all the elements of such solution tree branch through the hierarchy.GetProperty method, which in turn provides us with the specified properties for each one of the hierarchy nodes:

EnumHierarchyItemsFlat(VSConstants.VSITEMID_ROOT, MyProjectHierarchy, 
0, true);
  ...
public void EnumHierarchyItemsFlat(uint itemid, IVsHierarchy 
  hierarchy, int recursionLevel, bool visibleNodesOnly)
{
  if (hierarchy == null)
  return;
  int hr; object pVar;

  hr = hierarchy.GetProperty(itemid, 
    (int)__VSHPROPID.VSHPROPID_ExtObject, out pVar);

  ProjectItem projectItem = pVar as ProjectItem;
  if (projectItem != null)
  {
    ...
  }

  recursionLevel++;
  //Get the first child node of the current hierarchy being walked
  hr = hierarchy.GetProperty(itemid,
    (visibleNodesOnly ? (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)
      {
        EnumHierarchyItemsFlat(childId, hierarchy, recursionLevel, 
        visibleNodesOnly);
        hr = hierarchy.GetProperty(childId,
          (visibleNodesOnly ?
          (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;
        }
      }
    }
  }

  private uint GetItemId(object pvar)
  {
    if (pvar == null) return VSConstants.VSITEMID_NIL;
    if (pvar is int) return (uint)(int)pvar;
    if (pvar is uint) return (uint)pvar;
    if (pvar is short) return (uint)(short)pvar;
    if (pvar is ushort) return (uint)(ushort)pvar;
    if (pvar is long) return (uint)(long)pvar;
    return VSConstants.VSITEMID_NIL;
  }

A ProjectItem object that we've acquired for each one of the tree's nodes will allow us to obtain its corresponding Visual C++ object (as well as the object for any other custom project model) through the 'Object' filed, as was described earlier.

Published at DZone with permission of its author, Andrey Karpov.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)