Asked 1 month ago by PulsarCommander003
How can I maintain a smooth animated wait box while asynchronously retrieving data in a WinForms app?
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
Asked 1 month ago by PulsarCommander003
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
In my C# WinForms application (.NET 4.8), I need to display a wait box dialog with a progress bar in Marquee mode before the main view is shown. While the wait box is active and animated, an async method connects to a service to fetch data, and once the data is retrieved, the wait box should close.
I have implemented this functionality using a manual waiting loop and Application.DoEvents, as demonstrated below, but I’d like to modernize the approach without using BackgroundWorker or direct event management.
Here is my current implementation:
CSHARPtry { 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(); }
The supporting WaitBoxForm code is as follows:
CSHARPpublic 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; } } }
My main question is: How can I perform this async operation while keeping the message pump active so that the animation remains smooth, ideally using a more modern async/await pattern?
Comments 0
•Answers 3
•Views 84
Answer by TychoAI Agent • 1 month ago
Below is one “modern” approach that avoids a busy‐waiting loop with Application.DoEvents. The idea is to show a wait box (either on the UI thread as a modeless dialog or on its own thread) and then use async/await to run your service call. When the awaited work is complete the wait box is closed. For example, assuming GetLoginData is properly “awaitable,” you can do this on your main thread (so the message pump stays active):
CSHARP// Instead of manually spinning, simply await the async call. private async Task<bool> FetchDataAsync() { // Show the wait box (modeless) before starting the async operation. // Alternatively, if you want to run the wait box on its own thread, // you can modify ShowWaitBox accordingly. WaitBoxForm.ShowWaitBox(); try { // Await the async service call. apiAppKey = await GetLoginData(); return true; } catch(Exception ex) { MessageService.Information(apiExceptionMessage(ex.Message)); return false; } finally { // Ensure the wait box closes (on its thread if necessary) WaitBoxForm.CloseWaitBox(); } }
Using Async/Await Without DoEvents:
By awaiting GetLoginData, the UI thread is not blocked. The wait box form’s animation (the Marquee mode progress bar) remains smooth because the UI thread is free to process its message pump.
Wait Box on Its Own Thread (Optional):
If you still want the wait box to run on its own thread, you can create and run a new UI thread for it. For example, modify the WaitBoxForm static methods as follows:
CSHARPpublic partial class WaitBoxForm : Form { private static WaitBoxForm waitBoxForm; private static Thread uiThread; public WaitBoxForm() { InitializeComponent(); } public static void ShowWaitBox() { if (waitBoxForm != null) return; uiThread = new Thread(() => { waitBoxForm = new WaitBoxForm(); Application.Run(waitBoxForm); }); uiThread.SetApartmentState(ApartmentState.STA); uiThread.Start(); } public static void CloseWaitBox() { if (waitBoxForm != null) { // Use Invoke to marshal the Close call to the wait box's UI thread. waitBoxForm.BeginInvoke(new Action(() => { waitBoxForm.Close(); })); waitBoxForm = null; } } }
In this approach, the wait box runs on its own UI thread with its own message loop (via Application.Run), so animation remains smooth regardless of what the main thread is doing.
Where to Call the Async Operation:
Be sure to call your FetchDataAsync method from an async event handler (such as Form_Load marked as async void) or another async method. For example:
CSHARPprivate async void MainForm_Load(object sender, EventArgs e) { bool success = await FetchDataAsync(); if (success) { // Proceed to show main view or enable functionality. } else { // Handle error. } }
This modern async/await approach avoids low-level constructs like ManualResetEvent and Application.DoEvents while keeping your UI responsive during the asynchronous operation.
Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?
No comments yet.
Answer by MeteorCaptain301 • 1 month ago
The key to making this scheme behave is to make sure MainForm.Handle
is the first window created (because the OS typically considers the first visible top-level window to be the primary UI window for the process). Ordinarily, the Handle
is created when the form is shown. But in this case, we want to show the WaitBox
(and its asynchronous ProgressBar
) first. Here's one way to make this work:
_ = Handle;
SetVisibleCore
and prevent MainForm
from becoming visible until we're ready.BeginInvoke
, post a message at the tail of the message queue to show the wait box.CSHARPpublic partial class MainForm : Form { public MainForm() { InitializeComponent(); _ = Handle; BeginInvoke(new Action(() => ConnectToService())); // Setup the DataGridView Load += (sender, e) => dataGridView.DataSource = Responses; } protected override void SetVisibleCore(bool value) => base.SetVisibleCore(value && _initialized); bool _initialized = false; IList Responses { get; } = new BindingList<ReceivedHttpResponseEventArgs>(); private void ConnectToService() { using (var waitBox = new WaitBox()) { waitBox.ResponseReceived += (sender, e) => { Debug.Assert( !InvokeRequired, "Expecting that we are ALREADY ON the UI thread"); Responses.Add(e); }; waitBox.ShowDialog(); } _initialized = true; Show(); } }
WaitBox Minimal Example
This demo uses the https://catfact.ninja API as a stand-in for "an async method that connects to a service and retrieves some data". The received "facts" are used to populate the data source of a DataGridView
.
CSHARPpublic partial class WaitBox : Form { public WaitBox() { InitializeComponent(); StartPosition = FormStartPosition.CenterScreen; FormBorderStyle = FormBorderStyle.None; progressBar.Style = ProgressBarStyle.Marquee; progressBar.MarqueeAnimationSpeed = 50; } protected async override void OnVisibleChanged(EventArgs e) { base.OnVisibleChanged(e); if (Visible) { labelProgress.Text = "Connecting to service..."; // Includes some cosmetic delay for demo purposes for (int i = 0; i < 10; i++) { labelProgress.Text = await GetCatFactAsync(); await Task.Delay(TimeSpan.FromSeconds(1)); } DialogResult = DialogResult.OK; } } HttpClient _httpClient = new HttpClient(); private string _nextPageUrl = "https://catfact.ninja/facts?limit=1"; private async Task<string> GetCatFactAsync() { try { HttpResponseMessage response = await _httpClient.GetAsync(_nextPageUrl); if (response.IsSuccessStatusCode) { string jsonData = await response.Content.ReadAsStringAsync(); var catFacts = JsonConvert.DeserializeObject<ResponseParser>(jsonData); if (catFacts?.Data != null && catFacts.Data.Count > 0) { _nextPageUrl = $"{catFacts.Next_Page_Url}&limit=1"; ResponseReceived?.Invoke(this, catFacts.Data[0]); return catFacts.Data[0].Fact; } } } catch (Exception ex) { Debug.WriteLine($"Error: {ex.Message}"); } return null; } public event EventHandler<ReceivedHttpResponseEventArgs> ResponseReceived; } class ResponseParser { [JsonProperty("data")] public List<ReceivedHttpResponseEventArgs> Data { get; set; } [JsonProperty("next_page_url")] public string Next_Page_Url { get; set; } } public class ReceivedHttpResponseEventArgs : EventArgs { [JsonProperty("fact")] public string Fact { get; set; } }
No comments yet.
Answer by CosmicScientist065 • 1 month ago
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>
).
CSHARP[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.
No comments yet.
No comments yet.