VB6 Multiple Instances of ActiveX Object

1.7k Views Asked by At

In VB6 (due to client requirements), I need to be able to execute multiple instances of an ActiveX EXE that I wrote to download files to multiple units via RS232.

I have developed a test application that, I think mirrors what I need to do. First, an ActiveX EXE that simulates the download process called TClass. This ActiveX EXE raises events to report back its current progress as thus:

TClass.exe (ActiveX EXE, Instancing = SingleUse, Threading Model = Thread per Object)

Option Explicit

Private Declare Sub Sleep Lib "kernel32.dll" (ByVal dwMilliseconds As Long)

Public Event Progress(Value As Long)

Public SeedVal As Long

Public Sub MultByTwo()
    Dim i As Integer
    Dim lVal As Long

    lVal = SeedVal

    For i = 0 To 10
        Sleep (2000)
        lVal = lVal * 2
        RaiseEvent Progress(lVal)
    Next i

    Exit Sub
End Sub

Next a wrapper class to instantiate TClass and handle the call-back events (Progress), call it WClass (AxtiveX DLL, Instancing = MultiUse, Apartment Threaded):

Option Explicit

Public WSeedVal As Long
Public WResultVal As Long

Private WithEvents MYF87 As TClass.TargetClass

Private Sub Class_Initialize()
    ' Set MYF87 = CreateObject("TClass.TargetClass")
    Set MYF87 = New TClass.TargetClass
End Sub

Public Function Go() As Integer
    MYF87.SeedVal = WSeedVal
    MYF87.MultByTwo
End Function

Public Sub MYF87_Progress(Value As Long)
    WResultVal = Value
    DoEvents
End Sub

Public Function CloseUpShop() As Integer
    Set MYF87 = Nothing
End Function

And finally the UI to instantiate WClass. This is a simple forms app:

Option Explicit

Private lc1 As WClass.WrapperClass
Private lc2 As WClass.WrapperClass
Private lc3 As WClass.WrapperClass
Private lc4 As WClass.WrapperClass
Private lc5 As WClass.WrapperClass

Private Sub cmd1_Click()
    Set lc1 = CreateObject("WClass.WrapperClass")
    lc1.WSeedVal = CInt(txt1.Text)
    lc1.Go
End Sub

Private Sub cmd2_Click()
    Set lc2 = CreateObject("WClass.WrapperClass")
    lc2.WSeedVal = CInt(txt2.Text)
    lc2.Go
End Sub

Private Sub cmd3_Click()
    Set lc3 = CreateObject("WClass.WrapperClass")
    lc3.WSeedVal = CInt(txt3.Text)
    lc3.Go
End Sub

Private Sub cmd4_Click()
    Set lc4 = CreateObject("WClass.WrapperClass")
    lc4.WSeedVal = CInt(txt4.Text)
    lc4.Go
End Sub

Private Sub cmd5_Click()
    Set lc5 = CreateObject("WClass.WrapperClass")
    lc5.WSeedVal = CInt(txt5.Text)
    lc5.Go
End Sub

Private Sub Form_Load()
    Timer1.Interval = 2000
    Timer1.Enabled = True
End Sub

Private Sub Form_Unload(Cancel As Integer)
    If Not lc1 Is Nothing Then
        lc1.CloseUpShop
        Set lc1 = Nothing
    End If
    If Not lc2 Is Nothing Then
        lc2.CloseUpShop
        Set lc2 = Nothing
    End If
    If Not lc3 Is Nothing Then
        lc3.CloseUpShop
        Set lc3 = Nothing
    End If
    If Not lc4 Is Nothing Then
        lc4.CloseUpShop
        Set lc4 = Nothing
    End If
    If Not lc5 Is Nothing Then
        lc5.CloseUpShop
        Set lc5 = Nothing
    End If
End Sub

Private Sub Timer1_Timer()

    If Timer1.Enabled Then
        Timer1.Enabled = False

        If Not lc1 Is Nothing Then
            txtRes1.Text = CStr(lc1.WResultVal)
            txtRes1.Refresh
        End If

        If Not lc2 Is Nothing Then
            txtRes2.Text = CStr(lc2.WResultVal)
            txtRes2.Refresh
        End If

        If Not lc3 Is Nothing Then
            txtRes3.Text = CStr(lc3.WResultVal)
            txtRes3.Refresh
        End If

        If Not lc4 Is Nothing Then
            txtRes4.Text = CStr(lc4.WResultVal)
            txtRes4.Refresh
        End If

        If Not lc5 Is Nothing Then
            txtRes5.Text = CStr(lc5.WResultVal)
            txtRes5.Refresh
        End If

        Timer1.Interval = 2000
        Timer1.Enabled = True

    End If

    DoEvents

