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

等待Dispatcher.RunAsync时,将在计划工作时进行继续,而不是在工作完成时进行。 我怎么能等待完成的工作?

编辑

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

在使用异步委托等待Dispatcher.RunAsync ,在委托代码中使用await时,会在遇到await时发生延续,而不是在工作完成时。 我怎么能等待完成的工作?

编辑2

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

我在Microsoft github存储库上找到以下建议:如何等待从后台线程发送的UI任务 。

建立

CoreDispatcher定义此扩展方法:

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

完成后,您需要做的就是使用新的RunTaskAsync方法让您的后台任务等待UI工作。

用法示例

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

 public static async Task 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。

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

如果您将UI作为“主”并且后台线程是“从属”,您通常会发现您的代码更清晰,更容易理解(并且它肯定会更加便携)。

因此,不是让后台线程await UI线程做一些操作(使用笨拙和不可移植的Dispatcher.RunAsync ),你将让UI线程await后台线程做的一些操作(使用便携式,制作Task.Run -async Task.Run )。

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

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

但是, TaskCompletionSource类型被发明为一种不定期Task的木偶TaskCompletionSource 。 换句话说, TaskCompletionSource可以创建一个未安排执行任何操作的虚拟Task ,但是通过TaskCompletionSource上的方法可以看起来像正常作业一样运行和完成。

看这个例子。

 public Task PlayDemoAsync() { var completionSource = new TaskCompletionSource(); 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线程上完成的主要工作只是每隔100ms在屏幕上绘制一些线条。

TaskCompletionSource被创建为puppet master。 看看接近结尾,你会看到它有一个返回给调用者的Task属性。 返回Task满足编译器需求并使方法等待和异步。

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

看看在主UI TaskCompletionSource.SetResult我如何使用TaskCompletionSource.SetResult方法将结果强制执行到Task (因为返回给调用者)并传达该工作已经完成。

如果出现错误,我使用SetException “拉出另一个字符串”并使其看起来在puppet Task出现了exception。

async-await子系统没有什么不同,因此它可以按照您的预期运行。

编辑

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

  ///  /// Begins a demonstration drawing of the asterism. ///  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); } } } } 

干净的方式工作的好方法@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); 

下面的代码背后的理论,您可以在等待任何事情;

 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() { } }