1

Firstly, let me say that I've been on this issue for pretty much 2 days now: tried various alternatives and read tons of questions from your site with similar issues.

First let me paste my code so you get the whole picture.

EDIT: Removed some code that might be unnecessary to the problem, based on request by forum member.

Form 1.cs

namespace TestApp
{

public partial class Form1 : Form    
{
    //global declarations
    private static Form1 myForm;
    public static Form1 MyForm
    {
        get
        {
            return myForm;
        }
    }       
    List<DataSet> DataSets = new List<DataSet>();
    int typeOfDataset = 0;
    //delegate for background thread to communicate with the UI thread _
    //and update the metadata autodetection progress bar
    public delegate void UpdateProgressBar(int updateProgress);
    public UpdateProgressBar myDelegate;


    //***************************
    //****  Form Events  ********
    //***************************

    public Form1()
    {
       InitializeComponent();
       if (myForm == null)
       {
           myForm = this;
       }
       DataSets.Add(new DSVDataSet());
       DataSets[typeOfDataset].BWorker = new BackgroundWorker();
       DataSets[typeOfDataset].BWorker.WorkerSupportsCancellation = true;
       myDelegate = new UpdateProgressBar(UpdateProgressBarMethod);
     }

    private void Form1_FormClosing(object sender, FormClosingEventArgs e)
    {
        //e.Cancel = true;
        if (DataSets[typeOfDataset].BWorker != null || DataSets[typeOfDataset].BWorker.IsBusy)
        {
            Thread.Sleep(1000);
            DataSets[typeOfDataset].BWorker.CancelAsync();
        }
        Application.Exit();
    }

    //***************************
    //***  Menu Items Events  ***
    //***************************

    private void cSVToolStripMenuItem_Click(object sender, EventArgs e)
    {
        LoadFillDSVData(',');
    }

    //***************************
    //*** DataGridViews Events **
    //***************************

    private void dataGridView1_RowPostPaint(object sender, DataGridViewRowPostPaintEventArgs e)
    {
        using (SolidBrush b = new SolidBrush(this.dataGridView1.RowHeadersDefaultCellStyle.ForeColor))
        {
            e.Graphics.DrawString(e.RowIndex.ToString(System.Globalization.CultureInfo.CurrentUICulture), this.dataGridView1.DefaultCellStyle.Font, b, e.RowBounds.Location.X + 20, e.RowBounds.Location.Y + 4);
        }
        int rowHeaderWidth = TextRenderer.MeasureText(e.RowIndex.ToString(), dataGridView1.Font).Width;
        if (rowHeaderWidth + 22 > dataGridView1.RowHeadersWidth)
        {
            dataGridView1.RowHeadersWidth = rowHeaderWidth + 22;
        }
    }

    private void dataGridView2_RowPostPaint(object sender, DataGridViewRowPostPaintEventArgs e)
    {
        using (SolidBrush b = new SolidBrush(this.dataGridView2.RowHeadersDefaultCellStyle.ForeColor))
        {
            e.Graphics.DrawString(e.RowIndex.ToString(System.Globalization.CultureInfo.CurrentUICulture), this.dataGridView2.DefaultCellStyle.Font, b, e.RowBounds.Location.X + 20, e.RowBounds.Location.Y + 4);
        }
        int rowHeaderWidth = TextRenderer.MeasureText(e.RowIndex.ToString(), dataGridView2.Font).Width;
        if (rowHeaderWidth + 22 > dataGridView2.RowHeadersWidth)
        {
            dataGridView2.RowHeadersWidth = rowHeaderWidth + 22;
        }
    }

    //***************************
    //****** Other Methods ******
    //***************************

