How do I bind to data from a different thread?

J

Jamie Risk

I have a form with controls that bind to an object declared in
the form.

I have a communications class that is receiver driven. (to get
data I have to send a command).

How can I have the receiver thread manipulate the object my form
controls are bound to?

Eg.

namespace myApp
{
public partial class myMainForm : Form
{
public dataClass myData = new dataClass();

// Constructor
public myMainForm()
{
InitializeComponent();
this.myDataBindingSource.DataSource = this.myData;
}
// ...
private void Read_button_Click(object sender,
EventArgs e)
{
comm.myDevice dev = comm.myDevice();
dev.Open();
if (dev.IsOpen)
{
byte[] packet;
... // Create packet
dev.Transaction(packet);
}
}
}

namespace comm
{
private class responseData
{
public bool IsValid { ... }
}
public class myDevice : SerialPort
{
public void Transaction(byte[] packet)
{
try { base.Write(packet, 0, packet.Length); }
catch (TimeoutException) { return; }

System.Threading.Thread rdThrd;
rdThrd = new System.Threading.Thread(Response);
rdThrd.Start();
}
public void Response()
{
repsonseData resposne = new responseData();
while (!response.IsValid)
{
byte[] data = new byte[512];
try { base.Read(data, 0,
Math.Min(this.BytesToRead,512)); }
catch (TimeoutException) { return; }
response.scan(data);
}
// ****************************************
// bind something akin to this:
// ****************************************

myApp.myMainForm.myData.element1 = response.element1;

}
}
}
}
 
D

Dave Sexton

Hi Jamie,

You don't need a reference to MyMainForm, you need a reference to the data
source object. Try adding a constructor that accepts your DataClass as a
parameter:

public class MyDevice : SerialPort
{
private readonly DataClass data;

public MyDevice(DataClass data)
{
this.data = data;
}

public void Response()
{
// TODO: set data.element1
}
}

Supply the data source object to MyDevice when it's constructed in MyApp:

private void Read_button_Click(object sender, EventArgs e)
{
Comm.MyDevice dev = new Comm.MyDevice(myData);

// TODO: use dev
}

An alternative to using a constructor parameter is to accept a DataClass
parameter in the Transaction method itself. You can then accept DataClass as
a parameter in the Response method as well and use a ParameterizedThreadStart
(2.0 framework) by calling the appropriate Thread.Start method overload.

"ParameterizedThreadStart Delegate"
http://msdn2.microsoft.com/en-us/library/system.threading.parameterizedthreadstart.aspx

I like the constructor option better because it's type-safe, but it might not
be appropriate for your architecture.

BTW, the standard is to use upper camel case for namespace, class, struct and
enum declarations, just like in the FCL. For example:

namespace MyApp { public class MyMainForm : Form { ... } }

--
Dave Sexton

Jamie Risk said:
I have a form with controls that bind to an object declared in the form.

I have a communications class that is receiver driven. (to get data I have
to send a command).

How can I have the receiver thread manipulate the object my form controls
are bound to?

Eg.

namespace myApp
{
public partial class myMainForm : Form
{
public dataClass myData = new dataClass();

// Constructor
public myMainForm()
{
InitializeComponent();
this.myDataBindingSource.DataSource = this.myData;
}
// ...
private void Read_button_Click(object sender,
EventArgs e)
{
comm.myDevice dev = comm.myDevice();
dev.Open();
if (dev.IsOpen)
{
byte[] packet;
... // Create packet
dev.Transaction(packet);
}
}
}

namespace comm
{
private class responseData
{
public bool IsValid { ... }
}
public class myDevice : SerialPort
{
public void Transaction(byte[] packet)
{
try { base.Write(packet, 0, packet.Length); }
catch (TimeoutException) { return; }

System.Threading.Thread rdThrd;
rdThrd = new System.Threading.Thread(Response);
rdThrd.Start();
}
public void Response()
{
repsonseData resposne = new responseData();
while (!response.IsValid)
{
byte[] data = new byte[512];
try { base.Read(data, 0,
Math.Min(this.BytesToRead,512)); }
catch (TimeoutException) { return; }
response.scan(data);
}
// ****************************************
// bind something akin to this:
// ****************************************

myApp.myMainForm.myData.element1 = response.element1;

}
}
}
}
 
J

Jamie Risk

Marc said:
It isn't clear how your bindings work... however, if they are simple
Binding() instances, then yup they will break on cross-threaded calls.

Truthfully?
It's not clear to me either ... I'm pretty new at this; as an
embedded developer it's frightening how much code is
automagically created with just a few button clicks.
But here is a fix:
http://groups.google.co.uk/group/mi...threadedbinding&rnum=1&hl=en#2f078656d6f1ee1f

