13

Before you flag my question as being a duplicate, hear me out.

Most people have a long running non-UI operation that they are doing and need to unblock the UI thread. I have a long running UI operation which must run on the UI thread which is blocking the rest of my application. Basically, I am dynamically constructing DependencyObjects at run time and adding them to a UI component on my WPF application. The number of DependencyObjects that need to be created depends upon user input, of which there is no limit. One of the test inputs I have has about 6000 DependencyObjects that need to be created and loading them takes a couple minutes.

The usual solution of using a background worker in this case does not work, because once the DependencyObjects are created by the background worker, they can no longer be added to the UI component since they were created on the background thread.

My current attempt at a solution is to run the loop in a background thread, dispatch to the UI thread for each unit of work and then calling Thread.Yield() to give the UI thread a chance to update. This almost works - the UI thread does get the chance to update itself a couple times during the operation, but the application is still essentially blocked.

How can I get my application to keep updating the UI and processing events on other forms during this long running operation?

EDIT: As requested, an example of my current 'solution':

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
            ...
        };

    Action background = () =>
        {
            foreach (var nonDependencyObject in myCollection)
            {
                 if (nonDependencyObject.NeedsToBeAdded())
                 {
                     Dispatcher.Invoke(doWork, nonDependencyObject);
                     Thread.Yield();  //Doesn't give UI enough time to update
                 }
            }
        };
    background.BeginInvoke(background.EndInvoke, null);
}

Changing Thread.Yield() to Thread.Sleep(1) seems to work, but is that really a good solution?

Taedrin
  • 445
  • 1
  • 4
  • 16
  • Post an example of your code. – Federico Berasategui Feb 06 '14 at 01:11
  • It seems that the main issue is the number of DependencyObjects you need to create. In that case, you can try breaking up the creation into multiple tasks you enqueue on the dispatcher. As long as the creation of an individual DependencyObject is not too long running, you can process them in chunks and still keep the UI fairly responsive. "If we break up the task of calculation into manageable chunks, we can periodically return to the Dispatcher and process events. We can give WPF an opportunity to repaint and process input." - http://msdn.microsoft.com/en-us/library/ms741870(v=vs.110).aspx – DavidN Feb 06 '14 at 01:14
  • 1
    `Basically, I am dynamically constructing DependencyObjects at run time and adding them to a UI component on my WPF application` - I keep thinking if all you really need is an `ItemsControl` and a proper ViewModel. – Federico Berasategui Feb 06 '14 at 02:38
  • Based upon the code you posted, I agree with @HighCore. However, if you want to stick to the UI thread, I've posted an update to my [answer](http://stackoverflow.com/a/21592778/1768303). Note that `_initializeTask = background()` is an **asynchronous operation**, despite taking place on the UI thread. You won't be able to make it synchronous without a modal Dispatcher message loop (which would be a really bad idea). – noseratio Feb 06 '14 at 02:49
  • @Noseratio - Unfortunately, I don't think an ItemsControl will help me (although I bet being experienced at MVVM would help me considerably). The DependencyObjects are not UIElements, and aren't directly part of the UI. They merely provide data to the UI Component. What I am doing is essentially taking records from one or more arbitrary database queries and displaying them as points on a map. – Taedrin Feb 06 '14 at 04:12
  • @user1512185 which map? isn't the map already an `ItemsControl`? does it not have a `ItemsSource` or `PointsSource` or the like property that you can bind to an `ObservableCollection` or the like? – Federico Berasategui Feb 06 '14 at 12:36
  • @HighCore we are using ESRI's ArcGIS for WPF v10.2. I am adding Graphics to a GraphicsCollection on a GraphicsLayer which was created at runtime. The map control, GraphicsLayer, GraphicsCollection and Graphic objects are all DependencyObjects which must be created, modified and accessed only on the UI thread (from my understanding). I must not be seeing something - how can I use binding to modify my UI from a background operation without dispatching to the UI thread? – Taedrin Feb 06 '14 at 13:44

1 Answers1

14

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();
}
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • 1
    Thanks for all of the help. I will give it a try tomorrow morning when I get into work and mark the accepted answer if it works for me. – Taedrin Feb 06 '14 at 04:06
  • I should also mention that I *want* a fire-and-forget, asynchronous operation. While the form is disabled until initializing completes in the background, I want other forms in the application to be responsive. – Taedrin Feb 06 '14 at 04:21
  • @user1512185, so you don't really care when the background operation actually ends, and all `dependencyObject`s have been added? Or whether there were any exceptions during the course? That's what fire-and-forget is. – noseratio Feb 06 '14 at 06:52
  • Well, I care about exceptions, but I figure those can be handled on the background operation, no? But I don't care about when the operation ends, as the operation sets a property which enables the UI on completion via binding. – Taedrin Feb 06 '14 at 13:49
  • Will calling an async method like that (_initializeTask = background();) execute it synchronously? – Alex Hope O'Connor Jun 23 '15 at 01:26
  • @AlexHopeO'Connor, it will execute it synchronously until it it hits the `await` inside that `async` lambda. – noseratio Jun 23 '15 at 02:21
  • @Noseratio I have been trying to find examples of executing async methods in WPF without the issues associated with Task.Wait() where it will hang in certain situations where it tries to resume on the thread that is currently waiting for the task to complete. Would executing async code in this avoid such problems? Sorry I should probably ask my own question haha – Alex Hope O'Connor Jun 23 '15 at 04:15
  • @AlexHopeO'Connor, yes - if it's async "all-the-way-down", without blocking constructs like `Task.Wait` or `Task.Result` anywhere in the logical chain of calls. Feel free to ask a separate question with some relevant code and tag it with `async-await`, I'm sure you'll receive a lot of help :) – noseratio Jun 23 '15 at 04:23
  • 1
    @Pangamma it worked pretty well last time I did any extensive WPF development, which was around the time of this answer. The actual solution was a bit more involved than this one. It was checking the status of the UI thread's input queue with `GetQueueStatus` Win32 API and yielding as soon as it saw any pending input, to avoid any lags in mouse and keyboard processing. – noseratio Jul 12 '21 at 09:07