Delegate Invoke in Thread throws InvalidOperationException

joe_pool_is

Contributor
Joined
Jan 18, 2004
Messages
507
Location
Longview, TX [USA]
I have a test application that sends data from a new thread back to the main thread. I would prefer to use the BackgroundWorker class (which is perfect for doing this), but I can not, as explained in my earlier thread No BackgroundWorker Allowed.

I designed the custom class ThreadParameter (below) with a delegate and a corresponding an event. The event, in turn, is intended to Invoke the method in the main thread; however, whenever the method in the main thread is called and I attempt to access one of the controls on my form, I get an InvaidOperationException telling me that I can not access the control from a thread other than where it is owned. I thought I was!

So, I wrote an even more basic application that was Console based because I wanted to cut out all of the fat before posting it here. Ugh! The Console Application runs without a hitch, and it is mostly from cut and paste from my Windows Application!

Below are the "meat and potatoes" of my two basic applications. Could someone with vast knowledge (i.e. Marble Eater or Plausibly) kindly help me see the error to my ways?

Both versions (Windows and Console) take an instance of this custom class that I created as the thread's Start parameter:
C#:
public class ThreadParameter {

  string _serialNumber;
  public delegate void ReportProgressDelegate(int step, DataTable data);
  public event ReportProgressDelegate ProgressChanged;

  public ThreadParameter(string serialNumber) {
    _serialNumber = serialNumber;
    ProgressChanged = null;
  }

  public void ReportProgress(int step, DataTable data) {
    if (ProgressChanged != null) {
      ProgressChanged.Invoke(step, data);
    }
  }

  public string SerialNumber { get { return _serialNumber; } }

}

Here is the Windows version, which throws the InvalidOperationException:
C#:
public partial class Form1 : Form {

  public Form1() {
    InitializeComponent();
  }

  void button1_Click(object sender, EventArgs e) {
    Go();
  }

  void Go() {
    listView1.Clear();
    listView1.Columns.Add("TestName", 140);
    listView1.Columns.Add("Result", (listView1.Width - listView1.Columns[0].Width - 2));
    string serialNumber = "123456789A";
    ThreadParameter tp = new ThreadParameter(serialNumber);
    tp.ProgressChanged += new ThreadParameter.ReportProgressDelegate(Thread_Report);
    Thread th = new Thread(new ParameterizedThreadStart(Thread_Routine));
    th.IsBackground = true;
    th.Start(tp);
    while (!th.IsAlive) Thread.Sleep(0);
    button1.Enabled = false;
    th.Join();
    button1.Enabled = true;
  }

  void Thread_Report(int step, object data) {
    if ((data != null) && (data is DataTable)) {
      DataTable table = (DataTable)data;
      const string TEST_RESULT = "Test_Result";
      switch (step) {
        case 0:
          listView1.Items.Add(new ListViewItem(new string[] { "Row 1", string.Empty }));
          listView1.Items.Add(new ListViewItem(new string[] { "Row 2", string.Empty }));
          listView1.Items.Add(new ListViewItem(new string[] { "Row 3", string.Empty }));
          listView1.Items.Add(new ListViewItem(new string[] { "Row 4", string.Empty }));
          listView1.Items.Add(new ListViewItem(new string[] { "Row 5", string.Empty }));
          listView1.Items.Add(new ListViewItem(new string[] { "Row 6", string.Empty }));
          break;
        case 1:
          if ((2 < table.Rows.Count) && (table.Columns.Contains(TEST_RESULT))) {
            listView1.Items[0].SubItems[1].Text = table.Rows[1][TEST_RESULT].ToString();
            listView1.Items[1].SubItems[1].Text = table.Rows[2][TEST_RESULT].ToString();
          }
          break;
        case 2:
          if ((4 < table.Rows.Count) && (table.Columns.Contains(TEST_RESULT))) {
            listView1.Items[2].SubItems[1].Text = table.Rows[3][TEST_RESULT].ToString();
            listView1.Items[3].SubItems[1].Text = table.Rows[4][TEST_RESULT].ToString();
          }
          break;
        case 3:
          if ((6 < table.Rows.Count) && (table.Columns.Contains(TEST_RESULT))) {
            listView1.Items[4].SubItems[1].Text = table.Rows[5][TEST_RESULT].ToString();
            listView1.Items[5].SubItems[1].Text = table.Rows[6][TEST_RESULT].ToString();
          }
          break;
        default:
          break;
      }
    }
  }

