Tuesday, May 3, 2011

WPF: Dispatcher - Monitoring Activity

The designers of the Dispatcher class made it easy for us to watch in-flight operations. Operation events are raised by the DispatcherHooks object belonging to the Dispatcher we wish to monitor.

namespace System.Windows.Threading
{
    public sealed class DispatcherHooks
    {
        // boring stuff elided
        public event EventHandler DispatcherInactive { addremove; }
        public event DispatcherHookEventHandler OperationPosted { addremove; }
        public event DispatcherHookEventHandler OperationCompleted { addremove; }
        public event DispatcherHookEventHandler OperationPriorityChanged { addremove; }
        public event DispatcherHookEventHandler OperationAborted { addremove; }
    }
}

The names of these events are self-explanatory. DispatcherInactive doesn't need to provide any additional information, but the other four events need to identify the affected operation.

namespace System.Windows.Threading
{
    public delegate void DispatcherHookEventHandler(object sender, DispatcherHookEventArgs e);
    public sealed class DispatcherHookEventArgs : EventArgs
    {
        public DispatcherHookEventArgs(DispatcherOperation operation{ ... }
        public Dispatcher Dispatcher get; }
        public DispatcherOperation Operation get; }
    }
}


Now that we are familiar with the DispatcherOperation class, we know what information is easily available to us once we receive a set of DispatcherHookEventArgs. Again, I find it odd that DispatcherOperation doesn't expose its Name property, but we can use reflection to easily get around that.

Starting with a standard WPF application, I edited my App.xaml.cs file to hook into the UI Dispatcher and log events to standard output.

public partial class App : Application
{
    private PropertyInfo namePropertyInfo;

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        namePropertyInfo = typeof (DispatcherOperation).GetProperty("Name",
            BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.NonPublic);

        var hooks = Dispatcher.Hooks;
        hooks.DispatcherInactive += HandleDispatcherInactive;
        hooks.OperationAborted += HandleOperationAborted;
        hooks.OperationCompleted += HandleOperationCompleted;
        hooks.OperationPosted += HandleOperationPosted;
        hooks.OperationPriorityChanged += HandleOperationPriorityChanged;
    }

    protected override void OnExit(ExitEventArgs e)
    {
        var hooks = Dispatcher.Hooks;
        hooks.DispatcherInactive -= HandleDispatcherInactive;
        hooks.OperationAborted -= HandleOperationAborted;
        hooks.OperationCompleted -= HandleOperationCompleted;
        hooks.OperationPosted -= HandleOperationPosted;
        hooks.OperationPriorityChanged -= HandleOperationPriorityChanged;

        base.OnExit(e);
    }

    private void HandleDispatcherInactive(object sender, EventArgs e)
    {
        LogOperationEvent("Inactive"null);
    }

    private void HandleOperationAborted(object sender, DispatcherHookEventArgs e)
    {
        LogOperationEvent("Aborted", e.Operation);
    }

    private void HandleOperationCompleted(object sender, DispatcherHookEventArgs e)
    {
        LogOperationEvent("Completed", e.Operation);
    }

    private void HandleOperationPosted(object sender, DispatcherHookEventArgs e)
    {
        LogOperationEvent("Posted", e.Operation);
    }

    private void HandleOperationPriorityChanged(object sender, DispatcherHookEventArgs e)
    {
        LogOperationEvent("PriorityChanged", e.Operation);
    }

    private void LogOperationEvent(string eventName, DispatcherOperation operation)
    {
        var builder = new StringBuilder();
        builder.AppendFormat("{0,-16}", eventName);

        if (operation != null)
        {
            var operationName = namePropertyInfo.GetValue(operation, new object[0]);

            builder.AppendFormat("{0,-16}", operation.Priority);
            builder.Append(operationName);
        }

        Console.WriteLine(builder.ToString());
    }
}

When you run this example, interact with the main window by clicking on it, typing characters, moving it, resizing it, etc. The output window will show you a stream of operations posted to the Dispatcher's work queue to respond to your requests:

Posted          Input           System.Windows.Input.InputManager.ContinueProcessingStagingArea
Posted          Background      System.Windows.Input.CommandManager.RaiseRequerySuggested
Inactive        
Posted          Input           System.Windows.Interop.HwndMouseInputProvider.<FilterMessage>b__0
Completed       Input           System.Windows.Interop.HwndMouseInputProvider.<FilterMessage>b__0
Inactive       
Posted          Input           System.Windows.Interop.HwndMouseInputProvider.<FilterMessage>b__0
Posted          Render          System.Windows.Media.MediaContext.RenderMessageHandler
Posted          Inactive        System.Windows.Input.InputManager.HitTestInvalidatedAsyncCallback
Posted          Inactive        System.Windows.Threading.DispatcherTimer.FireTick
Completed       Render          System.Windows.Media.MediaContext.RenderMessageHandler
Completed       Input           System.Windows.Interop.HwndMouseInputProvider.<FilterMessage>b__0
Inactive       
PriorityChanged Inactive        System.Windows.Threading.DispatcherTimer.FireTick
PriorityChanged Inactive        System.Windows.Input.InputManager.HitTestInvalidatedAsyncCallback
Completed       Background      System.Windows.Threading.DispatcherTimer.FireTick
Completed       Input           System.Windows.Input.InputManager.HitTestInvalidatedAsyncCallback
Inactive       
Posted          Normal          System.Windows.Interop.HwndSource.RestoreCharMessages
Posted          Input           System.Windows.Input.InputManager.ContinueProcessingStagingArea
Aborted         Normal          System.Windows.Interop.HwndSource.RestoreCharMessages
Completed       Input           System.Windows.Input.InputManager.ContinueProcessingStagingArea
Inactive       
Posted          Input           System.Windows.Interop.HwndMouseInputProvider.<FilterMessage>b__0
Posted          Input           System.Windows.Input.InputManager.ContinueProcessingStagingArea
Posted          Render          System.Windows.Media.MediaContext.RenderMessageHandler
Posted          Input           System.Windows.Input.KeyboardDevice.ReevaluateFocusCallback
Posted          Input           System.Windows.Input.InputManager.HitTestInvalidatedAsyncCallback
Posted          Loaded          System.Windows.BroadcastEventHelper.BroadcastUnloadedEvent
Posted          Background      System.Windows.Input.CommandManager.RaiseRequerySuggested
Posted          Normal          System.Windows.Application.ShutdownCallback
Posted          Normal          MS.Win32.HwndWrapper.UnregisterClass

I considered putting this information in a separate WPF Window, but I was worried about how that would affect the Dispatcher queue! I hope to play with multiple UI Dispatchers shortly and should then be able to work up a separately-dispatched "spy" window. For now, this output gives us good feedback on what our main Dispatcher is doing for us; naturally, you could apply the same technique to non-UI Dispatchers.

[Wolfgang Gartner: Illmerica -- YouTube]

No comments:

Post a Comment