如何在ActiveX方法调用期间防止WPF事件处理程序的重入?

时间:2011-11-29 10:20:07

标签: c# .net wpf com activex

我们在WPF和STA应用程序中调用ActiveX组件上的方法。此调用是通过以下方式执行的:

res = ocx.GetType().InvokeMember(methodName, flags, null, ocx, args);

...其中ocx是使用System.Windows.Forms.AxHost.GetOcx()方法检索的ActiveX对象。

此调用是在WPF事件处理程序中执行的,例如“鼠标单击”。

现在问题。如果我们双击'鼠标点击'事件将触发,运行InvokeMember()。但是,在此次调用期间,我们看到重新输入了“鼠标点击”事件。所以在同一个线程中,我们在调用堆栈上看到两次事件处理程序。这是非常意外的,我们正在努力防止这种情况发生。我们怎样才能防止这种情况发生?

我们能够想到为什么发生的唯一原因是:

  • COM对象是在另一个STA中创建的,因此我们正在执行需要编组的跨STA调用
  • 跨线程STA调用使用Windows消息向COM组件发送RPC请求
  • 跨线程STA调用使用Windows消息泵来接收RPC答案
  • 在等待期间会出现另一种类型的事件(例如“鼠标点击”),这会在处理RPC答案之前处理。
  • 处理此RPC回答

我们试图解决问题的事情:

  • 在所有事件处理程序中使用lock()。这不起作用,因为lock()将锁定一个线程,在这种情况下,它是重新进入事件处理程序的相同线程。
  • 使用自定义锁定,例如'bool locked = false; if(!locked){locked = true;的InvokeMethod(); ...; locked = false; }”。这部分工作:它会丢弃事件而不是将它们排队等待,并且需要对所有我们的事件处理程序进行大量更改,这样做并不好。
  • 使用Dispatcher.DisableProcessing来停止(其他)消息的处理。这没有帮助:因为正在处理消息而抛出异常。
  • 在新线程中创建第二个调度程序,并通过Dispatcher.Invoke()运行ocx.InvokeMehod()以使其由另一个线程处理。这给出了'一个事件无法调用任何订阅者(来自HRESULT的异常:0x80040201)'(是的,我们还订阅了ActiveX对象的COM事件)。
  • 使用Dispatcher.PushFrame()来停止发生事件处理。这也失败了。

可能有效,但不知道如何实现这一点的疯狂想法是创建一个新的消息泵作为WPF消息泵,可配置为临时只处理RPC调用。这与http://jmorrill.hjtcentral.com/Home/tabid/428/EntryId/430/WPF-MediaKit-Updates.aspx一致,但与这种情况有所不同。

所以问题归结为我们如何能够像我们预期的那样同步进行ActiveX调用而不是异步?

更新

为了更清楚地说明所涉及的机制不仅仅是关于鼠标事件,而是“在处理旧事件时处理新事件”的更一般问题,我将给出另一个带有堆栈跟踪的示例:

上下文:我们有一个WPF网格,我们得到一个鼠标点击(Grid_MouseDown),我们有一个ActiveX对象,我们在其上执行'CloseShelf'方法。打开架子需要时间,所以我们订阅了事件'EventShelfClosed',在EventShelfClosed的事件处理程序中将调用'ListShelf'来知道剩下哪些架子。

这就是托管堆栈跟踪的样子(汉斯要求提供非托管堆栈跟踪,但我不知道如何获得):

