使用Interop触发VB6事件

6

我有一个遗留的VB6组件,使用tlbimp.exe将其导入到VS中,生成我的互操作程序集。VB6组件定义了一个事件,允许我在VB6中传递消息。

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

我希望能够在我的C#程序中触发此事件,但它被导入为事件而不是委托或其他有用的东西。因此,我只能监听,但永远无法触发。有人知道如何触发包含在VB6中的事件吗?C#事件看起来像:

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

很遗憾,我无法更改VB6代码。谢谢。


如果一个对象正在触发事件,那么你只能监听它。您想以哪种方式从C#触发它?谁将处理该事件? - vgru
它已经在VB6应用程序中订阅了,我希望能够提高它,因为它实际上更像是一个委托而不是一个事件。 - Steve
2个回答

8
实际上,希望还没有失去。可以从对象类外部触发COM对象上的事件。这个功能实际上是由COM本身提供的,尽管以间接的方式。
在COM中,事件采用发布/订阅模型工作。具有事件的COM对象(“事件源”)发布事件,一个或多个其他COM对象通过将事件处理程序附加到源对象(处理程序称为“事件接收器”)来订阅事件。通常,源对象通过简单地循环遍历所有事件接收器并调用适当的处理程序方法来引发事件。
那么这怎么帮助您呢?恰好,COM允许您查询事件源,以获取当前订阅源对象事件的所有事件接收器对象列表。一旦您拥有事件接收器对象列表,就可以通过调用每个接收器对象的事件处理程序来模拟引发事件。
请注意:我过于简化细节并且对某些术语进行了自由解释,但这是COM中事件如何工作的简短(并且有点政治不正确)版本。
您可以利用这些知识从外部代码上提高COM对象上的事件。实际上,使用System.Runtime.InteropServices和System.Runtime.InteropServices.ComTypes命名空间中的COM互操作支持,可以在C#中完成所有这些。
编辑:
我编写了一个实用程序类,可让您从.NET上提高COM对象上的事件。它非常容易使用。以下是使用您问题中的事件接口的示例:
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;
    }
}

以下是 ComEventUtils 类的代码(以及帮助类 SafeIntPtr,因为我很谨慎,想要一种好的方式来处理与 COM 相关的代码所需的 IntPtr): 免责声明:我没有彻底测试下面的代码。在一些地方,代码执行手动内存管理,因此可能会在您的代码中引入内存泄漏的可能性。此外,我没有添加错误处理到代码中,因为这只是一个示例。请小心使用。
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

3
在VB6中,事件只能在声明事件的类(或窗体)内部引发。如果要在VB6中强制引发事件,则需要在类上公开一种方法来实现。如果您没有源代码,则会很麻烦。
从文档中可以了解到:
RaiseEvent eventname [(argumentlist)]
必需的 eventname 是模块内声明的事件名称,并遵循基本变量命名约定。
例如:
Option Explicit

Private FText As String

Public Event OnChange(ByVal Text As String)

'This exposes the raising the event

Private Sub Change(ByVal Text As String)
  RaiseEvent OnChange(Text)
End Sub

Public Property Get Text() As String
  Text = FText
End Property


Public Property Let Text(ByVal Value As String)
  FText = Value
  Call Change(Value)
End Property

很抱歉要给您带来不好的消息。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接