Sometimes it is indeed required to do the background work on the UI thread, particularly, when the majority of work is to deal with the user input.
Example: real-time syntax highlighting, as-you-type. It might be possible to offload some sub-work-items of such background operation to a pool thread, but that wouldn't eliminate the fact the text of the editor control is changing upon every new typed character.
Help at hand: await Dispatcher.Yield(DispatcherPriority.ApplicationIdle)
. This will give the user input events (mouse and keyboard) the best priority on the WPF Dispatcher event loop. The background work process may look like this:
async Task DoUIThreadWorkAsync(CancellationToken token)
{
var i = 0;
while (true)
{
token.ThrowIfCancellationRequested();
await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);
// do the UI-related work
this.TextBlock.Text = "iteration " + i++;
}
}
This will keep the UI responsive and will do the background work as fast as possible, but with the idle priority.
We may want to enhance it with some throttle (wait for at least 100 ms between iterations) and better cancellation logic:
async Task DoUIThreadWorkAsync(CancellationToken token)
{
Func<Task> idleYield = async () =>
await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);
var cancellationTcs = new TaskCompletionSource<bool>();
using (token.Register(() =>
cancellationTcs.SetCanceled(), useSynchronizationContext: true))
{
var i = 0;
while (true)
{
await Task.Delay(100, token);
await Task.WhenAny(idleYield(), cancellationTcs.Task);
token.ThrowIfCancellationRequested();
// do the UI-related work
this.TextBlock.Text = "iteration " + i++;
}
}
}
Updated as the OP has posted a sample code.
Based upon the code you posted, I agree with @HighCore's comment about the proper ViewModel.
The way you're doing it currently, background.BeginInvoke
starts a background operation on a pool thread, then synchronously calls back the UI thread on a tight foreach
loop, with Dispatcher.Invoke
. This only adds an extra overhead. Besides, you're not observing the end of this operation, because you're simply ignoring the IAsyncResult
returned by background.BeginInvoke
. Thus, InitializeForm
returns, while background.BeginInvoke
continues on a background thread. Essentially, this is a fire-and-forget call.
If you really want to stick to the UI thread, below is how it can be done using the approach I described.
Note that _initializeTask = background()
is still an asynchronous operation, despite it's taking place on the UI thread. You won't be able to make it synchronous without a nested Dispatcher event loop inside InitializeForm
(which would be a really bad idea because of the implications with the UI re-entrancy).
That said, a simplified version (no throttle or cancellation) may look like this:
Task _initializeTask;
private void InitializeForm(List<NonDependencyObject> myCollection)
{
Action<NonDependencyObject> doWork = (nonDepObj) =>
{
var dependencyObject = CreateDependencyObject(nonDepObj);
UiComponent.Add(dependencyObject);
// Set up some binding on each dependencyObject and update progress bar
...
};
Func<Task> background = async () =>
{
foreach (var nonDependencyObject in myCollection)
{
if (nonDependencyObject.NeedsToBeAdded())
{
doWork(nonDependencyObject);
await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);
}
}
};
_initializeTask = background();
}