MyAxWrapper.dll!MyAxWrapper.MyAxWrapper.InvokeMember(string methodName, System.Reflection.BindingFlags flags, object[] args, int refArgIdx) Line 53 C#
MyAxWrapper.dll!MyAxWrapper.LoggingMyAxWrapper.InvokeMember(string methodName, System.Reflection.BindingFlags flags, object[] args, int refArgIdx) Line 151 + 0x14 bytes    C#
MyAxWrapper.dll!MyAxWrapper.MyAxWrapper.InvokeMethod(string methodName, object[] args) Line 92 + 0x18 bytes C#
MyAxWrapper.dll!MyAxWrapper.MyAxAppWrapper.ListShelfs(string CanvasPageId) Line 300 + 0x42 bytes    C#
PACS.dll!PACS.MyAxDatabase.GetShelfIdsOn(string canvasPageId) Line 223 + 0xf bytes  C#
MyAxCanvas.dll!MyAxCanvas.MyAxCanvasPlugin.UpdateTimeLineSelection(string canvasPageId) Line 123 + 0x10 bytes   C#
MyAxCanvas.dll!MyAxCanvas.MyAxCanvasPlugin.EventShelfClosed(string canvasPageId, string shelfId) Line 180 + 0xb bytes   C#
MyAxWrapper.dll!MyAxWrapper.MyAxAppWrapper.FireEvent(string eventName, object[] args) Line 21 + 0x73 bytes  C#
MyAxWrapper.dll!MyAxWrapper.MyAxEventForwarder.EventShelfClosed(string CanvasPageID, string ShelfID) Line 177 + 0x58 bytes  C#
[Native to Managed Transition]  
[Native to Managed Transition]  
MyAxWrapper.dll!MyAxWrapper.MyAxWrapper.InvokeMember(string methodName, System.Reflection.BindingFlags flags, object[] args, int refArgIdx) Line 75 + 0x2b bytes    C#
MyAxWrapper.dll!MyAxWrapper.LoggingMyAxWrapper.InvokeMember(string methodName, System.Reflection.BindingFlags flags, object[] args, int refArgIdx) Line 151 + 0x14 bytes    C#
MyAxWrapper.dll!MyAxWrapper.MyAxWrapper.InvokeMethod(string methodName, object[] args) Line 92 + 0x18 bytes C#
MyAxWrapper.dll!MyAxWrapper.MyAxAppWrapper.CloseShelf(string a) Line 218 + 0x42 bytes   C#
MyAxCanvas.dll!MyAxCanvas.MyAxCanvasPlugin.EventCanvasPageCreated.AnonymousMethod__0(DataModel.Item exam) Line 110 + 0x1d bytes C#
ItemPresenter.dll!ItemPresenter.ItemPresenter.OnItemClicked(DataModel.Item study) Line 36 + 0x14 bytes  C#
ItemPresenter.dll!ItemPresenter.ItemPresenter.ItemPresenterPerYearControls_Click(object sender, System.Windows.RoutedEventArgs e) Line 215 + 0x1e bytes C#
PresentationCore.dll!System.Windows.RoutedEventHandlerInfo.InvokeHandler(object target, System.Windows.RoutedEventArgs routedEventArgs) + 0x78 bytes    
PresentationCore.dll!System.Windows.EventRoute.InvokeHandlersImpl(object source, System.Windows.RoutedEventArgs args, bool reRaised) + 0x1ae bytes  
PresentationCore.dll!System.Windows.UIElement.RaiseEventImpl(System.Windows.DependencyObject sender, System.Windows.RoutedEventArgs args) + 0x79 bytes  
PresentationCore.dll!System.Windows.UIElement.RaiseEvent(System.Windows.RoutedEventArgs e) + 0x17 bytes 
ItemPresenter.dll!ItemPresenter.ItemPresenterControl.Grid_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e) Line 47 + 0x29 bytes    C#
PresentationCore.dll!System.Windows.Input.MouseButtonEventArgs.InvokeEventHandler(System.Delegate genericHandler, object genericTarget) + 0x31 bytes    
PresentationCore.dll!System.Windows.RoutedEventArgs.InvokeHandler(System.Delegate handler, object target) + 0x29 bytes  
PresentationCore.dll!System.Windows.RoutedEventHandlerInfo.InvokeHandler(object target, System.Windows.RoutedEventArgs routedEventArgs) + 0x3e bytes    
PresentationCore.dll!System.Windows.EventRoute.InvokeHandlersImpl(object source, System.Windows.RoutedEventArgs args, bool reRaised) + 0x1ae bytes  
PresentationCore.dll!System.Windows.UIElement.RaiseEventImpl(System.Windows.DependencyObject sender, System.Windows.RoutedEventArgs args) + 0x79 bytes  
PresentationCore.dll!System.Windows.UIElement.RaiseTrustedEvent(System.Windows.RoutedEventArgs args) + 0x41 bytes   
PresentationCore.dll!System.Windows.UIElement.RaiseEvent(System.Windows.RoutedEventArgs args, bool trusted) + 0x2c bytes    
PresentationCore.dll!System.Windows.Input.InputManager.ProcessStagingArea() + 0x1ff bytes   
PresentationCore.dll!System.Windows.Input.InputManager.ProcessInput(System.Windows.Input.InputEventArgs input) + 0x45 bytes 
PresentationCore.dll!System.Windows.Input.InputProviderSite.ReportInput(System.Windows.Input.InputReport inputReport) + 0x62 bytes  
PresentationCore.dll!System.Windows.Interop.HwndMouseInputProvider.ReportInput(System.IntPtr hwnd, System.Windows.Input.InputMode mode, int timestamp, System.Windows.Input.RawMouseActions actions, int x, int y, int wheel) + 0x263 bytes 
PresentationCore.dll!System.Windows.Interop.HwndMouseInputProvider.FilterMessage(System.IntPtr hwnd, MS.Internal.Interop.WindowMessage msg, System.IntPtr wParam, System.IntPtr lParam, ref bool handled) + 0x46d bytes 
PresentationCore.dll!System.Windows.Interop.HwndSource.InputFilterMessage(System.IntPtr hwnd, int msg, System.IntPtr wParam, System.IntPtr lParam, ref bool handled) + 0x75 bytes   
WindowsBase.dll!MS.Win32.HwndWrapper.WndProc(System.IntPtr hwnd, int msg, System.IntPtr wParam, System.IntPtr lParam, ref bool handled) + 0xbe bytes    
WindowsBase.dll!MS.Win32.HwndSubclass.DispatcherCallbackOperation(object o) + 0x7d bytes    
WindowsBase.dll!System.Windows.Threading.ExceptionWrapper.InternalRealCall(System.Delegate callback, object args, int numArgs) + 0x53 bytes 
WindowsBase.dll!MS.Internal.Threading.ExceptionFilterHelper.TryCatchWhen(object source, System.Delegate method, object args, int numArgs, System.Delegate catchHandler) + 0x42 bytes    
WindowsBase.dll!System.Windows.Threading.Dispatcher.InvokeImpl(System.Windows.Threading.DispatcherPriority priority, System.TimeSpan timeout, System.Delegate method, object args, int numArgs) + 0xb4 bytes    
WindowsBase.dll!MS.Win32.HwndSubclass.SubclassWndProc(System.IntPtr hwnd, int msg, System.IntPtr wParam, System.IntPtr lParam) + 0x104 bytes    
[Native to Managed Transition]  
[Managed to Native Transition]  
WindowsBase.dll!System.Windows.Threading.Dispatcher.PushFrameImpl(System.Windows.Threading.DispatcherFrame frame) + 0xc1 bytes  
WindowsBase.dll!System.Windows.Threading.Dispatcher.PushFrame(System.Windows.Threading.DispatcherFrame frame) + 0x49 bytes  
WindowsBase.dll!System.Windows.Threading.Dispatcher.Run() + 0x4c bytes  
PresentationFramework.dll!System.Windows.Application.RunDispatcher(object ignore) + 0x17 bytes  
PresentationFramework.dll!System.Windows.Application.RunInternal(System.Windows.Window window) + 0x6f bytes 
PresentationFramework.dll!System.Windows.Application.Run(System.Windows.Window window) + 0x26 bytes 
PresentationFramework.dll!System.Windows.Application.Run() + 0x1b bytes 
MyAxCanvasStandalone.exe!MyAxCanvasStandalone.App.Main(string[] args) Line 37 + 0xa bytes   C#
[Native to Managed Transition]  
[Managed to Native Transition]  
mscorlib.dll!System.AppDomain.ExecuteAssembly(string assemblyFile, System.Security.Policy.Evidence assemblySecurity, string[] args) + 0x6d bytes    
Microsoft.VisualStudio.HostingProcess.Utilities.dll!Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly() + 0x2a bytes  
mscorlib.dll!System.Threading.ThreadHelper.ThreadStart_Context(object state) + 0x63 bytes   
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool ignoreSyncCtx) + 0xb0 bytes    
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state) + 0x2c bytes    
mscorlib.dll!System.Threading.ThreadHelper.ThreadStart() + 0x44 bytes   
[Native to Managed Transition]  

