In order to test and debug in-process components (.dll and .ocx files), you will need a multithreaded client application. The steps to create a simple multithreaded application are as follows:
Important To ensure that each new MultiUse object starts a new thread, you must use Thread per Object rather than Thread Pool.
Sub Main executes for every new thread. The reason for this is that Visual Basic maintains a separate copy of your global data for each thread (each apartment). In order to initialize global data for the thread, Sub Main must execute. This means that if your Sub Main loads a hidden form, or displays your application's main user interface, new copies of those forms will be loaded for every new thread you create.
The following code determines whether or not Sub Main is executing in the first thread, so that you can load the hidden form only once and display the test application's main user interface only once.
' Root value for hidden window caption
Public Const PROC_CAPTION = "ApartmentDemoProcessWindow"
Public Const ERR_InternalStartup = &H600
Public Const ERR_NoAutomation = &H601
Public Const ENUM_STOP = 0
Public Const ENUM_CONTINUE = 1
Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
(ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Declare Function GetWindowThreadProcessId Lib "user32"_
(ByVal hwnd As Long, lpdwProcessId As Long) As Long
Declare Function EnumThreadWindows Lib "user32" _
(ByVal dwThreadId As Long, ByVal lpfn As Long, ByVal lParam As Long) _
As Long
' Window handle retrieved by EnumThreadWindows.
Private mhwndVB As Long
' Hidden form used to identify main thread.
Private mfrmProcess As New frmProcess
' Process ID.
Private mlngProcessID As Long
Sub Main()
Dim ma As MainApp
' Borrow a window handle to use to obtain the process
' ID (see EnumThreadWndMain call-back, below).
Call EnumThreadWindows(App.ThreadID, AddressOf EnumThreadWndMain, 0&)
If mhwndVB = 0 Then
Err.Raise ERR_InternalStartup + vbObjectError, , _
"Internal error starting thread"
Else
Call GetWindowThreadProcessId(mhwndVB, mlngProcessID)
' The process ID makes the hidden window caption unique.
If 0 = FindWindow(vbNullString, PROC_CAPTION & CStr(mlngProcessID)) Then
' The window wasn't found, so this is the first thread.
If App.StartMode = vbSModeStandalone Then
' Create hidden form with unique caption.
mfrmProcess.Caption = PROC_CAPTION & CStr(mlngProcessID)
' The Initialize event of MainApp (Instancing =
' PublicNotCreatable) shows the main user interface.
Set ma = New MainApp
' (Application shutdown is simpler if there is no
' global reference to MainApp; instead, MainApp
' should pass Me to the main user form, so that
' the form keeps MainApp from terminating.)
Else
Err.Raise ERR_NoAutomation + vbObjectError, , _
"Application can't be started with Automation"
End If
End If
End If
End Sub
' Call-back function used by EnumThreadWindows.
Public Function EnumThreadWndMain(ByVal hwnd As Long, ByVal _
lParam As Long) As Long
' Save the window handle.
mhwndVB = hwnd
' The first window is the only one required.
' Stop the iteration as soon as a window has been found.
EnumThreadWndMain = ENUM_STOP
End Function
' MainApp calls this Sub in its Terminate event;
' otherwise the hidden form will keep the
' application from closing.
Public Sub FreeProcessWindow()
Unload mfrmProcess
Set mfrmProcess = Nothing
End Sub
Note This technique for identifying the first thread may not work in future versions of Visual Basic.
Notice that Sub Main takes no action for any thread after the first. When you add code that creates MultiUse objects (in order to start subsequent threads) be sure to include code to initialize those objects.
EnumThreadWindows is used with a call-back, EnumThreadWndMain, to locate one of the hidden windows Visual Basic creates for its internal use. The window handle of this hidden window is passed to GetWindowThreadProcessId, which returns the process ID. The process ID is then used to create a unique caption for the hidden window (frmProcess) that Sub Main loads. Subsequent threads detect this window, and thus can tell that they don't need to create the MainApp object. These gyrations are necessary because Visual Basic does not provide a way to identify the application's main thread.
The MainApp class, in its Initialize event, displays the test application's main form. MainApp should pass its Me reference to the main form, so that the form keeps MainApp from terminating. From the main user interface you can create all subsequent threads. Setting the Instancing property for MainApp to PublicNotCreatable helps you avoid displaying two main user interface forms.
A simple example of a MainApp class and its associated form (steps 5 and 6, above) might look like this:
' Code for a MainApp class.
Private mfrmMTMain As New frmMTMain
Private Sub Class_Initialize()
Set mfrmMTMain.MainApp = Me
mfrmMTMain.Caption = mfrmMTMain.Caption & " (" & App.ThreadID & ")"
mfrmMTMain.Show
End Sub
Friend Sub Closing()
Set mfrmMTMain = Nothing
End Sub
Private Sub Class_Terminate()
' Clean up the hidden window.
Call FreeProcessWindow
End Sub
' Code for the form frmMTMain.
Public MainApp As MainApp
Private Sub Form_Unload(Cancel As Integer)
Call MainApp.Closing
Set MainApp = Nothing
End Sub
Including the process ID in the hidden window caption allows multiple instances of the test application to run without interfering with each other.
When you call CreateObject, the instance of the public class you create will be on a thread in the current application instance. This is because CreateObject always attempts to create an object in the current application before looking for other running Exe components that might supply the object.
You may find it useful to expose the process ID as a read-only property of the module that contains Sub Main:
'This code not required for the test application
Public Property Get ProcessID() As Long
ProcessID = mlngProcessID
End Property
This allows any object on the thread to get the process ID by calling the unqualified ProcessID property. You may also find it useful to expose a Boolean IsMainThread property in this fashion.
The Thread per Object option causes every public object that is externally created - that is, created using the CreateObject function - to start on a new thread. To create a new thread, simply use the programmatic ID (ProgID) of one of your MultiUse classes:
'This code not included in the test application
Dim tw As ThreadedWindow
Set tw = CreateObject("ThreadDemo.ThreadedWindow")
The variable tw now contains a reference to an object on a new thread. All calls to the properties and methods of this object that are made using tw will be subject to the extra overhead of cross-thread marshaling.
Note An object created with the New operator is not created on a new thread. It resides on the same thread where the New operator was executed. See "Designing Multithreaded Out-of-Process Components" and "How Object Creation Works in Visual Basic Components,."
To ensure that MainApp doesn't terminate until all of the other threads are finished, you can give each public class a MainApp property. When an object creates a MultiUse object on a new thread, it can pass the new object a reference to the MainApp object as part of the initialization process. (You can also pass MainApp a reference to the new object, so that MainApp has a collection of references to all objects that control threads; however, remember that this will create circular references. See "Dealing with Circular References.")
If you want a class that controls a thread to show a form, you should provide it with an Initialize method (not to be confused with the Initialize event) or a Show method that displays the form. Don't show the form in the Class_Initialize event procedure, as this could cause timing errors when you create instances of the class. In a very simple case, the code for a MultiUse ThreadedWindow class and its form, frmThreadedWindow, might look like this:
'Code for a MultiUse ThreadedWindow class.
Private mMainApp As MainApp
Private mfrm As New frmThreadedWindow
Public Sub Initialize(ByVal ma As MainApp)
Set mMainApp = ma
Set mfrm.ThreadedWindow = Me
mfrm.Caption = mfrm.Caption & " (" & App.ThreadID & ")"
mfrm.Show
End Sub
Friend Sub Closing()
Set mfrm = Nothing
End Sub
'Code for the form frmThreadedWindow.
Public ThreadedWindow As ThreadedWindow
Private Sub Form_Unload(Cancel As Integer)
Call ThreadedWindow.Closing
Set ThreadedWindow = Nothing
End Sub
The following code snippet shows how you might initialize the ThreadedWindow object:
'Code for the test application's main form (frmMTMain).
Private Sub mnuFileNewTW_Click()
Dim tw As ThreadedWindow
Set tw = CreateObject("ThreadDemo.ThreadedWindow")
' Tell the new object to show its form, and
' pass it a reference to the main
' application object.
Call tw.Initialize(Me.MainApp)
End Sub
If you have a number of classes that can control threads, you can make your code more generic by defining an IApartment interface to contain the Initialize method. When you implement IApartment in each class, you can provide the appropriate Initialize method code for that class. Your thread creation code might look like this:
'This code not required for the test application
Private Sub mnuFileNewObject_Click(Index As Integer)
Dim iapt As IApartment
Select Case Index
Case otThreadedWindow
Set iapt = CreateObject("ThreadDemo.ThreadedWindow")
' (other cases...)
End Select
' Common code to initialize objects.
Call iapt.Initialize(MainApp)
End Sub
Note You can make an IXxxxApartment interface that's known only to the multithreaded application by defining the interface in a separate type library. In the ActiveX Exe project, set a reference to the type library.
To ensure proper shutdown of a multithreaded application, you must keep careful track of all references to the MultiUse objects you use to create and control threads.
Define your object lifetime goals clearly. For example, consider the case of a MultiUse object that shows a form. The easiest way to manage object lifetime is to have the object pass the form a Me reference; the form then keeps the object alive. When the user closes the form, the form's Unload event must set all references to the MultiUse object to Nothing, so that the object can terminate and in turn clean up its reference to the form. (You may find it useful to give the MultiUse object a Friend method that cleans up the reference to the form, and all other internal object references; the form's Unload event can call this method.)
If the object that controls a thread creates additional objects on the thread, using the New operator, make sure you clean up references to those objects. The thread cannot close until all references to objects that were created on the thread have been released. Open threads consume system resources.
Because Friend properties and methods are not part of the public interface of a class, you cannot call them from another thread. Cross-thread calls between objects are limited to properties and methods that are declared Public.
If a method of an object yields by calling DoEvents, showing a modal form, or making a secondary call to an object on another thread, then the code in the method can be entered by a second caller before the first call completes. If such a method uses or changes property values or module-level variables, this could result in an invalid internal state for the object. To protect against reentrancy, you can:
Visual Basic doesn't provide a way to fork execution - that is, to have one thread initiate a method call on a new thread and immediately resume processing on the original thread. You can simulate this behavior in your test application by having the original method call turn on a timer and then return immediately. When the timer event occurs, you can turn the timer off and perform the asynchronous processing. This technique is discussed in "Asynchronous Call-Backs and Events", and is demonstrated (see "Creating an ActiveX Exe Component") and in the Coffee sample application.
You must compile the multithreaded test application in order to test your apartment-threaded component, because the Visual Basic development environment does not currently support multiple threads of execution. If you have Visual Studio, you may find it useful to compile the test application with debugging information for native code debugging, so that you can use the Visual Studio debugger.