KnowDotNet Visual Organizer

Handling Nested ProjectItems in an Addin ( Happens in ASP.NET Projects and When Project Items are in Folders)

by Les Smith
Print this Article Discuss in Forums

How do I handle nested project items in an addin?  Nested ProjectItems can occur for several reasons.  First in an ASP.NET project, the code file (aspx.vb or aspx.cs) is nested under the Web Form (aspx) file.  Secondly, if you place folders in your project to contain your projectitems, such as Forms, Classes, Images, etc., then the ProjectItems are nested and a simple top-level loop through the ProjectItems will not get you to the code files and you will wonder why you can't find code that you know is in your project.

I first discovered this in working with the FileCodeModel of an aspx page and found that I was not scanning any code behind the web forms.  Then, I discovered it when scanning a project that had its classes in a Classes Folder and forms in a Forms folder.

To solve the problem, since I am an add-in developer, I wrote a generic Class for scanning ProjectItems that can drill down as far as needed to get to the code windows.  I will use a simple illustration of the class that is trying to determine if a certain utility file is already in the project, so I can add it to the project if it is not.

First, I will show you the Class that does the "drilling".   The code for the ProjectItemScan Class is shown in Figure 1.  I have written it as a class with Shared methods so that I don't have to instantiate it to use it.

Figure 1 - ProjectItemScan Class

Imports EnvDTE
Imports Extensibility
Imports System
' This class processes project items and when
' a lowest level project item is found, it does
' a call back to the caller thru invoking a delegate.
Public Class ProjectItemScan
  
Private oVS As EnvDTE.DTE
  
Private prj As Project
  
Private projItem As ProjectItem
  
Private projItems As ProjectItems
  
' the following delegate is what must be changed for the
   ' project you are working on
   Public Delegate Sub ProcessPIScanResult(ByVal pi As ProjectItem)

  
Public Shared Sub ScanForProjectItems(ByVal proj As Project, _
      
ByVal psr As ProcessPIScanResult)
      
Dim projItems As ProjectItems
      
For Each projItem As ProjectItem In proj.ProjectItems
         projItems = projItem.ProjectItems
        
If Not projItems Is Nothing AndAlso projItems.Count > 0 Then
            DrillDownInProjectItems(projItems, psr)
        
Else
            ' call back to the user function delegated to
            ' handle a single project item
            psr.Invoke(projItem)
        
End If
      Next
   End Sub

   Private Shared Sub DrillDownInProjectItems(ByVal projectItems As ProjectItems, _
    
ByVal psr As ProcessPIScanResult)
      
Dim projItems As ProjectItems

      
For Each projItem As ProjectItem In projectItems
         projItems = projItem.ProjectItems
        
If Not projItems Is Nothing AndAlso projItems.Count > 0 Then
            ' recurse to get to the bottom of the tree
            DrillDownInProjectItems(projItems, psr)
        
Else
            ' call back to the user function delegated to
            ' handle a single project item
            psr.Invoke(projItem)
        
End If
      Next
   End Sub

End
Class

There are two methods, one Private and one Public.  The Public method is called and it will only call the Private method if there are any nested ProjectItems.  Note, that I check for ProjectItems being null and for the ProjectItems.Count being greater than zero.  The ProjectItems may not be null (Nothing), but if the ProjectItems.Count is zero, there is no nested ProjectItem and no need to call the DrillDown method.  

Once the decesion to call back is made, I simply call the passed Delegated method by using the Invoke method of the Delegate.  You will note that I have a Public Delegate (ProcessPIScanResult), which allows the caller to be called back when a find on a low level Project Item is found.  When one is found, the ProjectItem is passed as a parameter to the delegated method and it is up to the caller to handle the ProjectItem.

Next, I will show the code to call the scan class.  First, I create a Delegate of type ProcessPIScanResult to pass to the scan class.  

      Dim delg As ProjectItemScan.ProcessPIScanResult = _
        
AddressOf HandleProjectItems

Next, I will create a HandleProjectItems method to handle the call back from the scanning class.  In this case, I simply want to know if there is a file in the Project called "TBEvents.vb" or "TBEvents.cs", depending on the type of language that I am processing.  Figure 2 shows the code form my call back method.

Figure 2 - HandleProjectItems Method.

   Private Sub HandleProjectItems(ByVal pi As ProjectItem)
      
If langType = 8 Then
         If pi.Name.ToLower = "tbevents.vb" Then
            tbeExtant = True
         End If
      Else
         If pi.Name.ToLower = "tbevents.cs" Then
            tbeExtant = True
         End If
      End If
   End Sub

When the scanning class finds a low level ProjectItem, it calls the delegated method, shown in Figure 2, passing it an object of ProjectItem type.  HandleProjectItems examines the name of the ProjectItem to see if it matches the sought after name and sets a Boolean to True if found.  The variable "langType" and "tbeExtant" happen to be global (defined at the class level).

Finally, I make a call to the ProjectItemScan class's method with the following call.  It passes the Project object and the Delegate object to the ScanForProjectItems method of the ProjectItemScan class.

      ProjectItemScan.ScanForProjectItems(prj, delg)

Not too much to it, but without some code like this, your add-in will quit working if you are only looping through the top-level ProjectItems.

Writing Add-Ins for Visual Studio .NET
Writing Add-ins for Visual Studio .NET
by Les Smith
Apress Publishing