  void Thread_Routine(object threadParam) {
    ThreadParameter tp = (ThreadParameter)threadParam;
    const string sqlConn = "SUPPLY_YOUR_CONNECTION_STRING";
    using (SqlConnection conn = new SqlConnection(sqlConn)) {
      int step = 0;
      string sqlText = "sp_GetPartRecord";
      DataSet ds = new DataSet();
      using (DataTable table = new DataTable()) {
        tp.ReportProgress(step++, table);
      }
      using (SqlDataAdapter da = new SqlDataAdapter(sqlText, sqlConn)) {
        DataTable table = new DataTable();
        da.SelectCommand.CommandType = CommandType.StoredProcedure;
        da.SelectCommand.Parameters.AddWithValue("@SN", tp.SerialNumber);
        da.Fill(table);
        ds.Tables.Add(table);
        tp.ReportProgress(step++, ds.Tables[ds.Tables.Count - 1]);
      }
      using (SqlDataAdapter da = new SqlDataAdapter(sqlText, sqlConn)) {
        DataTable table = new DataTable();
        da.SelectCommand.CommandType = CommandType.StoredProcedure;
        da.SelectCommand.Parameters.AddWithValue("@SN", tp.SerialNumber);
        da.Fill(table);
        ds.Tables.Add(table);
        tp.ReportProgress(step++, ds.Tables[ds.Tables.Count - 1]);
      }
      using (SqlDataAdapter da = new SqlDataAdapter(sqlText, sqlConn)) {
        DataTable table = new DataTable();
        da.SelectCommand.CommandType = CommandType.StoredProcedure;
        da.SelectCommand.Parameters.AddWithValue("@SN", tp.SerialNumber);
        da.Fill(table);
        ds.Tables.Add(table);
        tp.ReportProgress(step++, ds.Tables[ds.Tables.Count - 1]);
      }
    }
  }

}

Getting this to work would be a considerable milestone for me.

If (after getting the code itself to work) someone sees bad logic or bad techniques, I would very much like to hear your input. Years ago, my major was physics, so I don't always take the best approach to software development.

Regards,
Joe
 
