admin管理员组

文章数量:1304921

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

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;
     }           
   }
 }
Share Improve this question edited Feb 4 at 7:57 John asked Feb 4 at 7:39 JohnJohn 1,9155 gold badges34 silver badges62 bronze badges 10
  • Regarding the code in the question, could you clarify where is it located? Is it inside the Main entry point of the application? – Theodor Zoulias Commented Feb 4 at 7:51
  • 1 @TheodorZoulias It is before: Application.Run(new MainForm()); I edited the original question adding more code. – John Commented Feb 4 at 7:58
  • Application.DoEvents(); - if you see this, something is probably going very wrong. What are you trying to do, at all? Some sort of progress dialog while doing background processing? – Fildor Commented Feb 4 at 8:35
  • @Fildor That's right. I want to do the job (like fetching data from local net) and actively waiting for the result back again while animation is still working. – John Commented Feb 4 at 8:44
  • 2 Regarding your intention to migrate from the BackgroundWorker to async/await, you could take a look at this answer. – Theodor Zoulias Commented Feb 4 at 9:38
 |  Show 5 more comments

2 Answers 2

Reset to default 1

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.

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:

  1. Force the main for window creation using _ = Handle;
  2. Override SetVisibleCore and prevent MainForm from becoming visible until we're ready.
  3. Using BeginInvoke, post a message at the tail of the message queue to show the wait box.

public 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.


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

本文标签: