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

Kic*_*aha 44 c# wpf asynchronous

我有一个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;
    }
}
Run Code Online (Sandbox Code Playgroud)

还有一个worker方法的类:

class OtherClass
{
    internal Task ExecuteLongProcedureAsync(MainWindow gui, int param1, int param2, int param3)
    {       
        var tcs = new TaskCompletionSource<int>();       

        //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;        
    }
}
Run Code Online (Sandbox Code Playgroud)

这会运行,但是一旦工作方法启动,WPF功能窗口仍会被阻止.

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

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

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

Biz*_*han 89

长话短说:

private async void ButtonClick(object sender, RoutedEventArgs e)
{
    // modify UI object in UI thread
    txt.Text = "started";

    // run a method in another thread
    await Task.Run(()=> HeavyMethod(txt));
    // <<method execution is finished here>>

    // modify UI object in UI thread
    txt.Text = "done";
}

// This is a thread-safe method. You can run it in any thread
internal void HeavyMethod(TextBox textBox)
{
    while (stillWorking)
    {
        textBox.Dispatcher.Invoke(() =>
        {
            // UI operation goes inside of Invoke
            textBox.Text += ".";
        });

        // CPU-bound or I/O-bound operation goes outside of Invoke
        System.Threading.Thread.Sleep(51);
    }
}
Run Code Online (Sandbox Code Playgroud)

说明:

  1. 你只能async在一个await方法中.

  2. awaitasync线程池中对a 进行排队(即它使用线程池中的现有线程或在线程池中创建新线程来运行任务)

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

  4. 魔术Task关键字是,它创建另一个线程.它只允许编译器放弃取消对该方法的控制.

  5. 值得一提的是,如果Task<T>是类型,ValueTask<T>async返回值Task.if await是类型Task.Run然后Task不返回任何东西(或返回await)

所以

你的主线程调用async-await方法(awaited Task)就像普通方法一样,到目前为止还没有线程......现在你可以在这里运行一个任务async:

Result:
started....................done
Run Code Online (Sandbox Code Playgroud)

或者干脆

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

或者如果async返回值为typeasync

private async void MyButton_Click(object sender, RoutedEventArgs e)
{
    await Task.Run(()=>
        ExecuteLongProcedure(this, intParam1, intParam2, intParam3));
}
Run Code Online (Sandbox Code Playgroud)

或者干脆

private async void MyButton_Click(object sender, RoutedEventArgs e)
{
    Task<string> task = Task.Run(()=>
        ExecuteLongProcedure(this, intParam1, intParam2, intParam3));
    string returnValue = await task;
}
Run Code Online (Sandbox Code Playgroud)

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

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

    //or in cases where you already have a "Task returning" method:
    //  var httpResponseInfo = await httpRequestInfo.GetResponseAsync();
}
Run Code Online (Sandbox Code Playgroud)

注1:

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

async不是 Task

笔记2:

await 即使在带有await关键字的方法中调用它,也会阻塞主线程.

ExecuteLongProcedure 防止任务因string关键字而阻塞主线程.

//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 ExecuteLongProcedure(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
        //  ExecuteLongProcedure is called in an async method
        System.Threading.Thread.Sleep(51);
    }

    gui.UpdateWindow("Done and Done");
} 
Run Code Online (Sandbox Code Playgroud)

注3(GUI):

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

private async Task<int> GetOneAsync()
{
    return 1; // return type is a simple int 
              // while the method signature indicates a Task<int>
}
Run Code Online (Sandbox Code Playgroud)

但是,如果由于属性从ViewModel 更改回调而启动任务,则无需使用,private async ReturnType Method() { ... }因为回调实际上是从UI线程执行的.

在非UI线程上访问集合

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

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

如何启用跨线程访问

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

int number = await Task.Run(() => 1); // right hand of await is a Task<int>
                                      // while the left hand is an int
Run Code Online (Sandbox Code Playgroud)

阅读更多

MSDN解释道 void

MSDN解释道 Method();

void - 在幕后

await Method(); - 常问问题

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

调查并发命名空间

最后阅读这本电子书:Patterns_of_Parallel_Programming_CSharp

  • 很好的答案.我会添加一个选项.如果将UI绑定到实现`INotifyPropertyChanged`(通常称为ViewModel)的对象,则可以从异步线程更改对象的属性.WPF的数据绑定机制将为您切换到UI线程. (2认同)

Ste*_*ary 9

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

因此,编写ExecuteLongProcedure同步方法更为自然.这也是使用标准的类型标准的行为,尤其是一个好主意,使用IProgress<T>了最新进展CancellationToken注销:

internal void ExecuteLongProcedure(int param1, int param2, int param3,
    CancellationToken cancellationToken, IProgress<string> 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");
}
Run Code Online (Sandbox Code Playgroud)

现在,您有一个使用适当约定的更可重用的类型(没有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<string>(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;
  }
}
Run Code Online (Sandbox Code Playgroud)


Eri*_*c D 7

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

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

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

“避免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));
}
Run Code Online (Sandbox Code Playgroud)

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");
} 
Run Code Online (Sandbox Code Playgroud)

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

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

或者,

void UpdateWindow(string text)
{
    //simply
    txt.Text += text;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

重要的:

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


Gus*_*dor 6

这是一个使用async/await,IProgress<T>和的示例CancellationTokenSource。这些是您应该使用的现代 C# 和 .Net Framework 语言功能。其他解决方案让我的眼睛流血了一点。

代码特点

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

风景

<Window x:Class="ProgressExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" SizeToContent="WidthAndHeight" Height="93.258" Width="316.945">
    <StackPanel>
        <Button x:Name="Button_Start" Click="Button_Click">Start</Button>
        <ProgressBar x:Name="ProgressBar_Progress" Height="20"  Maximum="100"/>
        <Button x:Name="Button_Cancel" IsEnabled="False" Click="Button_Cancel_Click">Cancel</Button>
    </StackPanel>
</Window>
Run Code Online (Sandbox Code Playgroud)

编码

    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    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<int> progress = new Progress<int>(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<int> 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();
        }
    }
Run Code Online (Sandbox Code Playgroud)


归档时间:

查看次数:

51017 次

最近记录:

5 年,9 月 前