24

I'm using async/await to asynchronously load my data from database and during the loading process, I want to popup a loading form, it's just a simple form with running progress bar to indicate that there's a running process. After data has been loaded, the dialog will automatically be closed. How can I achieve that ? Below is my current code:

 protected async void LoadData() 
    {
       ProgressForm _progress = new ProgressForm();  
       _progress.ShowDialog()  // not working
       var data = await GetData();          
       _progress.Close();
    }

Updated:

I managed to get it working by changing the code:

 protected async void LoadData() 
        {
           ProgressForm _progress = new ProgressForm();  
           _progress.BeginInvoke(new System.Action(()=>_progress.ShowDialog()));
           var data = await GetData();          
           _progress.Close();
        }

Is this the correct way or there's any better ways ?

Thanks for your help.

davidcoder
  • 898
  • 4
  • 11
  • 25
  • 2
    Your updated solution has a potential race condition; `GetData` could theoretically complete before the dialog box is ever shown; thus it would remain on screen indefinitely (or, more likely, you would get an `ObjectDisposedException`). – Bradley Smith Oct 29 '15 at 07:09
  • Do you need the form to be modal? – Philippe Paré Oct 29 '15 at 18:30

5 Answers5

37

It's easy to implement with Task.Yield, like below (WinForms, no exception handling for simplicity). It's important to understand how the execution flow jumps over to a new nested message loop here (that of the modal dialog) and then goes back to the original message loop (that's what await progressFormTask is for):

namespace WinFormsApp
{
  internal static class DialogExt
  {
    public static async Task<DialogResult> ShowDialogAsync(this Form @this)
    {
      await Task.Yield();
      if (@this.IsDisposed)
        return DialogResult.Cancel;
      return @this.ShowDialog();
    }
  }

  public partial class MainForm : Form
  {
    public MainForm()
    {
      InitializeComponent();
    }

    async Task<int> LoadDataAsync()
    {
      await Task.Delay(2000);
      return 42;
    }

    private async void button1_Click(object sender, EventArgs e)
    {
      var progressForm = new Form() { 
        Width = 300, Height = 100, Text = "Please wait... " };

      object data;
      var progressFormTask = progressForm.ShowDialogAsync();
      try 
      {
        data = await LoadDataAsync();
      }
      finally 
      {
        progressForm.Close();
        await progressFormTask;
      }

      // we got the data and the progress dialog is closed here
      MessageBox.Show(data.ToString());
    }
  }
}
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • 1
    I should return the favor and offer a modest reward to this brilliant answer of yours. I am still baffled about how you came up with this effective, dead-simple, and totally unintuitive solution! – Theodor Zoulias Jul 15 '20 at 07:01
  • 2
    @TheodorZoulias, thank you, it's nice to have a feedback like this :) I think I just happened to be heavily involved in COM + desktop app development in my early days, so I had a chance to learn a thing or two about how message loops and pumping work under Windows and .NET. It's much less relevant these days, but there are some hidden treasures out there I still keep coming back to, like [this one](https://learn.microsoft.com/en-us/archive/blogs/cbrumme/apartments-and-pumping-in-the-clr). RIP, Chris Brumme, the author of that blog... :( – noseratio Jul 15 '20 at 07:38
  • 1
    That was a nice reading, thanks! It filled some holes in my knowledge about STAs and MTAs, and I also learned that NAs are a thing (Neutral apartments). :-) – Theodor Zoulias Jul 16 '20 at 12:03
  • Looks great! Question though, are you sure you need to check IsDisposed? Does Close dispose of it implicitly? I didn't think so because technically there's still a reference to it when you get to the outer 'await' call (i.e. not the one on Task.Yield), but I could be wrong. Looking at ReferenceSource shows it is sending `WM_CLOSE` so maybe it is. – Mark A. Donohoe Dec 28 '20 at 12:41
  • One other small suggestion... you may want to change you call to 'Close' to set the DialogResult to dismiss it. Ok may have semantics if someone is actually awaiting on the result. Granted, the only way they'd get there is if they did like you and manually closed it beforehand, but I think that would make a better case to use `Cancel` over `Ok` for that very reason. (i.e. it was manually closed for a different/whatever reason, so cancel any results.) – Mark A. Donohoe Dec 28 '20 at 12:47
  • 2
    @MarkA.Donohoe, I think your suggestions make sense, I didn't test all code paths for this snippet at that time. These days, I don't do much of WinForms/WPF development, but I was to touch this code, I'd probably change it to throw an exception if `IsDisposed` is `true`. I'd still check it, because in theory the form might get closed before the async code flow continues after `Task.Yield()`. – noseratio Dec 28 '20 at 13:29
  • @noseratio can we use the same for another `ShowDialog()` instead of return data method? like an await for ShowDialog complete. – Thrainder Jun 26 '21 at 06:06
  • @Thrainder not sure I understand the question, could you elaborate? – noseratio Jun 26 '21 at 13:50
  • 1
    @noseratio I added a question by using an example using your code here. If this makes sense to you then please mark as up. [Click here for MyQuestion](https://stackoverflow.com/questions/68140392/continue-code-execution-after-calling-showdialog) – Thrainder Jun 26 '21 at 13:59
  • @noseratio what if `LoadDataAsync` contains smth like `Thread.Sleep(5000)` or another hard computation work at the beginning ? will dialog be shown ? – isxaker Mar 10 '22 at 17:03
  • 1
    @isxaker you'd need to wrap anything computational with `Task.Run`, otherwise it will most likely block the message loop. – noseratio Mar 10 '22 at 23:56
4

Here's a pattern that uses Task.ContinueWith and should avoid any race condition with your use of the modal ProgressForm:

protected async void LoadDataAsync()
{
    var progressForm = new ProgressForm();

    // 'await' long-running method by wrapping inside Task.Run
    await Task.Run(new Action(() =>
    {
        // Display dialog modally
        // Use BeginInvoke here to avoid blocking
        //   and illegal cross threading exception
        this.BeginInvoke(new Action(() =>
        {   
            progressForm.ShowDialog();
        }));
        
        // Begin long-running method here
        LoadData();
    })).ContinueWith(new Action<Task>(task => 
    {
        // Close modal dialog
        // No need to use BeginInvoke here
        //   because ContinueWith was called with TaskScheduler.FromCurrentSynchronizationContext()
        progressForm.Close();
    }), TaskScheduler.FromCurrentSynchronizationContext());
}
Jace
  • 1,445
  • 9
  • 20
1

ShowDialog() is a blocking call; execution will not advance to the await statement until the dialog box is closed by the user. Use Show() instead. Unfortunately, your dialog box will not be modal, but it will correctly track the progress of the asynchronous operation.

Bradley Smith
  • 13,353
  • 4
  • 44
  • 57
-1

Always call ShowDialog(), LoadDataAsync and Close() in order to avoid IsDisposed as in @noseratio 's answer. So use Task.Yield() to delay LoadDataAsync() and not ShowDialog()

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }
    async Task<int> LoadDataAsync()
    {
        Console.WriteLine("Load");
        await Task.Delay(2000);
        return 42;
    }

    private async void button1_Click(object sender, EventArgs e)
    {
        var progressForm = new Form()
        {
            Width = 300,
            Height = 100,
            Text = "Please wait... "
        };

        async Task<int> work()
        {
            try
            {
                await Task.Yield();
                return await LoadDataAsync();
            }
            finally
            {
                Console.WriteLine("Close");
                progressForm.Close();
            }
        }
        var task = work();
        Console.WriteLine("ShowDialog");
        progressForm.ShowDialog();
        object data = await task;

        // we got the data and the progress dialog is closed here
        Console.WriteLine("MessageBox");
        MessageBox.Show(data.ToString());
    }
}
Bruno Martinez
  • 2,850
  • 2
  • 39
  • 47
-2

You could try the following:

protected async void LoadData()
{
    ProgressForm _progress = new ProgressForm();
    var loadDataTask = GetData();
    loadDataTask.ContinueWith(a =>
        this.Invoke((MethodInvoker)delegate
        {
            _progress.Close();
        }));
    _progress.ShowDialog();
}
Pieter Nijs
  • 435
  • 3
  • 7