KnowDotNet

Using the CodeModel and FileCodeModel

Copying Controls and Event Code

by Les Smith

The CodeModel and FileCodeModel work in concert to explore the code of a project.  They can be used in an add-in to copy selected controls and their respective event code from one form to another.  One of the most useful functionalities in an add-in is the ability to copy a group of selected controls and the event code associated with them.

The CCopyControlsAndCode Class contains all of the code, with the exception of some utility parsing functions, that are needed to copy the controls to the clipboard and to concatenate the event code into a StringBuilder for inserting into another forms code window.   It also contains the code to paste the controls and the code into another form.  This class is the core of the functionality, but it would have to have a UI in the add-in to direct the user on the copying and pasting of the controls.  This class was taken from
NetCommander, which provides this functionaliy as one of the over 50 time-saving features that are added to the Visual Studio .NET IDE.  You can download a fully feature copy for a free 30-day trial by clicking here.  

The constructor for this class requires that the applicationObject of the IDE be passed to it.  I will intermingle a description of the code, where it is needed, with the code of the class.


Imports System.Windows.Forms
Imports EnvDTE
Imports Extensibility
Imports System.Text
Imports System.Math
Imports System.ComponentModel
Imports System.ComponentModel.Design
Imports System.Drawing
Public Class CCopyControlsAndCode
  
Private oVB As EnvDTE.DTE
  
Private msEventCode As String
   Private sList As String = String.Empty

The CopyControlsAndCode method is the top level method of the class.  It is called from the add-in UI Form that instructs the user about the copying feature.  It creates an IDesignerHost object in order to access the the form and the controls that the user wants to copy.  It then creates an ISelectionService interface to access the selected controls.  Next it creates a collection of the names of the selected controls.  That array of names will be used in calling the CodeModel and FileCodeModel to retrieve the event code behind the controls.  After the controls have been enumerated and copied to the ClipBoard, it calls the GetAllEventsForControl method, which gathers the event code in the StringBuilder.

   Friend Function CopyControlsAndCode(ByRef oFrm As frmCopyControlsAndCode) As Boolean
      Dim aComps As New ArrayList()

      
Dim j As Integer
      Dim oCM As New CCodeModel(oVB, modMain.miPrj)
      
Dim oUtil As New CUtilities(oVB)
      
Dim sbCodeOut As New StringBuilder()
      
Dim IC As IComponent
      
Dim c As Component

      
Try
         ' get a list of the selected components
         Dim fdHost As IDesignerHost
         fdHost =
CType(oVB.ActiveWindow.Object, IDesignerHost)
        
Dim sel As ISelectionService
         sel =
CType(fdHost.GetService(Type. _
            GetType("System.ComponentModel.Design.ISelectionService,System")), _
               System.ComponentModel.Design.ISelectionService)
        
If sel.GetSelectedComponents.Count = 0 Then
            MsgBox("No controls selected", vbExclamation)
            
Exit Function
         End If
         Dim cmp As Component

        
' build arraylist of control names
         For Each cmp In sel.GetSelectedComponents
            aComps.Add(cmp.Site.Name)
        
Next

         ' now destroy the designer object as we no
         ' longer need it.
         cmp = Nothing
         sel = Nothing
         fdHost = Nothing

         ' now having the list of control names in the array
         ' attempt to open the code window so we can examine
         ' the text via TextSelection object
         ' activate the code window

         ' first, copy the controls to the clipboard
         oVB.ExecuteCommand("Edit.Copy")

        
Dim oWin As New CWindows(oVB)

        
For j = 0 To aComps.Count - 1 ' iCmpCount - 1
            ' get any events for this control name
            sbCodeOut.Append(GetAllEventsForControl(aComps(j)))
        
Next j

        
' place the concatenated code in module level
         ' variable so that the paste method can see it
         Me.msEventCode = sbCodeOut.ToString
        
Return True
      Catch ex As System.Exception
         MsgBox("Error in CopyControlsAndCode: " & _
            ex.Message & "; Ensure that the form designer is active window.", _
            MsgBoxStyle.Exclamation)
        
Return False
      End Try
   End Function

