|
|
| White Papers Home | White Papers | Message Board | Search | Products | Purchase | News | Web Log | |
![]()
Implementing two-way Data Binding
|
|
IwwWebDataControl Property |
Description |
|
BindingSourceObject |
The object that the control is bound to. This will be a DataSet, DataRow, DataTable/View or it could be a custom object on the form. Syntax can use . syntax like: Customer.DataRow. |
|
BindingSourceProperty |
This is the property or field that the data is bound to. |
|
BindingProperty |
This is the property of the control that the binding occurs against. |
|
DisplayFormat |
A format string that is compatible with String.Format() for the specified type. Example: {0c} for currency or {0:f2} for fixed 2 decimals |
|
UserFieldName |
Descriptive name of the field. Used if an error occurs to provide an error message. |
|
BindingErrorMessage |
Internally used value that gets set if a unbinding error occurs. Controls that have this set can optionally generate error information next to them. |
|
IwwWebDataControl Method |
Description |
|
BindData() |
Binds data to the control from the BindingSource |
|
UnbindData() |
Unbinds data back into the BindingSource |
If you look at the code for wwWebTextBox you’ll see that there really is nothing there except forwarding calls to wwWebDataHelper, which actually performs all the hard work of doing the .
wwWebDataHelper is a class with all static members. The class works essentially by using Reflection to evaluate the value in the data source and in the control and then assigning the value into one or the other depending on whether you are binding or unbinding. To help with the Reflection tasks there’s another helper class – wwUtils – which includes wrapper methods that do things like GetProperty, GetPropertyEx, SetProperty and SetPropertyEx. These methods use the PropertyInfo (or FieldInfo) classes to retrieve the values. The Ex versions provide a little more flexibility by allowing you to walk an object hierarchy and by retrieving and setting value further down the object chain. For example you can do:
wwUtils.SetProperty(this,"Customer.Address.Street","32 Kaiea")
which is lot more friendly than the 3 long Reflection calls you’d have manually write to get there. Let’s start with Control binding and unbinding which is shown in Listing 3.
Listing 3 – Binding a control with data from a datasource
public static void
ControlBindData(Page WebPage,
IwwWebDataControl ActiveControl) {
string BindingSourceObject = ActiveControl.BindingSourceObject;
string BindingSourceProperty = ActiveControl.BindingSourceProperty;
string BindingProperty = ActiveControl.BindingProperty;
try
{
if (BindingSourceObject == null || BindingSourceObject.Length == 0 ||
BindingSourceProperty == null || BindingSourceProperty.Length == 0)
return;
// *** Get a reference to the actual control source object
object loBindingSource = null;
loBindingSource = wwUtils.GetPropertyEx(WebPage,BindingSourceObject);
if (loBindingSource == null)
return;
// *** Retrieve the control source value
object loValue;
if (loBindingSource is System.Data.DataSet)
{
string lcTable = BindingSourceProperty.Substring(0,
BindingSourceProperty.IndexOf("."));
string lcColumn = BindingSourceProperty.Substring(
BindingSourceProperty.IndexOf(".")+1);
DataSet Ds = (DataSet) loBindingSource;
loValue = Ds.Tables[lcTable].Rows[0][lcColumn];
}
else if(loBindingSource is System.Data.DataRow)
{
DataRow Dr = (DataRow) loBindingSource;
loValue = Dr[BindingSourceProperty];
}
… DataTable, DataView omitted
else // we have a property
loValue = wwUtils.GetPropertyEx(loBindingSource,
BindingSourceProperty);
/// *** Figure out the type of the control we're binding to
object loBindValue = wwUtils.GetProperty(ActiveControl,
BindingProperty);
string lcBindingSourceType = loBindValue.GetType().Name;
if (loValue == null || loValue == DBNull.Value)
if (lcBindingSourceType == "String")
wwUtils.SetProperty(ActiveControl,BindingProperty,"");
else if (lcBindingSourceType == "Boolean")
wwUtils.SetProperty(ActiveControl,BindingProperty,false);
else
wwUtils.SetProperty(ActiveControl,BindingProperty,"");
else
{
if (lcBindingSourceType == "Boolean")
wwUtils.SetProperty(ActiveControl,BindingProperty,loValue);
else
{
if (wwUtils.Empty(ActiveControl.DisplayFormat))
wwUtils.SetProperty(ActiveControl,BindingProperty,
loValue.ToString());
else
wwUtils.SetProperty(ActiveControl,BindingProperty,
String.Format(ActiveControl.DisplayFormat,
loValue));
}
}
}
catch(Exception ex)
{
string lcException = ex.Message;
throw(new Exception("Can't bind " + ((Control) ActiveControl).ID );
}
}
The code starts by retrieving the BindingSourceObject and tries to get a reference to the object. If that works it retrieves the property string. At this point a check is performed on what type of object is being bound against, which determines where the data comes from. If it’s a DataSet – use the field of the first row of the table specified in the property string. If it’s DataRow use the field. If it’s an object use Reflection to retrieve the actual value.
Once we have a value we can then try and assign that value to the property specified in the BindingProperty. But before we can do that a few checks need to be made for the type of the property as well as checks for null values which would crash the controls if bound to. Yup this code actually automatically handles nulls by assigning empty values to display. The assignment of the value is done using Reflection again by using SetProperty(). Note that if a format string is provided the format is applied to the string as it’s written out.
The process of Unbinding a control is very similar – the same process in reverse as shown in Listing 4.
Listing 4 – Unbinding data from the control back into the data source.
public static void ControlUnbindData(Page WebPage,
IwwWebDataControl ActiveControl) {
string BindingSourceObject = ActiveControl.BindingSourceObject;
string BindingSourceProperty = ActiveControl.BindingSourceProperty;
string BindingProperty = ActiveControl.BindingProperty;
if (BindingSourceObject == null || BindingSourceObject.Length == 0 ||
BindingSourceProperty == null || BindingSourceProperty.Length == 0)
return;
object loBindingSource = null;
if (BindingSourceObject == "this" || BindingSourceObject.ToLower() == "me")
loBindingSource = WebPage;
else
loBindingSource = wwUtils.GetPropertyEx(WebPage,BindingSourceObject);
if (loBindingSource == null)
throw(new Exception("Invalid BindingSource"));
// Retrieve the new value from the control
object loValue = wwUtils.GetPropertyEx(ActiveControl,BindingProperty);
// Try to retrieve the type of the BindingSourceProperty
string lcBindingSourceType;
string lcDataColumn = null;
string lcDataTable = null;
// *** figure out the type of the binding source by reading the value
if (loBindingSource is System.Data.DataSet) {
// *** Split out the datatable and column names
int lnAt = BindingSourceProperty.IndexOf(".");
lcDataTable = BindingSourceProperty.Substring(0,lnAt);
lcDataColumn = BindingSourceProperty.Substring(lnAt+1);
DataSet Ds = (DataSet) loBindingSource;
lcBindingSourceType =
Ds.Tables[lcDataTable].Columns[lcDataColumn].DataType.Name;
}
else if(loBindingSource is System.Data.DataRow) {
DataRow Dr = (DataRow) loBindingSource;
lcBindingSourceType = Dr.Table.Columns[BindingSourceProperty].DataType.Name;
}
else if (loBindingSource is System.Data.DataTable) {
DataTable dt = (DataTable) loBindingSource;
lcBindingSourceType = dt.Columns[BindingSourceProperty].DataType.Name;
}
else {
// *** It's an object property or field - get it
MemberInfo[] loInfo =
loBindingSource.GetType().GetMember(BindingSourceProperty,
wwUtils.MemberAccess);
if (loInfo[0].MemberType == MemberTypes.Field) {
FieldInfo loField = (FieldInfo) loInfo[0];
lcBindingSourceType = loField.FieldType.Name;
}
else {
PropertyInfo loField = (PropertyInfo) loInfo[0];
lcBindingSourceType = loField.PropertyType.Name;
}
}
// *** Convert the control value to the proper type
object loAssignedValue;
if ( lcBindingSourceType == "String")
loAssignedValue = loValue;
else if (lcBindingSourceType == "Int16")
loAssignedValue = Int16.Parse( (string) loValue, NumberStyles.Integer );
else if
(lcBindingSourceType == "Int32")
loAssignedValue = Int32.Parse( (string) loValue, NumberStyles.Integer );
else if
(lcBindingSourceType == "Int64")
loAssignedValue = Int32.Parse ( (string) loValue, NumberStyles.Integer)
else if (lcBindingSourceType == "Byte")
loAssignedValue = Convert.ToByte(loValue);
else if (lcBindingSourceType == "Decimal")
loAssignedValue = Decimal.Parse( (string) loValue,NumberStyles.Any);
else if (lcBindingSourceType == "Double")
loAssignedValue = Double.Parse( (string) loValue,NumberStyles.Any);
else if (lcBindingSourceType == "Boolean") {
loAssignedValue = loValue;
else if (lcBindingSourceType == "DateTime")
loAssignedValue = Convert.ToDateTime(loValue);
else // Not HANDLED!!!
throw(new Exception("Field Type not Handled by Data unbinding"));
/// Write the value back to the underlying object/data item
if (loBindingSource is System.Data.DataSet) {
DataSet Ds = (DataSet) loBindingSource;
Ds.Tables[lcDataTable].Rows[0][lcDataColumn] = loAssignedValue;
}
else if(loBindingSource is System.Data.DataRow) {
DataRow Dr = (DataRow) loBindingSource;
Dr[BindingSourceProperty] = loAssignedValue;
}
else if(loBindingSource is System.Data.DataTable) {
DataTable dt = (DataTable) loBindingSource;
dt.Rows[0][BindingSourceProperty] = loAssignedValue;
}
else if(loBindingSource is System.Data.DataView) {
DataView dv = (DataView) loBindingSource;
dv[0][BindingSourceProperty] = loAssignedValue;
}
else
wwUtils.SetPropertyEx(loBindingSource,BindingSourceProperty,loAssignedValue);
}
This code starts by retrieving the Control Source object and the value contained in the control held by the BindingProperty field. This is most likely the Text field, but could be anything the user specified, such as Checked for a CheckBox or SelectedValue for a ListBox or DropDownList. The ControlSource is also queried for its type by retrieving the current value. The type is needed so we can properly convert the type back into the type that the control source expects. This involves String to type conversion including the proper type parsing so you can use things like currency symbols for decimal values etc. The Parse method is quite powerful for this sort of stuff. Finally once the value has been converted Reflection is used one more time to set the value into the binding source field based on the type of object we’re dealing with. DataSets,Tables and Rows write to the Field collection, while objects and properties are written natively to the appropriate member.
These two methods are the core of the binding operations and they are fully self contained to bind back controls. This process lets us bind individual controls. These methods are then called by each control’s BindData() and UnbindData() methods respectively as shown in Listing 2.
The next thing we need to do is bind all the controls on a form so we don’t have to individually bind them. This is pretty easy in concept. We know all of our controls implement the IwwWebDataControl interface, so it’s fairly easy to walk the Web form’s Controls collection (and child collections) and look for any controls that implement the IwwWebDataControl interface and then call the BindData() method. Listings 5 and 6 show the FormBindData() and FormUnbindData() methods that do just that.
Listing 5 – Binding all controls on a form
static void FormBindData(Control Container, Page WebForm) {
// *** Drill through each control on the form
foreach( Control loControl in Container.Controls) {
// ** Recursively call down into any containers
if (loControl.Controls.Count > 0)
wwWebDataHelper.FormBindData(loControl, WebForm);
// ** only work on those that support interface
if (loControl is IwwWebDataControl ) {
IwwWebDataControl control = (IwwWebDataControl) loControl;
try {
//*** Call
the BindData method on the control
control.GetType().GetMethod("BindData",
wwUtils.MemberAccess).Invoke(control,
new object[1] { WebForm } );
}
catch(Exception) {
// *** Display Error info
try {
control.Text = "** Field binding Error **";
}
catch(Exception) {;}
}
}
}
}
As you can see FormBindData() runs through the controls collection and checks for the IwwWebControl interface. Note that this method is recursive and calls itself if it finds a container and drills into them. This makes sure the entire form databinds. When a control is found the BindData() method of the control is called dynamically using Reflection.
When an error occurs the Text of the control is set to Field binding error so you can immediately see the error without throwing an exception on the page. This is handy as you don’t get errors individually. This is likely to be a developer error – not a runtime error so this handling is actually preferable.
The unbinding works in a similar fashion as shown in Figure 6.
Listing 6 – Unbinding all controls into their datasource
public static BindingError[] FormUnbindData(Page WebForm)
{
BindingError[] Errors = null;
FormUnbindData(WebForm,WebForm,ref Errors);
return Errors;
}
static BindingError[] FormUnbindData(Control Container, Page WebForm,
ref BindingError[] Errors) {
// *** Drill through each of the controls
foreach( Control loControl in Container.Controls) {
// ** Recursively call down into containers
if (loControl.Controls.Count > 0)
FormUnbindData(loControl, WebForm,ref Errors);
if (loControl is IwwWebDataControl ) {
IwwWebDataControl control = (IwwWebDataControl) loControl;
try {
// *** Call the UnbindData method on the control
control.GetType().GetMethod("UnBindData",
wwUtils.MemberAccess).Invoke(control,
new object[1] { WebForm } );
}
catch(Exception ex) {
// *** Display Error info
try
{
BindingError loError = new BindingError();
control.BindingErrorMessage = loError.Message;
// … more error handling code here
if (Errors == null) {
Errors = new BindingError[1];
Errors[0] = loError;
}
else {
// *** Resize the array and assign Error
int lnSize = Errors.GetLength(0);
Array loTemp =
Array.CreateInstance(typeof(BindingError),
lnSize + 1);
Errors.CopyTo(loTemp,0);
loTemp.SetValue(loError,lnSize);
Errors = (BindingError[]) loTemp;
}
}
catch(Exception) {;} // ignore additional exceptions
}
}
}
return Errors;
}
This code is very similar to the FormBindData() method. The difference here is that we call the UnbindData method and that we deal with errors on unbinding differently. It’s much more likely that something goes wrong with binding back then binding as users can enter just about anything into a textbox like characters for numeric data or non data formats for date fields. This scenario throws an exception in the control’s bindback code which has handled here.
This method creates an array of BindingError objects which contains information about the error. You can configure custom binding error messages by setting a binding error message on the control (see Figure 4). Otherwise the following code assigns a generic error message to the property with this code (omitted in Figure 6):
Listing 7 – Assigning binding error messages when unbinding
BindingError loError = new BindingError();
if (wwUtils.Empty(control.BindingErrorMessage))
{
if ( control.UserFieldName != "")
loError.Message = "Invalid format for " + control.UserFieldName;
else
loError.Message = "Invalid format for " + loControl.ID.Replace("txt","");
}
else
loError.Message = control.BindingErrorMessage;
// *** Assign the error message to the control
// *** this will cause the control to render it
control.BindingErrorMessage = loError.Message;
loError.ErrorMsg = ex.Message;
loError.Source = ex.Source;
loError.StackTrace = ex.StackTrace;
loError.ObjectName = loControl.ID;
if (Errors == null)
{
Errors = new BindingError[1];
Errors[0] = loError;
}
else
{
// *** Resize the array and assign Error
int lnSize = Errors.GetLength(0);
Array loTemp = Array.CreateInstance(typeof(BindingError),lnSize + 1);
Errors.CopyTo(loTemp,0);
loTemp.SetValue(loError,lnSize);
Errors = (BindingError[]) loTemp;
}
This array of binding errors if any is returned from the Unbind operation. A couple of helper methods exist to turn the array into HTML. The code for the Inventory example we saw earlier then looks something like this:
…
BindingError[] Errors = wwWebDataHelper.FormUnbindData(this);
if (Errors != null)
{
this.ShowErrorMessage( wwWebDataHelper.BindingErrorsToHtml(Errors) );
return;
}
if (!Inventory.Save())
…
In addition each of the control contains some custom code to display error information as shown in Figure 5.

Figure 5 – Binding errors can be automatically flagged and converted into an HTML display (top).
The code that accomplishes that has a few dependencies that I’ve not had time to abstract away at this point so some of this is hardcoded into the control:
protected override void Render(HtmlTextWriter writer)
{
// *** Write out the existing control code
base.Render (writer);
// *** now append an error icon and ‘tooltip’
if (this.BindingErrorMessage != null && this.BindingErrorMessage != "" )
writer.Write(" <img src='images/warning.gif' alt='" +
this.BindingErrorMessage + "')'>");
}
As you can see it’s quite easy to add additional output to controls. This extensibility model is just very flexible and easy to work with.
While in the process of subclassing and dealing with data
binding it’s also useful to address some things that just don’t quite seem
to work right in ASP.Net. For example, listboxes do not persist their
SelectedValue unless you use ViewState, which is very annoying if you don’t
want to ship the content of your lists over the wire each time. This is
actually quite easy to fix with
override protected void OnLoad(EventArgs e)
{
base.OnLoad(e);
/// *** Handle auto-assigning of SelectedValue
/// *** so we don't need Viewstate to make this happen
if (!this.EnableViewState && this.Page.IsPostBack)
{
string lcValue = this.Page.Request.Form[this.ID];
if (lcValue != null)
this.SelectedValue = lcValue;
}
}
Voila, you no longer need Viewstate to postback the selected value.
Another problem I ran into on several admin forms is that Passwords in text boxes are not posted back to forms. This is possibly not a bad idea, but a problem when you really need to post a password back for admin purposes and you don’t want to have people keep retyping the password each time.
override protected void OnLoad(EventArgs e)
{
base.OnLoad(e);
// *** Post back password values as well - you can always clear manually
if (this.TextMode == TextBoxMode.Password)
this.Attributes.Add("value", this.Text);
}
Ok, all of this stuff probably sounds pretty good to you right about now. But be aware that there are a few limitations to what I’ve shown you so far.
None of these are show stoppers, but they are things you should be aware of before you take off on this path.
Although it’s such a downer that ASP.Net doesn’t include better data binding support natively, it also say a lot for the architecture that you can extend controls easily enough to provide this functionality with a relatively little amount of code. I suspect most serious developers end up subclassing the stock controls anyway and so adding this stuff in is only a small step anyway.
There’s a lot more that can be done with the basic extensions I’ve built here. For example it’d be real nice to build better input formatting into this stuff, providing things like InputMasks that could be handled client side. ASP.Net provides Validation controls, but again the design is generally more work than it needs to be. A single validation property would be very cool. In any case there are many extensions that would be useful, but I hope you find this base useful and something you can extend. If you end up enhancing this stuff please send me a line so I can check it out.
Next time around I’ll take a look at Windows Forms and how we can build simple data binding controls in much the same way as we did here to simplify behind in rich client applications.
As always you can reach me via email at rstrahl@west-wind.com or even better on our Message Board at http://www.west-wind.com/wwThreads/Default.asp?Forum=Code+Magazine.
Source Code for this article:
http://www.west-wind.com/presentations/ASPNET/ASPNET.zip
By Rick Strahl
Rick Strahl is president of West Wind Technologies on Maui, Hawaii. The company specializes in Web and distributed application development and tools with focus on Windows Server Products, .NET, Visual Studio and Visual FoxPro. Rick is author of West Wind Web Connection, a powerful and widely used Web application framework for Visual FoxPro and West Wind HTML Help Builder. He's also a Microsoft Most Valuable Professional, and a frequent contributor to magazines and books. He is co-publisher and co-editor of CoDe magazine, and his book, "Internet Applications with Visual FoxPro 6.0", is published by Hentzenwerke Publishing. For more information please visit: http://www.west-wind.com/ or contact Rick at rstrahl@west-wind.com.
in .Net
is not one of .Net’s strong features either in Web Forms or Windows Forms. Although Windows Forms provides a ton of flexibility and power for the implementation of the actual code in the final form can be messy and code intensive. Web Forms on the other hand are much less complete in their in that they provide only one-way binding. Both mechanisms can easily be extended by subclassing the existing form controls and adding the required functionality with relatively little amounts of code. For Windows forms this means little more than wrapping the existing functionality with some higher level wrappers at the control level that allow control level properties to be set. In Web Forms the binding is much more primitive and requires digging deeper by using Reflection to read and write values from the controls into the data source and vice versa.
Interfaces
Interfaces are a great mechanism for identifying a group of objects that all implement common functionality. Interfaces enforce a specific property/method/event/field interface that any class that implements it must implement at the compiler level. In addition, interfaces are great if you need to reference a specific type ‘generically’ as you can cast an object this generic interface reference. You can then easily determine whether an object is part of a specific group. This article uses one interface implementation extensively to describe the custom interfaces built ontop of the existing user interface controls and the interface is used extensively to differentiate standard controls from the custom controls.
Reflection
Reflection is .Net’s type discovery and access mechanism and it’s the key to making the custom binding and unbinding described in this article work. Reflection allows you to ‘evaluate’ string based property and method names and run the code necessary dynamically by discovering the names of methods and properties and evaluating them dynamically at runtime. This allows the ability to store the property and field names as strings and then dynamically bind and unbind data from the control in question. Reflection can be quite unwieldy, especially if your objects accessed are complex and multiple levels deep. This article includes a helper class (wwUtils) that greatly simplifies common Reflection tasks such as retrieving and setting properties and fields and drilling down multiple levels in an object’s internal hierarchy.
Format Strings
The ability to write out data in common display formats is something that almost every application needs to do and should provide this functionality natively. .Net supports simple formatting via the String.Format() method which is used behind the scenes for many formatting operations. Whenever you see syntax like {0:c} or {0:d} you’re likely looking at some functionality that behind the scenes is using String.Format(). This syntax means actually that the first parameter (parameter 0) is supposed to be converted into a specific format. The full syntax really is: String.Format("{0:c}",curValue);. This means that a format string often can be embellished with additional characters like this: String.Format("{0:c}% pure power",curValue); which is perfectly legal. The reverse of Format is the Parse method available on many of the type objects (like Int32, Decimal, Currence, DateTime etc.) which can take standard format expressions and parse them back into their underlying types. For example, Decimal.Parse("$3,000.12") understands the US currency syntax (if it’s running in the US version of Windows) and sets the value to 3000.12.
|
|
| White Papers Home | White Papers | Message Board | Search | Products | Purchase | News | |