KnowDotNet

Multi-Threadiing in an Add-in to Solve Timing Issues

Open Code Window of new ProjectItem

by Les Smith

Why would I ever need to use threading, or asynchronous processing, in an add-in?  Better yet, how do I get the Code Window for a Class or Form, that was just added to the current project, to open and become the active window in the Visual Studio IDE?  I want to automatically place code in the new code window.  The answer is, since there are timing issues involved, you must use threading.  The timing problem is caused by the fact that at the time the ProjectItemsEvent fires, the new item has not yet been placed in the Solution Explorer and it must be there before we can open its code window.  

The first thought might be to do a DoEvents call to give the system time to list the new object in the explorer.  Although a simple solution, it will not work because DoEvents allows Windows to handle waiting events, but it does not return control from your add-in to the IDE.  That only happens when you exit from the event handler, and you must return to the IDE, from the add-in, for the desired results to happen.  By then, the add-in has given up control and will not be called again by the IDE.  Starting another thread in the add-in allows you to get control after a time period, so that when the add-in get control again, the new project item will be in the Solution Explorer.  I am not actually using the System.Threading object, rather I am simply creating an asynchronous object that will wait for 100ms and then its Timer will expire causing it to run.  In the mean time I will return to the IDE so that the IDE can insert the new ProjectItem into the Solution Explorer.

When an object, such as a Class or Form is added to a project, you can be notified of the event by setting up event handlers for ProjectItemsEvents in the Connect class of your add-in.  First, you need to declare the following objects in the declarations section of your Connect class.

   Public WithEvents eventsPIVB As EnvDTE.ProjectItemsEvents
  
Public WithEvents eventsPICSharp As EnvDTE.ProjectItemsEvents
  
Public Shared StopAutoEvents As Boolean
    
Next, in the OnConnection event of the add-in, place the following code.

   ' sink the event handlers for events
   events = oVB.Events
   eventsPIVB = oVB.Events.GetObject("VBProjectItemsEvents")
   eventsPICSharp = oVB.Events.GetObject("CSharpProjectItemsEvents")

Finally, you need to create the event handlers for the two ProjectItemsEvents that you connected above.  These events are doing several things.  First, they check a boolean to see if the event is already busy.  If so, exit the handler.  Code in the CWindowTimer will set the boolean to False once the processing of the added item is complete.  Next, they check to see if the project item being added is a RESX file (added when a Form is added).  If so, we simply exit the handler because I am not interested in the RESX, as it does not have a code window associated with it.  Next, I create an instance of the CWindowTimer class.  The contstructor, for this class, will start a timer that waits for 100 ms.  That will normally give the IDE time to get the new Class or Form into the Solution Explorer.  What I do when the timer expires will be discussed below.

   Private Sub eventsPIVB_ItemAdded(ByVal ProjectItem As EnvDTE.ProjectItem) _
      
Handles eventsPIVB.ItemAdded
      
Try
         If StopAutoEvents Then Exit Sub
         If ProjectItem.Name.ToLower.IndexOf(".resx") > -1 Then Exit Sub
         StopAutoEvents = True
         PI = ProjectItem
        
Dim o As New CWindowTimer(oVB)
      
Catch ex As System.Exception
         StructuredErrorHandler(ex)
      
End Try
   End Sub

   Private Sub eventsPICSharp_ItemAdded(ByVal ProjectItem As EnvDTE.ProjectItem) _
      
Handles eventsPICSharp.ItemAdded
      
Try
         If StopAutoEvents Then Exit Sub
         If ProjectItem.Name.ToLower.IndexOf(".resx") > -1 Then Exit Sub
         StopAutoEvents = True
         PI = ProjectItem
        
Dim o As New CWindowTimer(oVB)
      
Catch ex As System.Exception
      
End Try
   End Sub

The code in Figure 1 is the CWindowTimer Class.  As noted previously, the constructor starts a 100ms timer.  When the timer expires, the WindowTime_Elapsed event fires.  It sets a static Busy flag so that the possibility of the timer firing again will not allow the code to run again.  The first method that I call is to OpenRequestedCodeWindow, passing the new project item object.  That method is shown in Figure 2 and I will discuss that method there. I then get the language type and object type, Class or Form, because I am going to use that information in the processing of the event.

Figure 1 - CWindowTimer Class.

Imports System.Timers

Public Class CWindowTimer
  
Private oVB As EnvDTE.DTE

  
WithEvents WindowTimer As New System.Timers.Timer()
  
Public Sub New(ByRef roVB As EnvDTE.DTE)
      oVB = roVB
      
Me.WindowTimer.Interval = 100
      WindowTimer.Enabled =