方法'CloseShelf'将关闭架子,但在这种情况下,'CloseShelf'是如此之快以至于在调用CloseShelf期间发出并处理事件'EventShelfClosed'。现在,CloseShelf将调用ListShelfs,但ListShelfs将失败并返回null,因为ActiveX组件被仍然处于活动状态的'CloseShelf'调用锁定。

为什么这是一个问题?因为程序员不希望方法调用是异步的。创建一个大型程序后,这就打击了我们,现在这意味着审核所有出现意外行为的调用。

在这种情况下,我们希望看到什么?我们希望在调用期间看到'CloseShelf'返回而不处理其他事件。该方法应该是同步的,并且在主(非递归)消息循环期间处理任何挂起事件。

拥有'lock'类型的布尔值在这里没有用,因为我们会在这里丢失事件,这会锁定应用程序。

5 个答案:

答案 0 :(得分:2)

<强>&LT; tldr&GT;在尝试#3中尝试以下代码。当我坐下来写这篇文章时,电视上什么也没有。&lt; / tldr&gt;

感谢您的澄清!我看到这里只有一个线程;而且,由于CloseShelf帧仍然在堆栈上,看起来COM调用实际上是阻塞的。

从堆栈跟踪中,看起来com对象正在调用GetMessage或PeekMessage API(或者如果它是VB6,DoEvents或类似的),它将检查消息队列和PROCESS消息 - 不管它是否如此会引起反感。 AKA&#39;抽取消息队列&#39; - 但是如果它使用了peekmessage,它会在没有消息的情况下阻止,但仍会执行消息。在您的堆栈跟踪中,这些调用可以隐藏在不可见的本机部分中。堆栈跟踪实际证明,不知何故,COM调用正在回调您的代码!看起来你已经意识到了这一点。如果对某些读者来说有点模糊,这里有几个链接:

