0

I've spent 4 hours on this and totally failed. I know that i need to use BackgroundWorker but all the tutorials refer to running a progress script on the actual form you are running the worker on.

I have a large datagrid, which the user can use a check box to "select all" and then press "UPDATE ALL" This updates every grid with a bunch of options they choose. For some users this may be 5 records which is nothing, but some might update 200 records with 5 options which takes about... 10-15 secs to iterate through them.

I have tried so many variations of running BGworker which loads a FrmLoading.Showdialog

Or trying to have BGworker "do work" running the code and then the main thread having the FrmLoading.Show() However nothing is working.

If i have the update code in the background worker, it fails because the datagrid and everything is in a different thread.

The other way round, and it just hangs on FrmLoading.Show()

Any advice would be great.

I just can't seem to get my head around how to get this working for what seems to be an easy idea!

Current Update Code:

foreach (DataGridViewRow rowx in dataGridpatients.Rows)
{
    //MessageBox.Show(Convert.ToBoolean(rowx.Cells["clnselected"].Value).ToString());
    if (Convert.ToBoolean(rowx.Cells["clnselected"].Value) == true)
    {
         //if cycle has a value.
         if (cmbcycle.SelectedIndex != -1)
         {
            rowx.Cells["clncycletype"].Value = cycle;
            rowx.Cells["clnpackscollect"].Value = packs;
         }

         //if location has a value
         if (cmblocation.SelectedIndex != -1)
         {
            location = Convert.ToInt32(cmblocation.SelectedValue);
            rowx.Cells["clnlocation1"].Value = location;   
         }

         if (cmbsize.SelectedIndex != -1)
         {
            size = Convert.ToInt32(cmbsize.SelectedValue);
            rowx.Cells["clnpacksize"].Value = size;   
         }

         if (chkDelivery.Checked == true)
         {
            rowx.Cells["clnDelivery"].Value = true;
         }

         if (chkSignSheet.Checked == true)
         {
             rowx.Cells["clnSigningSheet"].Value = true;
         }
    }
    countupdated++;
 }

 foreach (DataGridViewRow row in dataGridpatients.Rows)
 {
     row.Cells["clnselected"].Value = false;
     row.DefaultCellStyle.BackColor = Color.White;
 }

 cmbsize.SelectedIndex = -1;
 cmblocation.SelectedIndex = -1;
 cmbcycle.SelectedIndex = -1;
 chkDelivery.Checked = false;
 chkSignSheet.Checked = false;
 @countupdated++;

I also have @CountSelected.

What i want to do is run this code above but have a popup overlay (dialog) with my logo + "Updating X%" Where X = countupdated/countselected * 100

I now know i need to use the background worker and invoke for the above, but literally have no idea regarding how to invoke the grid and go from there. I understand i need to invoke the variables I'm using (eg. cmbcycle.SelectedIndex)

I know iterating through 150 records and updating individual cells is probably wrong,

My other option is creating a datatable from "selected" cells on that datatable then Running the update via SQL instead of iterating through a bound table. Then after the SQL i can re-create the table which will now have the new cell values updated in it? Would that be a more appropriate way to do it?

Max rows on this table would be 200. Average ~70 so we are never talking 500 or 1000

EDIT: So the checked answer works to run the background worker and refer to the controls on the form. The issue is that if i do this:

backgroundWorker1.RunWorkerAsync();
                splashy.ShowDialog();

Then the splash screen pops up after the background worker ends

If i do this:

splashy.ShowDialog();
backgroundWorker1.RunWorkerAsync();

Then the popup semi-forms and hangs until the end of the background worker, at which time it closes because of the RunWorkerCompleted event.

EDIT: I have no updated the code in DoWork and used Invokes to refer to the controls. This works and the code runs fine.

I now need a popup ot appear showing the progress through the updates.

splashy.InvokeBy(() =>
                {
                    splashy.Show();
                });
                backgroundWorker1.RunWorkerAsync();

Does not work. It causes the popup but freeze

splashy.ShowDialog();
backgroundWorker1.RunWorkerAsync();

Allows the Dialog to show (not 'frozen' and distorted) However the Lab (lblprogress) does not update.

This is because the form never get to the RunWorker method, it is stuck at ShowDialog.

