等待Dispatcher.RunAsync
等待工作安排时,而不是工作完成时。我如何等待完成工作?
修改
我的原始问题假设过早延续是由API的设计引起的,所以这是真正的问题。
在使用异步委托等待Dispatcher.RunAsync
时,在委托代码中使用await
时,会在遇到await
时继续进行,而不是在工作完成时进行。我如何等待完成工作?
编辑2
您可能需要分派已经在UI线程上工作的一个原因是解决细微的时序和布局问题。对于可视化树中元素的大小和位置的值来说,通常用于UI的后续迭代的调度工作是很常见的。
答案 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() { }
}