How to wait for WaitHandle while serving WPF Dispatcher events?

后端 未结 2 1616
终归单人心
终归单人心 2020-12-06 13:59

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 w

相关标签:
2条回答
  • 2020-12-06 14:11

    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.

    0 讨论(0)
  • 2020-12-06 14:31

    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;
    }
    
    0 讨论(0)
提交回复
热议问题