вызов события vb6 с помощью взаимодействия - PullRequest
6 голосов
/ 16 марта 2009

У меня есть устаревший компонент VB6, который я импортировал в VS, используя tlbimp.exe для генерации сборки взаимодействия. Компонент 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. Спасибо.

Ответы [ 2 ]

7 голосов
/ 22 марта 2009

На самом деле, надежда еще не потеряна. можно вызвать событие для COM-объекта извне класса объекта. Эта функциональность фактически обеспечивается самим COM, хотя и косвенным образом.

В COM события работают по модели публикации / подписки. COM-объект, у которого есть события («источник событий»), публикует события, и один или несколько других COM-объектов подписываются на событие, присоединяя обработчик событий к исходному объекту (обработчики называются «приемниками событий»). Обычно исходный объект вызывает событие, просто зацикливаясь на всех приемниках событий и вызывая соответствующий метод-обработчик.

Так как это вам поможет? Так уж получилось, что COM позволяет запрашивать у источника событий список всех объектов приемников событий, которые в настоящее время подписаны на события исходного объекта. Получив список объектов приемника событий, вы можете имитировать возникновение события, вызывая каждый из обработчиков событий объекта приемника.

Примечание: Я слишком упрощаю детали и либерален в отношении некоторых терминов, но это короткая (и несколько политически некорректная) версия того, как события работают в COM.

Вы можете воспользоваться этими знаниями, чтобы вызывать события для COM-объекта из внешнего кода. Фактически, все это можно сделать в C # с помощью поддержки взаимодействия COM в пространствах имен System.Runtime.Interop и System.Runtime.Interop.ComTypes.


EDIT

Я написал служебный класс, который позволит вам вызывать события для COM-объекта из .NET. Это довольно легко использовать. Вот пример использования интерфейса события из вашего вопроса:

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, потому что я параноик и хотел хороший способ справиться с IntPtr S, необходимым для кода, связанного с COM) :

Отказ от ответственности : Я не полностью протестировал приведенный ниже код. Код выполняет ручное управление памятью в нескольких местах, и поэтому существует вероятность, что он может привести к утечкам памяти в вашем коде. Кроме того, я не добавил обработку ошибок в код, потому что это только пример. Используйте с осторожностью.

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 голосов
/ 17 марта 2009

В VB6 событие может быть вызвано только из класса (или формы, в зависимости от обстоятельств), объявляющего Событие. Чтобы вызвать событие в VB6, вам нужно предоставить метод класса для этого. Если у вас нет исходного кода, вам не повезло.

Из документации

Имя события RaiseEvent [(список аргументов)]

Обязательным именем события является имя событие объявлено в модуле и следует Основному именованию переменных конвенций.

Например

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

Извините, что несу плохие новости.

...