Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

INFO: How databinding really works v0.1

38 views
Skip to first unread message

NoiseEHC

unread,
Aug 4, 2002, 8:20:20 AM8/4/02
to
There are two types of databinding
1. Simple binding (when you bind a value to a control)
2. Complex binding (when you bind a list to a control like DataGrid or ComboBox)

Simple binding:
ms-help://MS.VSCC/MS.MSDNVS/vbcon/html/vbconConsumersOfDataOnWindowsForms.htm
The picture in this page is not correct since in a form you can have more than one BindingContext
and a datasource (array, DataTable) can have more than one CurrencyManager.

CurrencyManager
---------------
The idea is that the datasource does not have a position (unlike ADO or Delphi) and you have to
create a distinct object which keeps track of the position and gets the data from the datasource
then puts them to the control then back.

BindingContext
--------------
Since the data binding infrastructure (databinding) creates CurrencyManagers automagically
there has to be a mechanism which makes it possible that every control bound to the same
datasource (table) are synchronized with each other. So there is a BindingContext object which
keeps tracks of the CurrencyManagers and gives the same CurrencyManager to the controls
bound to the same datasource. Of course the controls have to get the same BindingContext
to get the same CurrencyManager.

The (pseudo) code in Control:
private BindingContext privateBindingContext;
public virtual BindingContext BindingContext {
get {
if(privateBindingContext != null)
return privateBindingContext;
if(Parent != null)
return Parent.BindingContext;
return null;
}
set {
privateBindingContext = value;
OnBindingContextChanged(...);
}
}

The code in ContainerControl (Form and UserControl):
public virtual BindingContext BindingContext {
get {
if(base.BindingContext != null)
return base.BindingContext;
base.BindingContext = new BindingContext();
return base.BindingContext;
}
}

So Control primarily tries to use his parent control's BindingContext.
If you set a Control's BindingContext, then it will use that.

ContainerControl also tries to use his parent control's BindingContext.
If it does not have a parent then it will create a new BindingContext and
every control in a form will get the same BindingContext. WOW!!!
If you want to keep in sync a newly opened form with the current then type:
Form2 newform = new Form2();
newform.BindingContext = this.BindingContext;
...

The only sux is that every time the Parent property changes, the Control
needs to rebind. That is the reason why so many controls have bugs when
they are placed onto a TabPage. The controls cannot rebind properly and
when you change the active page the have to (rebind). So the bugs appear
which would not appear elsewhere. The workaround:
tabpage.BindingContext = form.BindingContext;


To remove the need to write 1 line of code the BindingContext automatically
creates CurrencyManager objects when somebody queries for them. It means
that when you type:
textBox1.DataBindings.Add("Text", datatable, "Sukka");
the created Binding object will sooner or later reference the BindingContext's
Item property and it will create a new CurrencyManager if it will not find an
appropriate one (in BindingContext.EnsureListManager).
So you do not have to type this (pseudo) code:
this.BindingContext.CreateCurrencyManagerFor(datatable);