This method returns all event code for a specified control.  It retrieves it from the current active window, using that ProjectItems FileCodeModel.  It calls the GetMembersList method to get a list of all event methods in the subject form that are associated with the specified control.  The description of the string that is returned from this method is documented in the comments of the function.  This method also uses a utility function, not shown in this article, to read through the strng that is delimited by CRLFs.  Finally, it calls the GetEventCode method to retrieve the code for each event in the members list.

   Friend Overloads Function GetAllEventsForControl( _
      
ByVal sControlName As String) As String
      '` Returns all event code for a specified control.
      '` At end returns the string from
      '` the stringbuilder, which is all of the code for
      '` the passed control name.
      Dim j As Integer
      Dim sLine As String
      Dim sb As New StringBuilder()

      
Try
         Dim pi As ProjectItem = oVB.ActiveWindow.ProjectItem
        
Dim sModule As String = pi.Name

        
' dont chase the member list if we already have it
         If sList.Length = 0 Then
            sList = Me.GetMembersList(1, _
            Replace(Replace(sModule, ".vb", ""), ".cs", ""), _
            sControlName & "_")
        
End If

         ' return string from GetMembersList:
         ' Class: frmName Kind vsCMElementClass
         ' Class: frmName,Method: sControlNameName,Start: 211, Lines: 21
         If sList Is Nothing Then Exit Function
         Dim iPtr As Integer = InStr(sList, sControlName)
        
If iPtr > 0 Then
            ' get the second line
            Dim oUtil As New CUtilities(oVB)
            
Dim sCode As String

            With oUtil
              
Dim iNL As Integer = MLCount(sList)
              
If iNL < 2 Then
                  Return ""
              
End If
               ' get name and strip unneeded tokens
               ' Class: frmName,Method: membername, Start: 211, Lines: 21
               For j = 1 To iNL - 1
                  sLine = .MemoLine(j)
                  
If Not sLine Is Nothing AndAlso sLine.Length > 0 Then
                     sLine = Replace(sLine, "Class:", "")
                     sLine = Replace(sLine, "Method:", "")
                     sLine = Replace(sLine, "Start:", "")
                     sLine = Replace(sLine, "Lines:", "")

                    
' discard module name
                     Dim i As Integer = 1
                    
Dim sTrash As String = _
                        .GetNextCommaDelimitedToken(i, sLine)
                    
Dim sName As String = _
                        Trim(.GetNextCommaDelimitedToken(i, sLine))
                    
Dim iStart As Integer = _
                        
CInt(Val(.GetNextCommaDelimitedToken(i, sLine)))
                    
Dim iLines As Integer = _
                        
CInt(Val(.GetNextCommaDelimitedToken(i, sLine)))
                     sCode =
Me.GetEventCode(iStart, iStart + iLines - 1)
                     sb.Append(sCode & vbCrLf)
                  
End If
               Next j
              
Return sb.ToString
            
End With
         Else
            Return ""
        
End If

      Catch ex As System.Exception
         MsgBox("GetAllEventsForControl: " & ex.Message)
      
End Try
   End Function

This method uses the CodeModel object of the automation object to retrieve a list of all event methods for the specified control name.  It calls the GetMembers method to help accumulate the list of events.

   Public Function GetMembersList(ByVal PI As Integer, _
      
Optional ByVal rsCompName As String = "", _
      
Optional ByVal rsMemberName As String = "") _
      
As String
      ' this method and its helper sub GetMethods
      ' will list all of the members in the project.
      ' If passed a rsCompName, only that component will
      ' be searched.
      ' If passed a rsMemberName, only that name will be
      ' reported as found.  This feature is used when we
      ' want to know if a member is already in a component.
      ' In that case, the caller will pass both the component
      ' name and the membername and if list is not blank on
      ' return, the member was found.
      Dim cm As CodeModel
      
Dim bDone As Boolean = False
      Dim prj As Project = oVB.Solution.Projects.Item(1)
      cm = prj.CodeModel

      
' Look for all the namespaces and classes in the
      ' project.
      Dim list As String
      Dim ce As CodeElement
      
On Error Resume Next

      Cursor.Current = Cursors.WaitCursor

      
For Each ce In cm.CodeElements
        
