如何从WPF gui运行异步任务并与之交互 [英] How to run and interact with an async Task from a WPF gui

查看:70
本文介绍了如何从WPF gui运行异步任务并与之交互的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个WPF GUI,在这里我想按一个按钮来启动一个长任务,而又不冻结任务期间的窗口.在任务运行时,我想获取进度报告,并且希望合并另一个按钮,该按钮将在我选择的任何时间停止任务.

我不知道使用异步/等待/任务的正确方法.我无法提供我尝试过的所有内容,但这就是我目前所拥有的.

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 slowBurn = _burnBabyBurn.ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3);
            
        await slowBurn;
    }
        
    //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<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;        
    }
}

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

我需要知道如何安排异步/等待/任务声明以允许

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

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

解决方案

长话短说:

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

    // run a method in another thread
    await 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 async Task HeavyMethod(TextBox textBox)
{
    while (stillWorking)
    {
        textBox.Dispatcher.Invoke(() =>
        {
            // UI operation goes inside of Invoke
            textBox.Text += ".";
            // Note that: 
            //    Dispatcher.Invoke() blocks the UI thread anyway
            //    but without it you can't modify UI objects from another thread
        });
        
        // CPU-bound or I/O-bound operation goes outside of Invoke
        // await won't block UI thread, unless it's run in a synchronous context
        await Task.Delay(51);
    }
}

Result:
started....................done


您需要了解(1)如何编写async代码和(2)如何在另一个线程中运行UI操作.

asyncawait的魔力:

  1. 您只能在async方法中使用await.

  2. 您只能await一个awaitable对象(即TaskTask<T>ValueTask<T>等)

  3. async方法的返回类型包装在Task中,而await对其进行解包. (请参阅包装和展开部分)

  4. Task.Run 通常通常在线程池

    中将Task排队

