如何从WPF gui运行异步任务并与之交互

我有一个WPF GUI,在这里我想按一个按钮来启动一个长任务,而不会在任务期间冻结窗口。 当任务正在运行时,我想获得有关进度的报告,我想在我选择的任何时候添加另一个按钮来停止任务。

我无法想出使用async / await / task的正确方法。 我不能包括我尝试过的所有东西,但这就是我现在拥有的东西。

一个WPF窗口类:

public partial class MainWindow : Window { readonly otherClass _burnBabyBurn = new OtherClass(); internal bool StopWorking = false; //A button method to start the long running method private async void Button_Click_3(object sender, RoutedEventArgs e) { Task burnTheBaby = _burnBabyBurn.ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3); await burnTheBaby; } //A button Method to interrupt and stop the long running method private void StopButton_Click(object sender, RoutedEventArgs e) { StopWorking = true; } //A method to allow the worker method to call back and update the gui internal void UpdateWindow(string message) { TextBox1.Text = message; } } 

还有一个worker方法的类:

 class OtherClass { internal Task ExecuteLongProcedureAsync(MainWindow gui, int param1, int param2, int param3) { var tcs = new TaskCompletionSource(); //Start doing work gui.UpdateWindow("Work Started"); While(stillWorking) { //Mid procedure progress report gui.UpdateWindow("Bath water n% thrown out"); if (gui.StopTraining) return tcs.Task; } //Exit message gui.UpdateWindow("Done and Done"); return tcs.Task; } } 

这会运行,但是一旦worker方法启动,WPF函数窗口仍会被阻止。

我需要知道如何安排async / await / task声明来允许

A)不阻止gui窗口的worker方法
B)让worker方法更新gui窗口
C)允许gui窗口停止中断并停止worker方法

任何帮助或指针都非常感谢。

快速提示:

 private async void Button_Click_3(object sender, RoutedEventArgs e) { txt.Text = "started"; await Task.Run(()=> HeavyMethod(this)); txt.Text = "done"; } internal void HeavyMethod(MainWindow gui) { while (stillWorking) { UpdateGUIMethod(gui, "."); System.Threading.Thread.Sleep(51); } } void UpdateGUIMethod(MainWindow gui, string text) { gui.Dispatcher.Invoke(() => { txt.Text += text; }); } 

说明:

  1. 必须在同一方法上使用asyncawait

  2. Task.Run线程池中排队方法(作为TaskTask )(它使用/创建另一个线程来运行任务)

  3. 执行等待await任务完成并抛出其结果,而不会因为async关键字的魔法能力而阻塞主线程。

  4. async关键字的神奇之处在于它不会创建另一个线程。 它只允许编译器放弃收回对该方法的控制。

所以

您的主线程调用async方法( Button_Click_3 )就像普通方法一样,到目前为止还没有线程…现在您可以在Button_Click_3运行任务,如下所示:

 private async void Button_Click_3(object sender, RoutedEventArgs e) { //queue a task to run on threadpool Task task = Task.Run(()=> ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3)); //wait for it to end without blocking the main thread await task; } 

或简单地说

 private async void Button_Click_3(object sender, RoutedEventArgs e) { await Task.Run(()=> ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3)); } 

或者,如果ExecuteLongProcedureAsync的返回值为string类型

 private async void Button_Click_3(object sender, RoutedEventArgs e) { Task task = Task.Run(()=> ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3)); string returnValue = await task; } 

或简单地说

 private async void Button_Click_3(object sender, RoutedEventArgs e) { string returnValue = await Task.Run(()=> ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3)); //or in cases where you already have a "Task returning" method: // var httpResponseInfo = await httpRequestInfo.GetResponseAsync(); } 

任务内部的方法(或ExecuteLongProcedureAsync )以异步方式运行,如下所示:

 //change the value for the following flag to terminate the loop bool stillWorking = true; //calling this method blocks the calling thread //you must run a task for it internal void ExecuteLongProcedureAsync(MainWindow gui, int param1, int param2, int param3) { //Start doing work gui.UpdateWindow("Work Started"); while (stillWorking) { //put a dot in the window showing the progress gui.UpdateWindow("."); //the following line will block main thread unless //ExecuteLongProcedureAsync is called with await keyword System.Threading.Thread.Sleep(51); } gui.UpdateWindow("Done and Done"); } 

注1:

Task.Run是较新的(.NetFX4.5)和更简单的Task.Factory.StartNew版本

