65.9K
CodeProject 正在变化。 阅读更多。
Home

Beyond DataBinder

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.56/5 (10投票s)

2005 年 2 月 17 日

8分钟阅读

viewsIcon

66473

downloadIcon

1133

标准 DataBinder 的替代品,可以处理缺失数据和验证失败。

引言

DataBinder.Eval 方法是在 Web Forms 应用程序中实现数据绑定的一个非常便捷的方法。因为它使用了反射,所以它可以处理各种类型的数据源。当然,这种通用性是有代价的。反射的性能影响经常被提及。然而,在我看来,更重要的是它无法处理数据项不存在的情况,也无法访问诸如验证错误消息之类的辅助信息。

如果我们愿意将数据源限制在 `DataSet`,我们可以大大增强其功能,从而生产出用于处理验证错误的丰富 UI,如下面的示例所示。

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

在本文中,我假设你至少已经接触过 Visual Studio 中的 Web Forms 设计器,并且熟悉 Web Forms 绑定表达式。

数据不存在

在 UI 中,数据绑定时数据不存在的情况并不少见。一个例子是为发票选择客户,系统可能会自动用该客户的地址填充各种控件。在客户被选中之前,这些控件将没有数据可供绑定。不幸的是,标准的 DataBinder.Eval 函数在这种情况下会生成一个异常。

下面的屏幕截图有一个稍微牵强一点的例子,我们在其中输入一组员工信息记录。最初,没有记录,所以我们期望所有数据录入控件都设置为只读或禁用。

Screen grab before any records have been added

以出生日期文本框为例。它有一个绑定,可以确保在没有要显示的数据行时 ReadOnly 属性被设置。这是通过调用 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>

使用 DataSetBinder.Eval 方法绑定 Text 属性与使用 DataBinder 的方式非常相似,甚至参数也相同。但是,DataSetBinder.Eval 会为不存在的任何数据返回 DefaultValue,而不是抛出异常。它从 DataSet 获取此值,而 DataSet 又从数据集模式派生出此值。(ToolTipCssClass 的绑定用于验证,下面将进行讨论。)

传递给 Eval 的第一个参数是起始数据源。通常是 DataSet,尽管如果控件在模板内部,它也可以是 Container。第二个参数使用的语法类似于 Windows Forms 绑定中寻址数据的方式。该参数通常以表名开头,后跟一系列子关系名称(如果有),最后是列名。但是,如果控件在模板内部,它会像你期望的那样以 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}")

RowExists 的参数与上面的 Eval 完全相同。但是,第二个参数中省略了最后的 .{ColumnName},因为指定的是整行而不是其中的某个列。

除了将数据录入控件设置为只读或禁用外,在需要数据行存在的场景下,按钮也应该被禁用。例如,主员工信息记录的 **添加** 按钮显然应该始终启用,而培训课程明细的 **添加** 按钮,只有在存在主记录可附加时才应该启用。正如你可能预期的那样,这是通过调用 DataSetBinder.RowExists 来绑定 Enabled 属性来处理的。

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

当用户按下 **添加** 按钮以添加第一条员工信息记录时,你可以看到下面的屏幕截图是如何更新各种控件的 EnabledReadOnly 状态的。

Screen grab after the add button has been pressed

验证

在实际应用中,验证往往会非常复杂。它可能涉及检查输入数据之间复杂的关联,或者查找客户端上不可用的额外数据。因此,虽然在客户端进行一些简单的健全性检查是值得的,但我更倾向于在用户尝试将数据保存到数据层时进行大部分验证。ADO.NET 有一种机制可以针对特定行的特定列记录错误,因此这是将验证返回到客户端的明显方法。

下面是员工信息实例的一个示例验证。它执行一些常规操作,例如检查必需的数据。然而,它还检查课程日期是否存在重叠——这是经常需要的更复杂验证的一个例子。

' 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

对于良好的客户端体验来说,一次性返回所有验证错误非常重要。然而,仅仅将所有错误打包在一起让用户去查看是不够的。为了方便使用,将错误消息与包含错误数据的控件关联起来很重要。在复杂的 UI 中,在每个控件旁边显示错误消息可能会显得混乱和令人困惑。一个显而易见的解决方案是突出显示有错误的控件,并在用户将鼠标悬停在控件上时显示带有错误消息的工具提示。

