如何在为 WPF Dispatcher 事件提供服务时等待 WaitHandle?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/21642381/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to wait for WaitHandle while serving WPF Dispatcher events?
提问by noseratio
Someone emailed me and asked if I have a version of WaitOneAndPumpfor WPF. The goal is to wait for a handle (similar to WaitHandle.WaitOne) and pump WPF Dispatcher events while waiting, on the same stack frame.
有人给我发了电子邮件,问我是否有WaitOneAndPumpWPF的版本。目标是WaitHandle.WaitOne在同一堆栈帧上等待句柄(类似于)并在等待时泵送 WPF Dispatcher 事件。
I really don't think an API like this should be used in any production code, either for WinForms or WPF(perhaps, besides for UI Automation). WPF doesn't expose an explicit version of WinForms' DoEvents, which is a very good design decision, given the fair share of abuse the DoEventsAPI has been taking.
我真的不认为像这样的 API 应该用于任何生产代码,无论是 WinForms 还是 WPF(也许,除了 UI 自动化)。WPF 不公开 WinForms' 的显式版本DoEvents,这是一个非常好的设计决策,因为API 一直在滥用DoEvents。
Nevertheless, the question itself is interesting, so I'm going to take it as an exercise and post whatever I may come up with as the answer. Feel free to post your own version too, if interested.
尽管如此,这个问题本身很有趣,所以我将把它作为一个练习,并发布我可能想出的任何答案作为答案。如果有兴趣,也可以随意发布您自己的版本。
回答by noseratio
The version of WaitOneAndPumpI've come up with uses DispatcherHooksEventsand MsgWaitForMultipleObjectsEx, to avoid running a busy-waiting loop.
WaitOneAndPump我提出的版本使用DispatcherHooksEvents和MsgWaitForMultipleObjectsEx,以避免运行繁忙等待循环。
Again, using this WaitOneAndPump(or any other nested message loop variants) in the production code is almost always will be a bad design decision.I can think of only two .NET APIs which legitimately use a nested message loop: Window.ShowDialogand Form.ShowDialog.
同样,WaitOneAndPump在生产代码中使用这个(或任何其他嵌套的消息循环变体)几乎总是一个糟糕的设计决策。我只能想到两个合法使用嵌套消息循环的 .NET API:Window.ShowDialog和Form.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<Task> 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<uint> 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
}
This code hasn't been heavily tested and may contain bugs, but I think I've got the concept right, as far as the question goes.
这段代码没有经过大量测试,可能包含错误,但就问题而言,我认为我的概念是正确的。
回答by Mike Zboray
I've had to do similar things before for testing UI's in-proc with UI Automation. The implementation is something like this
我之前不得不做类似的事情来使用 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;
}
I've run into issues scheduling at lower than Background priority before. The issue is, I believe, that WPF hit testing occurs at a higher priority so depending on where the mouse is the ApplicationIdlepriority may never get run.
我之前遇到过以低于后台优先级进行调度的问题。问题是,我相信,WPF 命中测试发生在更高的优先级,因此根据鼠标的ApplicationIdle优先级可能永远不会运行。
Update
更新
So it seems the above method will peg the CPU. Here's an alternative that uses a DispatcherTimerto check while the method pumps for messages.
所以看起来上面的方法会钉住CPU。这是DispatcherTimer在方法泵送消息时使用 a进行检查的替代方法。
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;
}

