1

I have an windows form, where I can enter the number of records to create at once. If I mention to create 1 million records, it takes more time using a single thread using for loop. So I'm trying to create these 1 million records in a batch of 100/10 (configurable) using Parallel.ForEach. While the operation is in progress, I'm trying to update the progress in the UI textbox (like showing many records completed). But the problem is, till before calling the Parallel.ForEach progress status textbox is updating but within the Parallel.ForEach operation, UI element is not updating. Once after finishing the Parallel.ForEach only its updating.

public partial class CreateRecordsForm : Form
{
    private void CreateRecordsBtn_Click(object sender, EventArgs e)
    {
        this.progressStatusTxt.Text = "Started Creating Records"; //Updating in UI
        int count = 0;
        await Task.Run(() => Parallel.ForEach(bulkCreateRequests, new ParallelOptions { MaxDegreeOfParallelism = parallelThreadsSizeInt }, request =>
         {
             dataservice.CreateRecords(request);
             Interlocked.Increment(ref count);
             string progressStatus = count + " of " + bulkCreateRequests.Count + " Requests Completed";
             this.progressStatusTxt.Text = progressStatus; //NOT updating in UI
         }));
        await Task.WhenAll();
        this.progressStatusTxt.Text = "Finished Creating Records"; //Updating in UI
    }
}

I have tried by creating a delegate to update the UI but still not working.

public delegate void BulkOrderCreateFormDelegate(string status);
//...
public BulkOrderCreateFormDelegate BulkOrderCreateFormCallback;
//...
BulkOrderCreate()
{
    this.BulkOrderCreateFormCallback += new BulkOrderCreateFormDelegate(this.SetProgressStatusValueFn);
}
private void SetProgressStatusValueFn(string progressStatusValue)
{
    if(!string.IsNullOrWhiteSpace(progressStatusValue))
        this.progressStatusTxt.Text = progressStatusValue;
}
private void CreateRecordsBtn_Click(object sender, EventArgs e)
{
    this.progressStatusTxt.Text = "Started Creating Records"; //Updating in UI
    int count = 0;
    await Task.Run(() => Parallel.ForEach(bulkCreateRequests, new ParallelOptions { MaxDegreeOfParallelism = parallelThreadsSizeInt }, request =>
     {
         dataservice.CreateRecords(request);
         Interlocked.Increment(ref count);
         string progressStatus = count + " of " + bulkCreateRequests.Count + " Requests Completed";
         BulkOrderCreateFormCallback(progressStatus); //Getting error : Cross thread operation
     }));
    await Task.WhenAll();
    this.progressStatusTxt.Text = "Finished Creating Records"; //Updating in UI
}

Is anyone know how to do this? Can you refer me to a sample working code?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Nagesh Hugar
  • 43
  • 1
  • 5
  • Did you try after `this.progressStatusTxt.Text = progressStatus;` add new line `this.progressStatusTxt.Refresh();` ? btw. I think is better use `Label` for this than `TextBox`, just my personal opinion. – nelek Sep 06 '22 at 17:44
  • winforms runs in a "primary" thread by default, if you want to update the UI from a different thread you need to call Dispatcher.Invoke or Dispatcher.BeginInvoke – BurnsBA Sep 06 '22 at 17:48
  • 2
    what `await Task.WhenAll();` does in here? – kriss Sep 06 '22 at 17:49
  • Related, but not a problem, Parallel.ForEach will automatically run in a separate thread (probably) which is all managed behind the scenes. That is, you don't need to pass through to Task.Run unless you want the `await`. If .NET 6 is an option, Parallel.ForEachAsync was added" https://stackoverflow.com/a/68901782/1462295 – BurnsBA Sep 06 '22 at 17:50
  • @BurnsBA the `Parallel.ForEachAsync` is intended for parallelizing asynchronous work. This is not the case here. The OP has synchronous work to do, and they only want to offload it on the `ThreadPool` in order to keep the UI responsive. – Theodor Zoulias Sep 06 '22 at 18:08
  • @BurnsBA: WinForms doesn't use `Dispatcher` (as does WPF). Instead, the base Form class and all the control classes inherit from `Control` and it has an `InvokeRequired` property, as well as the `Invoke` and `BeginInvoke` (and `EndInvoke`) methods. When used correctly, the `BackgroundWorker` class is able to marshal state and UI-updating properly from background threads to the UI thread – Flydog57 Sep 06 '22 at 19:25

1 Answers1

2

For reporting progress you can use the Progress<T> class and the IProgress<T> interface. It is important to instantiate the Progress<T> class on the UI thread, because it captures the current SynchronizationContext during its instantiation, and uses the captured context to propagate the progress messages.

public partial class CreateRecordsForm : Form
{
    private async void CreateRecordsBtn_Click(object sender, EventArgs e)
    {
        IProgress<string> progress = new Progress<string>(
            message => this.progressStatusTxt.Text = message);
        progress.Report("Started Creating Records");
        int count = 0;
        ParallelOptions options = new() { MaxDegreeOfParallelism = parallelThreadsSizeInt };
        await Task.Run(() => Parallel.ForEach(bulkCreateRequests, options, request =>
        {
            dataservice.CreateRecords(request);
            int localCount = Interlocked.Increment(ref count);
            string progressStatus = localCount + " of " +
                bulkCreateRequests.Count + " Requests Completed";
            progress.Report(progressStatus);
        }));
        progress.Report("Finished Creating Records");
    }
}

The UI controls are affine to the UI thread. They are supposed to be manipulated exclusively on this thread.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104