Delegate Invoke in Thread throws InvalidOperationException

J

jp2msft

I have a test application that I am working on to create a way for my
threads to send information back to the main thread. I would prefer to use
the BackgroundWorker class (which is perfect for doing this), but I can not
because I am working on a reduced Windows feature set (Mobile).

I designed my custom threading class with a delegate that is used to create
an event. The event, in turn, should 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 is 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 kindly help me see the error in my design?

Both versions (Windows and Console) take an instance of this custom class
that I created as the thread's Start parameter:
Code:
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:
Code:
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]);
}
}
}

}

Here is the Console version, which does not throw an exception (though I
can't actually 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]);
}
}
}
}

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
 
I

Ignacio Machin ( .NET/ C# MVP )

I have a test application that I am working on to create a way for my
threads to send information back to the main thread. I would prefer to use
the BackgroundWorker class (which is perfect for doing this), but I can not
because I am working on a reduced Windows feature set (Mobile).

Hi,

I think you should understand how the "meat" work and then you will
understand why the potato worked.
The UI thread has a "message pump" that is always checking for new
messages (usually from windows) and process them this is done in the
famous WndProc method.
In another unrelated issue the UI can only be modified from the UI
thread. So if you want to update the UI you need a way to make a piece
of code execute in the UI, how you do this then?
Simply you put a message in the UI message queue (this is done from
the worker thread) and then you expect that the UI will (in turn)
process it.
I hope the above is clear enough for you to understand. here is a
resume anyway
The act of execute a method in the UI thread from another thread is
a two step process, and each step has no knowledge of the other
the first step is done in the worker thread and consist of putting a
message in the UI thread (this is done using Control.Invoke) this is
as far as the background thread does.
The second step happen when the message pump of the UI process the
message that the worker thread placed in the queue, it will send it to
the correct control and execute the delegate.

With that in mind I will comment your code


th.Start(tp);
while (!th.IsAlive) Thread.Sleep(0);
button1.Enabled = false;
th.Join();
button1.Enabled = true;

Is the code with the problem, you do not want to sync the threads (as
I mentioned before the worker thread has a way to inform the UI) and
you definetely do not want the UI thread to be blocked.
Just remove those lines and the app will work as expected
 
J

jp2msft

Hi Patrice,

I've been in contact with Pablo for the last couple of days (the author of
the article you referenced), and he wasn't able to find a clean way of doing
it either.
 
P

Patrice

I gave this a closer look and still simplified a bit to hopefully focused on
the actual problem.

Basically I added in Thread_Report :

if (this.listView1.InvokeRequired)
{
listView1.Invoke(new
ThreadParameter.ReportProgressDelegate(Thread_Report), step, data);
return;
}
// Code...

So that if it is called on the wrong thread, it will call itself on the UI
thread.

Rather than JOINing the thread, based on your code I added a "Completed"
event (also an overally stategy could be to be close as possible from the
BackgRoundWorkker component, it could be worth to look at this source code
(using referencesource.microsoft.com or the well known Reflector tool).
Basically it likely embed all this as the component knows on which UI
container it is hosted. It can then handled the switch itself which would be
cleaner. You could do the same by passing a form reference to your
background task whose goal will be just to call invokerequired ealier than
what is shown above).

I added also few checkboxes I can play with while the background task is
running to see if the UI is kept responsive...

--
Patrice


Patrice said:
For now I still don't see where you handle the switch. It seems for now it
could be just a confusion between Delegate.Invoke and Control.Invoke. Or
do I miss something ?

If I'm way off I'll try to repro and give this a closer look later
today...

--
Patrice

"jp2msft" <[email protected]> a écrit dans le message de
groupe de discussion :
(e-mail address removed)...
 
J

jp2msft

Thanks Patrice!

_host.Invoke took care of the problem.

Thanks for sticking with this problem, too!

Patrice said:
I also gave this a try (trying to turn your class into a BackgroundWorker
like class). It would give something like :

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

_host being just the form passed to the BackgroundWorker class when
created...

This way the thread switching is handled by the BackgroundWorker class
itself and you have no more to worry about that... The full code would be
(also added few checkboxes to test UI responsiveness while running) :

public partial class Form1 : Form
{
BackgroundWorker backgroundWorker;

public Form1()
{
InitializeComponent();
button1.Click += button1_Click;
}

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

void Go() {
listView1.Clear();
listView1.Columns.Add("TestName", 140);
// ThreadParameter starting to be turned to a backgroundWorker
component...
backgroundWorker = new BackgroundWorker(this);
backgroundWorker.DoWork=new ThreadStart(Thread_Routine);
backgroundWorker.ProgressChanged += new
BackgroundWorker.ReportProgressDelegate(Thread_Report);
backgroundWorker.ReportCompleted+=new
BackgroundWorker.ReportCompletedDelegate(Thread_Completed);
button1.Enabled = false;
backgroundWorker.RunWorkerAsync();
}

void Thread_Report(int step, object data) {
if ((data != null) && (data is DataTable)) {
DataTable table = (DataTable)data;
listView1.Items.Add(new ListViewItem(new string[] {
table.Rows[0][0].ToString() + step.ToString(), string.Empty }));
}
}

void Thread_Completed(int step,object data)
{
button1.Enabled = true;
}


void Thread_Routine() {
//ThreadParameter tp = (ThreadParameter)threadParam;
DataTable table=new DataTable();
int step;
table.Columns.Add("A",typeof (String));
table.Rows.Add("Test");
for(step=0;step<4;step++)
{
// Simulate some workload
for(var i=1;i<10000000;i++) {
double d;
d=Math.Cos(i);
d=Math.Sin(i);
}
backgroundWorker.ReportProgress(step, table);
}
backgroundWorker.ReportComplete(step, table);
}

public class BackgroundWorker
{
private Control _host;
//string _serialNumber;
public delegate void ReportProgressDelegate(int step, DataTable
data);
public delegate void ReportCompletedDelegate(int step,DataTable
data);
public event ReportProgressDelegate ProgressChanged;
public event ReportCompletedDelegate ReportCompleted;
public ThreadStart DoWork;

public BackgroundWorker(Control host)
{
_host = host;
ProgressChanged = null;
ReportCompleted = null;
}

public void RunWorkerAsync()
{
Thread th = new Thread(DoWork);
th.IsBackground = true;
th.Start();
}

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

public void ReportComplete(int step,DataTable data)
{
_host.Invoke(ReportCompleted, step, data);
//if (ReportCompleted != null) ReportCompleted.Invoke(step,
data);
}
}
}
}

Hope it helps though still to be brushed up a bit...
 
J

jp2msft

I agree Pete (see below - Haha!).

I've tried contacting MS about problems on here (the web version, not the
version I can read through Outlook Express), but those goobers in India keep
wanting to know what NewsGroup reader I use and what SNTP server I have
punched in. I'll write and tell them, then a day later I'll get a response
from someone else in India asking me what NewsGroup reader I use and what
SNTP server it is set up with. WOW! Deja Vu! That happened 5 times the last
time I tried sending them a message, then I just deleted it and forgot about
it.

For what it's worth: The online newsgroup reader does not function correctly
if you are located behind a firewall. I can (almost) always view messages,
but I can't post or reply until I get home using my standard DSL. (Outlook
Express is blocked at by Group Policy at work - Ugh!)

In short: I feel your pain, Pete! Good luck with those in India!
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top