Gishu
Gishu

Reputation: 136613

Why does Dispatcher.Invoke not execute the delegate argument in this example?

    [Test]
    public void A()
    {
        var d = Dispatcher.CurrentDispatcher;

        Action action = () => Console.WriteLine("Dispatcher invoked me!");

        var worker = new BackgroundWorker();
        worker.DoWork += SomeWork;

        //worker.RunWorkerAsync( (Action) delegate { Console.WriteLine("This works!"); } );
        worker.RunWorkerAsync((Action) delegate { d.Invoke(action); } );

        System.Threading.Thread.Sleep(2500);
    }

    private void SomeWork(object sender, DoWorkEventArgs e)
    {
        (e.Argument as Action)();
    }

This block of code doesn't throw an exception. At the same time, Dispatcher.Invoke does nothing. I found that odd.

I extracted a helper method into a base ViewModel. Worker threads used this method DoOnUIThread() to avoid the thread affinity issue. However in my unit-tests, I find that attempting to test the view model objects results in failures due to the above issue.

I could move this whole behavior out into a pluggable dependency that I could substitute in my tests. e.g. ViewModelBase depends on UIThreadExecutor.Execute(Action) and I use a fake that just calls the action in my tests. However I'm curious as to why Dispatcher behaves the way it does..

Upvotes: 1

Views: 2845

Answers (3)

BrainSlugs83
BrainSlugs83

Reputation: 6416

Here's what I use, it works for me, YMMV:

using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Windows.Threading;

namespace DispatcherFun
{
    public static class Program
    {
        public static void Main(string[] args)
        {
            var d = Dispatcher.CurrentDispatcher;

            Action action = () => Console.WriteLine("Dispatcher invoked me!");

            var worker = new BackgroundWorker();
            worker.DoWork += SomeWork;

            //worker.RunWorkerAsync( (Action) delegate { Console.WriteLine("This works!"); } );
            worker.RunWorkerAsync((Action)(() =>
            {
                d.Invoke(action);
            }));

            while (worker.IsBusy)
            {
                Dispatcher.CurrentDispatcher.DoEvents();
                Thread.Yield();
                Thread.Sleep(50);
            }
        }

        private static void SomeWork(object sender, DoWorkEventArgs e)
        {
            (e.Argument as Action)();
        }

        // Based On: http://msdn.microsoft.com/en-us/library/system.windows.threading.dispatcherframe.aspx
        [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
        public static void DoEvents(this Dispatcher dispatcher)
        {
            if (dispatcher != null)
            {
                // This is the "WPF" way:
                try
                {
                    DispatcherFrame frame = new DispatcherFrame();
                    dispatcher.Invoke(DispatcherPriority.Background, new DispatcherOperationCallback(ExitFrame), frame);
                    Dispatcher.PushFrame(frame);
                }
                catch { /* do nothing */ }

                // This is the "WinForms" way (make sure to add a reference to "System.Windows.Forms" for this to work):
                try
                {
                    dispatcher.Invoke(System.Windows.Forms.Application.DoEvents, DispatcherPriority.Send);
                }
                catch { /* do nothing */ }
            }
        }

        private static object ExitFrame(object f)
        {
            try
            {
                ((DispatcherFrame)f).Continue = false;
            }
            catch { /* do nothing */ }

            return null;
        }
    }
}

Notes:

  • Tested in a .NET 4.0 Console App in VS 2012 -- depending on your Unit Test Runner, YMMV.

  • In the DoEvents extension method you can comment out one or the other of the try/catch blocks (i.e. only do it the WPF way, or only do it the WinForms way) -- and it will still work -- I like to have them both, just in case -- if you want to do it the WinForms way: you'll need to add a reference to System.Windows.Forms to your project.

  • The Thread.Yield / Thread.Sleep are not required, and do not add value towards solving the problem (neither sleep nor yield will run any queued dispatcher events) -- but they will decrease CPU usage on that thread (i.e. better laptop battery life, quieter CPU fan, etc.) and play more nicely with windows than if you're just waiting in a forever busy loop. It will also add some overhead, as that's time that could've otherwise been spent running queued dispatcher events.

  • Calling dispatcher.Invoke from the same thread as the Dispatcher seems to just call the method directly, i.e. no reason to call dispatcher.DoEvents() -- however, calling dispatcher.BeginInvoke from the same thread will not execute the call immediately, it will wait until you call dispatcher.DoEvents().

Upvotes: 0

SwDevMan81
SwDevMan81

Reputation: 49978

It looks like your just passing it as a parameter to the BackgroundWorker. Try adding this in your SomeWork function:

  public void SomeWork(object sender, DoWorkEventArgs e)
  {
     ((MulticastDelegate)e.Argument).DynamicInvoke();
  }

Instead of a straight Sleep you could try this:

  while (worker.IsBusy)
  {
     Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Background,new EmptyDelegate(delegate{}));
     //Application.DoEvents();
     System.Threading.Thread.Sleep(10);
  }

Upvotes: 0

Hans Passant
Hans Passant

Reputation: 941377

Dispatcher can only perform its Begin/Invoke() duty when the main thread goes idle and re-enters the dispatch loop. At that point the main thread is quiescent and it can safely execute the dispatched requests. As well as any notifications sent to it by Windows.

It isn't idle in your case, it is stuck inside of Sleep(2500).

Upvotes: 5

Related Questions