This is the reason why so many times were asked in this NG why a DataGrid
and simple controls do not keep sync. The answer:
http://support.microsoft.com/default.aspx?scid=kb;EN-US;Q313482
The trick is that the DataSources and DataMembers have to match (dataset is
a DataSet and Customers is a DataTable and Sukka is a column's name).

In the case of complex binding (for example DataGrid)
datagrid.BindingContext[dataset.Customers]
equals with
datagrid.BindingContext[dataset.Customers, ""]
or with
datagrid.BindingContext[dataset.Customers, null]
or with
datagrid.BindingContext[dataset.Tables["Customers"], null]
but DOES NOT EQUALS with
datagrid.BindingContext[dataset, "Customers"]

In the case of simple binding (for example TextBox)
textbox.DataBindings.Add("Text", dataset.Customers, "Sukka");
DOES NOT EQUALS with

textbox.DataBindings.Add("Text", dataset, "Customers.Sukka");

DOES NOT EQUALS means that they will create and refer to
two different CurrencyManagers.

There are two consequences:
1. You have to bind in the same way:
datagrid.DataSource = dataset.Customers;
textbox.DataBindings.Add("Text", dataset.Customers, "Sukka");
OR

datagrid.DataSource = dataset;
datagrid.DataMember = "Customers";
textbox.DataBindings.Add("Text", dataset, "Customers.Sukka");
2. If you want to create a generic function which works with the
datagrid's CurrencyManager then
datagrid.BindingContext[datagrid.DataSource, datagrid.DataMember]
will always get the good CurrencyManager (which is used by the datagrid).
3. :) It is not a consequence but the simplest (and safest) way to get the TextBox's
CurrencyManager (or another Control's) is to type (if you bind to the Text poperty):
textbox.DataBindings["Text"].BindingManagerBase

If you do not know why you should get the CurrencyManager then remember that
it keeps the position. So in the Prev and Next buttons' Click handler you should write:
BindingManagerBase bm = this.BindingContext[dataset.datatable];
if(bm.Position > 0)
bm.Position -= 1;
AND
BindingManagerBase bm = this.BindingContext[dataset.datatable];
if(bm.Position < bm.Count-1)
bm.Position += 1;

This is the time to say that BindingContext in fact does not manages CurrencyManagers
but BindingManagerBases. There are two types derived from BindingManagerBase:
CurrencyManager and PropertyManager. You will use only CurrencyManagers but if
you only want to use BindingManagerBase's functionality the you do not have to cast
it to CurrencyManager. EG this is not required:
CurrencyManager cm = this.BindingContext[dataset.datatable] as CurrencyManager;

Another issue is that when you use two ListBoxes or ComboBoxes to look up values
from the same datasource (for examle look up the ID from the Tires table for columns
LeftTire and RightTire in table Cars) the both of the ComboBoxes will get the same
CurrencyManager (this.BindingContext[dataset.Tires]). So when you change one of
the ComboBoxes then both of them will change. To avoid that you have two options:
1. Use different BindingContextes.
leftTireCombo.BindingContext = new BindingContext();
rightTireCombo.BindingContext = new BindingContext();
The problem is that you will not be able to bind the two ComboBoxes to the same position
in Cars since they will get different CurrencyManagers in table Cars too.
2. Bind to different DataViews.
leftTireCombo.DataSource = new DataView(dataset.Tires);
rightTireCombo.DataSource = new DataView(dataset.Tires);
It's perfect.


Binding
-------
This is the object which connects a column in the datasource to a property of a Control. If
you bind more that one property to the same Control or more than one Control to the same
column then you will have different Binding object for every binding. Databinding checks that
the Bindings have to be unique.

If you write:
textbox.DataBindings.Add("Text", dataset.Customers, "Sukka");
ControlBindingsCollerction will create a Binding object then call
textbox.DataBindings.Add(newlycreatedbinding);
then finally call
Binding.SetControl(textbox);

Binding.SetControl first unbinds (if it has bound to a control) and then binds (if it can).
It happens in Binding.BindTarget when Binding attaches two eventhandlers to the
Control (more on this later). "If it can" means that the following conditions must be
met in order to successfully bind to a column:
1. All the properties must be set correcly (DataSource, DataMember, PropertyName,
Control)
2. Control must be Created
3. DataSource's current position has to be a bindable object (for example DataRowView).

1. Since in the current serialization architercture there is no determinded order in control
initialization the binding occurs in more than one step. If the binding cannot become finished
then it remains in a half binded state and later it becomes finished. To achive this all the
datasources and controls subscribe to and fire different events to notify each other that
there is a chance to finish the binding. In certain property set function they also try to
finish the binding. I will not cover this since without source code with comments I am not
able to figure out what happens and when. We can only hope that this VERY complex code
works as it should and you will not get some weird random binding errors. (This is the drawback
of not having a deterministic initialization order like Delphi's.)

2. It implies that the Control has to have a Created Parent too. In the case of TabPage it is
not always true so you have to use the workaround to lessen the rebinding errors:
tabpage.BindingContext = form.BindingContext;
Sometimes it will not be enought like binding DateTimePicker or ComboBox in a TabPage.

3. Binding will create an internal object of type BindToObject. It will be the link to the datasource.
????????????????????????????
BindToObject.CheckBinding
CurrencyManager.GetItemProperties
PropertyManager.GetItemProperties
PropValueChanged->OnCurrentChanged
GetValue/SetValue -> BeginEdit

When finishing the binding the Binding object will attach to the following events and properties:
1. Binding.PropertyName+"Changed" event (for example "TextChanged" case sensitive). Binding
gets it via reflection. The attached event handler simply sets the dirty bit when the event fires.
If Binding does not find an eventhandler named like this then it will suppose that the dirty bit is
always true so it will read the property of the Control every time even if you do not change anything.
There is a flag in Binding (inSetPropValue) which is set when Binding sets the attached property so
if the Control fires an XXXXChanged event it will be discarded by Binding. As an effect when you
browse throught a datatable the rows will not became modified because when Binding sets the
Text property and TextBox fires TextChanged the Binding will not became dirty and at Validating
time the datatable will not be affected.

2. "Validating" event. It is a predefined event of Control so you do not have to declare that. It fires
when the control loses the focus. The attached event handler does the following (Binding.PullData):
a. calls the Parse event on Binding with the value (if exists)
b. if the value is IConvertible then changes the type to the desired datatype in the datasource
c. if the conversion or parse fails it gets the value from the datasource (and uses it in d.)
d. reformats the data and sets back to the control's property (so if you typed '01 /1/2002' you
will get back '1/1/2002' in the TextBox)
e. stores the value to the datasource (if not failed in c.)

3. Binding.PropertyName property. It has to be a public property of any type. It has to be readwrite.

4. Binding.PropertyName+"IsNull" property (for example "TextIsNull" case sensitive). It has to be a
readwrite public boolean property. This is optional.

There is only one thing remains: How the data moves between the datasource and the control.


Binding.PushData
----------------
It is called when the data moves from the datasource to the control (for example after scrolling
to a new record).
a. Binding gets the data value from the datasource via BindToObject
b. Calls Binding.FormatObject (it normally CONVERTS the value not formats that).
c. Sets the value to the control's property by reflection.
If the value == null then it will use the XXXXIsNull property (if there is one). If there is no
XXXXIsNull and the control's property's type is Object (a complex type) then it will set DBNull.Value instead of null.
Otherwise it will set null as a value. If the value != null then it will simply set the value to the property.

In Binding.FormatObject there are more steps while Binding tries to convert the value to the type
of the control's property:
b1. if the control's property's type is Object then no formatting required.
b2. if there's an OnFormat event handler then it is called
b3. if the value is not null (DBNull.Value) and value is IConvertible and needs conversion
to the property's type then Convert.ChangeType is called which calls value.ConvertToXXXX if the
property's type is in the types IConvertible supports.
The ChangeType is called with the current thread's CurrentCulture. If the value is IConvertible
(for example int or string) but the control's property is not in the IConvertible supported types
(for example MyDateTimeDifference) then it will result in an exception but I am not sure!!!!!!! ***********
b4. if after b2 and b3 the value does not need conversion (the value's type equal or subtype of the
property's type) then the conversion finished. Otherwise b5 continues.
b5. if value's type has a TypeConverter (get by TypeDescriptor.GetConverter(value.GetType())) then its
CanConvertTo and ConvertTo is called and the returned value is used.
b6. if value does not have a TypeConverter or !CanConvertTo then IConvertible is tried again (WHY???).
b7. if none of them works then FormatException


Binding.PullData (more detailed that the text at the "Validating" event)
------------------------------------------------------------------------
It is called when the data moves from the control to the datasource (for example after tabbing out
of the control or calling EndEdit).
It will call Binding.ParseObject ONLY if the Binding is DIRTY (EG XXXChanged event fired) OR there
is no XXXChanged event.
a. Gets the value from the control by Binding.GetPropValue. If there is an XXXXIsNull property and
it is true then the value will be DBNull.Value. Otherwise it will be the value returned by reflection
(for example the "Text" property). If the value == null then it will be treated as DBNull.Value.
b. Calls Binding.ParseObject.
c. If the datatype conversion fails then Binding will discard the control's value and gets the original
value from the datasource.
d. Binding does a PushData so the controls will have the formatted values (so if you typed '01 /1/2002'
you will get back '1/1/2002' in the TextBox)
e. If there were no errors Binding stores the value back to the datasource via BindToObject.
f. The Binding will be unmodified (not dirty).

In Binding.ParseObject there are more steps.
b1. if there is a OnParse event handler then it is called
b2. if the value is not null (DBNull.Value) and value is IConvertible and needs conversion to the
column's type then Convert.ChangeType is called which calls value.ConvertToXXXX if the column's type
is in the types IConvertible supports.
The ChangeType is called with the current thread's CurrentCulture. If the value is IConvertible
(for example int or string) but the column is not in the IConvertible supported types (for example
MyDateTimeDifference) then it will result in an exception. It means that if the property's type is
IConvertible but the column's type is not supported by IConvertible then you will not be able to do
two way binding (EG you can only show the values not edit them). It's a design flaw because with
DataGrid it's possible to store the data (of course only if you type a convertible string value).
So the design is inconsistent at least.

b3. if after b2 the value does not need conversion (the value's type equal or subtype of the column's
type) then the conversion finished. Otherwise b4 continues.
b4. if value's type has a TypeConverter (get by TypeDescriptor.GetConverter(value.GetType())) then
its CanConvertTo and ConvertTo is called and the returned value is used.
b5. if value does not have a TypeConverter or !CanConvertTo then IConvertible is tried again (WHY???).
b6. if none of them works then null is returned.

So the bug in b2 has the following consequences:
1. If you bind to a control's property of type string (or any other IConvertible value) but the
column's type is not supported by IConvertible (for example MyDateTimeDifference) then your
TypeConverter will not be called by binding and your only option is to provide an OnParse event
handler. In the opposite way MyDateTimeDifference is not IConvertible so your TypeConverter's ConvertTo
will be called. It's SUX!!!
2. Providing a TypeConverter is an option only if neither the column or property is IConvertible.
It can be achieved by using custom types for both (which means that you use a custom control) but in
that case there is probably no need for conversion.


UserControl example 1
---------------------
The idea is that you want to create an UserControl with some controls on it and you want to create
some property for each control which you can bind to. (Hmmmmmmmm.... is this sentence correct?)

public class MyUserControl : System.Windows.Forms.UserControl
{
private System.Windows.Forms.TextBox textBox1;
private System.Windows.Forms.TextBox textBox2;
...
private void InitializeComponent()
{
this.textBox1 = new System.Windows.Forms.TextBox();
this.textBox2 = new System.Windows.Forms.TextBox();
...
this.textBox1.Name = "textBox1";
this.textBox1.Validating += new System.ComponentModel.CancelEventHandler(this.common_Validating);
this.textBox1.TextChanged += new System.EventHandler(this.textBox1_TextChanged);
...
this.textBox2.Name = "textBox2";
this.textBox2.Validating += new System.ComponentModel.CancelEventHandler(this.common_Validating);
this.textBox2.TextChanged += new System.EventHandler(this.textBox2_TextChanged);
...
}

public event EventHandler LeftTextChanged ;
public event EventHandler RightTextChanged ;
private void textBox1_TextChanged(object sender, System.EventArgs e) {
if ( null != LeftTextChanged ) LeftTextChanged(this, e) ;
}
private void textBox2_TextChanged(object sender, System.EventArgs e) {
if ( null != RightTextChanged ) RightTextChanged(this, e) ;
}
private void common_Validating(object sender, System.ComponentModel.CancelEventArgs e) {
OnValidating(e);
}

public string LeftText {
get {return textBox1.Text;}
set {textBox1.Text = value;}
}
public string RightText {
get {return textBox2.Text;}
set {textBox2.Text = value;}
}
}

1. There are two properties LeftText and RightText.
2. Both of them have an XXXChanged event handler (LeftTextChanged and RightTextChanged case sensitive).
When you modify any of the TextBoxes then the textBoxXXX_TextChanged fires and it will raise a
LeftChanged or RightChanged event respectively.
3. There is a common Validating event handler. When you tab off any of the TextBoxes the
common_Validating fires and it will reflect the event back to databinding so the modified value will
be stored in the datatable. Without this the values would be stored only when you tab off the whole
UserControl and it can cause some bugs to appear (more on this later).

So there are two critical issues:
a. use public event EventHandler RightTextChanged;
The following will create a delegate and hence will not be recognized by Binding:
public EventHandler RightTextChanged;

The following in VB.NET also wrong:
Public Event RightTextChanged(ByVal sender As Object, ByVal e As System.EventArgs)
It's not well documented but this declaration is essentially the same as (C#):
public delegate void AutoGeneratedDelegateType(object sender, EventArgs e);
public event AutoGeneratedDelegateType RightTextChanged;
So it will create a new delegate (event callback) type which won't match System.EventHandler
and databinding won't find it.
The GOOD one in VB.NET is:
Public Event RightTextChanged AS EventHandler

b. There is a bug in databinding which appears when you are at the first row in the datasource (row 0)
and you modify more then one property of the control. This bug causes the second modification to be lost.
So reflecting back the Validating event for every inner control is REQUIRED.
Another issue is that you cannot create an inner control with two properties (for example a simple TextBox
which accepts text in a form "aaaa:bbbbb" where "aaaa" and "bbbbb" would be LeftText and RigthText.
If you omit the XXXChanged events then both of the properties will be dirty and it is the case when you
omit the Validating event. So BOTH 2 and 3 are REQUIRED!!!

UserControl example 2
---------------------
The picture is more complicated if you want to support DBNull.Value.
XXXIsNull is not an option since it will not be called in Binding.FormatObject and Binding will try
to convert it to column's type what will result in an exception (the value will be lost). It will only
work if the column's type is object which rarely is the case. So you have to create a property of type
object and compare with DBNull.Value.

public class NullAllowedUserControl : System.Windows.Forms.UserControl
{
private System.Windows.Forms.RadioButton trueOption;
private System.Windows.Forms.RadioButton falseOption;
private System.Windows.Forms.RadioButton nullOption;
...
private void InitializeComponent()
{
this.trueOption = new System.Windows.Forms.RadioButton();
this.falseOption = new System.Windows.Forms.RadioButton();
this.nullOption = new System.Windows.Forms.RadioButton();
...
this.trueOption.Name = "trueOption";
this.trueOption.Text = "TRUE";
this.trueOption.Click += new System.EventHandler(this.trueOption_Click);
...
this.falseOption.Name = "falseOption";
this.falseOption.Text = "FALSE";
this.falseOption.Click += new System.EventHandler(this.falseOption_Click);
...
this.nullOption.Name = "nullOption";
this.nullOption.Text = "NULL";
this.nullOption.Click += new System.EventHandler(this.nullOption_Click);
...
}

private void trueOption_Click(object sender, System.EventArgs e) {
falseOption.Checked = false;
nullOption.Checked = false;
if(SukkaChanged != null)
SukkaChanged(this, EventArgs.Empty);
}
private void falseOption_Click(object sender, System.EventArgs e) {
trueOption.Checked = false;
nullOption.Checked = false;
if(SukkaChanged != null)
SukkaChanged(this, EventArgs.Empty);
}
private void nullOption_Click(object sender, System.EventArgs e) {
trueOption.Checked = false;
falseOption.Checked = false;
if(SukkaChanged != null)
SukkaChanged(this, EventArgs.Empty);
}
public object Sukka {
get {
if(nullOption.Checked)
return DBNull.Value; // or return null
return trueOption.Checked;
}
set {
if(value == DBNull.Value) { // it will never get a null
trueOption.Checked = false;
falseOption.Checked = false;
nullOption.Checked = true;
} else {
trueOption.Checked = (bool)value;
falseOption.Checked = !(bool)value;
nullOption.Checked = false;
}
if(SukkaChanged != null)
SukkaChanged(this, EventArgs.Empty);
}
}
public event EventHandler SukkaChanged;
}

1. There is an object property called "Sukka". It can be binded to a bool column (which supports DBNull).
2. There is a SukkaChanged event which is raised when you click one of the RadioButtons.
3. There is no Validating event but you HAVE TO create one in your UserControl (it is only an example).

Note that in Sukka.get you can either return DBNull.Value or null since Binding will convert null
to DBNull.Value. However in Sukka.set you will never get a value null so do not try to compare with that.

So that's all this time.
------------------------
BindToObject is missing.
Complex binding next time.
I will to write about master/detail relations, expressions and views in depth when I will have some time.

NoiseEHC


Any feedback/corrections are welcome!


Robert Christian

unread,
Aug 5, 2002, 1:17:18 AM8/5/02
to
Good Job! I was considering putting something similar together as a primer
for our developers, but now I can just pass this on. Thanks for posting
this.


"NoiseEHC" <nois...@freemail.hu> wrote in message
news:uIoMSF7OCHA.2416@tkmsftngp09...

Martin Robins

unread,
Aug 13, 2002, 12:01:40 PM8/13/02
to
Heavy reading, but probably one of the most informative posts on the
newsgroup for a while. I have struggled to understand the holes in data
binding for some time and although I have merely perused this post, I
believe that it will probably clear up some of the confusion (and probably
cause some more with it).

I look forward to v0.2.

--
Martin Robins.

This message is posted "as is" etc., confers no rights etc., so don't sue me
etc.. You know the script.

Person Me = New Person();
while (Me.Alive)
{
try
{
Me.Drink(Alcohol.Any);
}
catch (HeaveException h)
{
Me.PrayToCeramicGod();
Me.Promise("Never again");
}
}
Me.Dispose();


"NoiseEHC" <nois...@freemail.hu> wrote in message
news:uIoMSF7OCHA.2416@tkmsftngp09...

0 new messages