Raising events in VB6 from .NET via COM Interop

So I've been experimenting with VB6 and a .NET DLL compiled with COM interop to see how it behaves.  The results are a little surprising.

What I did was I built a test scenario where the "Client" (vb6 app) hangs for 5 seconds while the "Server" (.NET COM object) fires an event once per second.

First, I start the timer.  Inside the DLL on each timer tick, it adds 1 to an internal counter ("Count").  When the timer fires, it raises an event and passes the value of Count at that time as a parameter (nTicks).

When the client app (vb6) hangs, internally the count continues to run, but the events get queued up.  VB6 gets all the events in a rush when the 5 second sleep is over.  

Here's the part where it gets interesting. During that rush, the events come in out-of-order.  The order seems random.  I've run it several times and the order always varies.  I circled the interesting part in red, below.

Aside from the return order issue I think this threading behavior could be interesting, such as launching a complicated calculation or SQL query, and then returning the result when it is completed.  As long as the entire operation is "atomic", it won't matter if the results come out of order, or a sequence number could be used to ensure the results are processed in the proper order.

Here is the VB6 code for the test app above...

 Option Explicit  
Dim WithEvents foo As ClassTest1.ComClass3
Private Declare Sub SleepAPI Lib "kernel32" Alias "Sleep" (ByVal dwMilliseconds As Long)
Public Sub WriteLn(ByVal buf$)
List1.AddItem Time$ & ": " & buf$
List1.ListIndex = List1.NewIndex
End Sub
Private Sub Command1_Click()
foo.CauseClickEvent 2, 3
End Sub
Private Sub Command2_Click()
End Sub
Private Sub Command3_Click()
WriteLn "Start Timer"
End Sub
Private Sub Command4_Click()
WriteLn "Stop timer"
End Sub
Private Sub Command5_Click()
WriteLn "Sleep 5 seconds..."
SleepAPI 5000
WriteLn "...Done"
End Sub
Private Sub foo_Click(ByVal x As Long, ByVal y As Long)
MsgBox "foo_click(" & x & ", " & y & ")"
End Sub
Private Sub foo_Pulse(ByVal nTicks As Long)
WriteLn "nTicks=" & nTicks & ", Count=" & foo.GetCount
End Sub
Private Sub Form_Load()
Set foo = New ClassTest1.ComClass3
End Sub

And, here is the VB.NET code for the COM Server DLL...

 Option Explicit On  
Option Strict On
Imports System
Imports System.Runtime.InteropServices
Namespace MyComClass3
Public Delegate Sub ClickDelegate(x As Integer, y As Integer)
Public Delegate Sub ResizeDelegate()
Public Delegate Sub PulseDelegate(nTicks As Integer)
<InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIDispatch)> _
Public Interface IComClass3Events
Sub Resize()
Sub Click(x As Integer, y As Integer)
Sub Pulse(nTicks As Integer)
End Interface
Public Interface IComClass3
'Subs, Functions, Properties go here
'No subs, functions, or events need to be declared here
'to make our events work properly in COM
End Interface
<ComClass(ComClass3.ClassId, ComClass3.InterfaceId, ComClass3.EventsId)>
Public Class ComClass3 : Implements IComClass3
#Region "COM GUIDs"
' These GUIDs provide the COM identity for this class
' and its COM interfaces. If you change them, existing
' clients will no longer be able to access the class.
Public Const ClassId As String = "0f3173c9-d101-40db-9317-333e5b1948be"
Public Const InterfaceId As String = "0dd277ac-567d-4899-8b60-f8b67a8dac70"
Public Const EventsId As String = "fff2962c-52a8-486a-b68f-8af64a419d41"
#End Region
Public Event Click As ClickDelegate
Public Event Resize As ResizeDelegate
Public Event Pulse As PulseDelegate
Private _Enabled As Boolean
Private _Counter As Integer
Dim WithEvents myTimer As New Timers.Timer
' A creatable COM class must have a Public Sub New()
' with no parameters, otherwise, the class will not be
' registered in the COM registry and cannot be created
' via CreateObject.
Public Sub New()
End Sub
Public Sub CauseClickEvent(x As Integer, y As Integer)
RaiseEvent Click(x, y)
End Sub
Public Sub CauseResizeEvent()
RaiseEvent Resize()
End Sub
Public Function GetCount() As Integer
GetCount = _Counter
End Function
Public Sub CausePulse()
RaiseEvent Pulse(_Counter)
End Sub
Public Sub StartTicker()
_Enabled = True
myTimer.Interval = 1000
myTimer.Enabled = True
End Sub
Public Sub StopTicker()
_Enabled = False
myTimer.Enabled = False
End Sub
Private Sub myTimer_Elapsed(sender As Object, e As Timers.ElapsedEventArgs) Handles myTimer.Elapsed
_Counter = _Counter + 1
Call CausePulse()
End Sub
End Class
End Namespace


Popular posts from this blog

Installing OTRS 4.0 on Ubuntu Linux 14.04 with a MSSQL Backend

that new old thing: a date2num implementation in vb.net

Getting Started with GitHub and Visual Studio 2017