Introduction
Aren't you tired of writing code like this in your WebForms?
...
lName.Text = cus.Name;
lSurname.Text = cus.Surname;
lEmail.Text = cus.Email;
lAddress.Text = cus.Address;
lAge.Text = cus.Age.ToString();
...
cus.Name = lName.Text;
cus.Surname = lSurname.Text;
cus.Email = lEmail.Text;
cus.Address = lAddress.Text;
cus.Age = Int32.Parse(lAge.Text);
...
Me too. Unfortunately ASP.NET databinding is only one way so it isn't useful to avoid writing the above code (You can avoid some code using the DataBind
method, but not all). After reading this article you'll be able to turn the above code to something like this:
...
bindingManager1.BindToWebForm(this);
...
bindingManager1.BindFromWebForm(this);
...
Background
The ASP.NET data binding mechanism works only in one way: When you add a databinding to an ASP.NET WebForm in design time, Visual Studio .NET adds a databinding expression to the ASPX file (<%# %><%# ... %> ). When the WebForm is requested for the first time, the ASPX file is parsed, JIT compiled and executed. For each data binding expression in a control, a handler is added to its DataBinding event. The DataBinding event is fired when the control's DataBind method is called, and the handlers added by the ASP runtime evaluates the data binding expression and performs the actual data binding. So far, so good.
At runtime we can only rely on ASP.NET to do one way data binding. If we only want to display information this works fine, but if we need to get data from a form, we have to get the values from the controls ourselves, which is a non-orthogonal mechanism and a tedious task.
If we were able to get the data bindings at run time, we could use reflection to perform 2 way databinding. However there is no way to get the data bindings at runtime. Of course, we can add properties in our controls to store the data bindings in order to fix that problem but i don't like that idea.
What to do then? Thanks to the design time architecture and reflection you'll have 2 way data binding without modifying any control (see Points of Interest).
Using the code
Using the BindingManager
in order to have 2 way data binding is very easy:
- Add the
BindingManager
component to the toolbox.
- Add a
BindingManager
component to a WebForm.
- Use the designer to set the databindings.
- When you need to load the WebForm with your data call to the
BindingManager
's BindToWebForm
method.
- In order to get the data from the WebForm, you have to call to the
BindingManager
's BindFromWebForm
method.
And that's all!
Sometimes you don't need 2 way data binding in all controls (for example, in a form to show/update customer data you can have a label that shows when was the customer account was created). If you click on the BindingManager
component, and locate the data binding in the DataBinding collection you can set the TwoWay
property to false to provide one way support only.
Points of Interest
How does the BindingManager
work?
As we said before, there's no way to access to the data bindings at run time. Luckily for us, in design time, we can get the data binding information because the Control class implements the IDataBindingsAccessor
interface. IDataBindingsAccessor
has 2 properties: HasDataBindings
and DataBindings
, that can be used to obtain the databindings. The BindingManager
stores the data binding information in a collection (DataBindingInfoCollection
) and thanks to the services offered to the component designers, we could monitor any component change to keep the data binding information updated using the IComponentChangeService
and the IReferenceService
interfaces.
protected void UpdateDataBindings()
{
DataBindingInfoCollection newBindings = new DataBindingInfoCollection();
IReferenceService service = (IReferenceService)GetService(
typeof(IReferenceService));
object[] references = service.GetReferences(typeof(Control));
foreach(Control control in references){
if ((control.NamingContainer == null) ||
(control.NamingContainer.GetType() != typeof(Page))) continue;
IDataBindingsAccessor dba = (IDataBindingsAccessor)control;
if (dba.HasDataBindings){
foreach (DataBinding db in dba.DataBindings){
DataBindingInfo dbi = GetBindingInformation(db, control);
UpdateDataBindingInfo(dbi, bindingManager.DataBindings);
newBindings.Add(dbi);
}
}
}
if (CheckBindingChanges(bindingManager.DataBindings, newBindings)){
RaiseComponentChanging(null);
bindingManager.DataBindings.Clear();
foreach(DataBindingInfo dbi in newBindings){
bindingManager.DataBindings.Add(dbi);
}
RaiseComponentChanged(null, null, null);
}
}
We need to make available the data binding information to our WebForm in run time so we serialize that information to code thanks to a custom TypeConverter
.
The class diagram is:
The actual data binding is implemented in two methods in our BindingManager
: BindToWebForm
and BindFromWebForm
. BindToWebForm
is easy to implement, because it uses the DataBinding architecture of ASP.NET so we have nearly all the work done. BindFromWebForm
is more complex, because it has to iterate through the data bindings, and for each databinding, get the control's property type and compare it with the destination property. If they match, then it can perform the data binding. If not, it tries to convert from the source property type to the destination type before assigning the data. This conversion can be done successfully if the destination type has a TypeConverter
that does the job or if it has a static method called Parse
.
public void BindFromWebForm(Page form)
{
foreach (DataBindingInfo dbi in _dbic){
if (!dbi.TwoWay) continue;
object sourceProp = GetFieldOrPropertyEx(dbi.Control, dbi.PropControl);
if (sourceProp == null) throw new DataBindingException(
"DataBinding error, can't find: " + dbi.PropControl, dbi.Control);
string fullPropObject = dbi.Object;
if (dbi.PropObject != null){
fullPropObject += "." + dbi.PropObject;
}
Type sourcePropType = sourceProp.GetType();
Type destPropType = GetFieldOrPropertyTypeEx(form, fullPropObject);
if (destPropType == null){
throw new DataBindingException(
"DataBinding error, can't find: " + fullPropObject, dbi.Control);
}
if (!sourcePropType.Equals(destPropType)){
try {
sourceProp = ConvertTypes(sourceProp, destPropType);
} catch (Exception e){
throw new DataBindingException(
"DataBinding error, can't convert types: ", e, dbi.Control);
}
if (sourceProp == null){
throw new DataBindingException(
"DataBinding error, can't convert bound property: " +
dbi.PropControl + " for: " + fullPropObject, dbi.Control);
}
}
if (!SetFieldOrPropertyEx(form, fullPropObject, sourceProp)){
throw new DataBindingException("DataBinding error, can't set: "
+ fullPropObject, dbi.Control);
}
}
}
Notes
The actual implementation has some limitations:
- When the
BindingManager
parses the data bindings through the IDataBindingAccessor
interface it doesn't expect a complex data binding expression. For example: If you want to bind the full name of a customer to a label with something like <%# DataBinder.Eval(customer1, "Name") + " " + DataBinder.Eval(customer1, "Surname") %>
it will fail. You can do the same with a panel and two labels, each one with a simple data binding expression (Label1 with: <%# DataBinder.Eval(customer1, "Name") %>
and Label2 with <%# DataBinder.Eval(customer1, "Surname")) %>
- By design, if the bound controls are inside a
NamingContainer
(for example, if you have a template column in a DataGrid
), they don't get added to the binding collection of the BindingManager
, because controls like the DataGrid
aren't designed to support two way databinding. You can change this behaviour if you want
History
- v1.0. Initial Release.
- v1.1. Added support for int and string indexers and better type conversion.
Last comments
This is my first article and I don't have a lot of free time to write articles, so if you have enjoyed this one, please rate it and it will encourage me to write more.