RunAsync - 如何等待UI线程上的工作完成?

时间:2013-10-02 09:18:01

标签: c# windows-runtime windows-store-apps async-await

等待Dispatcher.RunAsync等待工作安排时,而不是工作完成时。我如何等待完成工作?

修改

我的原始问题假设过早延续是由API的设计引起的,所以这是真正的问题。

在使用异步委托等待Dispatcher.RunAsync时,在委托代码中使用await时,会在遇到await时继续进行,而不是在工作完成时进行。我如何等待完成工作?

编辑2

您可能需要分派已经在UI线程上工作的一个原因是解决细微的时序和布局问题。对于可视化树中元素的大小和位置的值来说,通常用于UI的后续迭代的调度工作是很常见的。

4 个答案:

答案 0 :(得分:15)

我在Microsoft github repository上找到了以下建议:如何await a UI task sent from a background thread

设置

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

一旦你这样做,你需要做的就是使用新的RunTaskAsync方法让你的后台任务等待UI工作。

用法示例

让我们假装这是需要在UI线程中运行的方法。注意调试语句,这将有助于遵循流程:

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

要等待您的后台主题,您可以使用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}.");
}

然后输出如下:

  

已调用Object_Callback()。

     

等待用户选择......

     

用户做出了选择。返回结果。

     

Object_Callback()再次运行。用户点击了按钮1。

答案 1 :(得分:8)

您的问题是假设您想要从后台线程在上安排(并等待)工作

如果您的 UI 是“主”并且后台主题是“主”,那么您通常会发现您的代码更清晰,更容易理解(并且它肯定更具可移植性)奴”。

所以,不要让后台线程await为UI线程做一些操作(使用笨拙和不可移植的Dispatcher.RunAsync),你将拥有一些UI线程await后台线程的操作(使用可移植的,异步的Task.Run)。

答案 2 :(得分:5)

您可以在您自己的异步方法中将RunAsync的调用包装起来,并控制任务的完成,从而继续等待来电者。

由于async-await以Task类型为中心,因此您必须使用此类型编排工作。但是,通常Task会调度自己在线程池线程上运行,因此不能用于安排UI工作。

然而,TaskCompletionSource类型被发明为一种未经计划的Task的木偶操纵者。换句话说,TaskCompletionSource可以创建一个未安排执行任何操作的虚拟Task,但通过TaskCompletionSource上的方法可能看起来像正常作业一样正在运行和完成。< / p>

见这个例子。

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

注意:在UI线程上完成的主要工作只是每隔100毫秒在屏幕上绘制一些线条。

创建TaskCompletionSource作为木偶大师。看看接近结尾,你会发现它有一个Task属性,返回给调用者。返回Task满足编译器的需要,并使方法等待和异步。

但是,Task只是一个木偶,是UI线程中实际工作的代理。

请参阅主UI代理中的方法,我使用TaskCompletionSource.SetResult方法将结果强制转换为Task(因为返回给调用者)并传达该工作已完成。

如果出现错误,我会使用SetException来“拉出另一个字符串”,并使其显示异常在傀儡Task中冒出来。

async-await子系统没有什么不同,因此它可以按预期工作。

修改

根据svick的提示,如果该方法只能从UI线程调用,那么这就足够了:

    /// <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);
                }
            }
        }
    }

答案 3 :(得分:0)

一种很好的方法来处理干净的方式@StephenCleary建议即使你出于某种原因必须从工作线程开始,也就是使用一个简单的帮助对象。使用下面的对象,您可以编写如下代码:

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

在您的App.OnLaunched中,您必须初始化对象:

    DispatchToUIThread.Initialize(rootFrame.Dispatcher);

您可以在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() { }
}