How to let the UI refresh during a long running *UI* operation

后端 未结 1 569
北海茫月
北海茫月 2020-12-06 02:19

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

相关标签:
1条回答
  • 2020-12-06 02:45

    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();
    }
    
    0 讨论(0)
提交回复
热议问题