4

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
    You can step through all three files at once if you change the exe from singleuse to multiuse. You don't have to work with a compiled version until you go to SingleUse on the exe, and you never have to with the dll. – BobRodes Feb 07 '15 at 22:22

1 Answers1

3

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.

BobRodes
  • 5,990
  • 2
  • 24
  • 26
  • BobRodes, thank you for your reply. In the actual application I am an array of UDTs, and the instance of the DLL is one member of the UDT. This was a quick and dirty UI to work out the communication between the three components. More testing on my part revealed that when I start a second instance, the first counter actually halts, then resumes after the second counter finishes. I don't believe it is a stacking of the events because if I debug the UI, lc1.WResultVal has not changed. More playing to ensue. Also, theinitial environment is a single core CPU. I will test on my Core i7 and update. – Keith Kibler Jan 28 '15 at 12:55
  • Clarification: what I said is true of all the events for one button push. You're talking about multiple button pushes, and the event queue has time to flush between the time that you're pushing those buttons. So, the order is click event, all of your Report events, all of your timer events, if you just push one button. If you push another button, then that same order of events for the second button will be mixed in with the events from the first one, depending on when they fire in relation to your other stuff. (...) – BobRodes Jan 29 '15 at 00:00
  • If you decide that that isn't a stable architecture, then I would suggest you use the code I gave you as a starting point, except that you'll have to take the lc array and write them all out as you have them. (You should still use arrays for all of your controls, though.) To avoid that, you'll need to use callbacks, and I'm disinclined to complicate things by telling you how to do that until you decide to dump your timer concept. :) – BobRodes Jan 29 '15 at 00:02
  • BobRodes, Much thanks for your continued analysis... I appreciate it! I am trying your suggestion of an event handler at the UI level. My only concern is that this is a small prototype - in the "wild" there is a potential I may have up to 128 instances of the ActiveX EXE to track, not just 5 but that's a beast of an entirely different color. I will post back later today my updated code implementing an event between the UI and the DLL. My primary question is do I have the settings correct for this to work on a multicore machine? will they run parallel? – Keith Kibler Jan 29 '15 at 15:12
  • Primary question: that concern is handled under the hood. As for tracking which instance you are dealing with, you may want to implement callbacks. The difference between events and callbacks is analogous to the difference between broadcast and point-to-point communication. – BobRodes Jan 29 '15 at 15:19
  • p. s. what is your reason for doing this singleuse instead of multiuse on the exe? – BobRodes Jan 29 '15 at 15:24
  • singleuse-versus-multiuse: just the last setting I used. i can see the different instances get fired off (in Resource Monitor) when i press each button. I did implement the callback from the WClass dll to the UI, and still the same results. The "real application" is an ActiveX EXE that downloads a given file to a serial device (115200 baud). I would like to be downloading as many as I can at one time, as we may have up to 128 devices to download in any given run. the "COM" ports are really networked terminal servers. The real code uses a .1 second sleep to throttle the communications. – Keith Kibler Jan 29 '15 at 18:28
  • Ok, makes sense as to SingleUse. "I did implement the callback...and still the same results"? Are you still trying to use that timer solution in some way? – BobRodes Jan 30 '15 at 01:33
  • In WClass, I use a "form-less" timer. the UI cmd1_Click calls a method in WClass to start a timer, then returns immediately. When the timer fires, it starts TClass. I got that Idea [here.](http://stackoverflow.com/questions/727386/making-a-c-sharp-kill-event-for-a-vb6-app/752841#752841) – Keith Kibler Jan 30 '15 at 12:35
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/69898/discussion-between-keith-kibler-and-bobrodes). – Keith Kibler Jan 30 '15 at 14:41