|
|
Using the CodeModel and FileCodeModelCopying Controls and Event Code | | 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
|
|