If (TypeOf ce Is CodeClass Or TypeOf ce Is CodeNamespace) Then
            ' See if that namespace or class contains
            ' other classes.
            If (rsCompName = "") Or _
               (ce.Name.ToUpper = rsCompName.ToUpper)
Or _
               (
TypeOf ce Is CodeNamespace) _
              
Then
               GetMembers(ce, list, rsMemberName, bDone, rsCompName)
              
If bDone Then Exit For
            End If
         End If
      Next
      Cursor.Current = Cursors.Default

      
Return list
  
End Function

The GetMembers method is recursive because CodeElements within a Namespace or Class can be nested, and the desired CodeFunctions will reside within those nested objects.

   Sub GetMembers(ByVal ct As CodeElement, _
      
ByRef list As String, _
      
ByVal rsMemberName As String, _
      
ByRef bDone As Boolean, _
      
ByVal rsCompName As String)
      
' ct could be a namespace or a class.
      ' Add it to the list
      ' if it is a class.
      Static sClass As String
      Dim sp As Integer
      Dim ep As Integer

      On Error Resume Next
      If (TypeOf ct Is CodeClass) Then
         If ct.Name.ToUpper = rsCompName.ToUpper Or _
            rsCompName = ""
Then
            list &= "Class: " & ct.Name & " Kind: " & _
               ct.Kind.ToString & vbCrLf
            sClass = ct.Name
        
Else
            Exit Sub
         End If
      ElseIf (TypeOf ct Is CodeNamespace) Then
         If ct.Name = "Microsoft" Then
            bDone = True
            Exit Sub
         End If
         sp = ct.StartPoint.Line
         ep = ct.EndPoint.Line
         list &= "NameSpace: " & ct.Name & _
            ", Start: " & sp.ToString & _
            ", Lines: " & (ep - sp + 1).ToString & vbCrLf
      
ElseIf (TypeOf ct Is CodeFunction) Then
         If (rsMemberName = "") Or _
            (rsMemberName = ct.Name)
Then
            sp = ct.StartPoint.Line
            ep = ct.EndPoint.Line
            list &= "Class: " & sClass & _
               ", Method: " & ct.Name & _
               ", Start: " & sp.ToString & _
               ", Lines: " & _
               (ep - sp + 1).ToString & vbCrLf
        
ElseIf rsMemberName.EndsWith("_") Then
            ' we were suppled with a control name and "_" which
            ' means that we are looking for all events for a
            ' specified control, regardless of the event type
            If ct.Name.StartsWith(rsMemberName) Then
               sp = ct.StartPoint.Line
               ep = ct.EndPoint.Line
               list &= "Class: " & sClass & _
                  ", Method: " & ct.Name & _
                  ", Start: " & sp.ToString & _
                  ", Lines: " & _
                  (ep - sp + 1).ToString & vbCrLf
            
End If
         End If
      ElseIf (TypeOf ct Is CodeVariable) Then
         If (rsMemberName <> "" And ct.Name = rsMemberName) Or _
            rsMemberName = ""
Then
            sp = ct.StartPoint.Line
            list &= "Class: " & sClass & _
               ", Variable: " & ct.Name & _
               ", Start: " & sp.ToString & vbCrLf
        
End If
      Else
         sp = ct.StartPoint.Line
        
If ct.Name = "Microsoft" Then
            bDone = True
            Exit Sub
         End If
         list &= "Class: " & sClass & _
            " Element: " & ct.Name & _
            " StartLine: " & sp.ToString & vbCrLf
      
End If

      ' See if there are any nested namespaces or
      ' classes that might
      ' contain other classes.
      Dim ce As CodeElement
      
For Each ce In ct.Members
        
If (TypeOf ce Is CodeNamespace) Or _
            (
TypeOf ce Is CodeClass) Or _
            (
TypeOf ce Is CodeFunction) Or _
            (
TypeOf ce Is CodeVariable) Then
            GetMembers(ce, list, rsMemberName, bDone, rsCompName)
            
If bDone Then Exit For
         End If
      Next
   End Sub

The GetEventCode method creates a ProjectItem object for the active window.  It then calls the GetCodeForEventMethod to get the actual code for the event.

   Friend Function GetEventCode(ByVal StPt As Integer, _
      
ByVal EndPt As Integer) As String
      ' new code using filecodemodel
      Dim pi As ProjectItem = oVB.ActiveWindow.ProjectItem
      
