如何在提供WPF Dispatcher事件时等待WaitHandle?

有人给我发了电子邮件,询问我是否有WaitOneAndPump于WPF的WaitOneAndPump版本。 目标是在同一堆栈帧上等待句柄(类似于WaitHandle.WaitOne )并在等待时泵送WPF Dispatcher事件。

我真的不认为这样的API应该在任何生产代码中使用,无论是对于WinForms还是WPF (可能除了UI自动化之外)。 WPF没有公开WinForms的DoEvents的显式版本,这是一个非常好的设计决策,考虑到DoEvents API一直滥用的公平份额 。

然而,这个问题本身很有意思,所以我将把它作为一个练习,并发布任何我想出的答案。 如果有兴趣,请随意发布您自己的版本。

我提出的WaitOneAndPump版本使用DispatcherHooks Events和MsgWaitForMultipleObjectsEx ,以避免运行忙等待循环 。

同样,在生产代码中使用此WaitOneAndPump (或任何其他嵌套的消息循环变体) 几乎总是一个糟糕的设计决策。 我只能想到两个合法使用嵌套消息循环的.NET API: Window.ShowDialogForm.ShowDialog

 using System; using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Threading; namespace Wpf_21642381 { #region MainWindow public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); this.Loaded += MainWindow_Loaded; } // testing async void MainWindow_Loaded(object sender, RoutedEventArgs e) { await Dispatcher.Yield(DispatcherPriority.ApplicationIdle); try { Func doAsync = async () => { await Task.Delay(6000); }; var task = doAsync(); var handle = ((IAsyncResult)task).AsyncWaitHandle; var startTick = Environment.TickCount; handle.WaitOneAndPump(5000); MessageBox.Show("Lapse: " + (Environment.TickCount - startTick)); } catch (Exception ex) { MessageBox.Show(ex.Message); } } } #endregion #region WaitExt // WaitOneAndPump public static class WaitExt { public static bool WaitOneAndPump(this WaitHandle handle, int millisecondsTimeout) { using (var operationPendingMre = new ManualResetEvent(false)) { var result = false; var startTick = Environment.TickCount; var dispatcher = Dispatcher.CurrentDispatcher; var frame = new DispatcherFrame(); var handles = new[] { handle.SafeWaitHandle.DangerousGetHandle(), operationPendingMre.SafeWaitHandle.DangerousGetHandle() }; // idle processing plumbing DispatcherOperation idleOperation = null; Action idleAction = () => { idleOperation = null; }; Action enqueIdleOperation = () => { if (idleOperation != null) idleOperation.Abort(); // post an empty operation to make sure that // onDispatcherInactive will be called again idleOperation = dispatcher.BeginInvoke( idleAction, DispatcherPriority.ApplicationIdle); }; // timeout plumbing Func getTimeout; if (Timeout.Infinite == millisecondsTimeout) getTimeout = () => INFINITE; else getTimeout = () => (uint)Math.Max(0, millisecondsTimeout + startTick - Environment.TickCount); DispatcherHookEventHandler onOperationPosted = (s, e) => { // this may occur on a random thread, // trigger a helper event and // unblock MsgWaitForMultipleObjectsEx inside onDispatcherInactive operationPendingMre.Set(); }; DispatcherHookEventHandler onOperationCompleted = (s, e) => { // this should be fired on the Dispather thread Debug.Assert(Thread.CurrentThread == dispatcher.Thread); // do an instant handle check var nativeResult = WaitForSingleObject(handles[0], 0); if (nativeResult == WAIT_OBJECT_0) result = true; else if (nativeResult == WAIT_ABANDONED_0) throw new AbandonedMutexException(-1, handle); else if (getTimeout() == 0) result = false; else if (nativeResult == WAIT_TIMEOUT) return; else throw new InvalidOperationException("WaitForSingleObject"); // end the nested Dispatcher loop frame.Continue = false; }; EventHandler onDispatcherInactive = (s, e) => { operationPendingMre.Reset(); // wait for the handle or a message var timeout = getTimeout(); var nativeResult = MsgWaitForMultipleObjectsEx( (uint)handles.Length, handles, timeout, QS_EVENTMASK, MWMO_INPUTAVAILABLE); if (nativeResult == WAIT_OBJECT_0) // handle signalled result = true; else if (nativeResult == WAIT_TIMEOUT) // timed out result = false; else if (nativeResult == WAIT_ABANDONED_0) // abandonded mutex throw new AbandonedMutexException(-1, handle); else if (nativeResult == WAIT_OBJECT_0 + 1) // operation posted from another thread, yield to the frame loop return; else if (nativeResult == WAIT_OBJECT_0 + 2) { // a Windows message if (getTimeout() > 0) { // message pending, yield to the frame loop enqueIdleOperation(); return; } // timed out result = false; } else // unknown result throw new InvalidOperationException("MsgWaitForMultipleObjectsEx"); // end the nested Dispatcher loop frame.Continue = false; }; dispatcher.Hooks.OperationCompleted += onOperationCompleted; dispatcher.Hooks.OperationPosted += onOperationPosted; dispatcher.Hooks.DispatcherInactive += onDispatcherInactive; try { // onDispatcherInactive will be called on the new frame, // as soon as Dispatcher becomes idle enqueIdleOperation(); Dispatcher.PushFrame(frame); } finally { if (idleOperation != null) idleOperation.Abort(); dispatcher.Hooks.OperationCompleted -= onOperationCompleted; dispatcher.Hooks.OperationPosted -= onOperationPosted; dispatcher.Hooks.DispatcherInactive -= onDispatcherInactive; } return result; } } const uint QS_EVENTMASK = 0x1FF; const uint MWMO_INPUTAVAILABLE = 0x4; const uint WAIT_TIMEOUT = 0x102; const uint WAIT_OBJECT_0 = 0; const uint WAIT_ABANDONED_0 = 0x80; const uint INFINITE = 0xFFFFFFFF; [DllImport("user32.dll", SetLastError = true)] static extern uint MsgWaitForMultipleObjectsEx( uint nCount, IntPtr[] pHandles, uint dwMilliseconds, uint dwWakeMask, uint dwFlags); [DllImport("kernel32.dll")] static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds); } #endregion } 

此代码尚未经过严格测试,可能包含错误,但我认为我的概念是正确的,就问题而言。

我之前必须做类似的事情,用UI自动化测试UI的进程。 实现是这样的

 public static bool WaitOneAndPump(WaitHandle handle, int timeoutMillis) { bool gotHandle = false; Stopwatch stopwatch = Stopwatch.StartNew(); while(!(gotHandle = waitHandle.WaitOne(0)) && stopwatch.ElapsedMilliseconds < timeoutMillis) { DispatcherFrame frame = new DispatcherFrame(); Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(ExitFrame), frame); Dispatcher.PushFrame(frame); } return gotHandle; } private static object ExitFrame(object f) { ((DispatcherFrame)f).Continue = false; return null; } 

我之前遇到过低于后台优先级的问题。 我相信,问题是WPF命中测试发生在更高的优先级,因此根据鼠标的位置, ApplicationIdle优先级可能永远不会运行。

更新

所以看来上面的方法会挂掉CPU。 这是一种替代方法,它使用DispatcherTimer来检查方法是否为消息泵送。

 public static bool WaitOneAndPump2(this WaitHandle waitHandle, int timeoutMillis) { if (waitHandle.WaitOne(0)) return true; DispatcherTimer timer = new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromMilliseconds(50) }; DispatcherFrame frame = new DispatcherFrame(); Stopwatch stopwatch = Stopwatch.StartNew(); bool gotHandle = false; timer.Tick += (o, e) => { gotHandle = waitHandle.WaitOne(0); if (gotHandle || stopwatch.ElapsedMilliseconds > timeoutMillis) { timer.IsEnabled = false; frame.Continue = false; } }; timer.IsEnabled = true; Dispatcher.PushFrame(frame); return gotHandle; }