|
The DataBinder.Eval
method is a very handy way to achieve data binding in Web Forms
applications. Because it uses reflection, it is totally generic in the types of
data source that it can handle. Of course this versatility comes at a price.
The performance implications of reflection are often remarked upon. However
even more important to my mind is the inability to handle situations where the
data item doesn't exist or to gain access to ancillary information such as
validation error messages.
If we are willing to restrict ourselves to the use of DataSets
as our data source, we can enhance the functionality considerably allowing us
to produce rich UIs for handling validation errors such as the example below.

In this article, I'm assuming you have at least played with the Web Forms designer in Visual Studio and are familar with Web Forms binding expressions.
It is not uncommon in a UI to have a situation where the data being bound-to
does not exist. One example would be the picking of a customer for an
invoice where the system might automatically fill in various controls with
the address of that customer. Before the customer has been picked,
there would be no data for these controls to be bound
to. Unfortunately the standard DataBinder.Eval function will
generate an exception in such a situation.
There is a slightly more contrived example in the screen grab below where we are entering a set of Staff Information records. Initially there are no records so we would expect all the data entry controls to be either read-only or disabled.

Take for example the "Date Of Birth" TextBox. It has a Binding that
ensures ReadOnly is set whenever no row exists for the
data that it intends to display. This is done with a call to DataSetBinder.RowExists.
<asp:textbox id=TextBox2 style="Z-INDEX: 106; LEFT: 144px; POSITION: absolute; TOP: 160px"
runat="server"
Text='<%# DataSetBinder.Eval(WebForm1_Staff1,"Staff.DateOfBirth","{0:d}") %>'
ReadOnly='<%# Not DataSetBinder.RowExists(WebForm1_Staff1,"Staff") %>'
ToolTip='<%# DataSetBinder.GetError(WebForm1_Staff1,"Staff.DateOfBirth") %>'
CssClass='<%# IIf(DataSetBinder.HasError(WebForm1_Staff1,"Staff.DateOfBirth"),"error","")%>'
></asp:textbox>
The binding of the Text property using the DataSetBinder.Eval
method is very similar to what would be done with DataBinder with
even the paramaters being the same. However DataSetBinder.Eval return
the DefaultValue for any data that doesn't exist instead of
throwing an exception. It obtains this from the DataSet which in
turn derives it from the dataset schema. (The Binding of ToolTip and
CssClass is used for validation which is discussed below.)
The first parameter to Eval is the data source from which to start,
normally the DataSet, although it could be "Container" if the
control is inside a template. The second parameter uses a syntax similar to the
way data is addressed in a Windows Forms Binding. This parameter normally
starts with a table name followed by a series of child relationship names if
any and followed finally by the column name. However in the case where the
control is inside a template, it starts as you might expect with "DataItem".
Binding Expression for a Top Level Control:
Eval(DataSet,"{TableName} [.{RelationName}] .{ColumnName}")
Binding Expression for a Control inside a template:
Eval(Container,"DataItem [.{RelationName}] .{ColumnName}")
The parameters for RowExists are exactly the same as Eval
above. However the final .{ColumnName} is omitted from the second parameter,
because the whole row is being specified rather than a column within it.
As well as making data entry control read-only or disabled, buttons should be
disabled in cases where they require a row of data to exist. For example the Add
button for a master Staff Information record should obviously be enabled
always, whereas the Add button for a Training course detail should
not be enabled unless there is a master record to attach it to. As you might
expect this is handled by binding the Enabled property using another call to DataSetBinder.RowExists.
<asp:button id=Button10 style="Z-INDEX: 111; LEFT: 680px; POSITION: absolute; TOP: 272px"
runat="server"
Enabled='<%# DataSetBinder.RowExists(WebForm1_Staff1,"Staff") %>'CommandName="Add" Text="Add"
></asp:button>
When the user presses the Add button to add the first Staff Information record,
you can see how the various controls update their Enabled or ReadOnly
states in the screen grab below.