    private void LoadFillDSVData(char delimiter)
    {
        //load file through openFileDialog
        //some more openFileDialog code removed for simplicity
        if (DataSets[typeOfDataset].BWorker != null || DataSets[typeOfDataset].BWorker.IsBusy)
        {
            Thread.Sleep(1000);
            DataSets[typeOfDataset].BWorker.CancelAsync();
            DataSets[typeOfDataset].BWorker.Dispose();
        }
        //if file was loaded, instantiate the class
        //and populate it 
        dataGridView1.Rows.Clear();
        dataGridView2.Rows.Clear();
        typeOfDataset = 0;
        DataSets[typeOfDataset] = new DSVDataSet();
        DataSets[typeOfDataset].DataGrid = this.dataGridView1;
        DataSets[typeOfDataset].BWorker = new BackgroundWorker();
        DataSets[typeOfDataset].InputFile = openFileDialog1.FileName;
        DataSets[typeOfDataset].FileName = Path.GetFileName(DataSets[typeOfDataset].InputFile);
        DataSets[typeOfDataset].InputPath = Path.GetDirectoryName(DataSets[typeOfDataset].InputFile);
        DataSets[typeOfDataset].Delimiter = delimiter;
        //read file to get number of objects and attributes
        DataSets[typeOfDataset].LoadFile(DataSets[typeOfDataset].InputFile, DataSets[typeOfDataset].Delimiter);
        //ask to autodetect metadata
        DialogResult dr = MessageBox.Show("Auto detect attributes?", "TestApp", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
        switch (dr)
        {
            case DialogResult.Yes:
                toolStripStatusLabel1.Text = "Autodetecting attributes...";
                toolStripProgressBar1.Value = 0;
                toolStripProgressBar1.Maximum = 100;
                toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
                DataSets[typeOfDataset].AutoDetectMetadata(DataSets[typeOfDataset].Attributes);
                break;
            case DialogResult.No: 
                break;
            default: 
                break;
        }
    }

    public void UpdateProgressBarMethod(int progress)
    {
        if (progress > 99)
        {
            toolStripProgressBar1.Value = progress;
            toolStripStatusLabel1.Text = "Done.";
        }
        else
        {
            toolStripProgressBar1.Value = progress;
        }
    } 

}

}

And one more class:

DSVDataSet.cs

namespace TestApp
{

public class DSVDataSet : DataSet
{
    static Form1 myForm = Form1.MyForm;
    //constructor(s)
    public DSVDataSet()
    {           
        InputType = DataSetType.DSV;
    }


    //autodetects metadata from the file if
    //the user wishes to do so
    public override void AutoDetectMetadata(List<Attribute> attributeList)
    {
        BWorker.WorkerReportsProgress = true;
        BWorker.WorkerSupportsCancellation = true;
        BWorker.DoWork += worker_DoWork;
        BWorker.ProgressChanged += worker_ProgressChanged;
        BWorker.RunWorkerCompleted += worker_RunWorkerCompleted;

        //send this to another thread as it is computationally intensive
        BWorker.RunWorkerAsync(attributeList);
    }

    private void worker_DoWork(object sender, DoWorkEventArgs e)
    {
        List<Attribute> attributeList = (List<Attribute>)e.Argument;
        using (StreamReader sr = new StreamReader(InputFile))
        {
            for (int i = 0; i < NumberOfAttributes; i++)
            {
                Attribute a = new Attribute();
                attributeList.Add(a);
            }
            for (int i = 0; i < NumberOfObjects; i++)
            {
                string[] DSVLine = sr.ReadLine().Split(Delimiter);
                int hoistedCount = DSVLine.Count();
                string str = string.Empty;
                for (int j = 0; j < hoistedCount; j++)
                {
                    bool newValue = true;
                    str = DSVLine[j];
                    for (int k = 0; k < attributeList[j].Categories.Count; k++)
                    {
                        if (str == attributeList[j].Categories[k])
                        {
                            newValue = false;
                        }
                    }
                    if (newValue == true)
                    {
                        attributeList[j].Categories.Add(str);
                        //removed some code for simplicity
                    }
                }
                int currentProgress = (int)((i * 100) / NumberOfObjects);
                if (BWorker.CancellationPending)
                {
                    Thread.Sleep(1000);
                    e.Cancel = true;
                    return;
                }
                BWorker.ReportProgress(currentProgress); //report progress
            }
        }          
        e.Result = 100; //final result (100%)     
    }

    private void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        int update = e.ProgressPercentage;
        myForm.BeginInvoke(myForm.myDelegate, update);
    }

