Introduction
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 DataSet
s 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 familiar with Web Forms binding expressions.
Non Existence of Data
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 parameters being the same. However DataSetBinder.Eval
returns 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. This is 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
.
Eval(DataSet,"{TableName} [.{RelationName}] .{ColumnName}")
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.
Validation
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 user's 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 necessary.
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
Private Function Age(ByVal dob As Date) As Integer
Dim today As Date = Date.Today
Dim rc = today.Year - dob.Year - 1
If today.Month > dob.Month Or ((today.Month = dob.Month) _
And (today.Day >= dob.Day)) Then
rc = rc + 1
End If
Return rc
End Function
Private Function ValidateData() As System.Boolean
Dim 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 (Age(CType(drv("DateOfBirth"), _
System.DateTime).Date) <= 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 Not 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 to 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 intervening 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 intervening 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, you might 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 appropriate 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 }
Overview of DataSetBinder
To achieve all the functionality described above, DataSetBinder
only needs a handful of functions as shown below:
Public Shared Function Eval(ByVal ds As System.Object,
ByVal dataMember As System.String) As System.Object
Public Shared Function Eval(ByVal ds As System.Object,
ByVal dataMember As System.String, ByVal format As System.String)
As System.Object
Public Shared Function RowExists(ByVal ds As System.Object,
ByVal dataMember As System.String) As System.Boolean
Public Shared Function HasError(ByVal ds As System.Object,
ByVal dataMember As System.String) As System.Boolean
Public Shared Function GetError(ByVal ds As System.Object,
ByVal dataMember As System.String) As System.String
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 illustrated 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 is probably simplest if you download the source and look at it directly. There are less than 150 lines including comments. What a nice surprise it is to achieve so much with so little.
Points of Interest
At the beginning of this article, there is a link to download a demonstration project for the full Staff Information example. To keep things simple, this example simulates a data tier in an XmlDocument
where in reality, Microsoft SQL Server or something similar would be employed. It buffers all user input in the ViewState
to prevent potentially costly unnecessary trips to the database. All the user input is validated and committed in one go when the Save button is pressed.
Alternatively, 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 Sekos Technology Ltd. which automatically maintains the bindings, datasets and SQL StoreProcs required for a specific user interface (for Windows or Web applications).
Conclusion
If you would like to see a version of DataSetBinder
for C#, please let me know. I would also like to further explore UIs for robust data validation in a Web Forms environment.
Please register your interest in follow-ups, by voting for this article.
History
- 28th February, 2005: Initial version
License
This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below. A list of licenses authors might use can be found here.