Here is the Console version, which does not throw an exception (though I can't see the output)
Code:
class Program {

  Button button1;
  ListView listView1;

  static void Main(string[] args) {
    Program pgm = new Program();
    pgm.Go();
  }

  public void Go() {
    button1 = new Button();
    button1.Enabled = true;
    listView1 = new ListView();
    listView1.Width = 500;
    listView1.Clear();
    listView1.Columns.Add("TestName", 140);
    listView1.Columns.Add("Result", (listView1.Width - listView1.Columns[0].Width - 2));
    string serialNumber = "123456789A";
    ThreadParameter tp = new ThreadParameter(serialNumber);
    tp.ProgressChanged += new ThreadParameter.ReportProgressDelegate(Thread_Report);
    Thread th = new Thread(new ParameterizedThreadStart(Thread_Routine));
    th.IsBackground = true;
    th.Start(tp);
    while (!th.IsAlive) Thread.Sleep(0);
    button1.Enabled = false;
    th.Join();
    button1.Enabled = true;
  }

  void Thread_Report(int step, object data) {
    if ((data != null) && (data is DataTable)) {
      DataTable table = (DataTable)data;
      const string TEST_RESULT = "Test_Result";
      switch (step) {
        case 0:
          listView1.Items.Add(new ListViewItem(new string[] { "Row 1", string.Empty }));
          listView1.Items.Add(new ListViewItem(new string[] { "Row 2", string.Empty }));
          listView1.Items.Add(new ListViewItem(new string[] { "Row 3", string.Empty }));
          listView1.Items.Add(new ListViewItem(new string[] { "Row 4", string.Empty }));
          listView1.Items.Add(new ListViewItem(new string[] { "Row 5", string.Empty }));
          listView1.Items.Add(new ListViewItem(new string[] { "Row 6", string.Empty }));
         break;
        case 1:
          if ((2 < table.Rows.Count) && (table.Columns.Contains(TEST_RESULT))) {
            listView1.Items[0].SubItems[1].Text = table.Rows[1][TEST_RESULT].ToString();
            listView1.Items[1].SubItems[1].Text = table.Rows[2][TEST_RESULT].ToString();
          }
          break;
        case 2:
          if ((4 < table.Rows.Count) && (table.Columns.Contains(TEST_RESULT))) {
            listView1.Items[2].SubItems[1].Text = table.Rows[3][TEST_RESULT].ToString();
            listView1.Items[3].SubItems[1].Text = table.Rows[4][TEST_RESULT].ToString();
          }
          break;
        case 3:
          if ((6 < table.Rows.Count) && (table.Columns.Contains(TEST_RESULT))) {
            listView1.Items[4].SubItems[1].Text = table.Rows[5][TEST_RESULT].ToString();
            listView1.Items[5].SubItems[1].Text = table.Rows[6][TEST_RESULT].ToString();
          }
          break;
        default:
          break;
      }
    }
  }

  void Thread_Routine(object threadParam) {
    ThreadParameter tp = (ThreadParameter)threadParam;
    const string sqlConn = "SUPPLY_YOUR_CONNECTION_STRING";
    using (SqlConnection conn = new SqlConnection(sqlConn)) {
      int step = 0;
      string sqlText = "sp_GetPartRecord";
      DataSet ds = new DataSet();
      using (DataTable table = new DataTable()) {
        tp.ReportProgress(step++, table);
      }
      using (SqlDataAdapter da = new SqlDataAdapter(sqlText, sqlConn)) {
        DataTable table = new DataTable();
        da.SelectCommand.CommandType = CommandType.StoredProcedure;
        da.SelectCommand.Parameters.AddWithValue("@SN", tp.SerialNumber);
        Console.WriteLine("Step {0}", step);
        da.Fill(table);
        ds.Tables.Add(table);
        tp.ReportProgress(step++, ds.Tables[ds.Tables.Count - 1]);
      }
      using (SqlDataAdapter da = new SqlDataAdapter(sqlText, sqlConn)) {
        DataTable table = new DataTable();
        da.SelectCommand.CommandType = CommandType.StoredProcedure;
        da.SelectCommand.Parameters.AddWithValue("@SN", tp.SerialNumber);
        Console.WriteLine("Step {0}", step);
        da.Fill(table);
        ds.Tables.Add(table);
        tp.ReportProgress(step++, ds.Tables[ds.Tables.Count - 1]);
      }
      using (SqlDataAdapter da = new SqlDataAdapter(sqlText, sqlConn)) {
        DataTable table = new DataTable();
        da.SelectCommand.CommandType = CommandType.StoredProcedure;
        da.SelectCommand.Parameters.AddWithValue("@SN", tp.SerialNumber);
        Console.WriteLine("Step {0}", step);
        da.Fill(table);
        ds.Tables.Add(table);
        tp.ReportProgress(step++, ds.Tables[ds.Tables.Count - 1]);
      }
    }
  }
}
 
Looking at just your console app, I changed the definition of Program to
Code:
    class Program
    {

        Button button1;
        ListView listView1;
        [COLOR="Red"]public Thread MainThread;[/COLOR]

        static void Main(string[] args) {
            Program pgm = new Program();
            [COLOR="Red"]pgm.MainThread = Thread.CurrentThread;[/COLOR]

            pgm.Go();
            Console.ReadLine();
        }
/*
        .
        .
        .    
*/

        void Thread_Report(int step) {
            [COLOR="Red"]Console.WriteLine((Thread.CurrentThread == MainThread).ToString());[/COLOR]
        }
// ...
The output was false. It looks as if you expect the Delegate.Invoke method to behave as the Control.Invoke method does. Delegate.Invoke will run the target method on the thread from which it is called.

For a quick solution, you could change the Thread_Report method as follows:
Code:
  void Thread_Report(int step, object data) {
[COLOR="Red"]    if (InvokeRequired) {
        Invoke(new AppropriateDelegateType(Thread_Report), new object[] { step, data });
        return;
    }[/COLOR]

    if ((data != null) && (data is DataTable)) {
      DataTable table = (DataTable)data;
//  ...
You would have to do this for each cross-thread invocation however.

The background-worker uses a more elaborate technique for the thread-safe calls, based on a SynchronizationContext object.
 
THANK YOU!

I do not use delegates on a daily basis, but I have found myself using them more as I become more familiar with them.

A distinction between Control.Invoke and Delegate.Invoke was not something I would have found on my own, and I could not think of a way to test to see if I was actually on a different thread or not. I briefly thought about trying to create handle variables for each of the threads that I could look at in the debugger, but I just did not see any way that Invoke could *not* be working. (Control.Invoke != Delegate.Invoke) was what I was missing.

With that, I can look into how to patch my little class.

The BackgroundWorker is fabulous, and that's what I started using; however, the files that the code resides in is linked in to the main project along with a Windows Mobile project - and the Windows Mobile (or Compact Framework) does not include a BackgroundWorker. This is a stab at creating something similar.

Again, Thank You! What bar can I leave $20 for your beer tab?
 
Rats. I thought I did this right, but my application now hangs when I call Invoke.

I'm hoping someone can show me my error from my code (a small 12.9 KB attachment).

The solution was created with VS2005, and it will appear to have 2 forms in it when viewed through the Solution Explorer. The forms will not display in the Designer, because they are created using a class that is derived from System.Windows.Form.

If someone can take the time to look this over, the small application will hang on Line 25 of the file ZParameters.cs "_owner.Invoke(methInvoker);".

Yes, I have created this so that it looks a lot like the BackgroundWorker class, but Windows Mobile does not support this class. I'm trying to create a threading class that sends data back to the main thread as it collects information.

Help and Pointers are appreciated.

~Joe
 

Attachments

Wow! You're right. I commented that out, and the hang went away.

Well... how will I know when my thread has finished now? I was thinking my routine would join the thread and continue after the thread completed. I don't want to continually poll the IsAlive property.
 
Back
Top