在下面的屏幕截图中可以看到“课程日期不能重叠”的错误消息。(顺便说一下,以防你在查看时感到困惑,我的本地日期格式是 dd/mm/yyyy,而不是月份在前。)

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

为了实现这个结果,我们首先需要将每个控件的 ToolTip 属性绑定到一个涉及 GetError 方法的表达式。此方法具有与 Eval 方法相同的参数,但它返回关联的错误消息而不是数据。它在下面有两种形式。首先,我们有一个顶层 **出生日期** 文本框的示例,其中 DataSetBinder.GetError 的第一个参数是 DataSet,第二个参数是点分隔的路径,以 TableName 开头并以 ColumnName 结尾。(在这种情况下,我们指定的是主表中的一个列,因此不需要中间的关系名称。)

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

其次,这里有一个包含在模板中的 **课程开始日期** 的示例。这里(就像 Eval 一样),第一个参数是 Container,第二个参数以 DataItem 开头并以 ColumnName 结尾。(同样,在此特定示例中,我们不需要中间的关系名称。)

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

(顺便说一句,你可能会注意到 ReadOnly 属性没有为此 textbox 绑定。这是因为在这种情况下,行总是存在的——否则,该行将不会由 DataList 生成。)

当然,用户不会知道哪些控件有错误消息工具提示,除非我们提供视觉反馈。这是通过将 CssClass 属性绑定到一个涉及 HasError 方法的表达式来实现的。通过将适当控件的样式设置为错误样式来标记验证错误,我们必须在样式表中为该样式设置相应的属性,例如

.error { background-color:#FFC080; border:1px solid }

DataSetBinder 概述

为了实现上面描述的所有功能,DataSetBinder 只需少量函数,如下所示

' 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

DataBinder 一样,它有两个版本的 Eval — 一个用于未格式化数据,一个用于已格式化数据。正如我提到的,这些版本不使用反射,并在数据不存在的情况下返回默认值。

RowExists 方法在绑定表达式中对于 ReadOnlyEnabled 非常有用,可以防止在不存在要操作的行的情况下进行数据输入或不当的按钮点击。

通过分别使用 HasErrorGetError 方法绑定 CssStyleToolTip,可以实现对验证失败的丰富反馈。

最后,我包含了一个 RowHasErrors 方法,虽然在示例中没有演示,但如果数据未在行中完全显示,则可用于标记 DataList 中有验证失败的行。

我可以详细描述这些函数的实现,但最简单的方法可能是下载源代码直接查看。代码行数不到 150 行,包括注释。用这么少的代码实现这么多功能真是惊喜。

关注点

本文开头有一个链接,可以下载完整的员工信息示例的演示项目。为了保持简单,这个示例在 XmlDocument 中模拟了一个数据层,而在实际中,会使用 Microsoft SQL Server 或类似的数据库。它将所有用户输入缓冲在 ViewState 中,以防止潜在的昂贵且不必要的数据库往返。所有用户输入在按下 **保存** 按钮时进行验证并一次性提交。

或者,如果你想直接使用 DataSetBinder,只需下载其源代码并将其包含在自己的项目中即可。你可以不受限制地使用此代码,无论你想如何使用。

本文随附的代码基于 AgileStudio 产品的一部分,该产品扩展了 Visual Studio。有趣的是,我在 AgileStudio 中花了一个半小时构建了员工信息示例,然后花了一天半的时间添加了所有额外的代码使其独立。

查看 Sekos Technology Ltd. 的免费评估版,它会自动维护特定用户界面(Windows 或 Web 应用程序)所需的绑定、数据集和 SQL 存储过程:Sekos Technology Ltd.

结论

如果您想看到 DataSetBinder 的 C# 版本,请告诉我。我还想在 Web Forms 环境中进一步探索用于稳健数据验证的 UI。

请通过投票支持本文来注册您对后续内容的兴趣。

历史

  • 2005年2月28日:初始版本

许可证

本文没有明确的许可证附带,但可能在文章文本或下载文件中包含使用条款。如有疑问,请通过下方的讨论区联系作者。您可以在此处找到作者可能使用的许可证列表。

© . All rights reserved.