Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

raising a vb6 event using interop

Tags:

c#

interop

vb6

I have a legacy VB6 component that I've imported into VS using tlbimp.exe to generate my interop assembly. The VB6 component defines an event that allows me to pass messages within VB6.

Public Event Message(ByVal iMsg As Variant, oCancel As Variant)

I would really like to be able to raise this even in my C# program, but its getting imported as an event, not a delegate or something else useful. So, I can only listen, but never fire. Does anyone know how to fire an event contained within VB6? The C# event looks like

[TypeLibType(16)]
[ComVisible(false)]
 public interface __MyObj_Event
 {
     event __MyObj_MessageEventHandler Message;
 }

I unfortunately cannot change the VB6 code. Thanks.

like image 761
Steve Avatar asked Mar 16 '09 19:03

Steve


1 Answers

Actually, hope is not lost yet. It is possible to raise an event on a COM object from outside of the object's class. This functionality is actually provided by COM itself, although in an indirect manner.

In COM, events work on a publish/subscribe model. A COM object that has events (the "event source") publishes events, and one or more other COM objects subscribe to the event by attaching an event handler to the source object (the handlers are called "event sinks"). Normally, the source object raises an event by simply looping through all the event sinks and calling the appropriate handler method.

So how does this help you? It just so happens that COM lets you query an event source for a list of all the event sink objects currently subscribed to the source object's events. Once you have a list of event sink objects, you can simulate raising an event by invoking each of the sink object's event handlers.

Note: I'm over-simplifying the details and being liberal with some of the terminology, but that's the short (and somewhat politically incorrect) version of how events work in COM.

You can take advantage of this knowledge to raise events on a COM object from external code. In fact, it is possible to do all of this in C#, with the help of the COM interop support in the System.Runtime.Interop and System.Runtime.Interop.ComTypes namespaces.


EDIT

I wrote a utility class that will allow you to raise events on a COM object from .NET. It's pretty easy to use. Here is an example using the event interface from your question:

MyObj legacyComObject = new MyObj();

// The following code assumes other COM objects have already subscribed to the 
// MyObj class's Message event at this point.
//
// NOTE: VB6 objects have two hidden interfaces for classes that raise events:
//
// _MyObj (with one underscore): The default interface.
// __MyObj (with two underscores): The event interface.
//
// We want the second interface, because it gives us a delegate
// that we can use to raise the event.
// The ComEventUtils.GetEventSinks<T> method is a convenience method
// that returns all the objects listening to events from the legacy COM object.

// set up the params for the event
string messageData = "Hello, world!";
bool cancel = false;

// raise the event by invoking the event delegate for each connected object...
foreach(__MyObj sink in ComEventUtils.GetEventSinks<__MyObj>(legacyComObject))
{
    // raise the event via the event delegate
    sink.Message(messageData, ref cancel);

    if(cancel == true)
    {
        // do cancel processing (just an example)
        break;
    }
}

