How to implement a progress bar in windows forms C#? - c#

I have my own solution to import monthly sales data in my windows form application. When a user click on import button, the program is actually running but it looks like it's not responding. The process takes a long time about 5 minutes.
So, I'd like to implement a progress bar with status strip label to display as an user interface and let the users know how much the task is done. This is also my first time using a progress bar in my program. So, I read through some tutorials which show how to use it. Some people use progress bar with background worker and timer.
But I don't understand where I should use the solution that I have. In background worker DoWork() event? I don't want to fake it by abusing the progress bar like setting the progressBar.Maximum = 100, progressBar.Value = 0 and as long as the timer is ticking increasing the value by 5. The progress bar must report the actual progress while the program is running.
The following is the solution I am using now to import the data:
private void btnImport_Click(object sender, EventArgs e)
{
if (lsbxBrowsedFiles.Items.Count != 0)
{
ArrayList salesHeaderArr = new ArrayList();
ArrayList salesDetailArr = new ArrayList();
int i = 0;
while (i < browsedXmlFileList.Count)
{
if (browsedXmlFileList[i].ToUpper().EndsWith("SALESHEADER.XML"))
{
salesHeaderArr.Add(browsedXmlFileList[i]);
}
if (browsedXmlFileList[i].ToUpper().EndsWith("SALESDETAIL.XML"))
{
salesDetailArr.Add(browsedXmlFileList[i]);
}
i++;
}
if (selectedFileIsNotInDestinationFolder(salesHeaderArr, salesDetailArr) == true)
{
i = 0;
while (i < salesHeaderArr.Count)
{
SalesHeader salesHeader = new SalesHeader();
string sourceFilePath = salesHeaderArr[i].ToString();
readXMLFiles(sourceFilePath, SALES_HEADER);
SalesHeader salesCheck = (SalesHeader)salesHeaderList[0];
string checkOutletCode = salesCheck.OutletCode;
DateTime checkBusDate = salesCheck.BusinessDate.Value;
if (SalesHeader.IsThisRowAlreadyImportedInSalesHeader(checkOutletCode, checkBusDate) == false)
{
salesHeader.ImportSalesHeader(salesHeaderList);
salesHeader.CreateImportDataLog(getDestinationFilePath(sourceFilePath),
DateTime.Now, salesHeaderList.Count, SALES_HEADER);
}
else
{
string errorDate = checkBusDate.ToString("dd MMMM, yyyy");
MessageBox.Show("Selected XML File with BusinessDate: " + errorDate + " has been already imported.",
"ABC Cafe Import Sales Wizard");
MessageBox.Show("Please select a file which has not been imported!",
"ABC Cafe Import Sales Wizard");
return;
}
MoveXMLFiletoDestinationFolder(sourceFilePath);
i++;
}
i = 0;
while (i < salesDetailArr.Count)
{
SalesDetail salesDetail = new SalesDetail();
string sourceFilePath = salesDetailArr[i].ToString();
readXMLFiles(sourceFilePath, SALES_DETAIL);
SalesDetail salesCheck = (SalesDetail)salesDetailList[0];
string checkOutletCode = salesCheck.OutletCode;
DateTime checkBusDate = salesCheck.BusinessDate.Value;
if (SalesDetail.IsThisRowAlreadyImportedInSalesDetail(checkOutletCode, checkBusDate) == false)
{
salesDetail.ImportSalesDetail(salesDetailList);
salesDetail.GenerateCarryForward(salesDetailList);
salesDetail.CalculateImportInventoryBalance(salesDetailList);
salesDetail.CreateImportDataLog(getDestinationFilePath(sourceFilePath), DateTime.Now, salesDetailList.Count, SALES_DETAIL);
}
else
{
string errorDate = checkBusDate.ToString("dd MMMM, yyyy");
MessageBox.Show("Selected XML File with BusinessDate: " + errorDate + " has been already imported.",
"ABC Cafe Import Sales Wizard");
MessageBox.Show("Please select a file which has not been imported!",
"ABC Cafe Import Sales Wizard");
return;
}
MoveXMLFiletoDestinationFolder(sourceFilePath);
i++;
}
MessageBox.Show("Import has been successfully completed!",
"ABC Cafe Import Sales Wizard");
clearListBoxItems();
lblMessage.Visible = false;
}
//Abort the import operation here!
else
{
MessageBox.Show("Please select a file which has not been imported!",
"ABC Cafe Import Sales Wizard");
clearListBoxItems();
lblMessage.Visible = false;
}
}
else
{
MessageBox.Show("Please select XML files to import!",
"ABC Cafe Import Sales Wizard");
}
}
Any help will be very much appreciated!

