Friday, May 13, 2011

Color: Getting Started

Overview

For me, one of the hardest aspects of designing a user interface is choosing an effective color palette. As I don't have a fantastic sense of color, I typically apply quantitative methods to generate a color palette instead of trusting my eye to choose "good" combinations. In my experience, employing a graphic designer to help with color choices is the best approach, but not everyone has that luxury.

When I started attacking color, I was hampered by my past coding experiences that always forced me to deal with colors as a mix of red, green, and blue. I could generate reasonable themes by altering component contribution proportions, but I always felt that the resulting values were a bit "off" unless I stuck with well-known techniques developed during the early days of the web. (#CCFF66 anyone?)

As my user interface concerns moved from single applications to firm-wide suites, my tried-and-true color generation schemes started to fail me. It wasn't until I started to dive into color science that I understood that other color models would serve me better than good old RGB. It was also during this research that I developed a better sense of how to mathematically construct a theme once a model had been chosen. It turns out that these themes look good, too. Bonus!

The purpose of this post is to collect and organize the links that I found to be the most useful in understanding color. I will provide more analysis in subsequent posts and will eventually release a library for advanced color management in WPF, where support for non-RGB models is frustratingly light.

Primers
  • Wikipedia is the best reference for an overview of color models. The related articles for RGB and HSL/HSV models provide the formulae for cross-model conversion.
  • The SAP Design Guild has a mostly-complete color glossary.
  • The folks over at Color Wheel Pro have a very good write-up on color schemes, with many helpful points of advice.
  • ArtSparx has a combination glossary/color scheme overview also well worth the read.
Color Tools
  • My favorite color tool is Petr Stanicek's Color Scheme Designer. Be warned that this tool uses traditional red-yellow-blue primary colors as opposed to red-green-blue, putting green at 180º on the color wheel instead of cyan. Regardless, its ease of use far exceeds anything else I've come across.
  • Color Wheel Pro is also a very good application, though you need to install it to run it.
  • Kaxaml is my favorite visual prototyping tool. I use its color picker for easy adjustment of chroma and brightness.
  • Daniel Flück's "Color Name & Hue" tool over on Colblindor is helpful to assign names to colors that don't align exactly with established nomenclatures. His work is derived from Chirag Mehta's "Name that Color" mini-app.
Conversions and Code
  • TTRider has a good post on codeproject that includes sample code for HSB support for .NET's System.Drawing.Color. It wouldn't be hard to apply these pricinples to its cousin in System.Windows.Media.
  • Bob Powell  has also done significant work in this space, providing both C# and VB samples for HSB conversion and manipulation.
  • Blackwasp also covers RGB vs. HSV thoroughly, providing a lot of commentary for their conversion code. 

[Ratatat: Drugs -- YouTube]

Thursday, May 5, 2011

WPF: Dispatcher - Managing Timers

In my last post, we took a look at the anatomy of the DispatcherTimer and discovered that much of the magic behind its operation is performed by the Dispatcher that the timer runs on. We will devote this post to the exploration of the Dispatcher's timer management facilities.

Consider the following requirements/goals of the DispatcherTimer subsystem:
  • A single Dispatcher can support any number of DispatcherTimers.
  • Each DispatcherTimer may fire at a different interval.
  • A DispatcherOperation representing a tick event cannot be handled on a normal work queue; there needs to be a timing component that determines when execution occurs.
  • The framework should minimize the system resources required to manage all of the timers for a given Dispatcher thread.
Let's see how the Dispatcher class addresses these needs.

Adding/Removing Timers

When a DispatcherTimer starts or stops, it calls Dispatcher.AddTimer() or Dispatcher.RemoveTimer() to adjust the Dispatcher's temporally-based management concerns. (These methods need to be declared internal so DispatcherTimer can access them.)

internal object _instanceLock = new object();
private List<DispatcherTimer> _timers = new List<DispatcherTimer>();
private bool _hasShutdownFinished;
private long _timersVersion;

internal void AddTimer(DispatcherTimer timer)
{
    lock (this._instanceLock)
    {
        if (!this._hasShutdownFinished)
        {
            this._timers.Add(timer);
            this._timersVersion +1L;
        }
    }
    this.UpdateWin32Timer();
}

internal void RemoveTimer(DispatcherTimer timer)
{
    lock (this._instanceLock)
    {
        if (!this._hasShutdownFinished)
        {
            this._timers.Remove(timer);
            this._timersVersion +1L;
        }
    }
    this.UpdateWin32Timer();
}


The _timers list is protected by the dispatcher's _instanceLock, and can cannot be modified once dispatcher shutdown has finished. After the timer is added or removed from the list, the _timersVersion one-up counter is incremented to indicate that we have a new timer configuration. After the _instanceLock is released (and oddly in all circumstances, even when no change is made to the _timers list), both methods call Dispatcher.UpdateWin32Timer() to revisit when the next tick should be generated.

Determining When to Fire

The UpdateWin32Timer() method makes sure the update request happens on the correct thread, dispatching to UpdateWin32TimerFromDispatcherThread() to get the real work done. This latter method figures out when the next timer should fire:

internal void UpdateWin32Timer()
{
    if (this.CheckAccess())
    {
        this.UpdateWin32TimerFromDispatcherThread(null);
        return;
    }


    this.BeginInvoke(DispatcherPriority.Send, new DispatcherOperationCallback(

        this.UpdateWin32TimerFromDispatcherThread), null);
}


private object UpdateWin32TimerFromDispatcherThread(object unused)
{
    lock (this._instanceLock)
    {
        if (!this._hasShutdownFinished)
        {
            bool dueTimeFound = this._dueTimeFound;
            int dueTimeInTicks = this._dueTimeInTicks;
            this._dueTimeFound = false;
            this._dueTimeInTicks = 0;
            if (this._timers.Count > 0)
            {
                for (int i = 0; i < this._timers.Count; i++)
                {
                    DispatcherTimer dispatcherTimer = this._timers[i];
                    if (!this._dueTimeFound || dispatcherTimer._dueTimeInTicks - this._dueTimeInTicks < 0)
                    {
                        this._dueTimeFound = true;
                        this._dueTimeInTicks = dispatcherTimer._dueTimeInTicks;
                    }
                }
            }
            if (this._dueTimeFound)
            {
                if (!this._isWin32TimerSet || !dueTimeFound || dueTimeInTicks !this._dueTimeInTicks)
                {
                    this.SetWin32Timer(this._dueTimeInTicks);
                }
            }
            else
            {
                if (dueTimeFound)
                {
                    this.KillWin32Timer();
                }
            }
        }
    }
    return null;
}


The UpdateWin32TimerFromDispatcherThread() method acquires the _instanceLock and performs the same shutdown check we saw in AddTimer() and RemoveTimer(), so we know that no changes to the _timers list can happen while we are examining the complete timer situation. This method loops over all of the timers known to the dispatcher to find the one with the soonest "due time in ticks". If the next "due time" represents a change from the previous due time, the dispatcher calls SetWin32Timer() with the new preferred due time. If no due time is found and this represents a change from the dispatcher's previous state, the dispatcher calls KillWin32Timer() to disable the Win32 timer.

Simply put, UpdateWin32TimerFromDispatcherThread() determines when the Win32 timer managed by this thread should go off so that the DispatcherTimer expecting the next tick event can be serviced on time.

Managing the Win32 Timer

UpdateWin32TimerFromDispatcherThread() uses SetWin32Timer() and KillWin32Timer() to manage the next "fire" time. Let's see how these methods work:

private void SetWin32Timer(int dueTimeInTicks)
{
    if (!this.IsWindowNull())
    {
        int num = dueTimeInTicks - Environment.TickCount;
        if (num < 1)
        {
            num = 1;
        }
        SafeNativeMethods.SetTimer(new HandleRef(this, this._window.Value.Handle), 2, num);
        this._isWin32TimerSet = true;
    }
}


private void KillWin32Timer()
{
    if (!this.IsWindowNull())
    {
        SafeNativeMethods.KillTimer(new HandleRef(this, this._window.Value.Handle), 2);
        this._isWin32TimerSet = false;
    }
}


Now we're getting into some real nuts and bolts. These methods are using Win32 calls to activate and deactivate the timer for the _window associated with this dispatcher. So where does that _window come from? And how would the Win32 events get handled once they fire? Let's look at the constructor for the Dispatcher to get some answers (thankfully, there is only a default constructor to consider):

private Dispatcher()
{
    // ignoring details not relevant to this discussion
    MessageOnlyHwndWrapper value = new MessageOnlyHwndWrapper();
    this._window = new SecurityCriticalData<MessageOnlyHwndWrapper>(value);
    this._hook = new HwndWrapperHook(this.WndProcHook);
    this._window.Value.AddHook(this._hook);
}

The Dispatcher creates a MessageOnlyHwndWrapper to serve as its "window"; this wrapper has no visual representation, but is able to process events on a Win32 event loop. The constructor also adds a hook to this window's loop to process the dispatcher's queue and timer-related events. When a timer event comes in, the hook calls Dispatcher.PromoteTimers() -- passing it the current Environment.TickCount -- to see what timers need to be serviced:

private void PromoteTimers(int currentTimeInTicks)
{
    try
    {
        List<DispatcherTimer> list = null;
        long num = 0L;
        lock (this._instanceLock)
        {
            if (!this._hasShutdownFinished && this._dueTimeFound && this._dueTimeInTicks - currentTimeInTicks <0)
            {
                list = this._timers;
                num = this._timersVersion;
            }
        }
        if (list !null)
        {
            DispatcherTimer dispatcherTimer = null;
            int i = 0;
            do
            {
                lock (this._instanceLock)
                {
                    dispatcherTimer = null;
                    if (num !this._timersVersion)
                    {
                        num = this._timersVersion;
                        i = 0;
                    }
                    while (< this._timers.Count)
                    {
                        if (list[i]._dueTimeInTicks - currentTimeInTicks <0)
                        {
                            dispatcherTimer = list[i];
                            list.RemoveAt(i);
                            break;
                        }
                        i++;
                    }
                }
                if (dispatcherTimer !null)
                {
                    dispatcherTimer.Promote();
                }
            }
            while (dispatcherTimer !null);
        }
    }
    finally
    {
        this.UpdateWin32Timer();
    }
}

At the core of this method, the dispatcher tries to find all of the timers that have elapsed, removing each one from the _timers list and then calling Promote() to activate their DispatcherOperation objects. These pre-queued operations are then candidates for execution on the next queue processing sweep. The DispatcherTimer objects will re-register themselves with the Dispatcher (if necessary) as they execute their FireTick() methods.

Putting It All Together

When we look back at the DispatcherHooks exercise from a couple of posts ago, we can now make some sense of the DispatcherTimer-related output:

(1) Posted          Inactive        System.Windows.Threading.DispatcherTimer.FireTick
(2) ...
(3) Inactive       
(4) PriorityChanged Inactive        System.Windows.Threading.DispatcherTimer.FireTick
(5) Completed       Background      System.Windows.Threading.DispatcherTimer.FireTick

When the DispatcherTimer is enabled, it queues an inactive DispatcherOperation in its Restart() method [1]. Time elapses [2] -- the dispatcher itself may go inactive [3] -- but after a while, the dispatcher receives a Win32 timer-related event in its WndProcHook() method, dispatching it to PromoteTimers() to determine which timers need servicing. PromoteTimers() calls the Promote() method on each elapsed timer, which brings about the change in priority of the DispatcherOperation [4]. On the next queue processing cycle, the dispatcher executes the newly-activated operation [5].

[PANTyRAiD: Get the Money -- YouTube]

Wednesday, May 4, 2011

WPF: DispatcherTimer - Anatomy

The DispatcherTimer class showed up a few times in the output of my DispatcherHooks sample app, so I want to devote this post to its anatomy.

Construction, Initialization, and State

DispatcherTimer offers four different constructors. Three of them leave the timer in a non-running state, while the fourth has sufficient parameters to get the timer going.

private object _instanceLock = new object();

public DispatcherTimer() : this(DispatcherPriority.Background) {}

public DispatcherTimer(DispatcherPriority priority)
{
    this.Initialize(Dispatcher.CurrentDispatcher, priority, TimeSpan.FromMilliseconds(0.0));
}


public DispatcherTimer(DispatcherPriority priority, Dispatcher dispatcher)
{
    // guard conditions elided... not important to our discussion

    this.Initialize(dispatcher, priority, TimeSpan.FromMilliseconds(0.0));
}

public DispatcherTimer(TimeSpan interval, DispatcherPriority priority, EventHandler callback, Dispatcher dispatcher)
{

    // guard conditions elided... not important to our discussion
    this.Initialize(dispatcher, priority, interval);
    this.Tick += callback;
    this.Start();
}


private void Initialize(Dispatcher dispatcher, DispatcherPriority priority, TimeSpan interval)
{

    // guard conditions elided... not important to our discussion
    this._dispatcher = dispatcher;
    this._priority = priority;
    this._interval = interval;
}


As you can see, all four constructors call DispatcherTimer.Initialize() to set the initial state of the timer. This method simply sets the first three state fields listed below:
  • _dispatcher: The Dispatcher the timer executes on. Can be retrieved through the DispatcherTimer.Dispatcher property but cannot be set after construction. 
  • _priority: The DispatcherPriority at which ticks from this timer are generated. Cannot be set or retrieved through public interfaces after construction.
  • _interval: The TimeSpan between successive ticks from the timer. Can be set or retrieved through the DispatcherTimer.Interval property. Setting the interval while the timer is running may prolong the current tick. Subsequent ticks will adopt the new time characteristic.
  • _instanceLock: Thread safety mechanism for the timer. Not accessible externally.
  • _isEnabled: Represents the running state of the timer. Can be set or retrieved through the DispatcherTimer.IsEnabled property. Setting IsEnabled to true while the timer is not running has the same effect as calling Start(). Setting IsEnabled to false while the timer is running has the same effect as calling Stop().
  • _operation: The DispatcherOperation queued by the timer to represent the current pending tick. Not directly accessible externally. 
  • _tag: User-supplied data. Not used by the timer itself. Can be set or retrieved through the Dispatcher.Tag property.

Callbacks

The fourth constructor above provides a callback for the timer and sets the timer in motion. If you use any of the other three constructors, you should attach an EventHandler to the DispatcherTimer.Tick event before starting the timer. The callback will occur on the corresponding Dispatcher's thread on the given interval until the timer is stopped. The timing may not be precise since the priority of the timer may force its tick events to occur after other higher-priority operations are serviced.

Starting and Stopping

You can control the running state of the timer through either the DispatcherTimer.IsEnabled property or the DispatcherTimer.Start() or DispatcherTimer.Stop() methods. As stated above, the IsEnabled property is equivalent to calling Start() or Stop(), and in fact calls these methods behind-the-scenes. Likewise, the Start() and Stop() methods need to keep the _isEnabled state consistent:

public bool IsEnabled
{
    get { return this._isEnabled; }
    set
    {
        lock (this._instanceLock)
        {
            if (!value && this._isEnabledthis.Stop();
            else
            {
                if (value && !this._isEnabledthis.Start();
            }
        }
    }
}


public void Start()
{
    lock (this._instanceLock)
    {
        if (!this._isEnabled)
        {
            this._isEnabled = true;
            this.Restart();
        }
    }
}

public void Stop()
{
    bool flag = false;
    lock (this._instanceLock)
    {
        if (this._isEnabled)
        {
            this._isEnabled = false;
            flag = true;
            if (this._operation !null)
            {
                this._operation.Abort();
                this._operation = null;
            }
        }
    }
    if (flag)
    {
        this._dispatcher.RemoveTimer(this);
    }
}


The activities carried out by the Stop() method are difficult to interpret until we analyze the call to Restart() made by the Start() method. Restart() is the most important method in the function of the timer:

private void Restart()
{
    lock (this._instanceLock)
    {
        if (this._operation == null)
        {
            this._operation = this._dispatcher.BeginInvoke(DispatcherPriority.Inactive,

                new DispatcherOperationCallback(this.FireTick), null);
            this._dueTimeInTicks = Environment.TickCount + (int)this._interval.TotalMilliseconds;
            if (this._interval.TotalMilliseconds == 0.0 && this._dispatcher.CheckAccess())
            {
                this.Promote();
            }
            else
            {
                this._dispatcher.AddTimer(this);
            }
        }
    }
}


internal void Promote()
{
    lock (this._instanceLock)
    {
        if (this._operation !null)
        {
            this._operation.Priority = this._priority;
        }
    }
}


Let's decompose this a bit and make sure we understand what's really happening:
  • The _instanceLock is held for the duration of the Restart() call. This -- along with similar locks called by IsEnabled, Start(), and Stop() -- make all timer control calls thread-safe.
  • Restart() checks to see if we already have an _operation in play. If we do, then we're already started, and no further action is necessary.
  • To operate with the Dispatcher, we need to queue up a DispatcherOperation to represent our tick. This is done by calling the Dispatcher's BeginInvoke() method. The resulting operation has a priority of Inactive, meaning that it is not going to run until its priority is modified.
  • Restart() calculates the "due time in ticks", which is the wall clock time when the tick operation should complete. This end time is recorded in an internal field accessible to the Dispatcher.
  • If the target interval is immediate and we are already on the Dispatcher's thread, Restart() calls Promote() to modify the tick operation's priority to the previously-supplied DispatcherTimer priority, thereby putting the operation in play.
  • If the target interval is not immediate or if we are not on the Dispatcher's thread, Restart() calls the Dispatcher's AddTimer() method to coordinate the handling of the tick operation.
The Stop() method now makes much more sense, aborting any operation in flight and calling the Dispatcher's RemoveTimer() method to disable the timer. To complete our analysis, we will need to took at these timer-related calls in Dispatcher to see what they do. As it turns out, this is a rather complicated topic and will require a full post to cover in detail; I plan to devote my next post to this.

The one aspect we haven't covered yet is how subsequent ticks get queued up. Looking at Restart(), we can see that our tick operation calls the FireTick() method when run. We would expect this method to fire the Tick event and Restart() the timer, and it does:

private object FireTick(object unused)
{
    this._operation = null;
    if (this.Tick !null)
    {
        this.Tick(this, EventArgs.Empty);
    }
    if (this._isEnabled)
    {
        this.Restart();
    }
    return null;
}


Conclusion

At this point, we have covered the full anatomy of the DispatcherTimer class, but haven't yet revealed the magic behind tick event dispatch. We will examine how the Dispatcher addresses this capability in my next post.

[Deadmau5: Right this Second -- YouTube]

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]