Below is the code for the ComEventUtils class (as well as helper class, SafeIntPtr, because I'm paranoid and wanted a nice way to deal with the IntPtrS needed by the COM-related code):

Disclaimer: I haven't thoroughly tested the code below. The code performs manual memory management in a few places, and therefore there is the possibility that it could introduce memory leaks into your code. Also, I didn't add error-handling to the code, because this is only an example. Use with care.

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using COM = System.Runtime.InteropServices.ComTypes;

namespace YourNamespaceHere
{

/// <summary>
/// A utility class for dealing with COM events.
/// Needs error-handling and could potentially be refactored
/// into a regular class. Also, I haven't extensively tested this code;
/// there may be a memory leak somewhere due to the rather
/// low-level stuff going on in the class, but I think I covered everything.
/// </summary>
public static class ComEventUtils
{
    /// <summary>
    /// Get a list of all objects implementing an event sink interface T
    /// that are listening for events on a specified COM object.
    /// </summary>
    /// <typeparam name="T">The event sink interface.</typeparam>
    /// <param name="comObject">The COM object whose event sinks you want to retrieve.</param>
    /// <returns>A List of objects that implement the given event sink interface and which
    /// are actively listening for events from the specified COM object.</returns>
    public static List<T> GetEventSinks<T>(object comObject)
    {
        List<T> sinks = new List<T>();
        List<COM.IConnectionPoint> connectionPoints = GetConnectionPoints(comObject);

        // Loop through the source object's connection points, 
        // find the objects that are listening for events at each connection point,
        // and add the objects we are interested in to the list.
        foreach(COM.IConnectionPoint connectionPoint in connectionPoints)
        {
            List<COM.CONNECTDATA> connections = GetConnectionData(connectionPoint);

            foreach (COM.CONNECTDATA connection in connections)
            {
                object candidate = connection.pUnk;

                // I tried to avoid relying on try/catch for this
                // part, but candidate.GetType().GetInterfaces() kept
                // returning an empty array.
                try
                {
                    sinks.Add((T)candidate);
                }
                catch { }
            }

            // Need to release the interface pointer in each CONNECTDATA instance
            // because GetConnectionData implicitly AddRef's it.
            foreach (COM.CONNECTDATA connection in connections)
            {
                Marshal.ReleaseComObject(connection.pUnk);
            }
        }

        return sinks;
    }

    /// <summary>
    /// Get all the event connection points for a given COM object.
    /// </summary>
    /// <param name="comObject">A COM object that raises events.</param>
    /// <returns>A List of IConnectionPoint instances for the COM object.</returns>
    private static List<COM.IConnectionPoint> GetConnectionPoints(object comObject)
    {
        COM.IConnectionPointContainer connectionPointContainer = (COM.IConnectionPointContainer)comObject;
        COM.IEnumConnectionPoints enumConnectionPoints;
        COM.IConnectionPoint[] oneConnectionPoint = new COM.IConnectionPoint[1];
        List<COM.IConnectionPoint> connectionPoints = new List<COM.IConnectionPoint>();

        connectionPointContainer.EnumConnectionPoints(out enumConnectionPoints);
        enumConnectionPoints.Reset();

        int fetchCount = 0;
        SafeIntPtr pFetchCount = new SafeIntPtr();

        do
        {
            if (0 != enumConnectionPoints.Next(1, oneConnectionPoint, pFetchCount.ToIntPtr()))
            {
                break;
            }

            fetchCount = pFetchCount.Value;

            if (fetchCount > 0)
                connectionPoints.Add(oneConnectionPoint[0]);

        } while (fetchCount > 0);

        pFetchCount.Dispose();

        return connectionPoints;
    }

    /// <summary>
    /// Returns a list of CONNECTDATA instances representing the current
    /// event sink connections to the given IConnectionPoint.
    /// </summary>
    /// <param name="connectionPoint">The IConnectionPoint to return connection data for.</param>
    /// <returns>A List of CONNECTDATA instances representing all the current event sink connections to the 
    /// given connection point.</returns>
    private static List<COM.CONNECTDATA> GetConnectionData(COM.IConnectionPoint connectionPoint)
    {
        COM.IEnumConnections enumConnections;
        COM.CONNECTDATA[] oneConnectData = new COM.CONNECTDATA[1];
        List<COM.CONNECTDATA> connectDataObjects = new List<COM.CONNECTDATA>();

        connectionPoint.EnumConnections(out enumConnections);
        enumConnections.Reset();

        int fetchCount = 0;
        SafeIntPtr pFetchCount = new SafeIntPtr();

        do
        {
            if (0 != enumConnections.Next(1, oneConnectData, pFetchCount.ToIntPtr()))
            {
                break;
            }

            fetchCount = pFetchCount.Value;

            if (fetchCount > 0)
                connectDataObjects.Add(oneConnectData[0]);

        } while (fetchCount > 0);

        pFetchCount.Dispose();

        return connectDataObjects;
    }
} //end class ComEventUtils

/// <summary>
/// A simple wrapper class around an IntPtr that
/// manages its own memory.
/// </summary>
public class SafeIntPtr : IDisposable
{
    private bool _disposed = false;
    private IntPtr _pInt = IntPtr.Zero;

    /// <summary>
    /// Allocates storage for an int and assigns it to this pointer.
    /// The pointed-to value defaults to 0.
    /// </summary>
    public SafeIntPtr()
        : this(0)
    {
        //
    }

    /// <summary>
    /// Allocates storage for an int, assigns it to this pointer,
    /// and initializes the pointed-to memory to known value.
    /// <param name="value">The value this that this <tt>SafeIntPtr</tt> points to initially.</param>
    /// </summary>
    public SafeIntPtr(int value)
    {
        _pInt = Marshal.AllocHGlobal(sizeof(int));
        this.Value = value;
    }

    /// <summary>
    /// Gets or sets the value this pointer is pointing to.
    /// </summary>
    public int Value
    {
        get 
        {
            if (_disposed)
                throw new InvalidOperationException("This pointer has been disposed.");
            return Marshal.ReadInt32(_pInt); 
        }

        set 
        {
            if (_disposed)
                throw new InvalidOperationException("This pointer has been disposed.");
            Marshal.WriteInt32(_pInt, Value); 
        }
    }

    /// <summary>
    /// Returns an IntPtr representation of this SafeIntPtr.
    /// </summary>
    /// <returns></returns>
    public IntPtr ToIntPtr()
    {
        return _pInt;
    }

    /// <summary>
    /// Deallocates the memory for this pointer.
    /// </summary>
    public void Dispose()
    {
        if (!_disposed)
        {
            Marshal.FreeHGlobal(_pInt);
            _disposed = true;
        }
    }

    ~SafeIntPtr()
    {
        if (!_disposed)
            Dispose();
    }

} //end class SafeIntPtr

} //end namespace YourNamespaceHere
like image 83
Mike Spross Avatar answered Sep 29 '22 23:09

Mike Spross