Search code examples
c#.netwinformsasync-await.net-4.8

Running async method with open form (wait box dialog) and animation (progress bar with marque style)


In a C# WinForms application, before the main view is displayed, I need to show a wait box dialog with a progress bar set to Marquee mode (where the blocks move from left to right).

While the animation is running, I need to execute an async method that connects to a service and retrieves some data. Once the data is fetched, the wait box should close.

I've completed this task and will share my results, but I feel the implementation is messy. I’d prefer a more modern approach instead of using BackgroundWorker or manually handling events.

I'm currently using .NET 4.8. My main question is: How can I perform this task while keeping the message pump active and ensuring the animation remains smooth?

try
{        
  WaitBoxForm.ShowWaitBox();

  var signal = new ManualResetEvent(false);               
  var task = Task.Run(async () =>
  {          
    apiAppKey = await GetLoginData();
    signal.Set();
  });
  while (!signal.WaitOne(TimeSpan.FromMilliseconds(1)))
  {
    Application.DoEvents();
  }       
}
catch (Exception ex)
{
  WaitBoxForm.CloseWaitBox();
  MessageService.Information(apiExceptionMessage(ex.Message));          
  return false;
}
finally
{
  WaitBoxForm.CloseWaitBox();
}

As requested:

 public partial class WaitBoxForm : Form
 {
   private static WaitBoxForm waitBoxForm;
       
   public WaitBoxForm()
   {
     InitializeComponent();
   }

   public static void ShowWaitBox()
   {
     if (waitBoxForm != null) return;
           
     waitBoxForm = new WaitBoxForm();      
     Task.Run(() => Application.Run(waitBoxForm));
   }

   public static void CloseWaitBox()
   {
     if (waitBoxForm != null)
     {
       waitBoxForm.Invoke((Action)(() => waitBoxForm.Close()));
       waitBoxForm = null;
     }           
   }
 }

Solution

  • One idea is to launch the Task that retrieves the data immediately after the application starts, before showing any UI to the user, and then await this task in the Load or Shown event handler of the WaitBoxForm. The example below starts two consecutive message loops on the same thread (the UI thread), one for the WaitBoxForm and later another one for the MainForm. The retrieved data are stored inside the task (it's a Task<TResult>).

    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
    
        Task<Data> task = Task.Run(async () =>
        {
            // Execute an async method that connects to a service and retrieves some data.
            return data;
        });
    
        var waitBox = new WaitBoxForm();
        waitBox.Shown += async (sender, e) =>
        {
            await task;
            await Task.Yield(); // Might not be nesessary
            waitBox.Close();
        };
        Application.Run(waitBoxForm); // Start first message loop
    
        if (!task.IsCompletedSuccessfully) return;
    
        Application.Run(new MainForm(task.Result)); // Start second message loop
    }
    

    It is assumed that the MainForm has a constructor with a single parameter, which represents the data retrieved from the service.

    The purpose of the await Task.Yield(); is to ensure that the Close will be called asynchronously. I know that some Form events, like the Closing, throw exceptions if you call the Close method inside the handler. I don't know if the Load/Shown are among these events. The above code has not been tested.