I have a managed component written in C#, which is hosted by a legacy Win32 app as an ActiveX control. Inside my component, I need to be able to get what normally would be Application.Idle
event, i.e. obtain a time slice of the idle processing time on the UI thread (it has to be the main UI thread).
However in this hosted scenario, Application.Idle
doesn't get fired, because there is no managed message loop (i.e., no Application.Run
).
Sadly, the host also doesn't implement IMsoComponentManager
, which might be suitable for what I need. And a lengthy nested message loop (with Application.DoEvents
) is not an option for many good reasons.
So far, the only solution I can think of is to use plain Win32 timers.
According to http://support.microsoft.com/kb/96006, WM_TIMER
has one of the lowest priorities, followed only by WM_PAINT
, which should get me as close to the idle as possible.
Am I missing any other options for this scenario?
Here is a prototype code:
// Do the idle work in the async loop
while (true)
{
token.ThrowIfCancellationRequested();
// yield via a low-priority WM_TIMER message
await TimerYield(DELAY, token); // e.g., DELAY = 50ms
// check if there is a pending user input in Windows message queue
if (Win32.GetQueueStatus(Win32.QS_KEY | Win32.QS_MOUSE) >> 16 != 0)
continue;
// do the next piece of the idle work on the UI thread
// ...
}
// ...
static async Task TimerYield(int delay, CancellationToken token)
{
// All input messages are processed before WM_TIMER and WM_PAINT messages.
// System.Windows.Forms.Timer uses WM_TIMER
// This could be further improved to re-use the timer object
var tcs = new TaskCompletionSource<bool>();
using (var timer = new System.Windows.Forms.Timer())
using (token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: true))
{
timer.Interval = delay;
timer.Tick += (s, e) => tcs.TrySetResult(true);
timer.Enabled = true;
await tcs.Task;
timer.Enabled = false;
}
}
I don't think Task.Delay
would be suitable for this approach, as it uses Kernel timer objects, which are independent of the message loop and its priorities.
Updated, I found one more option: WH_FOREGROUNDIDLE/ForegroundIdleProc. Looks exactly like what I need.
Updated, I also found that a Win32 timer trick is used by WPF for low-priority Dispatcher operations, i.e. Dispatcher.BeginInvoke(DispatcherPriority.Background, ...)
:
Well, WH_FOREGROUNDIDLE/ForegroundIdleProc hook is great. It behaves in a very similar way to
Application.Idle
: the hook gets called when the thread's message queue is empty, and the underlying message loop'sGetMessage
call is about to enter the blocking wait state.However, I've overlooked one important thing. As it turns, the host app I'm dealing with has its own timers, and its UI thread is pumping
WM_TIMER
messages constantly and quite frequently. I could have learnt that if I looked at it with Spy++, in the first place.For
ForegroundIdleProc
(and forApplication.Idle
, for that matter),WM_TIMER
is no different from any other message. The hook gets called after each newWM_TIMER
has been dispatched and the queue has become empty again. That results inForegroundIdleProc
being called much more often than I really need.Anyway, despite the alien timer messages, the
ForegroundIdleProc
callback still indicates there is no more user input messages in the thread's queue (i.e., keyboard and mouse are idle). Thus, I can start my idle work upon it and implement some throttling logic usingasync
/await
, to kept the UI responsive. This is how it would be different from my initial timer-based approach.