True
   End Sub

   Private Sub WindowTimer_Elapsed(ByVal sender As Object, _
      
ByVal e As System.Timers.ElapsedEventArgs) _
      
Handles WindowTimer.Elapsed
      
Static busy As Boolean

      Try
         If busy Then Exit Sub

         busy = True
         OpenRequestedCodeWindow(Connect.PI)

        
Dim i As Integer
        
' next call returns string("VF"|"VC"|"CF"|"CC")
         ' depending on language and form or class being added to project

         Dim s As String = GetCodeWindowTypeAndLanguage
        
If s.StartsWith("V") Then
            i = 8
        
ElseIf s.StartsWith("C") Then
            i = 9
        
Else
            Me.WindowTimer.Enabled = False
            Exit Sub
         End If

         s = oVB.ActiveWindow.Caption
        
If IsProjectItemAForm(s) Then
            s = "F"
        
Else
            s = "C"
        
End If

         ' Process of adding code to the window can go here,
         ' the code window is open and active.

         ' shut the timer down and since this object was created
         ' with a local variable in the event handler, it should
         ' now go away...

         Me.WindowTimer.Enabled = False
         Me.Finalize()
         StopAutoRegions =
False
         busy = False
      Catch ex As System.Exception
         StructuredErrorHandler(ex)
         busy =
False
         CRegions.StopAutoRegions = False
      End Try
   End Sub

   Protected Overrides Sub Finalize()
      
MyBase.Finalize()
      
On Error Resume Next
      WindowTimer.Enabled = False
      WindowTimer = Nothing
   End Sub
End
Class

Figure 2 shows the code for opening a code window using the passed ProjectItem object.  It calls a helper method to see if the active window is a code window.  If it is, it first selects the respective item in the Solution Explorer.  Once selected, the DoDefaultAction performs in the Solution explorer the same action as if the user double-clicked the item.  And the selected item now becomes the active window.  At this point, the window could be a Form designer, so I execute an IDE Command to view the code window.  If the active window is already a code window, nothing happens.

Figure 2 - OpenRequestedCodeWindow Method.

   Public Shared Function OpenRequestedCodeWindow(ByRef pi As ProjectItem) _
      
As Boolean
      ' Open the rsWin window if not already open.
      ' rsWin ="sln.name\prj.name\winname.vb"
      Dim prj As Project
      
Dim sln As String = oVB.Solution.Item(1).Name
      
Dim FN As String
      Dim bOpen As Boolean = False
      DoEvents()

      
On Error Resume Next
      prj = GetActiveSolutionProject()
      
If IsCodeWindow(pi.Name) Then
         Dim pn As String = prj.Name
        
Dim s As String = pi.Name.ToString
        
Dim s2 As String = sln & "\" & pn & "\" & s
         bOpen = pi.IsOpen
         oVB.Windows.Item(Constants.vsWindowKindSolutionExplorer).Activate()
         oVB.ActiveWindow.Object.GetItem(s2).Select( _
            vsUISelectionType.vsUISelectionTypeSelect)
         oVB.ActiveWindow.Object.DoDefaultAction()
         DoEvents()
         oVB.ExecuteCommand("View.ViewCode")
         DoEvents()
      
End If
      Return bOpen
  
End Function

The next function returns True if the ActiveWindow is a code window, otherwise False.

   Public Function IsProjectItemACodeWindow(ByRef pi As ProjectItem) _
      
As Boolean
      Try
         If pi.FileCodeModel Is Nothing Then
            Return False
         Else
            Return True
         End If
      Catch
         Return False
      End Try
   End Function

The next function returns True if the ActiveWindow is a Form Designer, otherwise False.

   Public Function IsProjectItemAForm() As Boolean
      Dim oWin As Window = oVB.ActiveWindow
      
Return TypeOf oWin.Object Is System.ComponentModel.Design.IDesignerHost

      
If TypeOf oWin.Object Is System.ComponentModel.Design.IDesignerHost Then
         Return True
      Else
         Return False
      End If
   End Function

The next function returns an object as Project, representing the active Project in the Solution.

   Public Function GetActiveSolutionProject() As Project
      
' Gets currently selected project and
      ' return the project to the caller.
      Dim projs As System.Array
      
Dim proj As Project
      
Dim projects As Projects

      
Try
         projs = Connect.oVB.ActiveSolutionProjects()
        
If projs.Length > 0 Then
            proj = CType(projs.GetValue(0), EnvDTE.Project)
            
Return proj
        
End If
      Catch ex As System.Exception
         StructuredErrorHandler(ex.ToString)
      
End Try
   End Function

Top of Page