Исходящий вызов не может быть выполнен, так как приложение отправляет синхронный вызов

Я получил это (ошибка в заголовке выше) из потока System.Thread.Timer, поэтому у меня есть TimerWrapper, который обертывает System.Thread.Timer, чтобы переместить фактическое выполнение в System.Thread.ThreadPool, и я все еще получить его, чтобы я переместил его новый поток (обратный вызов).Start(), и я все еще получаю его. Как отправить синхронный вызов, когда я помещаю его в новый поток?

Это очень маленькое прототипное приложение, в котором все, что я делаю, запускает таймер, который делает это...

    IEnumerable swc = SHDocVw.ShellWindows() 
    HashSet<WindowInfo> windows = new HashSet<WindowInfo>();
    foreach (SHDocVw.InternetExplorer ie in swc)
    {
        if (!ie.FullName.ToLower().Contains("iexplore.exe"))
            continue;

        IntPtr hwnd;
        IEPlugin.IOleWindow window = ie.Document as IEPlugin.IOleWindow;
        window.GetWindow(out hwnd);   

        WindowInfo info = new WindowInfo();
        info.handle = hwnd;
        info.extraInfo = ie;
        windows.Add(info);
    }

Ответ 1

Поздравляем; вам удалось наткнуться на один из моих любимых COM-причуд, в этом случае, восхитительно скрытое ограничение с помощью метода IOleWindow GetWindow - и сообщение об ошибке, которое мало подсказывает, что происходит. Основная проблема здесь заключается в том, что метод GetWindow() помечен как [input_sync] - из файла include\oleidl.idl в SDK:

interface IOleWindow : IUnknown
{
...
    [input_sync]
    HRESULT GetWindow
    (
        [out] HWND *phwnd
    );

К сожалению, документы для IOleWindow не упоминают этот атрибут, но документы для некоторых других, такие как IOleDocumentView:: SetRect() do:

Этот метод определяется атрибутом [input_sync], что означает, что объект представления не может выполнить или выполнить другой вызов noncode_sync RPC во время выполнения этого метода.

Идея, лежащая в основе этого атрибута, заключается в том, чтобы гарантировать, что вызывающий (который может быть приложением как Word или какой-либо другой узел управления OLE), что он может безопасно называть эти методы, не беспокоясь о повторном подключении.

Там, где все сложно, так это то, что COM решает принудительно выполнить это: он отклонит межсетевые вызовы методу [input_sync], если он считает, что он может нарушить эти ограничения. Итак, IIRC, вы не можете выполнить вызов межсетевого входа [input_sync], если вы находитесь в SendMessage(), - это тот случай, когда сообщение об ошибке несколько намекает. И - это тот, который доставит вас сюда - вы не можете вызвать метод cross-apartment [input_sync] из потока MTA. Возможно, COM сейчас немного преувеличивает в своем исполнении, но это то, с чем вам приходится иметь дело.

(Краткий комментарий о потоках MTA vs STA: в COM потоки и объекты - это STA или MTA. STA, Single-Threaded-Aparment - это то, как работает пользовательский интерфейс Windows, один поток владеет интерфейсом пользователя и всеми объектами, связанными с это и те объекты, которые ожидают, что их вызовет только один поток. MTA или Multi-Threaded-Aparment - это скорее бесплатная функция для всех объектов, которые могут быть вызваны из любого потока в любое время, поэтому вам нужно сделать их собственная синхронизация является потокобезопасной. Потоки MTA обычно используются для рабочих и фоновых задач. Таким образом, вы можете управлять пользовательским интерфейсом в одном потоке STA, но загружать кучу файлов в фоновом режиме с использованием одного или нескольких потоков MTA. куча работы, позволяющая двум взаимодействовать друг с другом и пытается скрыть некоторые сложности. Часть проблемы здесь заключается в том, что вы смешиваете эти метафоры: ThreadPools связаны с фоновой работой, поэтому MTA, но IOleWindow - это интерфейс UI- так и STA - и GetWindow оказывается единственным методом, который действительно очень строг относительно en заставляя это.)

Короче говоря, вы не можете вызывать этот метод из thePadPool thead, потому что это потоки MTA. Кроме того, новые потоки по умолчанию являются MTA, поэтому просто создание нового потока для выполнения работы является недостаточным.

Вместо этого создайте новый поток, но используйте tempThread.SetApartmentState(ApartmentState.STA); перед его запуском, это даст вам поток STA. Возможно, вам нужно будет поместить весь код, который имеет дело с объектом COM оболочки в этом потоке STA, а не только с одним вызовом GetWindow() - я не помню точные данные, но если вы в конечном итоге приобретете оригинал COM-объект (кажется, это ShellWindows здесь), в то время как в потоке MTA ThreadPool он останется связанным с этим MTA, даже если вы попытаетесь вызвать его из STA.

Если бы вы могли вместо этого выполнять всю работу из потока STA, а не из MTA, то из ThreadPool, тем лучше, что бы избежать этого в первую очередь. Вместо использования System.Threading.Timer, который предназначен для кода фона/не-интерфейса, попробуйте использовать пользовательский интерфейс System.Windows.Forms.Timer вместо этого. Это требует цикла сообщений - если у вас уже есть окна и формы в вашем приложении, у вас уже есть один, но если нет, самый простой способ сделать это в тестовом коде - это сделать MessageBox() в том же где ваш основной код ожидания ждет выхода (обычно с помощью Sleep или Console.ReadLine или аналогичного).