Click here to Skip to main content
15,895,794 members
Articles / Web Development / ASP.NET

Beyond DataBinder

Rate me:
Please Sign up or sign in to vote.
4.56/5 (11 votes)
28 Feb 20058 min read 65.4K   1.1K   42   6
An alternative to the standard DataBinder that can handle missing data and validation failures

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 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.

Screen grab showing validation failures after the save button has been pressed

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.

Screen grab before any records have been added

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.NET
<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.

VB
'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.NET
<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.

Screen grab after the add button has been pressed

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.

VB
' 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
    ' Return the age of a person, given his date of birth
    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

    ' This function is an example of the sort of constraints that might be
    ' applied to data prior to it being committed to the database
    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.)

Screen grab showing validation failures after the save button has been pressed

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.NET
<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.NET
<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:

HTML
.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:

VB
' 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 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.


Written By
Web Developer
Ireland Ireland
Declan Brennan is Chief Architect for Sekos Technology (www.sekos.com). Over the years he has worked in a huge range of technologies and environments and still gets a great thrill from the magic of computers.

Comments and Discussions

 
Generali have difficult with save ... Pin
mostafa_h21-Nov-05 8:47
mostafa_h21-Nov-05 8:47 
GeneralRe: i have difficult with save ... Pin
Declan Brennan21-Nov-05 23:27
Declan Brennan21-Nov-05 23:27 
GeneralRe: i have difficult with save ... Pin
mostafa_h22-Nov-05 1:04
mostafa_h22-Nov-05 1:04 
Generalgreat stuff and extremely useful Pin
olegk6531-Oct-05 11:00
olegk6531-Oct-05 11:00 
GeneralGreat!!! Rich UI with rapid validations. Pin
BALAJI KULKARNI29-Sep-05 20:50
BALAJI KULKARNI29-Sep-05 20:50 
GeneralThanks for this....Yes C# please! Pin
magister21-Feb-05 23:57
magister21-Feb-05 23:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.