await 不是 Task.Wait()

笔记2:

即使在使用async关键字的方法中调用它, Sleep也会阻塞主线程。

await 阻止任务因async关键字而阻塞主线程。

 private async void Button_Click(object sender, RoutedEventArgs e) { ExecuteLongProcedureAsync();//blocks await Task.Run(() => ExecuteLongProcedureAsync());//does not block } 

注3(GUI):

如果必须异步访问GUI(在ExecuteLongProcedureAsync方法内),请调用涉及访问GUI字段的任何操作:

 void UpdateWindow(string text) { //safe call Dispatcher.Invoke(() => { txt.Text += text; }); } 

但是,如果由于属性从ViewModel 更改回调而启动任务,则无需使用Dispatcher.Invoke因为回调实际上是从UI线程执行的。

在非UI线程上访问集合

WPF使您可以访问和修改除创建集合之外的线程上的数据集合。 这使您可以使用后台线程从外部源(如数据库)接收数据,并在UI线程上显示数据。 通过使用另一个线程来修改集合,您的用户界面仍然可以响应用户交互。

由INotifyPropertyChanged触发的值更改会自动编组回调度程序。

如何启用跨线程访问

请记住, async方法本身在主线程上运行。 所以这是有效的:

 private async void Button_Click_3(object sender, RoutedEventArgs e) { txt.Text = "starting"; await Task.Run(()=> ExecuteLongProcedureAsync1()); txt.Text = "waiting"; await Task.Run(()=> ExecuteLongProcedureAsync2()); txt.Text = "finished"; } 

阅读更多

MSDN解释了Task

MSDN解释了async

async await – 在幕后

async await – 常见问题解答

您还可以阅读一个简单的异步文件编写器,以了解应该并发的位置。

调查并发命名空间

最后阅读这本电子书: Patterns_of_Parallel_Programming_CSharp

您对TaskCompletionSource的使用不正确。 TaskCompletionSource是一种为异步操作创建TAP兼容包装器的方法。 在ExecuteLongProcedureAsync方法中,示例代码都是CPU绑定的(即,本质上是同步的,而不是异步的)。

因此,将ExecuteLongProcedure编写为同步方法更为自然。 对标准行为使用标准类型也是一个好主意,特别是使用IProgress进行更新 ,使用CancellationToken进行取消 :

 internal void ExecuteLongProcedure(int param1, int param2, int param3, CancellationToken cancellationToken, IProgress progress) { //Start doing work if (progress != null) progress.Report("Work Started"); while (true) { //Mid procedure progress report if (progress != null) progress.Report("Bath water n% thrown out"); cancellationToken.ThrowIfCancellationRequested(); } //Exit message if (progress != null) progress.Report("Done and Done"); } 

现在,您有一个使用适当约定的更可重用的类型(没有GUI依赖项)。 它可以这样使用:

 public partial class MainWindow : Window { readonly otherClass _burnBabyBurn = new OtherClass(); CancellationTokenSource _stopWorkingCts = new CancellationTokenSource(); //A button method to start the long running method private async void Button_Click_3(object sender, RoutedEventArgs e) { var progress = new Progress(data => UpdateWindow(data)); try { await Task.Run(() => _burnBabyBurn.ExecuteLongProcedure(intParam1, intParam2, intParam3, _stopWorkingCts.Token, progress)); } catch (OperationCanceledException) { // TODO: update the GUI to indicate the method was canceled. } } //A button Method to interrupt and stop the long running method private void StopButton_Click(object sender, RoutedEventArgs e) { _stopWorkingCts.Cancel(); } //A method to allow the worker method to call back and update the gui void UpdateWindow(string message) { TextBox1.Text = message; } } 

这是Bijan最受欢迎的答案的简化版本。 我简化了Bijan的答案,帮助我使用Stack Overflow提供的漂亮格式来解决问题。

通过仔细阅读和编辑Bijan的post我终于明白了: 如何等待异步方法完成?

在我的情况下,另一篇文章的选择答案最终导致我解决了我的问题:

“避免async void 。让你的方法返回Task而不是void 。然后你可以await它们。”

我的简化版Bijan(优秀)答案如下:

1)这将使用async和await启动任务:

 private async void Button_Click_3(object sender, RoutedEventArgs e) { // if ExecuteLongProcedureAsync has a return value var returnValue = await Task.Run(()=> ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3)); } 

2)这是异步执行的方法:

 bool stillWorking = true; internal void ExecuteLongProcedureAsync(MainWindow gui, int param1, int param2, int param3) { //Start doing work gui.UpdateWindow("Work Started"); while (stillWorking) { //put a dot in the window showing the progress gui.UpdateWindow("."); //the following line blocks main thread unless //ExecuteLongProcedureAsync is called with await keyword System.Threading.Thread.Sleep(50); } gui.UpdateWindow("Done and Done"); } 

3)调用涉及gui属性的操作:

 void UpdateWindow(string text) { //safe call Dispatcher.Invoke(() => { txt.Text += text; }); } 

要么,

 void UpdateWindow(string text) { //simply txt.Text += text; } 

结束评论)在大多数情况下,您有两种方法。

  • 第一种方法( Button_Click_3 )调用第二种方法,并具有async修饰符,告诉编译器为该方法启用线程。

    • async方法中的Thread.Sleep阻塞主线程。 但等待任务却没有。
    • 执行在await语句的当前线程(第二个线程)上停止,直到任务完成。
    • 您不能在async方法之外使用await
  • 第二个方法( ExecuteLongProcedureAsync )包装在一个任务中,并返回一个通用的Task对象,可以通过在它之前添加await来指示异步处理。

    • 此方法中的所有内容都是异步执行的

重要:

列罗提出了一个重要问题。 将元素绑定到ViewModel属性时,将在UI线程中执行属性更改的回调 。 所以不需要使用Dispatcher.Invoke 。 由INotifyPropertyChanged触发的值更改会自动编组回调度程序。

以下是使用async/awaitIProgressCancellationTokenSource的示例。 这些是您应该使用的现代C#和.Net Framework语言function。 其他解决方案让我的眼睛有点流血。

代码function

  • 在10秒的时间内计数到100
  • 显示进度条上的进度
  • 在不阻止UI的情况下执行长时间运行的工作(“等待”期间)
  • 用户触发取消
  • 增量进度更新
  • 发布操作状态报告

风景

        

代码

  ///  /// Interaction logic for MainWindow.xaml ///  public partial class MainWindow : Window { private CancellationTokenSource currentCancellationSource; public MainWindow() { InitializeComponent(); } private async void Button_Click(object sender, RoutedEventArgs e) { // Enable/disabled buttons so that only one counting task runs at a time. this.Button_Start.IsEnabled = false; this.Button_Cancel.IsEnabled = true; try { // Set up the progress event handler - this instance automatically invokes to the UI for UI updates // this.ProgressBar_Progress is the progress bar control IProgress progress = new Progress(count => this.ProgressBar_Progress.Value = count); currentCancellationSource = new CancellationTokenSource(); await CountToOneHundredAsync(progress, this.currentCancellationSource.Token); // Operation was successful. Let the user know! MessageBox.Show("Done counting!"); } catch (OperationCanceledException) { // Operation was cancelled. Let the user know! MessageBox.Show("Operation cancelled."); } finally { // Reset controls in a finally block so that they ALWAYS go // back to the correct state once the counting ends, // regardless of any exceptions this.Button_Start.IsEnabled = true; this.Button_Cancel.IsEnabled = false; this.ProgressBar_Progress.Value = 0; // Dispose of the cancellation source as it is no longer needed this.currentCancellationSource.Dispose(); this.currentCancellationSource = null; } } private async Task CountToOneHundredAsync(IProgress progress, CancellationToken cancellationToken) { for (int i = 1; i <= 100; i++) { // This is where the 'work' is performed. // Feel free to swap out Task.Delay for your own Task-returning code! // You can even await many tasks here // ConfigureAwait(false) tells the task that we dont need to come back to the UI after awaiting // This is a good read on the subject - https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html await Task.Delay(100, cancellationToken).ConfigureAwait(false); // If cancelled, an exception will be thrown by the call the task.Delay // and will bubble up to the calling method because we used await! // Report progress with the current number progress.Report(i); } } private void Button_Cancel_Click(object sender, RoutedEventArgs e) { // Cancel the cancellation token this.currentCancellationSource.Cancel(); } } 

问题被问到已经有好几年了,但我认为值得注意的是,BackgroundWorker类的设计正是为了达到A,B和C的要求。

msdn参考页面中的完整示例: https ://msdn.microsoft.com/en-us/library/system.componentmodel.backgroundworker( v= vs.110).aspx