In real world applications, validation can often be quite complex. It may involve checking elaborate relationships between the data entered or looking up additional data not available on the client. For this reason, while it's worth doing some simple sanity checking on the client, I'm a big fan of doing the bulk of the validation when the attempt is made to save the users data to the data tier. ADO.NET has a mechanism for recording errors against specific columns in specific rows, so this is the obvious way to return validation to the client.
Below is a sample piece of validation for an instance of Staff Information. It does some of the usual things like checking for required data. However it also checks for overlaps in the dates of courses- an example of the more complex validation that is often neccessary.
' Return True if the two Date Ranges overlap
Private Function RangesOverlap(ByVal s1 As Date, ByVal e1 As Date, ByVal s2 As Date, ByVal e2
As Date)
If Date.Compare(s1, e2) <= 0 And Date.Compare(e1, s2) >= 0 Then
Return True
End If
Return False
End Function' This function is an example of the sort of constraints that might be applied
' to data prior to it being commited to the database
Private Function ValidateData() As System.BooleanDim dtStaff As DataTable = Me.WebForm1_Staff1.Tables("Staff")
Dim dvStaff As DataView = dtStaff.DefaultView()
For Each drv As DataRowView In dvStaff
drv.Row.ClearErrors()
If TypeOf drv("Name") Is DBNull Then
drv.Row.SetColumnError("Name", "Name is Required")
End If
If TypeOf drv("DateOfBirth") Is DBNull Then
drv.Row.SetColumnError("DateOfBirth", "Date Of Birth is Required")
ElseIf (System.DateTime.Today().
Subtract(CType(drv("DateOfBirth"), System.DateTime)).Days / 365 <= 15) Then
drv.Row.SetColumnError("DateOfBirth", "Staff must be over 15 in age")
End If
Dim dvTrainingCoursesTaken As DataView =
drv.CreateChildView("Staff_TrainingCoursesTaken")
Dim startDate(dvTrainingCoursesTaken.Count) As Date
Dim endDate(dvTrainingCoursesTaken.Count) As Date
Dim row(dvTrainingCoursesTaken.Count) As System.Data.DataRow
Dim i As Int16 = 0
For Each drv2 As DataRowView In dvTrainingCoursesTaken
Dim HasStartDate, HasEndDate As System.Boolean
If TypeOf drv2("CourseName") Is DBNull Then
drv2.Row.SetColumnError("CourseName", "Course Name is Required")
End If
If TypeOf drv2("StartDate") Is DBNull Then
drv2.Row.SetColumnError("StartDate", "Start Date is Required")
Else
startDate(i) = CType(drv2("StartDate"), DateTime).Date
HasStartDate = True
End If
If TypeOf drv2("EndDate") Is DBNull Then
drv2.Row.SetColumnError("EndDate", "End Date is Required")
Else
endDate(i) = CType(drv2("EndDate"), DateTime).Date
HasEndDate = True
End If
If (HasStartDate And HasEndDate) Then
row(i) = drv2.Row
End If
i = i + 1
Next
For i = 0 To dvTrainingCoursesTaken.Count
For j As Int16 = i + 1 To dvTrainingCoursesTaken.Count
If Not row(i) Is Nothing And Not row(j) Is Nothing And
RangesOverlap(startDate(i), endDate(i), startDate(j), endDate(j)) Then
row(i).SetColumnError("StartDate", "Course Dates cannot overlap")
row(i).SetColumnError("EndDate", "Course Dates cannot overlap")
row(j).SetColumnError("StartDate", "Course Dates cannot overlap")
row(j).SetColumnError("EndDate", "Course Dates cannot overlap")
End If
Next
Next
Next
Return Me.WebForm1_Staff1.HasErrors
End Function
It is very important for a good client experience to return all validation errors together. However it is not enough to return all the errors in one big bunch for the user to wade through. For ease of use it is important to associate the error message with the control containing the data in error. In complex UIs displaying error messages against each control can be messy and confusing. One obvious solution to this is the highlight the control in error and show a tool tip with the error message when the user moves the mouse across the control.
This can be seen in the screen grab below with the "Course Dates cannot overlap" error message. (By the way, in case there's any confusion when looking at this, my local date format is dd/mm/yyyy rather than month first.)
To achieve this result, first we need to bind the ToolTip property
for each control to an expression involving the GetError method.
This method has the same parameters as the Eval method, but
it returns the associated error message rather than the data. It can be
seen in two forms below. Firstly we have an example of the top-level "Date
Of Birth" TextBox where the first parameter to DataSetBinder.GetError is
the DataSet and the second parameter is a dot delimited path
that starts with the TableName and finishes with ColumnName. (In this case we
are specifying a column in the master table so no interveening relationship
names are required.)
<asp:textbox id=TextBox2 style="Z-INDEX: 106; LEFT: 144px; POSITION: absolute; TOP: 160px"
runat="server"
Text='<%# DataSetBinder.Eval(WebForm1_Staff1,"Staff.DateOfBirth","{0:d}") %>'
ReadOnly='<%# Not DataSetBinder.RowExists(WebForm1_Staff1,"Staff") %>'
ToolTip='<%# DataSetBinder.GetError(WebForm1_Staff1,"Staff.DateOfBirth") %>'
CssClass='<%# IIf(DataSetBinder.HasError(WebForm1_Staff1,"Staff.DateOfBirth"),"error","")%>'
></asp:textbox>
Secondly, here's an example of the "Course Start Date" which is included in a
template. Here (just like Eval), the first parameter is Container
and the second parameter starts with "DataItem." and finishes with the
ColumnName. (Again we don't need interveening relationship names in this
particular example.)
<asp:TextBox id=TextBox5 runat="server" Width="130px"
Text='<%# DataSetBinder.Eval(Container,"DataItem.StartDate","{0:d}") %>'
ToolTip='<%# DataSetBinder.GetError(Container,"DataItem.StartDate") %>'
CssClass='<%# IIf(DataSetBinder.HasError(Container,"DataItem.StartDate"),"error","")%>'
></asp:textbox>
(As an aside notice that the ReadOnly property was not bound
for this TextBox. This is because the row will always exist in this case -
otherwise the line would not be generated by the DataList.)
Of course the user won't know which controls have an error message tool tip
unless we provide visual feedback. This is done by binding the CssClass
property to an expression involving the HasError method. As we
flag a validation error by setting the style of the apppropriate control to
"error" style, appropriate attributes must be set up for this style in our
stylesheet, for example:
.error { background-color:#FFC080; border:1px solid }
To achieve all the functionality described above, DataSetBinder only
needs a handfull of functions as shown below.
' Return the value of the specified item or the appropriate default if this item doesn't exist
Public Shared Function Eval(ByVal ds As System.Object, ByVal dataMember As System.String)
As System.Object
' Return a formatted value of the specified item or the appropriate default
' if this item doesn't exist
Public Shared Function Eval(ByVal ds As System.Object, ByVal dataMember As System.String,
ByVal format As System.String) As System.Object' Return True if the specified Row exists
Public Shared Function RowExists(ByVal ds As System.Object, ByVal dataMember As System.String)
As System.Boolean' Return True if the specified item has an error associated with it
Public Shared Function HasError(ByVal ds As System.Object, ByVal dataMember As System.String)
As System.Boolean' Return the Error Message associated with an item if any
Public Shared Function GetError(ByVal ds As System.Object, ByVal dataMember As System.String)
As System.String' Return True if the specified row has any errors associated with it
Public Shared Function RowHasErrors(ByVal ds As System.Object,
ByVal dataMember As System.String) As System.Boolean
Just like DataBinder, it has two versions of Eval- one for
unformatted and one for formatted data. As I mentioned these versions don't
employ reflection and return default values for situations where data does not
exist.
The RowExists method is handy in binding expressions for ReadOnly
or Enabled to prevent data input or inappropriate button clicking
in situations where a row does not exist to operate on.
Rich feedback on validation failures can be achieved by binding CssStyle
and ToolTip using the HasError and GetError
methods respectively.
Finally I have included a RowHasErrors method that is not
illustated in the example but could be used to flag the lines in a DataList
that have validation failures in situations where not all the data is displayed
in the line.
I could describe the implementation of these functions in detail but it probably simplest if you download the source and look at it directly. There is less than 150 lines including comments. What a nice surprise it is to achieve so much with so little.
A demonstration project for the full Staff Information example used in this
article. To keep things simple, this example simulates a data tier in an XmlDocument
where in reality Microsoft SQLServer or something similar would be employed. It
buffers all user input in the ViewState to prevent
potentially costly uneccessary trips to the database. All the
user input is validated and committed in one go when the Save button
is pressed.
If you want to go straight through to using DataSetBinder, you only
have to download the source for it and include it in your own project. You are
granted unrestricted rights to use this code however you want.
The code released with this article is based on a portion of the AgileStudio product, which extends Visual Studio. Interestingly it took me half an hour to put together the Staff Information example in AgileStudio and a day and a half to add all the extra code to make it standalone.
Check out the free evaluation at www.sekos.com which automatically maintains the bindings, datasets and SQL StoreProcs required for a specific user interface (for Windows or Web applications).
If you would like to see a version of DataSetBinder for CSharp,
please let me know. I would also like to further explore UIs for robust
data validation in a Web Forms environment.