如何从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; }); }
说明:
-
必须在同一方法上使用
async
和await
。 -
Task.Run
在线程池中排队方法(作为Task
或Task
)(它使用/创建另一个线程来运行任务) -
执行等待
await
任务完成并抛出其结果,而不会因为async
关键字的魔法能力而阻塞主线程。 -
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/await
, IProgress
和CancellationTokenSource
的示例。 这些是您应该使用的现代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