http://discuss.joelonsoftware.com/default.asp?joel.3.456478.15
http://en.wikipedia.org/wiki/Event_loop

协作式多任务处理(整个操作系统的一个消息循环,如win3.0): http://en.wikipedia.org/wiki/Computer_multitasking#Cooperative_multitasking.2Ftime-sharing

报价&#39;自愿放弃时间......&#39;实际上是通过这些调用完成的。它仍然在每个Windows应用程序的GUI线程上一直发生!

由于实际调用是在COM中,如果您无法编辑它,您仍然需要围绕它进行编码。它可能只是这一种方法(希望如此)。

有时,程序和组件会故意检查消息循环,以及允许响应或重新绘制屏幕的时间。就像这张海报试图做的那样:

Is there a function like PeekMessage that doesn't process messages?

关于&#39;系统的报价也可以处理内部事件&#39;在这里唠叨你。

请注意,您所说的程序员不希望方法调用是异步的&#39; - 它不是异步,或者堆栈跟踪看起来不同。它的递归&#39;回到你的代码中。作为一个老的Win3.1程序员,这是我们为系统中的每个应用程序处理的地狱!

我发现链接谷歌搜索消息队列查询消息&#39;同时试图查看是否可以阻止Get / Peekmessage等处理消息。你可以从这篇文章中看到......

http://msdn.microsoft.com/en-us/library/windows/desktop/ms644943(v=vs.85).aspx

...发送类似PM_QS_INPUT |的内容PM_QS_PAINT可以防止出现问题。不幸的是,既然你没有打电话,你就无法改变它!而且我没有看到任何设置标志的方法来控制后续呼叫的后续消息处理。

如果读者仍然感到困惑(如果没有跳过这段代码)......举个简单的例子,制作一个VB Winforms应用程序并制作一个按钮并双击它并将此代码粘贴到 - (我说VB因为应用程序。 doevents是调用这个令人讨厌的消息队列检查的最方便的方法):

    For i As Integer = 0 To 20
        Text = i.ToString
        System.Threading.Thread.Sleep(100)
        Application.DoEvents()
    Next