(即它使用现有线程或在线程池中创建新线程来运行任务.除非它是

  • 执行在await处等待任务完成并返回其结果,而不会由于 magic 而阻塞主线程:

  • async-await magic 是它使用状态机让编译器放弃收回通过async方法控制awaited Task.

    (即async方法不会在另一个线程中运行.asyncawait本身与线程创建无关.)

  • 不要将带有async关键字的方法与包含在Task中的方法混淆; Task负责线程async负责魔术

    所以

    通过将async放在方法签名中,您可以告诉编译器使用状态机调用此方法(到目前为止没有线程).然后,通过运行Task,您使用线程在任务内调用该方法.通过await完成任务,您阻止了执行流经过await行而没有阻塞UI线程.

    事件处理程序看起来像下面的代码.

    说明了ExecuteLongProcedure(情况1和2)和MyButton_Click(情况A和B)的签名中存在异步的两种可能情况:

    private async void MyButton_Click(object sender, RoutedEventArgs e)
    {
        //queue a task to run on threadpool
    
        // 1. if ExecuteLongProcedure is a normal method and returns void
        Task task = Task.Run(()=>
            ExecuteLongProcedure(this, intParam1, intParam2, intParam3)
        );
        // or
        // 2. if ExecuteLongProcedure is an async method and returns Task
        Task task = ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3);
    
        // either way ExecuteLongProcedure is running asynchronously here
        // the method will exit if you don't wait for the Task to finish
    
        // A. wait without blocking the main thread
        //  -> requires MyButton_Click to be async
        await task;
        // or
        // B. wait and block the thread (NOT RECOMMENDED AT ALL)
        // -> does not require MyButton_Click to be async
        task.Wait();
    }
    

    返回类型:

    这很重要.假设您具有以下声明:

    private async ReturnType Method() { ... }
    

    • 如果ReturnTypevoid ,则不能await

      • 如果尝试编写await Method();,则会出现编译错误,提示无法等待void .
      • 您只能 开火而忘了,即只需正常调用该方法:Method();然后继续生活.
      • Method执行将是同步的,但是由于它具有async,因此您可以利用它的魔力,即可以在方法中编写await task来控制执行流程.
      • 这是WPF处理按钮单击事件处理程序的方式,显然是因为您的事件处理程序返回了void.
    • 如果ReturnTypeTask,则await Method();返回void

    • 如果ReturnTypeTask<T>,则await Method();返回类型为T

      的值

    异步方法的返回类型必须为voidTaskTask<T>,类似任务的类型,IAsyncEnumerable<T>IAsyncEnumerator<T>

    如果您不想了解更多信息,则可能需要向下滚动至 WPF GUI 部分!

    包装和展开:

    当您从async方法中return值时,它将被包装Task<T>中.例如,如果您期望async方法中的int,那么它应该返回Task<int>:

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

    要检索或解包Task<>包装的值:

    • 异步选项:await
    • 同步选项:task.Resulttask.GetAwaiter().GetResult()task.WaitAndUnwrapException()或阅读 MSDN .

      要展开任务结果,请始终尝试使用await而不是.Result,否则将没有异步的好处,而只有异步的缺点.

      注意:

      await是异步的,与同步的task.Wait()不同.但是他们俩都在做同样的事情,等待任务完成.

      await是异步的,与同步的task.Result不同.但是他们俩都在做同样的事情,就是等待任务完成,展开并返回结果.

      要具有包装值,可以始终使用Task.FromResult(1)而不是通过使用Task.Run(() => 1)创建新线程.

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

      命名约定

      使用Async关键字用async关键字简单地后缀方法名称.

      因为避免使用async void方法是一种好习惯(请参见下面的模式),所以可以说 Task返回的方法应后缀Async.

      此约定的目的是确保遵守异步病毒.


      WPF GUI:

      这是我解释如何在另一个线程中运行UI操作的地方.


      阻止:

      关于 WPF异步事件处理程序,您需要了解的第一件事是Dispatcher将提供在此处解释

      与CPU绑定或与IO绑定的操作(如Sleeptask.Wait())将阻塞并消耗线程,即使在使用async关键字的方法中调用了它们也是如此.但是await Task.Delay()告诉状态机停止在线程上执行流,以便它不消耗线程;意味着线程资源可以在其他地方使用:

      private async void Button_Click(object sender, RoutedEventArgs e)
      {
              Thread.Sleep(1000);//stops, blocks and consumes threadpool resources
              await Task.Delay(1000);//stops without consuming threadpool resources
              Task.Run(() => Thread.Sleep(1000));//does not stop but consumes threadpool resources
              await Task.Run(() => Thread.Sleep(1000));//literally the WORST thing to do
      }
      

      线程安全:

      如果必须异步访问GUI(在ExecuteLongProcedure方法内部),请调用任何涉及修改任何非线程安全对象的操作.例如,必须使用与GUI线程关联的Dispatcher对象来调用任何WPF GUI对象:

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

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

      访问非UI线程上的集合

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

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

      博客.

      任务使用线程?确定吗?

      不一定.阅读此答案,以了解有关async真实面孔的更多信息.

      Stephen Cleary 完美地解释了async-await.他还在其他博客帖子中进行了解释,不涉及线程.

      了解更多

      ValueTask和Task

      MSDN解释Task

      MSDN解释了async

      how-to-call-asynchronous-method -from-synchronous-method

      async await-幕后花絮

      async await-常见问题解答

      确保您知道异步,并行和并发之间的区别.

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

      调查并发名称空间

      最后,请阅读此电子书: Patterns_of_Parallel_Programming_CSharp

      I have a WPF GUI, where I want to press a button to start a long task without freezing the window for the duration of the task. While the task is running I would like to get reports on progress, and I would like to incorporate another button that will stop the task at any time I choose.

      I cannot figure the correct way to use async/await/task. I can't include everything I've tried, but this is what I have at the moment.

      A WPF window class :

      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 slowBurn = _burnBabyBurn.ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3);
                  
              await slowBurn;
          }
              
          //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;
          }
      }
       
      

      And a class for the worker method:

      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;        
          }
      }
      

      This runs, but the WPF function window is still blocked once the worker method starts.

      I need to know how to arrange the async/await/task declarations to allow

      A) the worker method to not block the gui window
      B) let the worker method update the gui window
      C) allow the gui window to stop interrupt and stop the worker method

      Any help or pointers are much appreciated.

      解决方案

      Long story short:

      private async void ButtonClick(object sender, RoutedEventArgs e)
      {
          // modify UI object in UI thread
          txt.Text = "started";
      
          // run a method in another thread
          await 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 async Task HeavyMethod(TextBox textBox)
      {
          while (stillWorking)
          {
              textBox.Dispatcher.Invoke(() =>
              {
                  // UI operation goes inside of Invoke
                  textBox.Text += ".";
                  // Note that: 
                  //    Dispatcher.Invoke() blocks the UI thread anyway
                  //    but without it you can't modify UI objects from another thread
              });
              
              // CPU-bound or I/O-bound operation goes outside of Invoke
              // await won't block UI thread, unless it's run in a synchronous context
              await Task.Delay(51);
          }
      }
      

      Result:
      started....................done
      


      You need to know about (1) how to write async code and (2) how to run UI operations in another thread.

      The magic of async and await:

      1. You can only await in an async method.

      2. You can only await an awaitable object (i.e. Task, Task<T> or ValueTask<T> etc)

      3. The return type of an async method is wrapped in a Task and await unwraps it. (see Wrapping and Unwrapping section)

      4. Task.Run usually queues a Task in the thread pool

      (i.e. it uses an existing thread or creates a new thread in the thread pool to run the task. Unless it's a pure operation)

      1. The execution waits at await for the task to finish and returns back its results, without blocking the main thread because of the magic:

      2. The magic of async-await is that it uses a state-machine to let the compiler give up and take back the control over the awaited Task in an async method.

        (i.e. async method does not run in another thread. async and await by themselves don't have anything to do with thread creation.)

      Don't confuse the method with async keyword with the method wrapped within a Task; The Task is responsible for threading, the async is responsible for the magic

      So

      By putting async in the method signature you tell the compiler to use state machine to call this method (no threading so far). Then by running a Task you use a thread to call the method inside the task. And by awaiting the task you prevent the execution flow to move past the await line without blocking UI thread.

      The event handler looks like the code below.

      Two possible cases for presense of async in the signature of ExecuteLongProcedure (case 1 and 2) and MyButton_Click (case A and B) are explained:

      private async void MyButton_Click(object sender, RoutedEventArgs e)
      {
          //queue a task to run on threadpool
      
          // 1. if ExecuteLongProcedure is a normal method and returns void
          Task task = Task.Run(()=>
              ExecuteLongProcedure(this, intParam1, intParam2, intParam3)
          );
          // or
          // 2. if ExecuteLongProcedure is an async method and returns Task
          Task task = ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3);
      
          // either way ExecuteLongProcedure is running asynchronously here
          // the method will exit if you don't wait for the Task to finish
      
          // A. wait without blocking the main thread
          //  -> requires MyButton_Click to be async
          await task;
          // or
          // B. wait and block the thread (NOT RECOMMENDED AT ALL)
          // -> does not require MyButton_Click to be async
          task.Wait();
      }
      

      Return Types:

      This is very important to know. Suppose you have the following declaration:

      private async ReturnType Method() { ... }
      

      • If ReturnType is void you can't await it

        • If you try writing await Method();, you will get a compile error saying cannot await void.
        • You can only fire and forget i.e. just call the method normally: Method(); and then go on with your life.
        • The Method execution will be synchronous, however since it has async it will allow you to take advantage of the magic, i.e. you can write await task within the method to control the flow of execution.
        • This is how WPF handles your button click event handler, obviously because your event handler returns void.
      • If ReturnType is Task then await Method(); returns void

      • If ReturnType is Task<T> then await Method(); returns a value of type T

      The return type of an async method must be void, Task, Task<T>, a task-like type, IAsyncEnumerable<T>, or IAsyncEnumerator<T>

      You might want to scroll down to WPF GUI section if you don't want to learn more!

      Wrapping and Unrwapping:

      When you return a value from an async method it will be wrapped in a Task<T>. E.g., if you expect an int from your async method then it should return a Task<int>:

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

      To retrieve or unwrap the value which is wrapped inside a Task<>:

      e.g.

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

      expand it like this:

      Func<int> GetOneFunc = () => 1; // synchronous function returning a number
      Task<int> GetOneTask = Task.Run(GetOneFunc); // a Task<int> is started
      int number = await GetOneTask; // waiting AND unwrapping Task<int> into int
      

      The whole code:

      private async Task<int> GetNumberAsync()
      {
          int number = await Task.Run(GetNumber); // unwrap int from Task<int>
          // int number = Task.FromResult(1); // the correct way for short operations
      
          // bad practices:
          // int number = Task.Run(GetNumber).GetAwaiter().GetResult(); // sync over async
          // int number = Task.Run(GetNumber).Result; // sync over async
          // int number = Task.Run(GetNumber).Wait(); // sync over async
      
          return number; // wrap int in Task<int>
      }
      

      Still confused? Read async return types on MSDN.

      To unwrap a task result, Always try to use await instead of .Result otherwise there will be no asynchronous benefit but only asynchronous disadvantages.

      Note:

      await is a asynchronous and is different from task.Wait() which is synchronous. But they both do the same thing which is waiting for the task to finish.

      await is a asynchronous and is different from task.Result which is synchronous. But they both do the same thing which is waiting for the task to finish and unwrapping and returning back the results.

      To have a wrapped value, you can always use Task.FromResult(1) instead of creating a new thread by using Task.Run(() => 1).

      Task.Run is newer (.NetFX4.5) and simpler version of Task.Factory.StartNew

      Naming Convention

      Simply postfix the name of the method with the async keyword with Async.

      Since avoiding async void methods is a good practice (see patterns below), you can say only Task returning methods should be postfixed with Async.

      The purpose of this convention is to make sure the Asynchronous Virality is respected.


      WPF GUI:

      This is where I explain how to run UI operations in another thread.


      Blocking:

      First thing you need to know about WPF async event handlers is that the Dispatcher will provide a synchronization context. Explained here

      CPU-bound or IO-bound operations such as Sleep and task.Wait() will block and consume the thread even if they are called in a method with async keyword. but await Task.Delay() tells the state-machine to stop the flow of execution on the thread so it does not consume it; meaning that the thread resources can be used elsewhere:

      private async void Button_Click(object sender, RoutedEventArgs e)
      {
              Thread.Sleep(1000);//stops, blocks and consumes threadpool resources
              await Task.Delay(1000);//stops without consuming threadpool resources
              Task.Run(() => Thread.Sleep(1000));//does not stop but consumes threadpool resources
              await Task.Run(() => Thread.Sleep(1000));//literally the WORST thing to do
      }
      

      Thread Safety:

      If you have to access GUI asynchronously (inside ExecuteLongProcedure method), invoke any operation which involves modification to any non-thread-safe object. For instance, any WPF GUI object must be invoked using a Dispatcher object which is associated with the GUI thread:

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

      However, If a task is started as a result of a property changed callback from the ViewModel, there is no need to use Dispatcher.Invoke because the callback is actually executed from the UI thread.

      Accessing collections on non-UI Threads

      WPF enables you to access and modify data collections on threads other than the one that created the collection. This enables you to use a background thread to receive data from an external source, such as a database, and display the data on the UI thread. By using another thread to modify the collection, your user interface remains responsive to user interaction.

      Value changes fired by INotifyPropertyChanged are automatically marshalled back onto the dispatcher.

      How to enable cross-thread access

      Remember, async method itself runs on the main thread. So this is valid:

      private async void MyButton_Click(object sender, RoutedEventArgs e)
      {
          txt.Text = "starting"; // UI Thread
          await Task.Run(()=> ExecuteLongProcedure1());
          txt.Text = "waiting"; // UI Thread
          await Task.Run(()=> ExecuteLongProcedure2());
          txt.Text = "finished"; // UI Thread
      }
      

      Patterns:

      Fire and forget pattern:

      For obvious reasons this is how your WPF GUI event handlers such as Button_Click are called.

      void Do()
      {
          // CPU-Bound or IO-Bound operations
      }
      async void DoAsync() // returns void
      {
          await Task.Run(Do);
      }
      void FireAndForget() // not blocks, not waits
      {
          DoAsync();
      }
      

      Fire and observe:

      Task-returning methods are better since unhandled exceptions trigger the TaskScheduler.UnobservedTaskException.

      void Do()
      {
          // CPU-Bound or IO-Bound operations
      }
      async Task DoAsync() // returns Task
      {
          await Task.Run(Do);
      }
      void FireAndWait() // not blocks, not waits
      {
          Task.Run(DoAsync);
      }
      

      Fire and wait synchronously while wasting thread resources:

      This is known as Sync over async, it is a synchronous operation but it uses more than one thread which may cause starvation. This happens when you call Wait() or try to read results directly from task.Result before the task is finished.

      (AVOID THIS PATTERN)

      void Do()
      {
          // CPU-Bound or IO-Bound operations
      }
      async Task DoAsync() // returns Task
      {
          await Task.Run(Do);
      }
      void FireAndWait() // blocks, waits and uses 2 more threads. Yikes!
      {
          var task = Task.Run(DoAsync);
          task.Wait();
      }
      

      Is this all to it?

      No. There is a lot more to learn about async, its context and its continuation. This blogpost is especially recommended.

      Task uses Thread? Are you sure?

      Not necessarily. Read this answer to know more about the true face of async.

      Stephen Cleary has explained async-await perfectly. He also explains in his other blog post when there is no thread involved.

      Read more

      ValueTask and Task

      MSDN explains Task

      MSDN explains async

      how-to-call-asynchronous-method-from-synchronous-method

      async await - Behind the scenes

      async await - FAQ

      Make sure you know the difference between Asynchronous, Parallel and Concurrent.

      You may also read a simple asynchronous file writer to know where you should concurrent.

      Investigate concurrent namespace

      Ultimately, read this e-book: Patterns_of_Parallel_Programming_CSharp

      这篇关于如何从WPF gui运行异步任务并与之交互的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

    查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