Dim fcm As FileCodeModel = pi.FileCodeModel

      
If modMain.mPrj Is Nothing Then
         modMain.GetActiveSolutionProject()
      
End If
      Dim rets As String = GetCodeForEventMethod(pi, StPt, EndPt)
      
Return rets
  
End Function

The GetCodeForEventMethod uses the FileCodeModel object to retrieve the events code.  The FileCodeModel is like the CodeModel object, except that it only provides the functionality to explore one project item.  Since the ProjectItem can have or not have a Namespace, unless it is a CSharp project, the code can handle that situation.  Obviously, this code will handle both Visual Basic .NET and C# projects.

   Function GetCodeForEventMethod(ByRef roPI As ProjectItem, _
      
ByVal iStPt As Integer, _
      
Optional ByVal iEndPt As Integer = 1) _
      
As String
      ' returns code for a module which is pointed
      ' to by projectitem, start and end pts.
      ' handles VB and C#, with/without namespaces.
      Dim pi As ProjectItem = roPI
      
Dim filecm As FileCodeModel = pi.FileCodeModel
      
Dim ce As CodeElement
      
Dim i As Integer

      Try
         If filecm.CodeElements.Count = 0 Then Exit Function
         For Each ce In filecm.CodeElements
            
If ce.Kind = vsCMElement.vsCMElementNamespace Then
               Dim cns As CodeNamespace = ce
              
Dim cl As CodeClass
              
For Each cl In cns.Members
                  
Dim cetype As CodeType = CType(cl, CodeType)
                  
Dim ep As EditPoint = _
                     cetype.GetStartPoint(vsCMPart.vsCMPartHeader).CreateEditPoint
                  
Dim sTextLine As String
                  Dim sb As New Text.StringBuilder()
                  ep.MoveToLineAndOffset(iStPt, 1)

                  
' loop to get all lines in the method
                  ' use for instead of do so c# will work
                  For i = 1 To iStPt - iEndPt 'Do
                     sTextLine = ep.GetText(ep.LineLength)
                     sb.Append(sTextLine & vbCrLf)
                     ep.LineDown()
                  
Next i 'Loop
                  Return sb.ToString
              
Next
            Else
               Dim celt As CodeElement = ce
              
Dim cetype As CodeType = CType(celt, CodeType)
              
Dim ep As EditPoint = _
                  cetype.GetStartPoint(vsCMPart.vsCMPartHeader).CreateEditPoint
              
Dim sTextLine As String
               Dim sb As New Text.StringBuilder()
               ep.MoveToLineAndOffset(iStPt, 1)

              
' loop to get all lines in the method
               ' use for instead of do so c# will work
               For i = 0 To iEndPt - iStPt 'Do
                  sTextLine = ep.GetText(ep.LineLength)
                  sb.Append(sTextLine & vbCrLf)
                  ep.LineDown()
              
Next i 'Loop
               Return sb.ToString
            
End If
         Next
      Catch ex As System.Exception
         MsgBox("Error in CFileCodelModel.GetCodeForMethod: " & ex.Message)
      
End Try
   End Function

The PastControlsAndCode method is called from the Add-ins UI after the controls have been copied to the ClipBoard and the code is in the module level variable msEventCode.  The user must have selected the form to receive the controls and code as the active window and it must be a Form Designer.

   Friend Sub PasteControlsAndCode()
      
'The copy function should have placed the event
      ' code in the variable msEventCode.  This method
      ' will simply paste the controls from the clipboard
      ' to the current form designer and then add the code
      ' to the end of the code module for the form.

      ' paste the controls to the form
      Try
         oVB.ExecuteCommand("Edit.Paste")
         Application.DoEvents()
         oVB.ExecuteCommand("View.ViewCode")
         Application.DoEvents()

        
' paste the code to the window
         Dim oUtil As New CUtilities(oVB)
         oUtil.AddMethodToEndOfDocument(msEventCode)

      
Catch ex As System.Exception
         MsgBox(ex.ToString)
      
End Try
   End Sub

   Public Sub New(ByRef roVB As EnvDTE.DTE)
      oVB = roVB
  
End Sub
End
Class

Top of Page