Other than that, you may need to catch events and .[Begin]Invoke()
manually.

Marc

As an example of my ignorance; in the xxx.Designer.cs source
file I see things akin to:

this.myTextData_textBox.DataBindings.Add(
new System.Windows.Forms.Binding(
"Text",
this.myDataBindingSource,
"myTextData", true ));

Do I simply replace the new System....Binding(...) with the
class you created? And if so, do I put that in the constructor
for the form?

- Jamie
 
M

Marc Gravell

Yes.

Basically, you aren't meant to touch the UI from any thread *except* the one
that created it - this is "thread affinity" and there are lots of reasons.
In 2.0 it will usually catch you doing this and laugh
(IllegalOperationException) in your face.

Now; I (and others) would argue that in an "observer" or "MVP" scenario, it
shouldn't matter which thread causes the change to the data object... the UI
should be able to update itself. Unfortunately the standard MS Binding
object isn't capable of this, and will ignore any updates that happen on the
wrong thread.

Fortunately, the example you have cited:
DataBindings.Add(
new System.Windows.Forms.Binding(
can be replaced (as you thought) with a new ThreadedBinding(blah). A word of
warning though. The designer (in the IDE) is notorious (really, really
infamous) for botching data bindings and either losing them randomly or
messing them up. My advice is to use the designer to *create* the bindings,
but then cut/paste all of the DataBindings code into a private method that
isn't owned by the designer. This would be a good time to use find/replace
to change to a ThreadedBinding.
The downside is that you don't get to see your bindings in the designer. The
upside is that it doesn't break (losing all your bindings) every 4th day
(YMMV).

The other answer here is to do your main work on the background thread, but
then switch back to the UI thread to do the all important change to the
objects that triggers the UI update. This is reasonably easy using
BeginInvoke or Invoke.

Marc
 
J

Jamie Risk

Marc said:
Fortunately, the example you have cited:
can be replaced (as you thought) with a new ThreadedBinding(blah). A word of
warning though. The designer (in the IDE) is notorious (really, really
infamous) for botching data bindings and either losing them randomly or
messing them up. My advice is to use the designer to *create* the bindings,
but then cut/paste all of the DataBindings code into a private method that
isn't owned by the designer. This would be a good time to use find/replace
to change to a ThreadedBinding.
The downside is that you don't get to see your bindings in the designer. The
upside is that it doesn't break (losing all your bindings) every 4th day
(YMMV).

I'd already added the bindings to the form's constructor, and
everything seemed to work like it used to. However, I'm
obviously missing something as the background object updating
isn't causing the UI to update.

Do I need to add an event handler somewhere?

- Jamie
 
M

Marc Gravell

OK; start simple.

If you forget the threading (just run the code in question from a
button-click or something), does it update? Get this working first, *then*
add threading. I suspect that you aren't providing the necessary hooks for
data-binding. Data-binding does (as you suggest) rely on eventing.

For reference, this can be done in several ways:
Method 1:
For a property "SomeProperty", add a matching event "SomePropertyChanged".
This is a magic name pair that the runtime looks for when it comes to look
for a notification sink. So in your case myTextDataChanged. Note that you
need to trigger this event when the text changes. Example (I've changed the
case to C#/CLR standards):

private string myTextData;
public string MyTextData {
get {return myTextData;}
set {
if(MyTextData != value) {
myTextData = value;
OnMyTextDataChanged();
}
}
}
public event EventHandler MyTextDataChanged;
protected void OnMyTextDataChanged() { // just convention
EventHandler handler = MyTextDataChanged;
if(handler!=null) handler(this, EventArgs.Empty);
}

Method 2:
As method 1, but you implement INotifyPropertyChanged, and include the
property name in the PropertyChangedEventArgs

Method 3:
more involved, but you can write custom component-model AddHandler and
RemoveHandler methods. I won't detail this unless you are genuinely
interested.

Marc
 
J

Jamie Risk

Thanks.
So.
Much.

FYI I used Method 1. It was a complicated search and replace
for all the properties in question, and seemingly inelegant in
all the repetitive code ... but it works. Thanks.
 
M

Marc Gravell

Welcome.

You'll probably want to hit me, but note also that if you have lots of
events on an object, you may wish to look at the EventHandlerList approach;
this reduces the size of each object instance by using a list of the
*subscribed* handlers.

In my defence, you only mentioned one property, so a single event was the
most appropriate solution... and even for a handful I would keep it simple
and use simple event backers... but if the count grew I would look at
EventHandlerList and static reference objects.

*But* the standard event approach will work *just fine*. This is just an
optimisation mentioned for completeness. It actually makes things slightly
more complex, so don't go near it unless you are happy to absorb that cost.

Marc
 

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