Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to wait for WaitHandle while serving WPF Dispatcher events?

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.

like image 801
noseratio Avatar asked Feb 08 '14 05:02

noseratio


1 Answers

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.

like image 112
noseratio Avatar answered Sep 18 '22 18:09

noseratio