0
votes

I'm having an issue with a form that uses a background worker thread to fetch data and update a datagrid.

The use case is a list with progress bars that shows how much tables have loaded in a database.

It goes in the following order,

  • FormA instantiates FormB
  • FormB kicks of a background worker that loops to fetch data and update a datagrid.datasource.

It works nicely sometimes but others the FormB.ShowDialog() comes back with a null reference exception. I can't understand why.

Here's the calling form code:

 private void button1_Click(object sender, EventArgs e)
    {


        using (var f = new FormFactProgress(_dwConnectionString, _sourceConnectionString, _etlConnectionString))
        {
            f.ShowDialog();  \\ THIS IS WHERE THE NULL REFERENCE EXCEPTION ALWAYS HAPPENS!
            labelLoadWarning.Visible = false;
            groupBoxLoadFacts.Enabled = false;
            buttonLoadFacts.BackColor = Color.FromArgb(128, 255, 128);
            buttonLoadFacts.Text = "Loaded";
            if (dynamicWarehouseCatalog.FactsLoaded(_dwConnectionString) && dynamicWarehouseCatalog.DimsLoaded(_dwConnectionString))
            {
                groupBoxSync.Enabled = true;
            }
        }

    }

The child form (It's the second backgroundworker that does the datagrid update):

private void buttonStart_Click(object sender, EventArgs e)
    {
        backgroundWorker1.DoWork += new DoWorkEventHandler(backgroundWorker1_DoWork);
        if (!backgroundWorker1.IsBusy)
        {
            backgroundWorker1.RunWorkerAsync();
        }
        backgroundWorker2.DoWork += new DoWorkEventHandler(backgroundWorker2_DoWork);
        if (!backgroundWorker2.IsBusy)
        {
            backgroundWorker2.RunWorkerAsync();
        }
        buttonStart.Enabled = false;


    }

    private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        try
        {
            BackgroundWorker worker = (BackgroundWorker)sender;

            sSISDBCatalog.StartFactLoad(_etlConnectionString);

            while (isLoading)
            {
                System.Threading.Thread.Sleep(1000);
                if (dynamicWarehouseCatalog.FactsLoaded(_dwConnectionString))
                {
                    if (dynamicWarehouseCatalog.AllFactsLoaded(_dwConnectionString))
                    {
                        isLoading = false;
                    }
                }
            }
        }
        catch(Exception ex)
        {
            MessageBox.Show(
           "Progress viewing has failed. The ETL is still running though. Please monitor your data warehouse growth manually. Error: " + ex, "Pregress Visualisation Failed",
           MessageBoxButtons.OK,
           MessageBoxIcon.Error);
            return;
        }


    }

    private void backgroundWorker2_DoWork(object sender, DoWorkEventArgs e)
    {
        try
        {
            Control.CheckForIllegalCrossThreadCalls = false;
            BackgroundWorker worker = (BackgroundWorker)sender;
            var dw = DwData(_dwConnectionString);
            var source = SourceData(_sourceConnectionString);
            dataGridView1.DataSource = GetProgress(dw, source);
            while (isLoading)
            {
                System.Threading.Thread.Sleep(1000);
                dw = DwData(_dwConnectionString);
                dataGridView1.DataSource = GetProgress(dw, source);
                dataGridView1.Refresh();
            }

            MessageBox.Show(
               "Your data warehouse facts are loaded!", "Facts Loaded",
               MessageBoxButtons.OK,
               MessageBoxIcon.Information);
            return;
        }
        catch (Exception ex)
        {
            MessageBox.Show(
           "Progress viewing has failed. The ETL is still running though. Please monitor your data warehouse growth manually. Error: " + ex, "Pregress Visualisation Failed",
           MessageBoxButtons.OK,
           MessageBoxIcon.Error);
            return;
        }

    }
2
Why are you using background worker and not a modern approach like async/await?rory.ap
@rory.ap I just happened to choose this as the solution. I'm happy to change it if there is a better approach. Like I said, when this weird error doesn't occur it works pretty well.Evan Barke
Don't update your UI directly from the background worker thread: use Invoke or backgroundWorker.ReportProgress() etc. instead. CheckForIllegalCrossThreadCalls = false is a very bad idea! For the exception: check the stack trace exactly, f cannot be null, or see: stackoverflow.com/questions/4660142/…René Vogt
You need to learn about multi-threaded programming and the approaches available before you just jump blindly into it. It's an advanced topic to put it mildly, and if you don't understand what you're doing, you'll have endless issues and unhappy customers.rory.ap
@EvanBarke you can't modify the UI from another thread, in any OS. This means you can't modify the grid from DoWork. You have to handle the RunWorkerCompleted event and modify the UI there. It's a lot easier to use async/await by this point. Combining two async operations with BGW is a lot harder tooPanagiotis Kanavos

2 Answers

0
votes

You should use the BackgroundWorker.ReportProgress Method (Int32, Object) as shown in the documentation here. I always use the second parameter to pass an enum to identify from which background worker was this progress reported. You can use a similar approach.

0
votes

So I found a solution that works nicely. Basically I changed the background worker to a task that gets the data and then invokes the dataGridView that needed updating. E.g.

Task.Factory.StartNew(() =>
                    UpdateUI()); 

Renamed the background worker to UpdateUI and inside:

var dw = DwData(_dwConnectionString);
            var source = SourceData(_sourceConnectionString);
            DataTable prog;
            while (isLoading)
            {
                System.Threading.Thread.Sleep(1000);
                dw = DwData(_dwConnectionString);
                if (dw.Rows.Count > 0)
                {                     
                    prog = GetProgress(dw, source);
                    if (prog.Rows.Count > 0)
                    {
                        dataGridView1.Invoke(new MethodInvoker(() => { dataGridView1.DataSource = prog; dataGridView1.Refresh(); }));
                    }
                }

            }