    private void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {               

        if (e.Error != null)
        {
            return;
        }
        if (e.Cancelled)
        {
            return;
        }
        else
        {
            int update = (int)e.Result;
            myForm.Invoke(myForm.myDelegate, update);
            for (int i = 0; i < Attributes.Count; i++)
            {
                DataGridViewRow item = new DataGridViewRow();
                item.CreateCells(DataGrid);
                item.Cells[0].Value = Attributes[i].Name;
                switch (Attributes[i].Type)
                {
                    case AttributeType.Categorical:
                        item.Cells[1].Value = "Categorical";
                        break;
                    //removed some cases for simplicity
                    default:
                        item.Cells[1].Value = "Categorical";
                        break;
                }
                item.Cells[2].Value = Attributes[i].Convert;
                DataGrid.Rows.Add(item);
            }
            BWorker.Dispose();
        }
    }
}

}

Long story short, I have a backGroundWorker in DSVDataset.cs that does some 'heavy computation' so the UI doesn't freeze (new to this), and I use a delegate to have the background thread to communicate with the UI thread to update some progress bar values. If the user decides to make a new DSVDataSet (through cSVToolStripMenuItem_Click or tSVToolStripMenuItem_Click) then I've added some ifs wherever I've found appropriate that check if there's already a backGroundWorker running: if there is, call CancelAsync so it stops whatever it's doing and then .Dispose it. Now, sometimes when I try to exit the Form1 while a backGroundWorker might be running (check for example Form1_FormClosing) I get the error that is in the title of this post. The error takes me to this chunk of code in DSVDataSet.cs:

    private void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        int update = e.ProgressPercentage;
        myForm.BeginInvoke(myForm.myDelegate, update);
    }

Pressing F10 takes me to Program1.cs:

   Application.Run(new Form1());

Based on another stackoverflow post I read (http://stackoverflow.com/questions/513131/c-sharp-compile-error-invoke-or-begininvoke-cannot-be-called-on-a-control-unti , look at the answer), I implemented that logic by exposing the instance of the Main class itself so it always points to this instance of main. So in Form1.cs:

private static Form1 myForm;
    public static Form1 MyForm
    {
        get
        {
            return myForm;
        }
    }       
 public Form1()
    {
       InitializeComponent();
       if (myForm == null)
       {
           myForm = this;
       }
     }

And in DSVDataset.cs:

  static Form1 myForm = Form1.MyForm;

and I use myForm wherever needed.

Still, I'm not able to get past that error, so I can only assume I haven't implemented that solution correctly, or I'm doing something wrong with handling the backGroundWorkers. Any help would be greatly appreciated and I did my best to be as detailed as a could (hope this wasn't an overkill :)

Regards

globetrotter
  • 997
  • 1
  • 16
  • 42
  • If you want to communicate between Form1 and DSVDataSet, you'd be better off using events than trying to pass a reference to the form into DSVDataSet. – Ade Stringer Feb 09 '12 at 07:48
  • **If you've been working on this for 2 days, surely you've had time to create a [short, self-contained example that reproduces the problem](http://sscce.org/).** You had to do that anyway in order to test things out. Post that instead, not all of the source code in your entire project. – Cody Gray - on strike Feb 09 '12 at 07:54
  • 1
    My apologies, I'm new here. Done. I've left things such as dataGridView_RowPostPaint because after following up the error with F10, those events fired after Application.Run(new Form1()); – globetrotter Feb 09 '12 at 08:07

3 Answers3

3

The code is too much to understand all of them. But it seems the background worker is still working when you're closing the form, thus, a progress change of background worker will make your program crash.

For the quick solution, check form state when progress change,as I don't have much experiences on winform, but it must exists a method to check if the form is closing or disposing.

This is not suggested as it will couple UI and logic.

Simon. Li
  • 404
  • 2
  • 11
3

As dBear says, it's likely that your background worker is still firing progress changed events after your form has been disposed. It'd be a good idea to either block the form from closing until the background worker is finished, or to kill the background worker before you close the form. Regardless of which of these options you choose, it's better to do this kind of communication using events. Add the following event definition to DSVDataSet, and modify the progress changed event handler:

public event ProgressChangedEventHandler ProgressChanged;

private void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    if (ProgressChanged != null) {
        ProgressChanged(this, e);
    }
}

Once you've done that, you'll need to make a change in Form1, so that once you've created a new instance of DSVDataSet, you add an event handler:

dsv.ProgressChanged += new ProgressChangedEventHandler(dsv_ProgressChanged);

