Как обрабатывать события COM из консольного приложения?

Я использую COM-объект из сторонней библиотеки, которая генерирует периодические события. Когда я использую библиотеку из приложения Winforms, имея объект в качестве члена класса и создавая его в основном потоке формы, все работает. Однако, если я создаю объект из другого потока, я не получаю никакого события.

Я предполагаю, что мне нужно иметь какой-то цикл обработки событий в том же потоке, который использовался для создания объекта.

Мне нужно использовать этот объект из консольного приложения. Думаю, я мог бы использовать Application.DoEvents, но я бы предпочел не включать пространство имен Winforms в консольное приложение.

Как я могу решить эту проблему?

Обновление 3 (2011-06-15): поставщик наконец-то ответил. Короче говоря, они говорят, что есть некоторая разница между насосом сообщений, созданным Application.Run, и тем, который создан Thread.Join, но они не знают, в чем заключается это различие.

Я согласен с ними; любой свет, пролитый по этому вопросу, был бы очень признателен.

Обновить:

От комментария Ричарда до ответа МДМ:

если другой компонент является однопоточным и создается из MTA, то Windows создаст рабочий поток + окно + насос сообщений и выполнит необходимую сортировку.

Пытаясь следовать его совету, я делаю следующее:

Обновление 2:

Я изменил код после ответа Жоао Анджело.

using System;

namespace ConsoleApplication2
{
    class Program
    {
        [STAThread]
        static void Main(string[] args)
        {
            MyComObjectWrapper wrapper = new MyComObjectWrapper();
        }
    }

    class MyComObjectWrapper
    {
        MyComObject m_Object;
        AutoResetEvent m_Event;

        public MyComObjectWrapper()
        {
            m_Event = new System.Threading.AutoResetEvent(false);

            System.Threading.Thread t = new System.Threading.Thread(() => CreateObject());
            t.SetApartmentState (System.Threading.ApartmentState.STA);
            t.Start();

            Wait();
        }

        void ObjectEvt(/*...*/)
        {
            // ...
        }

        void Wait()
        {
            m_Event.WaitOne();
        }

        void CreateObject()
        {
            m_Object = new MyComObject();
            m_Object.OnEvent += ObjectEvt;

            System.Threading.Thread.CurrentThread.Join();
        }    
    }
}

Я также попробовал следующее вместо этого:

        public MyComObjectWrapper()
        {
            CreateObject();
        }

6 ответов

Решение

Как уже указывалось в других ответах, STA-компоненты COM требуют запуска цикла сообщений, чтобы вызовы, происходящие в других потоках, были правильно маршалированы в поток STA, которому принадлежит компонент.

В Windows Forms вы получаете цикл сообщений бесплатно, но в консольном приложении вы должны сделать это явно, вызвав Thread.CurrentThread.Join в потоке, которому принадлежит компонент COM и который, вероятно, также является основным потоком для приложения. Эта тема должна быть STA.

Из записи MSDN Thread.Join Вы можете видеть, что это то, что вы хотите:

Блокирует вызывающий поток до тех пор, пока поток не завершится, продолжая при этом выполнять стандартную прокачку COM и SendMessage.

Если вы не хотите делать что-либо еще в главном потоке консоли, вы просто ждете бесконечно, в противном случае вы можете делать другие вещи, периодически вызывая Thread.CurrentThread.Join качать сообщения.

Примечание: это предполагает, что вы имеете дело с компонентом STA COM.


Упрощенный пример:

class Program
{
    [STAThread]
    static void Main(string[] args)
    {
        var myComObj = new MyComObject();

        myComObj.OnEvent += ObjectEvt;

        Thread.CurrentThread.Join(); // Waits forever
    }

    static void ObjectEvt(object sender, EventArgs e) { }
}

В этом примере консольное приложение будет находиться в бесконечном цикле, который ничего не должен делать, кроме как отвечать на события компонента COM. Если это не работает, вы должны попытаться получить поддержку от поставщика COM-компонентов.

Если вы используете STA, то вам так или иначе понадобится цикл обработки сообщений. Если в противном случае вам не нужен цикл обработки сообщений, MTA, пожалуй, самый простой способ, а также лучший вариант для консольного приложения.

Следует помнить, что с MTA не имеет значения, какой поток создал объект; все объекты, созданные потоком MTA, в равной степени принадлежат всем потокам MTA. (Или, если говорить в COM, у процесса есть ровно одна многопоточная квартира, в которой живут все потоки MTA.) Это означает, что, если вы используете подход MTA, вообще не нужно создавать отдельный поток - просто создайте объект из основного потока. Но вам также нужно знать, что входящие события будут доставляться в "случайном" потоке, поэтому вам придется предпринять отдельные шаги для связи с основным потоком.

using System;
using System.Threading;

class Program
{
    static MyComObject m_Object;
    static AutoResetEvent m_Event;


    [MTAThread]
    static void Main(string[] args)
    {
        m_Event = new AutoResetEvent(false);

        m_Object = new MyComObject();
        m_Object.OnEvent += ObjectEvt;

        Console.WriteLine("Main thread waiting...");
        m_Event.WaitOne();
        Console.WriteLine("Main thread got event, exiting.");
        // This exits after just one event; add loop or other logic to exit properly when appropriate.
    }

    void ObjectEvt(/*...*/)
    {
        Console.WriteLine("Received event, doing work...");

        // ... note that this could be on any random COM thread.

        Console.WriteLine("Done work, signalling event to notify main thread...");
        m_Event.Set();
    }
}

Пару комментариев к предыдущей версии кода: у вас были вызовы Wait () как в CreateObject, так и в конструкторе MycomObjectWrapper; кажется, у вас должен быть только один - если у вас есть два из них, только один из них будет освобожден при вызове m_Event.Set (), а другой все еще будет ждать. Кроме того, предложите добавить код отладки, чтобы вы знали, как далеко вы продвинулись. Таким образом, вы можете, по крайней мере, сказать, получаете ли вы событие от COM, и отдельно, успешно ли вы сообщаете это обратно в основной поток. Если объекты помечены как нейтральные или оба в реестре, тогда не должно возникнуть проблем при их создании из MTA.

Для событий IIRC, COM требуется цикл обработки событий, который качает сообщения и вызывает Win32 GetMessage функция.

Winforms делает это для вас, или вы можете эмулировать это с помощью вызовов Win32. Этот вопрос / ответ имеет хороший пример, который вы можете использовать.

Не могли бы вы попробовать это:

static class Program
{
    MyComObject m_Object;

    [STAThread]
    static void Main()
    {
        m_Object = new MyComObject();
        m_Object.OnEvent += ObjectEvt;
        System.Windows.Forms.Application.Run();
    }

    void ObjectEvt(/*...*/)
    {
        // ...
    }
}

Я думаю, что следующее должно работать:

[STAThread]
Main(...)
{
    var comObject = new YourComObject();
    comObject.Event += EventHandler;
    Console.WriteLine("Press enter to exit.");
    Console.ReadLine();
}

void EventHandler(...)
{
    // Handle the event
}

Вы определили модель квартиры потока?

    [STAThread]
    static void Main(string[] args)
    {
        // Create the thread that will manage the COM component
        Thread th = new Thread(...); 
        // Before starting the thread
        th.SetApartmentState (ApartmentState.STA);         
    }

В потоке, просто подождите, пока событие не сообщит о его завершении. Пока поток ожидает события, я думаю, что он должен обрабатывать сообщения в цикле потока.

Другие вопросы по тегам