现在点击按钮。请注意,您可以放大窗口和背景重新绘制 - 因为doevents允许通过检查消息队列来发生这些事件(REM了解doevents,它将等待&#39;直到计数完成;也尝试点击3x你将连续获得3个计数。

现在......踢球者。单击没有注释掉Doevents的按钮。单击按钮3次 - 倒计时相互中断,然后在上一次完成时恢复。您可以暂停IDE并查看3个单击callstack。

美味!

我还看到你尝试了一些东西来阻止处理消息或事件。如果触发EventShelfClosed的代码是从PeekMessage或者Doevents&#39;引起的重入调用中调用的,那么可能不会影响它。

请注意,这种做法有它的批评者:http://www.codinghorror.com/blog/2005/08/is-doevents-evil-revisited.html

最好是更改COM,这样它就不会进行任何正在检查消息循环的API调用。

祝你好运!

另一种改变它的方法是从控制EventShelfClosed的事件中移出,并在退出对CloseShelf的调用后调用它(因为com调用确实发生了)。不幸的是,你的程序的架构可能不会允许没有重大变化和/或增加内聚和其他东西弄脏漂亮的模型(不是时装模特提醒你: - )。

另一种方法是使一个新的线程对象指向一个使com调用的函数,然后启动它,然后加入它,希望像PeekMessage这样的东西在新线程上找不到消息泵,因此不干涉事物。看起来你有几次尝试涉及这类事情。不幸的是,如果COM偷看消息,并且线程上没有消息泵,kaboom。它可能会爆炸,而不是仅仅忽略事物。听起来就是这样。此外,如果COM依赖于只应从GUI / messagepump线程访问的其他项目,那么您就会遇到跨线程调用的麻烦(如果COM与UI或任何UI交互,情况肯定会出现这种情况。 UI对象)。

如果您不能停止检查消息队列,或者阻止EventShelfClosed一直触发,那么您别无选择,只能调用EventShelfClosed。但是你可以做的是让它等待,然后在CloseShelf完成时掉下来。

所以你仍然必须通过CloseShelf设置/取消设置类级布尔字段,以便EventShelfClosed知道它正在运行。

不幸的是,只是在while循环中检查这个,即使是睡眠,也会阻止你拥有的单个线程,并冻结应用程序。你可以尝试让EventShelfClosed重新提升自己并退出函数,只要设置了bool;但由于RaiseEvent保留在托管内部,因此立即运行代码,并且不会检查它将与stackoverflow崩溃的消息队列。如果你可以通过调用PostMessage API(而不是立即运行它的SendMessage)来弄清楚如何重新引发EventShelfClosed - 这将继续在COM线程的消息队列上进行多次调用COM调用窗户检查它。除非由于某些愚蠢的原因,所述COM等待队列为空 - 另一个锁定。不推荐。

Soo ......你可以用火来灭火。在这里,我正在实现另一个消息检查循环,以便在等待bool清除时允许您的事件发生。

请注意,在这种情况下,这只会解决这个问题。审核所有电话......这不是一个神奇的子弹。我的猜测是没有。非常混乱,这是一个彻头彻尾的黑客。

尝试#3

它不是真的尝试#3,它更像是#8的可能性。但是我在旧的答案中引用了这个,而且我懒得编辑它。

Boolean InCloseShelf
function CloseShelf(...)
    InCloseShelf=True;
    try
    {
         com call and all else
     }
     finally
         InCloseShelf=False

function EventShelfClosed(...
    while (InCloseShelf)
    {
         DoEvents
     }

现在当然在WPF中没有DoEvents,它在winforms&#39; &#39;应用&#39 ;.这个博客有一个实现

http://dedjo.blogspot.com/2007/08/how-to-doevents-in-wpf.html

void DoEvents(){ 
DispatcherFrame f = new DispatcherFrame(); 
Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,  
(SendOrPostCallback)delegate(object arg) { 
    DispatcherFrame fr =  arg as DispatcherFrame; 
    fr.Continue=True; 
}, f); 
Dispatcher.PushFrame(frame); 
}

当然没有经过测试!请注意,这是来自评论中的更正:

static void DoEvents()
{
DispatcherFrame frame = new DispatcherFrame(true);
Dispatcher.CurrentDispatcher.BeginInvoke
(
DispatcherPriority.Background, 
(SendOrPostCallback) delegate(object arg)
{
var f = arg as DispatcherFrame; 
f.Continue = false;
}, 
frame
);
Dispatcher.PushFrame(frame);
} 

或者你总是可以引用WinForms并调用Application.DoEvents。

我猜你已经知道了,但你现在处于一个不好的地方。如果这样做没有,祝你好运!如果你找到一个更好的方法请更新帖子,因为,好吧,邪恶的黑客像这样吮吸,但现在你可以看到为什么人们说要谨慎检查消息队列!

答案 1 :(得分:1)

你有答案!

这只是一个老黑客吗?不,它不是,它是涉及任何类型的重新进入的标准操作程序。对于我来说,这比我记忆中的情况更完美,从简陋的单面板VB3 crud弹出窗口到巨大的MVVM / DDD可用企业管理应用程序。

这是你提到的:'使用自定义锁定'静态bool locked = false; if(!locked){locked = true;的InvokeMethod(); ...; locked = false; }”。

修改

请注意OP的评论。好的,所以这不会解决问题!第二个事件不是虚假的点击;这是一个不同的事件,对系统的正常运行至关重要。

请再次尝试我的下一个答案。 #3是最丑陋的,但应该有用。

答案 2 :(得分:1)

我过去曾处理过类似的问题,但不是来自WPF。

在win32应用程序中,推荐的方法是使用IMessageFilter :: MessagePending - 这可以配置为说明当传出的STA调用正在进行时允许处理哪些类型的消息。在这里,您必须小心确保接受并处理来自被调用者对象的任何回调。

http://msdn.microsoft.com/en-us/library/windows/desktop/ms694352(v=vs.85).aspx

在WPF中,这不可用。我认为你使用另一个线程的方法是正确的方法。

原则上,您希望主线程阻塞子线程。然后子线程可以进行传出的COM调用。您可能希望将子线程设置为STA以避免引入其他未知问题。重要的是,消息在子线程上被泵送,并且任何指针或类型都被正确编组,因为子线程将位于不同的COM公寓中。避免重入是因为回调是唯一试图向正在发送的线程发送消息的东西。

在WPF中,我相信Dispatcher应该提供您需要的所有功能。

我不确定为什么使用Dispatcher尝试执行此操作失败 - 可能与此已知问题有关: http://support.microsoft.com/kb/926997

答案 3 :(得分:0)

如果您已经有异步行为,我会尝试Jeffrey Richter的PowerThreading库。它有AsyncEnumerator来简化异步编程。它还具有锁定原语,可以帮助您实现您的方案。据我所知,这个原语与常规Monitor类的不同之处在于,即使在同一个线程中也不允许重新输入代码,因此它可能对您有所帮助。不幸的是,我没有尝试过那个原语,所以不能添加太多。

以下是关于此原语的文章:http://msdn.microsoft.com/en-us/magazine/cc163532.aspx

答案 4 :(得分:0)

我知道这不是一个完整的答案,但我想澄清事件是如何运作的。这将有助于您了解重新入口不是问题,这是事件的工作方式。

  1. Envent处理程序调用始终是同步的,但这并不意味着堆栈中不会有任何其他事件。
  2. 假设您单击列表框内的按钮,然后手动引发列表更改事件,某些调用实际上会被调用。
  3. 典型的举办活动似乎是,

    .... OnClick(...)
    {
       if(SelectionChanged!=null)
           SelectionChanged(...)
    }
    

    请注意,OnClick调用仍然在堆栈上,而SelectionChanged事件被触发,OnClick将不会超出堆栈,直到SelectionChanged调用完成。

      

    在这种情况下,我们希望看到什么?我们希望看到这一点   'CloseShelf'返回时不会在通话过程中处理其他事件。   该方法应该是同步的,并处理任何挂起的事件   在主(非递归)消息循环期间。

    如果你在CloseShelf中提出事件,这怎么可能。

    您希望这样做的唯一方法是将事件处理程序排队,如

    .... OnClick(...)
    {
       Dispatcher.BeginInvoke(delegate(){
       if(SelectionChanged!=null)
           SelectionChanged(...)
       });
    }
    

    这将在OnClick完成后引发事件,在这种情况下,在执行SelectionChanged时,您将看不到OnClick on stack。