I am trying to use a C# DLL in VBA with events using the [ComSourceInterfaces] attribute, but I am encountering issues. Here's what I have so far:
C# Code
ITaskRunnerEvents.cs
using System;
using System.Runtime.InteropServices;
namespace ComEventTest
{
[ComVisible(true)]
[Guid("c8614250-4291-4fb0-8b45-4aa305b0c595")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface ITaskRunnerEvents
{
void OnTaskCompleted(string result);
}
}
TaskRunner.cs
using System.Runtime.InteropServices;
using System.Threading.Tasks;
namespace ComEventTest
{
[ComVisible(true)]
[Guid("ac9de195-73e8-44ae-8cf1-d8f110421923")]
[ClassInterface(ClassInterfaceType.None)]
[ComSourceInterfaces(typeof(ITaskRunnerEvents))]
public class TaskRunner
{
public delegate void TaskCompletedEventHandler(string result);
public event TaskCompletedEventHandler OnTaskCompleted;
public void RunTask(string input)
{
Task.Run(async () =>
{
await Task.Delay(5000); // Simulate work
OnTaskCompleted?.Invoke($"Task completed with input: {input}");
});
}
}
}
VBA Code
Class Module: TaskRunnerEventHandler
Option Compare Database
Option Explicit
Public WithEvents taskRunner As ComEventTest.taskRunner
Private Sub taskRunner_OnTaskCompleted(ByVal result As String)
MsgBox result
End Sub
Public Sub InitializeTaskRunner()
Set taskRunner = New ComEventTest.taskRunner
End Sub
Usage Module
Sub TestTaskRunner()
Set eventHandler = New TaskRunnerEventHandler
eventHandler.InitializeTaskRunner
eventHandler.taskRunner.RunTask "Test Input"
End Sub
Problem
Method or data member not found
Finally, I found a solution to the issue of handling events from a C# COM object in VBA using the WithEvents
keyword. Below is the code and explanation that helped me resolve the problem:
C# Code :
In the C# code, I implemented a COM object that raises an event (OnTaskCompleted)
when a task is completed. The key part is the use of the [ComSourceInterfaces]
attribute, which allows the COM object to expose the event to VBA.
TaskRunner.cs
using System;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Collections.Concurrent;
namespace ComEventTest
{
[Guid("cc6eeac0-fe23-4ce4-8edb-676a11c57c7c")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface ITaskRunner
{
[DispId(1)]
void RunTask(string input);
}
[Guid("619a141c-5574-4bfe-a663-2e5590e538e2")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface ITaskRunnerEvents
{
[DispId(1)]
void OnTaskCompleted(string result);
}
[ComVisible(true)]
[Guid("9acdd19f-b688-48c0-88d9-b81b7697d6d4")]
[ClassInterface(ClassInterfaceType.None)]
[ComSourceInterfaces(typeof(ITaskRunnerEvents))]
public class TaskRunner : ITaskRunner
{
[ComVisible(true)]
public delegate void TaskCompletedEventHandler(string result);
[DispId(1)]
public event TaskCompletedEventHandler OnTaskCompleted;
private ConcurrentQueue<string> taskQueue = new ConcurrentQueue<string>();
private bool isProcessingQueue = false;
public void RunTask(string input)
{
taskQueue.Enqueue(input);
ProcessQueue();
}
private async void ProcessQueue()
{
if (isProcessingQueue)
return;
isProcessingQueue = true;
while (taskQueue.TryDequeue(out string input))
{
try
{
await Task.Delay(5000); // Simulate work
OnTaskCompleted?.Invoke($"Task completed with input: {input}");
}
catch (Exception ex)
{
OnTaskCompleted?.Invoke($"Task failed: {ex.Message}");
}
}
isProcessingQueue = false;
}
}
}
ComEventTest.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net48</TargetFramework>
<AssemblyTitle>VbaEventLibrary</AssemblyTitle>
<ComVisible>true</ComVisible>
<LangVersion>8.0</LangVersion> <!-- Specify C# version 8.0 -->
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>ComEventTestKey.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
</Project>
Note: For VBA usage, you need to ensure the following steps are completed:
Create a Strong Name for Your COM Component
sn.exe
tool to generate a strong name key file for your
assembly.sn -k YourKeyName.snk
.snk
file. You can configure this in the AssemblyInfo file or in
your project settings under Signing.Register the COM Component in the Registry
regasm ComEventTest.dll /codebase /tlb:ComEventTest.tlb
/codebase
flag if your assembly is not in the
Global Assembly Cache (GAC)VBA Code :
In the VBA code, I used the WithEvents
keyword to handle the OnTaskCompleted
event. This allows VBA to listen for and process events raised by the C# COM object. I also created an event handler class (TaskRunnerEventHandler)
to handle the event and process the results.
Class Module: TaskRunnerEventHandler
Option Compare Database
Option Explicit
Public WithEvents taskRunner As ComEventTest.taskRunner
Private Sub taskRunner_OnTaskCompleted(ByVal result As String)
'MsgBox result
Debug.Print result
End Sub
Public Sub InitializeTaskRunner()
Set taskRunner = New ComEventTest.taskRunner
End Sub
Public Sub FireEvent(poraka As String)
taskRunner.RunTask poraka
End Sub
Usage module
Option Compare Database
Option Explicit
Dim eventHandlers As Collection
Sub InitializeEventHandlers()
Set eventHandlers = New Collection
End Sub
Sub TestTaskRunner(Optional retr As String)
If eventHandlers Is Nothing Then
InitializeEventHandlers
End If
Dim newEventHandler As TaskRunnerEventHandler
Set newEventHandler = New TaskRunnerEventHandler
newEventHandler.InitializeTaskRunner
eventHandlers.Add newEventHandler
Dim i As Integer
For i = 1 To 10
newEventHandler.FireEvent "Task " & retr & "-" & i
Sleep 100 ' Simulate delay for async task running
Debug.Print "Task " & retr & "-" & i & " is running asynchronously!"
Next i
End Sub
Sub TestTaskRunner_MultCalls()
' Fire multiple calls to TestTaskRunner
Dim i As Integer
For i = 1 To 10
Debug.Print "New CALL SUB fire " & i
TestTaskRunner CStr(i)
Sleep 500 ' Simulate delay between multiple calls
Next i
End Sub
Explanation:
The C# COM object exposes an event (OnTaskCompleted)
that is triggered after completing a task asynchronously.
In VBA, I used the WithEvents
keyword to declare the COM object and catch the (OnTaskCompleted)
event. This allows me to process the result of each task in the taskRunner_OnTaskCompleted
method.
I also simulated multiple task submissions in the VBA code using Sleep
to delay the execution and give time for the events to be raised and handled.
This solution worked, and now I can handle asynchronous events from the C# COM object seamlessly in VBA.
Any ideas to improve the above solution are welcome!