End Sub

txt1, txt2, txt3, txt4 and txt5 are Text items that provide a seed value that ends up getting passed to TClass as a property. txtRes1, txtRes2, txtRes3, txtRes4 and txtRes5 are Text items to hold the results of TClass.MultByTwo, as reported via the RaiseEvent Progress() call. cmd1, cmd2, cmd3, cmd4 and cmd5 are tied to the corresponding _Click functions above, and instantiate WClass.WrapperClass and get everything going. The form also has a Timer object called Timer1 set to fire every 2 seconds. The only purpose of this is to update the UI from the public properties in WClass.

I have built TClass to TClass.exe and WClass to WClass.dll and referenced WClass.dll from the UI app. When I run the form and click cmd1, the first thing i notice is that the Timer1_Timer no longer fires, so my UI never gets updated. Second, if I click on cmd2, it will fire, but appears to block the execution of the first instance.

I have spent a couple days reading posts and instructions on MSDN... no luck... any help would be greatly appreciated!

Thanks!

Update: I have changed the WClass.dll wrapper class to implement the recommendation of using callback functions. See below:

V2: WClass.dll (ActiveX DLL, Apartment Threading, Instancing = MultiUse)

Option Explicit

Public WSeedVal As Long
Public WResultVal As Long

Public Event WProgress(WResultVal As Long)

Private WithEvents MyTimer As TimerLib.TimerEx
Private WithEvents MYF87 As TClass.TargetClass
Private gInterval As IntervalData

Private Sub Class_Initialize()
    Set MyTimer = CreateObject("TimerLib.TimerEx")
    ' Set MyTimer = New TimerLib.TimerEx

    Set MYF87 = CreateObject("TClass.TargetClass")
    ' Set MYF87 = New TClass.TargetClass
End Sub

Public Function Go() As Integer
    gInterval.Second = 1
    MyTimer.IntervalInfo = gInterval
    MyTimer.Enabled = True
End Function

Private Sub MyTimer_OnTimer()
    MyTimer.Enabled = False
    MYF87.SeedVal = WSeedVal
    MYF87.MultByTwo
End Sub

Public Sub MYF87_Progress(Value As Long)
    WResultVal = Value
    RaiseEvent WProgress(WResultVal)
    DoEvents
End Sub

Public Function CloseUpShop() As Integer
    Set MYF87 = Nothing
End Function

Requisite changes in UI Class:

Option Explicit

Private WithEvents lc1 As WClass.WrapperClass
Private WithEvents lc2 As WClass.WrapperClass
Private WithEvents lc3 As WClass.WrapperClass
Private WithEvents lc4 As WClass.WrapperClass
Private WithEvents lc5 As WClass.WrapperClass

Private Sub cmd1_Click()
    ' MsgBox ("Begin UI1.cmd1_Click")
    Set lc1 = CreateObject("WClass.WrapperClass")

    lc1.WSeedVal = CInt(txt1.Text)
    lc1.Go
    ' MsgBox ("End UI1.cmd1_Click")
End Sub

Public Sub lc1_WProgress(WResultVal As Long)
    txtRes1.Text = CStr(WResultVal)
    txtRes1.Refresh

    DoEvents
End Sub

Private Sub cmd2_Click()
    Set lc2 = CreateObject("WClass.WrapperClass")
    lc2.WSeedVal = CInt(txt2.Text)
    lc2.Go
End Sub

Public Sub lc2_WProgress(WResultVal As Long)
    txtRes2.Text = CStr(WResultVal)
    txtRes2.Refresh

    DoEvents
End Sub

Private Sub cmd3_Click()
    Set lc3 = CreateObject("WClass.WrapperClass")
    lc3.WSeedVal = CInt(txt3.Text)
    lc3.Go
End Sub

Public Sub lc3_WProgress(WResultVal As Long)
    txtRes3.Text = CStr(WResultVal)
    txtRes3.Refresh

    DoEvents