Put whatever code you need to display the progress into the body of dsv_ProgressChanged, something like:

void dsv_ProgressChanged(object sender, ProgressChangedEventArgs e) {
     myForm.Invoke(myForm.myDelegate, update);
}
Ade Stringer
  • 2,611
  • 17
  • 28
  • This one seems pretty. I have another suggestion that I find your dataset object holds a reference to winform! This is very bad design - your data layer shouldn't depend on any UI component. As ade said, add an event to dataset and let winform attach it. When closing your winform, detach the event handler and tell dataset all jobs should be cancelled. – Simon. Li Feb 09 '12 at 08:25
  • in private void worker_ProgressChanged (the one you pasted), should I remove the first two lines? – globetrotter Feb 09 '12 at 09:00
  • from breakpointing I see that ProgressChanged is always null, so it never fires the event. BTW, I dont understand why your example has two ProgressChanged events, one for DSVDataSet and one for backGroundWorker(worker_ProgressChanged). – globetrotter Feb 09 '12 at 09:28
  • Sorry, those two lines have now been removed. The progress changed event in your DSVDataSet class will be null until you call `dsv.ProgressChanged += blah` from wherever you create the instance of DSVDataSet. The second event is basically just to pass the event on from the background worker to whatever external code needs to hook into the event. – Ade Stringer Feb 09 '12 at 09:49
  • Hi Ade, thanks for providing me with the proper way to handle those calls and events, however I'm still having the exact same error on public void dsv_ProgressChanged(object sender, ProgressChangedEventArgs e) { myForm.Invoke(myForm.myDelegate, e.ProgressPercentage); } – globetrotter Feb 09 '12 at 10:05
  • I'm guessing that I'm not killing the backGroundWorkers correctly during the form closing, but I can't think of a better way other than the one I've implemented. Closing the form when progress bar is at 100% works, but I need to be able to close it at any given time. – globetrotter Feb 09 '12 at 10:33
  • You should be able to avoid the error by overriding OnClosing in Form1 and removing the event handler `foo.ProgressChanged -= new ProgressChangedEventHandler(foo_ProgressChanged);`. – Ade Stringer Feb 09 '12 at 11:34
  • This works like a charm, no problems so far. Thanks :) I couldn't help but notice dBear's comment "your dataset object holds a reference to winform! This is very bad design - your data layer shouldn't depend on any UI component", what would be the proper way? Coding is one thing, coding correctly is another and I'd like to know that as a point of reference so I can update my code. – globetrotter Feb 09 '12 at 21:47
  • By using the event you've already removed the need for a reference to the form. – Ade Stringer Feb 10 '12 at 08:48
2

First off, it appears as if you are trying to create a singleton instance of Form1. The best way to do that is create a private constructor with a static method, property, or variable that creates the new instance of the form:

public class Form1 : Form
{
    private static Form1 instance;
    public static Form1 Instance
    {
        get 
        { 
           if(instance == null) { instance = new Form1(); }
           return instance;
        }
    }

    private Form1()
    {
        ......
    }
}

Then change:

    Application.Run(new Form1());

To:

    Application.Run(Form1.Instance);

You can then use Form1.Instance in any other class to access the main window. Now, the window handle error is typically associated with the form (Form1) not being created on the main thread.

Following the pattern I've provided may solve your issue.

developer
  • 113
  • 6
  • Hi, thanks for this, I already implemented Ade's solution which works fine. However, I need to be able to close the Form (either through a button, or the FormClosing event) even if any backGroundWorker is still running, and I fail to do that in a proper manner, so whenever I try to close the form when the progressBar is still updating (worker still running), I get - once again - the error I provided in the title. Do you have a better solution of making sure the backGroundWorker is cancelled/killed before closing the form, than the code I implemented? I can't seem to get past through that. – globetrotter Feb 09 '12 at 11:00
  • You should wait to close the window until the background worker has stopped. You are actually calling CancelAsync which does not necessarily kill the thread right away. You then proceed to call Application.Exit which probably kills the UI thread before your background thread has a chance to stop. Again, before you call Application.Exit you need to ensure the thread has stopped. I would recommend the following: Create a new thread that calls CancelAsync and waits until the background thread stops, then call Application.Exit. – developer Feb 19 '12 at 08:24