Dynamic Events in VB.NET
How to handle dynamic, late binding, events for COM objects in VB.NET.
Introduction
This article shows a way to use events with late bound COM objects. To solve the problem some C# script was used...
Background
There are two common ways in VB.NET to create a late bound object.
-
Using the
CreateObject
method.Dim ie As Object = CreateObject("InternetExplorer.Application")
-
Using the
System.Activator.CreateInstance
method.Dim iet As Type = Type.GetTypeFromProgID("InternetExplorer.Application") Dim ie As Object = Activator.CreateInstance(iet)
And there are three common ways in VB.NET to call methods and properties on a late bound object.
-
With
Option Strict
turned off' call a method ie.Navigate("about:blank") ' read a property Dim readyState As Integer = ie.ReadyState ' write a property ie.Visible = True
-
Using the
Microsoft.VisualBasic.Interaction.CallByName
method.' Call a method CallByName(ie, "Navigate", CallType.Method, "about:blank") ' Read a property readyState = CInt(CallByName(ie, "ReadyState", CallType.Get)) ' Write a property CallByName(ie, "Visible", CallType.Let, True)
-
Using the
System.Type.InvokeMember
method.' Call a method ie.GetType().InvokeMember("Navigate", BindingFlags.InvokeMethod, Type.DefaultBinder, ie, {"about:blank"}) ' Read a property readyState = CInt(ie.GetType().InvokeMember("ReadyState", BindingFlags.GetProperty, Type.DefaultBinder, ie, Nothing)) ' Write a property ie.GetType().InvokeMember("Visible", BindingFlags.SetProperty, Type.DefaultBinder, ie, {True})
There are some ways discussed on how to use events with late bound objects, see Google search ' Eventhandler for late bound objects). But there isn't any way to add event handlers on late bound COM objects like with the dynamic Type in C#.
-
Working in C#
public delegate void OnQuitDelegate(); static void Main(string[] args) { var iet = Type.GetTypeFromProgID("InternetExplorer.Application"); dynamic ie = Activator.CreateInstance(iet); ie.OnQuit += new OnQuitDelegate(ie_OnQuit); } public static void ie_OnQuit() { }
-
Not working in VB.NET
Dim iet As Type = Type.GetTypeFromProgID("InternetExplorer.Application") ' No dynamic type... Dim ie = Activator.CreateInstance(iet) ' Not working... ' AddHandler ie.OnQuit, AddressOf ie_OnQuit ' Not working also... 'ie.OnQuit += New OnQuitDelegate(AddressOf ie_OnQuit)
A first idea was to write a library in C# but the name of the event is hard coded. So that wasn't possible too, except ...It is when compiling C# script at runtime...
-
So first a interface was made to make calls into the compiled C# script.
Public Interface IEventBinder Sub [AddHandler](ByVal source As Object, ByVal [delegate] As [Delegate]) Sub [RemoveHandler](ByVal source As Object, ByVal [delegate] As [Delegate]) End Interface
-
Then a class was written that implemented the interface with Factory methods that compiled and returned instances of the class/interface. From then on events could be attached to and detached from an object.
Imports Microsoft.CSharp Imports System.CodeDom Imports System.CodeDom.Compiler Imports System.Reflection Imports System.Text Public NotInheritable Class EventBinder Implements IEventBinder Private _name As String Private _event As IEventBinder Private Sub New(ByVal name As String, ByVal [event] As IEventBinder) Me._name = name Me._event = [event] End Sub Public ReadOnly Property Name As String Get Return _name End Get End Property Public Sub [AddHandler](source As Object, [delegate] As System.Delegate) Implements IEventBinder.AddHandler Me._event.AddHandler(source, [delegate]) End Sub Public Sub [RemoveHandler](source As Object, [delegate] As System.Delegate) Implements IEventBinder.RemoveHandler Me._event.RemoveHandler(source, [delegate]) End Sub Public Shared Function CreateEventBinders(ByVal names() As String) As Dictionary(Of String, EventBinder) Using provider As CodeDomProvider = New CSharpCodeProvider() Dim parameters As New CompilerParameters() With { .GenerateInMemory = True } parameters.ReferencedAssemblies.AddRange({ "System.dll", "System.Core.dll", Assembly.GetExecutingAssembly().Location, "Microsoft.CSharp.dll" }) Dim uid As String = Guid.NewGuid().ToString("N") Dim code As New StringBuilder() code.Append( "using System;" & vbCrLf & "" & vbCrLf & "namespace DynamicEvent {" & vbCrLf ) For Each name As String In names code.Append( String.Format( "public class {0}Event{2} : {1}.IEventBinder" & vbCrLf & "{{" & vbCrLf & " public void AddHandler(dynamic source, Delegate del) {{ source.{0} += del; }}" & vbCrLf & " public void RemoveHandler(dynamic source, Delegate del) {{ source.{0} -= del; }}" & vbCrLf & "}}" & vbCrLf, name, GetType(IEventBinder).Namespace, uid ) ) Next code.Append("}") Dim codeResult As CompilerResults = provider.CompileAssemblyFromSource(parameters, code.ToString()) If codeResult.Errors.Count = 0 Then Dim result As New Dictionary(Of String, EventBinder) For Each name As String In names Dim codeType As Type = codeResult.CompiledAssembly.GetType(String.Format("DynamicEvent.{0}Event{1}", name, uid)) Dim codeObject As Object = Activator.CreateInstance(codeType) result.Add(name, New EventBinder(name, DirectCast(codeObject, IEventBinder))) Next Return result Else ' todo: Compiler errors... Return Nothing End If End Using End Function Public Shared Function CreateEventBinder(ByVal name As String) As EventBinder Return CreateEventBinders({name})(name) End Function End Class
Using the code
To attach an event now:
-
A delegate declaration is required.
Delegate Sub OnQuitDelegate()
-
A event handler is required.
Sub ie_OnQuit() ' ... End SUb
-
A instance of the delegate is required.
Dim [delegate] As [Delegate] = New OnQuitDelegate(AddressOf ie_OnQuit)
-
And a instance of EventBinder for the specific event is required.
Dim binder As EventBinder = EventBinder.CreateEventBinder("OnQuit")
-
From then on it's possible to attach, detach events...
Dim ie As Object = CreateObject("InternetExplorer.Application") binder.AddHandler(ie, [delegate]) binder.RemoveHandler(ie, [delegate])
But it's better for performance to create the interfaces for all events at once. (Not that speed matters, this is VB.NET and has the speed of light...)
- Bind to more then one event:
' Create event binders Dim binders As Dictionary(Of String, EventBinder) binders = EventBinder.CreateEventBinders({ "BeforeNavigate2", "DocumentComplete", "NavigateComplete2", "OnQuit", "StatusTextChange" }) ' Create event handlers Dim delegates() As [Delegate] = { New BeforeNavigate2Delegate(AddressOf ie_BeforeNavigate2), New DocumentCompleteDelegate(AddressOf ie_DocumentComplete), New NavigateComplete2Delegate(AddressOf ie_NavigateComplete2), New OnQuitDelegate(AddressOf ie_OnQuit), New StatusTextChangeDelegate(AddressOf ie_StatusTextChange) } ' Bondage... For i As Integer = 0 To binders.Count - 1 binders(binders.Keys(i)).AddHandler(ie, delegates(i)) Next
History
- 18/03/2013: First draft.