RunAsync - How do I await the completion of work on the UI thread?

前端 未结 4 687
野趣味
野趣味 2020-12-05 08:42

When awaiting Dispatcher.RunAsync the continuation occurs when the work is scheduled, not when the work has completed. How can I await the work completing?

相关标签:
4条回答
  • 2020-12-05 08:49

    I found the following suggestion on a Microsoft github repository: How to await a UI task sent from a background thread.

    Setup

    Define this extension method for the CoreDispatcher:

    using System;
    using System.Threading.Tasks;
    using Windows.UI.Core;
    
    public static class DispatcherTaskExtensions
    {
        public static async Task<T> RunTaskAsync<T>(this CoreDispatcher dispatcher, 
            Func<Task<T>> func, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal)
        {
            var taskCompletionSource = new TaskCompletionSource<T>();
            await dispatcher.RunAsync(priority, async () =>
            {
                try
                {
                    taskCompletionSource.SetResult(await func());
                }
                catch (Exception ex)
                {
                    taskCompletionSource.SetException(ex);
                }
            });
            return await taskCompletionSource.Task;
        }
    
        // There is no TaskCompletionSource<void> so we use a bool that we throw away.
        public static async Task RunTaskAsync(this CoreDispatcher dispatcher,
            Func<Task> func, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal) => 
            await RunTaskAsync(dispatcher, async () => { await func(); return false; }, priority);
    }
    

    Once you do that, all you need to do is use the new RunTaskAsync method to have your background task await on the UI work.

    Usage example

    Let's pretend that this is the method that needs to run in the UI thread. Pay attention to the debug statements, which will help follow the flow:

    public static async Task<string> ShowMessageAsync()
    {
        // Set up a MessageDialog
        var popup = new Windows.UI.Popups.MessageDialog("Question", "Please pick a button to continue");
        popup.Commands.Add(new Windows.UI.Popups.UICommand("Button 1"));
        popup.Commands.Add(new Windows.UI.Popups.UICommand("Button 2"));
        popup.CancelCommandIndex = 0;
    
        // About to show the dialog
        Debug.WriteLine("Waiting for user choice...");
        var command = await popup.ShowAsync();
    
        // Dialog has been dismissed by the user
        Debug.WriteLine("User has made a choice. Returning result.");
        return command.Label;
    }
    

    To await that from your background thread, this is how you would use RunTaskAsync:

    // Background thread calls this method
    public async void Object_Callback()
    {
        Debug.WriteLine("Object_Callback() has been called.");
    
        // Do the UI work, and await for it to complete before continuing execution
        var buttonLabel = await Dispatcher.RunTaskAsync(ShowMessageAsync);
        
        Debug.WriteLine($"Object_Callback() is running again. User clicked {buttonLabel}.");
    }
    

    The output then looks like this:

    Object_Callback() has been called.

    Waiting for user choice...

    User has made a choice. Returning result.

    Object_Callback() is running again. User clicked Button 1.

    0 讨论(0)
  • 2020-12-05 09:02

    You can wrap the call to RunAsync in your own asynchronous method that can be awaited and control the completion of the task and thus the continuation of awaiting callers yourself.

    Since async-await is centred on the Task type, you must orchestrate the work using this type. However, usually a Task schedules itself to run on a threadpool thread and so it cannot be used to schedule UI work.

    However, the TaskCompletionSource type was invented to act as a kind of puppeteer to an unscheduled Task. In other words, a TaskCompletionSource can create a dummy Task that is not scheduled to do anything, but via methods on the TaskCompletionSource can appear to be running and completing like a normal job.

    See this example.

    public Task PlayDemoAsync()
    {
        var completionSource = new TaskCompletionSource<bool>();
        this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
        {
            try
            {
                foreach (var ppc in this.Plots.Select(p => this.TransformPlot(p, this.RenderSize)))
                {
                    // For each subsequent stroke plot, we need to start a new figure.
                    //
                    if (this.Sketch.DrawingPoints.Any())
                        this.Sketch.StartNewFigure(ppc.First().Position);
    
                    foreach (var point in ppc)
                    {
                        await Task.Delay(100);
    
                        this.Sketch.DrawingPoints.Add(point.Position);
                    }
                }
    
                completionSource.SetResult(true);
            }
            catch (Exception e)
            {
                completionSource.SetException(e);
            }
        });
    
        return (Task)completionSource.Task;
    }
    

    Note: the main work being done on the UI thread is just some lines being drawn on screen every 100ms.

    A TaskCompletionSource is created as the puppet master. Look near the end and you'll see that it has a Task property that is returned to the caller. Returning Task satisfies the compilers needs and makes the method awaitable and asynchronous.

    However, the Task is just a puppet, a proxy for the actual work going on in the UI thread.

    See how in that main UI delegate I use the TaskCompletionSource.SetResult method to force a result into the Task (since returned to the caller) and communicate that work has finished.

    If there's an error, I use SetException to 'pull another string' and make it appear that an exception has bubbled-up in the puppet Task.

    The async-await subsystem knows no different and so it works as you'd expect.

    Edit

    As prompted by svick, if the method was designed to be callable only from the UI thread, then this would suffice:

        /// <summary>
        /// Begins a demonstration drawing of the asterism.
        /// </summary>
        public async Task PlayDemoAsync()
        {
            if (this.Sketch != null)
            {
                foreach (var ppc in this.Plots.Select(p => this.TransformPlot(p, this.RenderSize)))
                {
                    // For each subsequent stroke plot, we need to start a new figure.
                    //
                    if (this.Sketch.DrawingPoints.Any())
                        this.Sketch.StartNewFigure(ppc.First().Position);
    
                    foreach (var point in ppc)
                    {
                        await Task.Delay(100);
    
                        this.Sketch.DrawingPoints.Add(point.Position);
                    }
                }
            }
        }
    
    0 讨论(0)
  • 2020-12-05 09:05

    A nice way to work the clean way @StephenCleary suggests even if you have to start from a worker thread for some reason, is to use a simple helper object. With the object below you can write code like this:

        await DispatchToUIThread.Awaiter;
        // Now you're running on the UI thread, so this code is safe:
        this.textBox.Text = text;
    

    In your App.OnLaunched you have to initialize the object:

        DispatchToUIThread.Initialize(rootFrame.Dispatcher);
    

    The theory behind the code below you can find at await anything;

    public class DispatchToUIThread : INotifyCompletion
    {
        private readonly CoreDispatcher dispatcher;
    
        public static DispatchToUIThread Awaiter { get; private set; }
    
        private DispatchToUIThread(CoreDispatcher dispatcher)
        {
            this.dispatcher = dispatcher;
        }
    
        [CLSCompliant(false)]
        public static void Initialize(CoreDispatcher dispatcher)
        {
            if (dispatcher == null) throw new ArgumentNullException("dispatcher");
            Awaiter = new DispatchToUIThread(dispatcher);
        }
    
        public DispatchToUIThread GetAwaiter()
        {
            return this;
        }
    
        public bool IsCompleted
        {
            get { return this.dispatcher.HasThreadAccess; }
        }
    
        public async void OnCompleted(Action continuation)
        {
            if (continuation == null) throw new ArgumentNullException("continuation");
            await this.dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => continuation());
        }
    
        public void GetResult() { }
    }
    
    0 讨论(0)
  • 2020-12-05 09:12

    Your question is assuming that you want to schedule (and wait for) work on a UI thread from a background thread.

    You'll usually find your code is much cleaner and easier to understand (and it will definitely be more portable) if you have the UI be the "master" and the background threads be the "slaves".

    So, instead of having a background thread await some operation for the UI thread to do (using the awkward and unportable Dispatcher.RunAsync), you'll have the UI thread await some operation for the background thread to do (using the portable, made-for-async Task.Run).

    0 讨论(0)
提交回复
热议问题