End Sub

Private Sub cmd4_Click()
    Set lc4 = CreateObject("WClass.WrapperClass")
    lc4.WSeedVal = CInt(txt4.Text)
    lc4.Go
End Sub

Public Sub lc4_WProgress(WResultVal As Long)
    txtRes4.Text = CStr(WResultVal)
    txtRes4.Refresh

    DoEvents
End Sub

Private Sub cmd5_Click()
    Set lc5 = CreateObject("WClass.WrapperClass")
    lc5.WSeedVal = CInt(txt5.Text)
    lc5.Go
End Sub

Public Sub lc5_WProgress(WResultVal As Long)
    txtRes5.Text = CStr(WResultVal)
    txtRes5.Refresh

    DoEvents
End Sub

Private Sub Form_Load()
    ' Timer1.Interval = 2000
    ' Timer1.Enabled = True
    Timer1.Enabled = False
End Sub

Private Sub Form_Unload(Cancel As Integer)
    If Not lc1 Is Nothing Then
        lc1.CloseUpShop
        Set lc1 = Nothing
    End If
    If Not lc2 Is Nothing Then
        lc2.CloseUpShop
        Set lc2 = Nothing
    End If
    If Not lc3 Is Nothing Then
        lc3.CloseUpShop
        Set lc3 = Nothing
    End If
    If Not lc4 Is Nothing Then
        lc4.CloseUpShop
        Set lc4 = Nothing
    End If
    If Not lc5 Is Nothing Then
        lc5.CloseUpShop
        Set lc5 = Nothing
    End If
End Sub

I still see the same behavior... Click cmd1, then I see the results start in txtRes1. Click cmd2, results stop updating in txtRes1, and txtRes2 updates until it finishes, then txtRes1 updates.

I would not expect this to work in the VB6 debugger, as it is single-threaded, but creating an executable and running that executable still produces these same results.

I have also tried changing the way my TClass is instantiated (New versus CreateObject) - no difference noticed. I have also tried using New and CreateObject() when instantiating WClass too... still not doing what I would like it to do...

1

There are 1 best solutions below

10
On

Since you did such a nice job of asking your question, making it pretty easy to set everything up, I spent a little time fooling around with this. First, your DLL and EXE work fine. Your problem is that your Timer solution to handling screen updates has sent you down the rabbit hole.

First, the Timer event never fires unless the timer is enabled, so it's useless to check the Enabled property inside the event handler. Next, when you call DoEvents, it only flushes the event queue for the current object. So, calling DoEvents in MYF87_Progress does not run your Timer event. So it isn't correct that the Timer event doesn't fire; what's happening is that all your Timer events stack up in the form's event queue and they all get executed at once when the DLL is done executing. This design, as you are finding, isn't working, and even if you figure out a way to fix it you'll have something resembling Jed Clampett's truck.

A better design is to add a Progress event to your DLL as well, raise it from your MYF87_Progress handler, and let your form handle it. (I'm assuming that the reason for your wrapper DLL is that you have more stuff to put in it that should only go in one place, otherwise I'd suggest that you simplify your design by having your form call the EXE directly.) Call DoEvents in your form handler to update the screen.

Next, this implementation cries out for control arrays. You can put each of your command buttons, each of your sets of five text boxes, and each of your DLL instances in an array. This will greatly simplify the work you have to do. In fact, your entire Form code is pretty much reducible to this (plus the event handler I've mentioned):

Option Explicit

Private lc(4) As WClass.WrapperClass

Private Sub cmd_Click(Index As Integer)
    Set lc(Index) = CreateObject("WClass.WrapperClass")
    With lc(Index)
        .WSeedVal = CInt(txt(Index).Text)
        .Go
        txtRes(Index).Text = CStr(.WResultVal)
    End With
End Sub

This code will show the end result each time you push a button, but won't keep updating your text boxes every time there's a change posted from your EXE. To do that, you'll need to put in that event logic. I'll leave that to you since you appear to know how to do it already.

Suppose you have a go at all that, and post back if you have problems.

p. s. to make a control array, simply make all the controls in the array have the same name, and set the Index property to 0, 1, 2, 3, etc.

p. p. s. I forgot that you can't put WithEvents with an object array. I'm going to mess around with this and see if there's a way to get the objects in an array, but it might be necessary to have separate variables as you have them now.