How to wait for WaitHandle while serving WPF Dispatcher events?
Asked Answered
T

2

5

Someone emailed me and asked if I have a version of WaitOneAndPump for 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.

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 DoEvents API has been taking.

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.

Taillight answered 8/2, 2014 at 5:32 Comment(9)
Can't help wondering, what's the use case for this?Wivestad
@AntonTykhyy, so far I haven't used this in production code, although I think it can possibly be used in UI automation, unit testing and perhaps this scenario.Taillight
I still don't understand why do you need to wait-and-pump on the same stack frame. In the scenario you quoted, the accepted answer is much better than a dirty hack such as in your question here. You are relying on WPF implementation details, including undocumented ones. That's just asking for trouble, whether it's in application code or in unit tests, and I would avoid it if possible.Wivestad
Anything about how WPF handles messages.Wivestad
You cannot prevent a rough shut-down with a modal dialog. The user can just attempt to close it and then click "close unresponsive application" or kill it with task manager.Wivestad
WPF Dispatcher Hooks and MsgWaitForMultipleObjectsEx are well documented. I don't pump messages at all, I just wait for one and let the WPF pump it. As to the modal dialog, see the comment there about a robotic appliance. I can't prevent the user from killing the app, but I can do my best to make sure the shut-down sequence happens in the right order.Taillight
Still I see no need to wait on the same frame. You can prevent the main window from closing on Alt-F4 or equivalent just as well without introducing all this dodgy dispatcher hook stuff (I notice you're using .Abort on a dispatcher operation — with what result?..), wait in the background e.g. with RegisterWaitForSingleObject and post back to the dispatcher if that is called for.Wivestad
@AntonTykhyy, RegisterWaitForSingleObject would acquire a mutex on the wrong thread (more details). DispatcherOperation.Abort() is called to cancel my own operation when it's no longer needed. If you want to understand how this "dodgy" code actually works, go ahead and ask that. Otherwise, I don't think I have anything more to add to my first and second comments, above. I've suddenly found myself dealing with too many repetitive arguments like "dodgy", "dirty" and "hacks".Taillight
Shit, I forgot that there are waitable objects (mutexes) for which the thread you wait them on is important. Sorry.Wivestad
T
7

The version of WaitOneAndPump I've come up with uses DispatcherHooks Events and MsgWaitForMultipleObjectsEx, to avoid running a busy-waiting loop.

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.ShowDialog and 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.

Taillight answered 9/2, 2014 at 5:16 Comment(1)
Related: Cancelling a pending task synchronously on the UI thread.Taillight
F
3

I've had to do similar things before for testing UI's in-proc with UI Automation. The implementation is something like this

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 ApplicationIdle priority may never get run.

Update

So it seems the above method will peg the CPU. Here's an alternative that uses a DispatcherTimer to check while the method pumps for messages.

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;
}
Friedrick answered 8/2, 2014 at 7:34 Comment(5)
That would pump all events, but also would be a busy waiting loop, with one of the CPU cores cruising at 100%. Or am I missing something? +1 anyway :)Taillight
@Noseratio, AFAIK, it won't peg the CPU because most of the time is spent inside PushFrame doing other UI operations, but I'll double check tomorrow.Friedrick
when I'm trying it on my quad-core i5 CPU, ProcessExplorer shows ~25%, which means one of the cores is 100% busy. I used this test.Taillight
@Noseratio You were correct. See my update, which I tested with the same code and it does not peg the CPU.Friedrick
mikez, this approach still polls the handle (every 50ms), I'd rather go with the MsgWaitForMultipleObjectsEx-based solution, but this certainly looks better, and your solution is more portable. Whoever down-voted it should reconsider.Taillight

© 2022 - 2024 — McMap. All rights reserved.