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