Glenn Angel
  • 381
  • 1
  • 3
  • 14
  • While using `BackgroundWorker` or `Thread`, you would need to call `.Invoke()` or `.BeginInvoke()` to access the control and do operations which is supposed to be shown on the UI. Here is a [nice post](https://stackoverflow.com/questions/142003/cross-thread-operation-not-valid-control-accessed-from-a-thread-other-than-the?rq=1) to get u started with the same – boop_the_snoot Oct 13 '17 at 09:45
  • Thanks @o_O - So since in my case I would invoke the 4 comboboxes i have. Then invoke the datagrid? So i run the processing code, in the background worker, which then updates the invoked datagrid? – Glenn Angel Oct 13 '17 at 10:04
  • Any control(`DataGridView`, `Comboboxes`, `Textbox` etc...) which may fall under the `BackgroundWorker.RunWorkerAsync` method would need to be called using `.Invoke()`. In layman terms: your app's UI is running on a single UI Thread. You can create as many threads, bgw as u may like. But if you want to access the controls,(from the threads/bgw u created) you have to ask permission using [.Invoke()](https://msdn.microsoft.com/en-us/library/zyzhdc6b(v=vs.110).aspx)/[.BeginInvoke()](https://msdn.microsoft.com/en-us/library/a06c0dc2(v=vs.110).aspx) – boop_the_snoot Oct 13 '17 at 10:13
  • Thanks mate, ill just have to give it a pass, seems way too complex for me. I can see how to invoke a variable but can't see how to invoke the whole table so i can iterate through specific rows.. Or just send a datatable to the background worker?? – Glenn Angel Oct 13 '17 at 10:18
  • Tell u what, edit your question along with relevant codes and also mention the issues u face while accessing the controls using BGW. you can insert comments in the code to provide further insight. Hopefully I can help u better in that way. If not me, someone else may :) – boop_the_snoot Oct 13 '17 at 10:22
  • Thanks, ive updated with some code that I am using in the iteration, maybe you can slaughter my code and let me know how I'm maybe approaching this the wrong way./ – Glenn Angel Oct 13 '17 at 10:33
  • Is this code a part of `BackgroundWorker.RunWorkerAsync` method? – boop_the_snoot Oct 13 '17 at 10:37
  • Well I want it to be but it doesn't work because nothing is invoked yet – Glenn Angel Oct 13 '17 at 10:48

2 Answers2

1

It would be a good idea to make modifications on your DataSource itself and then bind it with the DataGridView.

But as from your existing code if you want to access your controls/UI to update or change values from BackgroundWorker.RunWorkerAsync method or any other Thread call for that matter, you can create an extension method to .Invoke() the controls like:

public static class MyExtensions
{
    public static void InvokeBy(this Control ctl, MethodInvoker method)
    {
        if (ctl.InvokeRequired)
            ctl.Invoke(method);
        else method();
    }
}

Keep this static class under the same Namespace as your main class for convenience.


Thus this code:

foreach (DataGridViewRow rowx in dataGridpatients.Rows)
{
  //your codes
}

Will become:

dataGridpatients.InvokeBy(() =>
{
   foreach (DataGridViewRow rowx in dataGridpatients.Rows)
   {
     //your codes
   }
});

Similarly,

if (cmbcycle.SelectedIndex != -1)
{
   //your codes
}

Will become:

cmbcycle.InvokeBy(() =>
{
   if (cmbcycle.SelectedIndex != -1)
   {
      //your codes
   }
});

This way you van safely access your controls, while keeping your UI responsive at the same time. Update your Popup Status UI the same way!

