|
|
Multi-Threadiing in an Add-in to Solve Timing IssuesOpen Code Window of new ProjectItem | | 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 |
|