How to update GUI continuously with async

2019-02-20 17:56发布

Just created a WPF project .net 4.6

And have put this code inside

lbl1 is a label on the GUI

But the label is never updated or the while loop continue only 1 time

        private void Button_Click_1(object sender, RoutedEventArgs e)
    {
        var t = Task.Run(
       async () =>
      {
           await AsyncLoop();
       });

    }

    async Task AsyncLoop()
    {
        while (true)
        {
            string result = await LoadNextItem();
            lbl1.Content = result;
        }
    }

    private static int ir11 = 0;
    async Task<string> LoadNextItem()
    {
        ir11++;
        return "aa " + ir11;
    }

3条回答
劫难
2楼-- · 2019-02-20 18:05

Please user Application.Current.Dispatcher.Invoke to access the ui thread

async Task AsyncLoop()
    {
        while (true)
        {
            string result = await LoadNextItem();
Application.Current.Dispatcher.Invoke(new Action(() => {  lbl1.Content = result; }));

        }
    }
查看更多
We Are One
3楼-- · 2019-02-20 18:15

The C# compiler is giving you a warning that tells you what the problem is.

Specifically, this method is not asynchronous:

async Task<string> LoadNextItem()
{
  ir11++;
  return "aa " + ir11;
}

The compiler message will inform you that this async method has no await statements, and thus will run synchronously. You should only use async where it makes sense, usually for I/O-based operations, e.g.:

async Task<string> LoadNextItemAsync()
{
  await Task.Delay(100); // Placeholder for actual asynchronous work.
  ir11++;
  return "aa " + ir11;
}

Alternatively, if you don't have asynchronous operations, but rather you have some tight CPU-bound loops, then you can push those off to a background thread via Task.Run:

string LoadNextItem()
{
  ir11++;
  return "aa " + ir11;
}

while (true)
{
  string result = await Task.Run(() => LoadNextItem());
  lbl1.Content = result;
}
查看更多
老娘就宠你
4楼-- · 2019-02-20 18:18

By invoking Task.Run you broke your association(SynchronizationContext) with the GUI thread (or WPF Dispatcher) and lost most of the async/await 'goodness'.

Why not use an async void event handler and just come back to the SynchronizationContext(GUI Thread/Dispatcher) for each step?

private async void Button_Click_1(object sender, RoutedEventArgs e)
{
    while (true)
    {
        string result = await LoadNextItem();
        lbl1.Content = result;
    }
}

private static int ir11 = 0;
Task<string> LoadNextItem()
{
    await Task.Delay(1000); // placeholder for actual async work
    ir11++;
    return "aa " + ir11;
}

Or if you really want to separate the state machine for the 'on-going' operations, try passing an IProgress<T> (the default impl. Progress<T> or specifically Progress<string> should work great in this case). See this article by @Stephen Cleary

His example is very close to what you stated in the question. I've copied it here for SO independence.

public async void StartProcessingButton_Click(object sender, EventArgs e)
{
  // The Progress<T> constructor captures our UI context,
  //  so the lambda will be run on the UI thread.
  var progress = new Progress<int>(percent =>
  {
    textBox1.Text = percent + "%";
  });

  // DoProcessing is run on the thread pool.
  await Task.Run(() => DoProcessing(progress));
  textBox1.Text = "Done!";
}

public void DoProcessing(IProgress<int> progress)
{
  for (int i = 0; i != 100; ++i)
  {
    Thread.Sleep(100); // CPU-bound work
    if (progress != null)
      progress.Report(i);
  }
}

Edit: I must admit, while Progress<T> is a nice abstraction, in this case it is just going to fall down to Dispatcher.Invoke as @Pamparanpa suggested.

查看更多
登录 后发表回答