boop_the_snoot
  • 3,209
  • 4
  • 33
  • 44
  • Ill give that a go first. I do already set a datasource and bind that to the form so maybe it is jsut smarter to do it the other way, but im going to do both so i can learn. Thanks for your time./ – Glenn Angel Oct 13 '17 at 12:12
  • So I'm getting an error even on the cmbcycle.InvokeBy section: COmbobox does not contain a definition invokeby Do i not need to use invoke(new Methodinvoker(delegate)) etc? If i change to .invoke instead of invokeby i get "Cannot convert type lambda to delegate" – Glenn Angel Oct 13 '17 at 12:16
  • Make a `static` class as shown in the first code block. Then you can access `.InvokeBy`. I've updated my answer – boop_the_snoot Oct 13 '17 at 12:23
  • k thanks, i had the class in the wrong place (cos im an idiot). Done that. and at the end of each should be ); as well yes? Ill give it a crack – Glenn Angel Oct 13 '17 at 12:24
  • Yes you are right, I made the correction. Also don't stress it, learning and understanding new things take some time. Just follow what I've done. Patiently debug if you find some error, cuz I'm gonna be out for a while. Hopefully u may find a solution by the time I get back :) Else we'll work on it. – boop_the_snoot Oct 13 '17 at 12:28
  • lol ty. the frustrating part is i have finished my own C# program and its big and working.. but I've never deal with invoking or background workers so now now it feeling like I'm back to noob school hahah – Glenn Angel Oct 13 '17 at 12:29
  • Right well i ahve the update script working, and i moved some bits around so its actually much faster now. The issue is i still cant get the popup to work! Updated question – Glenn Angel Oct 13 '17 at 12:46
  • after all that.. if im invoking every single thing on the page... does that mean Im not using the background worker at all!? Would it not be easier to run everything on the UI thread and just run the popup in the background worker? – Glenn Angel Oct 13 '17 at 13:48
  • If your UI is freezing because of some background activity, then u have to do this. Unless you are okay with the freeze. It would be a good time to google regarding the same. Have some books or tutorials to understand the concept of Invoke, Bgw and all. I'm sorry if I couldn't help u much – boop_the_snoot Oct 13 '17 at 14:01
  • You are helping I just dont think its the full solution yet. BGW starts another thread, but invoke uses the UI thread. so if in my BGW all im doing is invoking the main thread then i"m not achieving anything am i? I canget the form to display now, but e.result is not updating the e.progress section. – Glenn Angel Oct 13 '17 at 14:07
  • Update your question again to show how you're updating the progress. – boop_the_snoot Oct 13 '17 at 14:11
  • IVE DONE IT!! WOOOOOO fixed it :) have a look at my answer when u want, but it finally works. I used your ideas but reversed it a bit :) thanks so much for the help – Glenn Angel Oct 15 '17 at 17:37
0

This answer is based around o_O's answer.

The main issue is that i wanted the UI to actually update and the background worker to supply the splash.

Instead of running all the 'hard code' in the BGW, i left it in the original thread, but called a BGW to display a popup Dialog form.

so at the start of the "hard code" I used:

backgroundWorker1.RunWorkerAsync();

This called:

FrmSplash splashy;
private void backgroundWorker1_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
{
    splashy = new FrmSplash();
    splashy.ShowDialog();
}

In order to remove the dialog box, at the end of the code in the GUI thread, i used:

splashy.InvokeBy(() =>
 {
    splashy.Close();
 }
);

backgroundWorker1.CancelAsync();

Which uses the extension supplied by O_o

public static class MyExtensions
{
   public static void InvokeBy(this Control ctl, MethodInvoker method)
   {
       if (ctl.InvokeRequired)
           ctl.Invoke(method);
       else method();
   }
}

I have also built a label update into splashy So i could call

splashy.InvokeBy(() =>
  {
     splashy.SetStatus(countupdated.ToString());
  }
);

As i iterated through the datagridview rows. This updated the label on the splash screen :)

boop_the_snoot
  • 3,209
  • 4
  • 33
  • 44
Glenn Angel
  • 381
  • 1
  • 3
  • 14
  • Glad u got it working by yourself :) . I almost forgot about this as I got sick, sorry for that – boop_the_snoot Oct 15 '17 at 17:47
  • After pulling my hair out i took 2 days off and came back to it haha – Glenn Angel Oct 15 '17 at 17:54
  • @o_O is there a way to send the invokeby as a control to another method. I have some methods and i wanted to call splashy from them.. by using InvokeBy doesnt work from that method (no instance) even if i have (FrmSplashy splashy) in the top of the method seciton. – Glenn Angel Oct 16 '17 at 12:42
  • You don't have to send it. Keep the `static` class `public`, ie `public static void InvokeBy(this Control ctl, MethodInvoker method)` and then you can access `.InvokeBy` from anywhere. It should work as it's supposed to – boop_the_snoot Oct 16 '17 at 15:15
  • Thanks, i get that it works in the form im working on, but i have a public method Utility.addpacks public static void initpacks(object sender, EventArgs e,string formname ). I want to send splashy, so i can call it, but splashy isnt part of that UI thread, its part of the BGW thread :P Any ideas? – Glenn Angel Oct 16 '17 at 15:48
  • i did try just sending plashy, but then i get the 'no instance' error because splashy wasnt instantised in the thread, in was instantised in the BGW – Glenn Angel Oct 16 '17 at 15:49
  • You can create a new instance. But if u are looking to use the same instance then look at this [answer](https://stackoverflow.com/a/45733004/6611487). If you are not able to use this answer in your situation, kindly post a new question with codes and all the relevant details regarding your query, so that I (or someone else) can answer a fresh one :) – boop_the_snoot Oct 16 '17 at 17:30