This is how I have used Progress Bar in my application.
private void btnNext_Click(object sender, EventArgs e)
{
BackgroundWorker worker = new BackgroundWorker();
worker.WorkerReportsProgress = true;
worker.ProgressChanged += (se, eventArgs) => {
this.progressBar.Maximum = 100;
this.progressBar.Minimum = 0;
this.progressBar.Value = eventArgs.ProgressPercentage;
lblStatus.Text = eventArgs.UserState as String;
lblPercentage.Text = String.Format("Progress: {0} %", eventArgs.ProgressPercentage);
};
worker.DoWork += (se, eventArgs) => {
int progress = 0;
((BackgroundWorker)se).ReportProgress(progress, "Initializing the files...");
//Process that takes a long time
//Formula to calculate Progress Percentage
//This is how I calculated for my program. Divide 100 by number of loops you have
int findPercentage = ((i + 1) * 100) / salesHeaderArr.Count;
progress = 0;
progress += findPercentage / 2;
//Report back to the UI
string progressStatus = "Importing Sales Header... (" + getSourceFileName(sourceFilePath) + ")";
((BackgroundWorker)se).ReportProgress(progress, progressStatus);
//After Iterating through all the loops, update the progress to "Complete"
((BackgroundWorker)se).ReportProgress(100, "Complete...");
};
worker.RunWorkerCompleted += (se, eventArgs) =>
{
//Display smth or update status when progress is completed
lblStatus.Location = new Point(20, 60);
lblStatus.Text = "Your import has been completed. \n\nPlease Click 'Finish' button to close the wizard or \n'Back' button to go back to the previous page.";
lblPercentage.Visible = false;
progressBar.Visible = false;
btnBack.Enabled = true;
btnFinish.Enabled = true;
};
worker.RunWorkerAsync();
}

Your program has many loops,since it will be difficult to get the increment value and increment the progress bar value in each loop iteration.You could set the progress bar maximum to 100,Then divide 100 by the number of loops you have say X.
So the trick would be to fill the progress bar by this value when each loops completes
and yes you should put this code in backgroundworker's DoWork() otherwise it will freeze the form.Also there is no need for a timer.

You can use a ManualResetEvent, Now while you are processing, let the Progressbar Increase until it meets a certain point and wait for a Set.
Example:
you have this 2 fields
private int progress = 0;
private ManualResetEvent reset = new ManualResetEvent(false);
// Sets it to unsignalled
private ManualResetEvent reset2 = new ManualResetEvent(false);
while(progress < 40)
{
progress ++;
}
reset.WaitOne();
while(progress < 90)
{
progress ++;
}
reset2.WaitOne();
while(progress < 100)
{
progress ++;
}
// This finishes the progress, Now on your actual Work, you have to signal those wait.
DoWork()
{
// long process here. . . .
reset.Set();
// Another long process
reset2.Set();
}

Related

How can I use performance counter without using a timer in c # form? I get an error like Category name is not found

While writing a video converter program in c # form, I measure the convert speed with the progress bar. I am actually measuring using certain techniques, but I want to give the user how much CPU he uses with the actual values, that is, the speed of the process.
if (comboBox1.Text == "mp3")
{
var convert = new NReco.VideoConverter.FFMpegConverter();
convert.ConvertMedia(VideoPath, MusicPath, "mp3");
progressBar1.Value = (int)(performanceCounter1.NextValue());
label7.Text = "Processor Time: " + progressBar1.Value.ToString() + "%";
/* progressBar1.Value = 80;
label7.Text = "% 80";*/
MessageBox.Show("converst is okey");
progressBar1.Value = (int)(performanceCounter1.NextValue());
label7.Text = "Processor Time: " + progressBar1.Value.ToString() + "%";
I did this with the code I found from inetnet, but I am failing.
How can I fix?
In this pictures,
First we initialize the the relevant CPU counters that you want to capture, and on ButtonClick we start to read the performance counter and increment the progress bar. The forloop and progressbar increments may not be relevant in your case but I added it to demonstrate the whole scenario
As per your clarification on comments section, this will update the textbox with the real time information from the performanceCounters
public PerformanceCounter privateBytes;
public PerformanceCounter gen2Collections;
public Form1()
{
InitializeComponent();
var currentProcess = Process.GetCurrentProcess().ProcessName;
privateBytes = new PerformanceCounter(categoryName: "Process", counterName: "Private Bytes", instanceName: currentProcess);
gen2Collections = new PerformanceCounter(categoryName: ".NET CLR Memory", counterName: "# Gen 2 Collections", instanceName: currentProcess);
}
async Task LongRunningProcess()
{
await Task.Delay(500);
}
private async void button1_Click(object sender, EventArgs e)
{
for (int i = 0; i <100; i++)
{
progressBar1.Value = i;
textBox1.Text = "privateBytes:" + privateBytes.NextValue().ToString() + " gen2Collections:" + gen2Collections.NextValue().ToString() ;
await Task.Run(() => LongRunningProcess());
}
}
Note: Also Check Hans Passant's answer on ISupportInitialize

How to add a dynamically changing float value to a label

I'm coding this app that pulls your battery % and shows it to the user periodically. I've made it work so far as a console app, but as I re-write it in Windows Forms it gives me some issues when using the labels to display the value.
Edit: I have included the changes Niranjan Kala pointed out as I was using more than one label for this, but it still doesn't work
Also, as he has requested, I have added more of the program layout so as to provide some context and maybe find the error
This is what it looks like:
public partial class Form1 : Form
{
public Form1()
{ InitializeComponent();
// Imagine all the visual stuff going here (icons and menus, etc.)
//I start the thread
batThing = new Thread(new ThreadStart(batThing));
batThing.Start();
}
private void Form1_Load(object sender, EventArgs e)
{
Type power = typeof(PowerStatus);
PropertyInfo[] pi = power.GetProperties();
#region Cargador
//0 = PowerLineStatus --> Charger on or not?
object EdeCargador = pi[0].GetValue(SystemInformation.PowerStatus, null);
//turns charger state into int
int edc = Convert.ToInt32(EdeCargador);
int On = Convert.ToInt32(PowerLineStatus.Online);
On = 1;
int Off = Convert.ToInt32(PowerLineStatus.Offline);
Off = 0;
int Unk = Convert.ToInt32(PowerLineStatus.Unknown);
Unk = 255;
if (edc == On)
{
string CargadorConectado = "-Cargador Conectado-";
label2.Text = CargadorConectado;
string EdeBateria2 = "-Cargando-";
label4.Text = EdeBateria2;
}
else
{
string CargadorDesconectado = "-Cargador Desconectado-";
label2.Text = CargadorDesconectado;
#endregion
#region Cantidad de Bateria
//3 = BatteryLifePercent --> tells the % of bat available
object CantdeBat = pi[3].GetValue(SystemInformation.PowerStatus, null);
//string to float , then * 100 to get a %
float num = (Single.Parse(CantdeBat.ToString())) * 100;
// shows a % and says if battery is full or low
string EdeBateria = num.ToString() + "%";
if (num == 100)
{
EdeBateria = "-Batería Completa-";
label4.Text = EdeBateria;
}
else if (num <= 25)
{
EdeBateria = "-Batería Baja. Conecte el cargador-";
label4.Text = EdeBateria;
}
else if (num > 25 & num < 100)
{
//nada
}
if (num <= 0)
{
EdeBateria = "No tenes bateria gil";
label4.Text = EdeBateria;
}
}
#endregion
#region Tiempo Restante
//4 = BatteryLifeRemaining --> Indicates the remaining battery time
object TdeBat = pi[4].GetValue(SystemInformation.PowerStatus, null);
double tiempobat = (Double.Parse(TdeBat.ToString()));
if (tiempobat == -1)
{
string NoHayBat = "El equipo no está operando a través de una batería";
label5.Text = NoHayBat;
}
else if (tiempobat != -1)
{
//gets time in seconds and turns it into hours , min and secs
TimeSpan t = TimeSpan.FromSeconds(tiempobat);
string TiempoRestante = string.Format("El tiempo de uso restante es de: {0:D1} horas, {1:D2} minutos y {2:D2} segundos",
t.Hours,
t.Minutes,
t.Seconds
);
label5.Text = TiempoRestante ;
}
#endregion
} // fin de form1
Edit: I have found that there are certain issues when jumping from one method from another, for instance, the problem I originally had, which was with 'label 4' I came to find that was due to the logic in the charger state, so I added this:
string EdeBateria2 = "-Cargando-";
label4.Text = EdeBateria2;
After adding this when I have the charger connected it displays so nicely, but when I disconnect the charger entering the 'else' where I coded the conditionals for label 4 the problems begin.
The strings in both label 2 and label 5 change dinamically as I disconnect the charger out of the laptop PC with no problems whatsoever, the issue is with label 4 still. As I disconnect the charger it stops displaying the message I define as '-Cargando-' and it just shows its name 'label4'.
I'm stuck, any ideas?
I'm using C# in VS 2015.
If you want to display status then it should be like this. Create status of the battery and then update to the label. check this sample snippet and then update it according to your requirement..
object BatQuantity= pi[3].GetValue(SystemInformation.PowerStatus, null);
//convert the value I get from PowerStatus class to %
float num = (Single.Parse(CantdeBat.ToString())) * 100;
//shows battery % and if battery is full says it's full if not it says it's low
string batteryStatus = num.ToString() + "%";
if (num == 100)
{
batteryStatus = "-Full Battery-";
}
else if (num <= 25)
{
batteryStatust = "-Low Battery-";
}
label4.Text = batteryStatust;
Hope this help.

write file out of memory c#

I get some problems with c# windows form.
My goal is to slice a big file(maybe>5GB) into files,and each file contains a million lines.
According to the code below,I have no idea why it will be out of memory.
Thanks.
StreamReader readfile = new StreamReader(...);
StreamWriter writefile = new StreamWriter(...);
string content;
while ((content = readfile.ReadLine()) != null)
{
writefile.Write(content + "\r\n");
i++;
if (i % 1000000 == 0)
{
index++;
writefile.Close();
writefile.Dispose();
writefile = new StreamWriter(...);
}
label5.Text = i.ToString();
label5.Update();
}
The error is probably in the
label5.Text = i.ToString();
label5.Update();
just to make a test I've written something like:
for (int i = 0; i < int.MaxValue; i++)
{
label1.Text = i.ToString();
label1.Update();
}
The app freezes around 16000-18000 (Windows 7 Pro SP1 x64, the app running both x86 and x64).
What probably happens is that by running your long operation in the main thread of the app, you stall the message queue of the window, and at a certain point it freezes. You can see that this is the problem by adding a
Application.DoEvents();
instead of the
label5.Update();
But even this is a false solution. The correct solution is moving the copying on another thread and updating the control every x milliseconds, using the Invoke method (because you are on a secondary thread),
For example:
public void Copy(string source, string dest)
{
const int updateMilliseconds = 100;
int index = 0;
int i = 0;
StreamWriter writefile = null;
try
{
using (StreamReader readfile = new StreamReader(source))
{
writefile = new StreamWriter(dest + index);
// Initial value "back in time". Forces initial update
int milliseconds = unchecked(Environment.TickCount - updateMilliseconds);
string content;
while ((content = readfile.ReadLine()) != null)
{
writefile.Write(content);
writefile.Write("\r\n"); // Splitted to remove a string concatenation
i++;
if (i % 1000000 == 0)
{
index++;
writefile.Dispose();
writefile = new StreamWriter(dest + index);
// Force update
milliseconds = unchecked(milliseconds - updateMilliseconds);
}
int milliseconds2 = Environment.TickCount;
int diff = unchecked(milliseconds2 - milliseconds);
if (diff >= updateMilliseconds)
{
milliseconds = milliseconds2;
Invoke((Action)(() => label5.Text = string.Format("File {0}, line {1}", index, i)));
}
}
}
}
finally
{
if (writefile != null)
{
writefile.Dispose();
}
}
// Last update
Invoke((Action)(() => label5.Text = string.Format("File {0}, line {1} Finished", index, i)));
}
and call it with:
var thread = new Thread(() => Copy(#"C:\Temp\lst.txt", #"C:\Temp\output"));
thread.Start();
Note how it will write the label5 every 100 milliseconds, plus once at the beginning (by setting the initial value of milliseconds "back in time"), each time the output file is changed (by setting the value of milliseconds "back in time") and after having disposed everything.
An even more correct example can be written by using the BackgroundWorker class, that exists explicitly for this scenario. It has an event, ProgressChanged, that can be subscribed to update the window.
Something like this:
private void button1_Click(object sender, EventArgs e)
{
BackgroundWorker backgroundWorker = new BackgroundWorker();
backgroundWorker.WorkerReportsProgress = true;
backgroundWorker.ProgressChanged += backgroundWorker_ProgressChanged;
backgroundWorker.RunWorkerCompleted += backgroundWorker_RunWorkerCompleted;
backgroundWorker.DoWork += backgroundWorker_DoWork;
backgroundWorker.RunWorkerAsync(new string[] { #"C:\Temp\lst.txt", #"C:\Temp\output" });
}
private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker worker = sender as BackgroundWorker;
string[] arguments = (string[])e.Argument;
string source = arguments[0];
string dest = arguments[1];
const int updateMilliseconds = 100;
int index = 0;
int i = 0;
StreamWriter writefile = null;
try
{
using (StreamReader readfile = new StreamReader(source))
{
writefile = new StreamWriter(dest + index);
// Initial value "back in time". Forces initial update
int milliseconds = unchecked(Environment.TickCount - updateMilliseconds);
string content;
while ((content = readfile.ReadLine()) != null)
{
writefile.Write(content);
writefile.Write("\r\n"); // Splitted to remove a string concatenation
i++;
if (i % 1000000 == 0)
{
index++;
writefile.Dispose();
writefile = new StreamWriter(dest + index);
// Force update
milliseconds = unchecked(milliseconds - updateMilliseconds);
}
int milliseconds2 = Environment.TickCount;
int diff = unchecked(milliseconds2 - milliseconds);
if (diff >= updateMilliseconds)
{
milliseconds = milliseconds2;
worker.ReportProgress(0, new int[] { index, i });
}
}
}
}
finally
{
if (writefile != null)
{
writefile.Dispose();
}
}
// For the RunWorkerCompleted
e.Result = new int[] { index, i };
}
void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
int[] state = (int[])e.UserState;
label5.Text = string.Format("File {0}, line {1}", state[0], state[1]);
}
void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
int[] state = (int[])e.Result;
label5.Text = string.Format("File {0}, line {1} Finished", state[0], state[1]);
}

How to make run real time and faster refresh method with timer

I have script for refresh network with object label and panel but in script using looping mode with 'for'. I want to this real time refresh for 1 sec or 5 sec but because using 'for' make this procces need more time and get stuck screen. how to make the solution more quickly and in real time?
Thanks
public PosPing()
{
InitializeComponent();
RefreshPOS.Tick += new EventHandler(CheckPOSUG);
RefreshPOS.Start();
}
private void CheckPOSUG(object sender, EventArgs e)
{
Panel[] panelUG = new Panel[]{pnlPOSUG1,pnlPOSUG2,pnlPOSUG3,pnlPOSUG4,pnlPOSUG5,pnlPOSUG6,pnlPOSUG7,pnlPOSUG8};
Label[] LabelUG = new Label[]{lblUG1,lblUG2,lblUG3,lblUG4,lblUG5,lblUG6,lblUG7,lblUG8};
Label[] lblSpdUG = new Label[] { lblSpdUG1, lblSpdUG2, lblSpdUG3, lblSpdUG4, lblSpdUG5, lblSpdUG6, lblSpdUG7, lblSpdUG8 };
for (int x = 0; x < 8;x++ )
{
string IP = "192.168.135.1" + (x + 1).ToString();
var ping = new Ping();
var reply = ping.Send(IP, 10 * 1000);
LabelUG[x].Text = "POSBMS10" + x.ToString();
if (reply.Status == IPStatus.Success)
{
lblSpdUG[x].Text = reply.RoundtripTime.ToString() + " " + "ms";
panelUG[x].BackColor = Color.FromName("Lime");
}
else
{
lblSpdUG[x].Text = "Nonaktif";
panelUG[x].BackColor = Color.FromName("ButtonHighlight");
}
}
}
Without a good, minimal, complete code example, it's hard to know for sure how to best answer your question. But it looks like you are trying to ping eight different servers, which are represented by eight set of controls in your form.
If that is correct, then I agree with commenter Hans Passant that you should be using the SendPingAsync() method instead. This will allow you to execute the pings asynchronously, without blocking the UI thread, so that your program can remain responsive.
Because you are dealing with eight different servers, it makes sense to me that you should execute the eight pings asynchronously. To accomplish this, I would refactor the code a bit, putting the server-specific loop body into a separate method, so that each instance can be run concurrently.
Implementing it that way would look something like this:
private async void CheckPOSUG(object sender, EventArgs e)
{
Panel[] panelUG = new Panel[]{pnlPOSUG1,pnlPOSUG2,pnlPOSUG3,pnlPOSUG4,pnlPOSUG5,pnlPOSUG6,pnlPOSUG7,pnlPOSUG8};
Label[] LabelUG = new Label[]{lblUG1,lblUG2,lblUG3,lblUG4,lblUG5,lblUG6,lblUG7,lblUG8};
Label[] lblSpdUG = new Label[] { lblSpdUG1, lblSpdUG2, lblSpdUG3, lblSpdUG4, lblSpdUG5, lblSpdUG6, lblSpdUG7, lblSpdUG8 };
Task[] tasks = new Task[8];
for (int x = 0; x < 8; x++)
{
tasks[x] = PingServer(x, panelUG[x], LabelUG[x], lblSpdUG[x]);
}
try
{
await Task.WhenAll(tasks);
}
catch (Exception e)
{
// handle as appropriate, e.g. log and exit program,
// report expected, non-fatal exceptions, etc.
}
}
async Task PingServer(int index, Panel panel, Label ugLabel, Label spdLabel)
{
// NOTE: String concatenation will automatically convert
// non-string operands by calling calling ToString()
string IP = "192.168.135.1" + (index + 1);
var ping = new Ping();
var reply = await ping.SendPingAsync(IP, 10 * 1000);
ugLabel.Text = "POSBMS10" + x;
if (reply.Status == IPStatus.Success)
{
spdLabel.Text = reply.RoundtripTime + " ms";
// The Color struct already has named properties for known colors,
// so no need to pass a string to look Lime up.
panel.BackColor = Color.Lime;
}
else
{
spdLabel.Text = "Nonaktif";
panel.BackColor = Color.FromName("ButtonHighlight");
}
}

System stops responding while read a large text file using stramreader & pass them in to a data grid

System stops responding while read a large text file using stramreader & pass them in to a data grid.
Appreciates your support to resolve this issue.
here is my code:
private void button2_Click(object sender, EventArgs e)
{
button2.Enabled = false;
button3.Enabled = false;
int lno = 0;
try
{
char[]seps = {',','\n','\r','','','┐',' '};
StreamReader read = new StreamReader(fd.FileName);
line = read.ReadToEnd();
String[]spLine = line.Split(seps);
for (int i = 0; i < spLine.Length; i++)
{
if ((prv.Equals("1:")) || (prv.Equals("1-")))
{
lno++;
display.AppendText(" 1 : " + spLine[i]);
display.AppendText(Environment.NewLine);
dataGridView1.Rows.Add(lno, "1", spLine[i]);
}
else if (prv.Equals("1"))
{
lno++;
display.AppendText(" 1 : " + spLine[i + 1]);
display.AppendText(Environment.NewLine);
dataGridView1.Rows.Add(lno, "1", spLine[i + 1]);
}
else if (prv.Equals("2"))
{
lno++;
display.AppendText(" 2 : " + spLine[i + 1]);
display.AppendText(Environment.NewLine);
dataGridView1.Rows.Add(lno, "2", spLine[i + 1]);
}
prv = spLine[i].Trim();
button2.Enabled = true;
}
}
catch (Exception ex) {}
button2.Enabled = true;
button3.Enabled = true;
}
If you process lots of data in the UI thread, you are blocking the windows event loop, which prevents it painting, and causes it to report as unresponsive. You should do such processing on a worker thread, and then switch back to the UI thread (either at the end, or in batches - not for every row) to perform the UI updates like adding rows to grids.
Yes, this makes it more complicated. Yes, it is necessary if you don't want it to show as unresponsive.

Categories