73、数据绑定中的错误处理、验证与格式化

数据绑定中的错误处理、验证与格式化

1. 错误弹出框相关

在进行数据绑定时,红色弹出气球框用于显示错误信息。为了让其正常显示,文本框与浏览器窗口边缘之间需要有足够的空间。若文本框右侧有空间,气球框会出现在右侧;若没有,则出现在左侧。它会显示在同一位置的其他元素(如按钮或标签)之上,但不会超出浏览器窗口。若消息过长,部分内容会被截断。

将验证显示集成到控件模板有其便利之处,控件会处理视觉细节,开发者只需关注提供有用的错误消息。但缺点也很明显,如果想改变控件显示错误消息的方式(或完全禁用错误显示),就需要替换整个控件模板,且要包含所有其他无关的状态和标记细节。由于普通的控件模板通常很长,这个过程既繁琐又可能受限。

2. BindingValidationFailed 事件

错误通知能有效告知用户潜在问题,但在很多情况下,开发者需要对后续操作有更多控制,这时就需要拦截 BindingValidationFailed 事件并使用自定义代码。

要启用 BindingValidationFailed 事件,必须将 ValidatesOnExceptions 设置为 True 以检测错误,将 NotifyOnValidationError 设置为 True 以触发验证事件。示例代码如下:

<TextBox Margin="5" Grid.Row="2" Grid.Column="1" x:Name="txtUnitCost" 
  Text="{Binding UnitCost, Mode=TwoWay, ValidatesOnExceptions=True, 
NotifyOnValidationError=True}"></TextBox>

BindingValidationError 是一个冒泡事件,可在错误发生处(如文本框)或更高级别(如包含的 Grid )处理。在错误发生处处理能编写针对不同字段错误的特定错误处理逻辑;在更高级别处理则可对多种不同类型的错误重用相同的逻辑,示例代码如下:

<Grid Name="gridProductDetails" 
 BindingValidationError="Grid_BindingValidationError">

当问题发生时,开发者可以选择显示消息或更改应用程序部分外观, BindingValidationError 事件的强大之处在于还能执行其他操作,如更改焦点、重置错误值、尝试纠正错误或根据具体错误提供更详细的针对性帮助。以下是一个显示错误消息并将焦点返回给出错文本框的示例:

Private Sub Grid_BindingValidationError(ByVal sender As Object, _ 
  ByVal e As ValidationErrorEventArgs) 
    ' Display the error. 
    lblInfo.Text = e.Error.Exception.Message 
    lblInfo.Text &= Environment.NewLine & "The stored value is still: " & _ 
      (CType(gridProductDetails.DataContext, Product)).UnitCost 

    ' Suggest the user try again. 
    txtUnitCost.Focus() 
End Sub

BindingValidationError 事件仅在值更改且编辑提交时发生。对于文本框,直到文本框失去焦点才会触发。若想更快捕获错误,可使用 BindingExpression.UpdateSource() 方法在用户输入时强制立即更新。

3. Validation 类

无需等待 BindingValidationError 事件来检测无效数据,可随时使用 Validation 类的静态方法检查绑定控件。 Validation.GetHasErrors() 若控件验证失败则返回 True Validation.GetErrors() 返回相应的异常对象集合。

这些方法提供了额外的灵活性,例如可检查 HasErrors() 并在存在无效数据时拒绝让用户继续到新步骤或执行特定功能;也可在数据输入过程结束时使用 GetErrors() 汇总一系列错误,以便在一处提供详细的问题列表。

4. 创建具有内置验证的数据对象

早期,将验证逻辑构建到数据对象的最简单方法是为不良数据抛出异常,但这种方式存在风险。异常抛出代码可能会意外排除数据对象的合理使用情况。

大多数开发者倾向于使用其他技术标记无效数据。在 Silverlight 中,数据对象可通过实现 IDataErrorInfo INotifyDataErrorInfo 接口提供内置验证。这两个接口的目标相同,即用更温和的错误通知系统取代激进的未处理异常。 IDataErrorInfo 是最早的错误跟踪接口,为向后兼容而保留; INotifyDataErrorInfo 是 Silverlight 4 引入的类似但更丰富的接口,支持每个属性多个错误和丰富的错误对象等额外功能。

无论使用哪个接口,都需遵循以下基本步骤:
1. 在数据对象中实现 IDataErrorInfo INotifyDataErrorInfo 接口。
2. 告诉数据绑定基础结构检查数据对象的错误并显示错误通知。若使用 IDataErrorInfo 接口,将 ValidatesOnDataErrors 设置为 True ;若使用 INotifyDataErrorInfo 接口,将 ValidatesOnNotifyDataErrors 设置为 True
3. 若想接收 BindingValidationFailed 事件,可选择将 NotifyOnValidationError 设置为 True
4. 若想捕获其他类型的错误(如数据转换问题),可选择将 ValidatesOnExceptions 属性设置为 True

以下是使用 INotifyDataErrorInfo 接口检测 Product 对象问题的示例:

Public Class Product 
    Implements INotifyPropertyChanged, INotifyDataErrorInfo 
    ... 
End Class

INotifyDataErrorInfo 接口需要三个成员: ErrorsChanged 事件在添加或移除错误时触发; HasErrors 属性指示数据对象是否有错误; GetErrors() 方法提供完整的错误信息。

在实现这些方法之前,需要一种方式来跟踪代码中的错误,可使用私有集合:

Private errors As New Dictionary(Of String, List(Of String))()

为方便管理错误, Product 类添加了 SetErrors() ClearErrors() 两个私有方法:

Public Event ErrorsChanged As EventHandler(Of DataErrorsChangedEventArgs) _ 
  Implements INotifyDataErrorInfo.ErrorsChanged 

Private Sub SetErrors(ByVal propertyName As String, _ 
  ByVal propertyErrors As List(Of String)) 
    ' Clear any errors that already exist for this property. 
    errors.Remove(propertyName) 

    ' Add the list collection for the specified property. 
    errors.Add(propertyName, propertyErrors) 

    ' Raise the error-notification event. 
    RaiseEvent ErrorsChanged(Me, New DataErrorsChangedEventArgs(propertyName)) 
End Sub 

Private Sub ClearErrors(ByVal propertyName As String) 
    ' Remove the error list for this property. 
    errors.Remove(propertyName) 

    ' Raise the error-notification event. 
    RaiseEvent ErrorsChanged(Me, New DataErrorsChangedEventArgs(propertyName)) 
End Sub

以下是确保 Product.ModelNumber 属性只能包含字母和数字的错误处理逻辑:

Private _modelNumber As String 
Public Property ModelNumber() As String 
    Get 
        Return _modelNumber 
    End Get 
    Set(ByVal value As String) 
        _modelNumber = value 

        Dim valid As Boolean = True 
        For Each c As Char In _modelNumber 
            If (Not Char.IsLetterOrDigit(c)) Then 
                valid = False 
                Exit For 
            End If 
        Next 
        If (Not valid) Then 
            Dim errors As List(Of String) = New List(Of String)() 
            errors.Add("The ModelNumber can only contain letters and numbers.") 
            SetErrors("ModelNumber", errors) 
        Else 
            ClearErrors("ModelNumber") 
        End If 

        OnPropertyChanged(New PropertyChangedEventArgs("ModelNumber")) 
    End Set 
End Property

最后实现 GetErrors() HasErrors() 方法:

Public Function GetErrors(ByVal propertyName As String) As IEnumerable _ 
  Implements System.ComponentModel.INotifyDataErrorInfo.GetErrors 

    If propertyName = "" Then 
        ' Provide all the error collections. 
        Return (errors.Values) 
    Else 
        ' Provice the error collection for the requested property 
        ' (if it has errors). 
        If errors.ContainsKey(propertyName) Then 
            Return (errors(propertyName)) 
        Else 
            Return Nothing 
        End If 
    End If 
End Function 

Public ReadOnly Property HasErrors As Boolean _ 
  Implements INotifyDataErrorInfo.HasErrors 

    Get 
        ' Indicate whether this entire object is error-free. 
        Return (errors.Count > 0) 
    End Get 
End Property

要让 Silverlight 使用 INotifyDataErrorInfo 接口并在属性修改时检查错误,绑定的 ValidatesOnNotifyDataErrors 属性必须为 True

<TextBox Margin="5" Grid.Row="2" Grid.Column="1" x:Name="txtModelNumber" 
  Text="{Binding ModelNumber, Mode=TwoWay, ValidatesOnNotifyDataErrors=True, 
NotifyOnValidationError=True}"></TextBox>

若使用 NotifyOnValidationError 事件,当错误集合更改时 BindingValidationError 事件将触发,但 ValidationErrorEventArgs.Exception 属性不会提供异常对象,可通过 ValidationErrorEventArgs.Error.ErrorContent 属性获取从数据对象返回的错误信息。

可以将抛出异常和使用 IDataErrorInfo INotifyDataErrorInfo 接口的方法结合使用,但要注意,触发异常时数据对象的属性不会更新;而使用接口时,允许无效值但会标记,数据对象会更新,可使用通知和 BindingValidationFailed 事件通知用户。

5. 数据格式化和转换

在基本绑定中,信息从源到目标不做任何更改,但这并非总是所需的行为。数据源可能使用低级表示,不适合直接在用户界面显示,如数字代码需替换为可读字符串、数字需缩小显示、日期需长格式显示等。这时就需要将这些值转换为正确的显示形式,若使用双向绑定,还需将用户输入的数据转换为适合存储在数据对象中的表示形式。

Silverlight 提供了两个工具来帮助实现:
- 字符串格式化 :通过设置 Binding.StringFormat 属性,可将以文本形式表示的数据(如包含日期和数字的字符串)进行转换,适用于至少一半的格式化任务。
- 值转换器 :功能更强大但稍复杂,可将任何类型的源数据转换为任何类型的对象表示形式,然后传递给关联的控件。

6. 字符串格式化

字符串格式化是将数字显示为文本的理想工具。例如, Product 类的 UnitCost 属性存储为小数,在文本框中显示时可能会有过多小数位且缺少货币符号。可通过设置 Binding.StringFormat 属性解决此问题,Silverlight 会在控件显示前使用格式字符串将原始文本转换为显示值,在大多数情况下也会使用该字符串进行反向转换,以更新绑定属性。

设置 Binding.StringFormat 属性时,使用标准的 .NET 格式字符串。以下是应用货币格式字符串到 UnitCost 字段的示例:

<TextBox Margin="5" Grid.Row="2" Grid.Column="1" 
 Text="{Binding UnitCost, StringFormat='C' }"> 
</TextBox>

要使用 StringFormat 属性获得所需结果,需要正确的格式字符串。常见的数字和日期值格式字符串如下表所示:

类型 格式字符串 示例
货币 C $1,234.50 ,负数用括号表示:($1,234.50) ,货币符号与区域设置有关
科学(指数) E 1.234.50E+004
百分比 P 45.6%
固定小数 F? 取决于设置的小数位数,F3 格式化为 123.400 ,F0 格式化为 123
类型 格式字符串 格式
短日期 d M/d/yyyy ,例如:10/30/2008
长日期 D dddd, MMMM dd, yyyy ,例如:Wednesday, January 30, 2008
长日期和短时间 f dddd, MMMM dd, yyyy HH:mm aa ,例如:Wednesday, January 30, 2008 10:00 AM
长日期和长时间 F dddd, MMMM dd, yyyy HH:mm:ss aa ,例如:Wednesday, January 30, 2008 10:00:23 AM
ISO 可排序标准 s yyyy-MM-dd HH:mm:ss ,例如:2008-01-30 10:00:23
月和日 M MMMM dd ,例如:January 30

以下是使用自定义日期格式字符串格式化 OrderDate 属性的绑定表达式:

<TextBlock Text="{Binding Date, StringFormat='0:MM/dd/yyyy' }"></TextBlock>

数据绑定中的错误处理、验证与格式化

7. 值转换器

值转换器是一种更为强大且灵活的工具,它能够将任意类型的源数据转换为任意类型的目标对象表示形式,然后传递给与之关联的控件。当字符串格式化无法满足需求时,值转换器就可以发挥重要作用。

要使用值转换器,需要创建一个实现 IValueConverter 接口的类。这个接口定义了两个方法: Convert ConvertBack Convert 方法用于将源数据转换为目标对象,而 ConvertBack 方法则用于将目标对象转换回源数据,适用于双向绑定的情况。

以下是一个简单的值转换器示例,用于将布尔值转换为字符串:

Imports System.Windows.Data

Public Class BooleanToStringConverter
    Implements IValueConverter

    Public Function Convert(ByVal value As Object, ByVal targetType As System.Type, ByVal parameter As Object, ByVal culture As System.Globalization.CultureInfo) As Object Implements IValueConverter.Convert
        If TypeOf value Is Boolean Then
            If CBool(value) Then
                Return "是"
            Else
                Return "否"
            End If
        End If
        Return value
    End Function

    Public Function ConvertBack(ByVal value As Object, ByVal targetType As System.Type, ByVal parameter As Object, ByVal culture As System.Globalization.CultureInfo) As Object Implements IValueConverter.ConvertBack
        If TypeOf value Is String Then
            If value.ToString() = "是" Then
                Return True
            ElseIf value.ToString() = "否" Then
                Return False
            End If
        End If
        Return value
    End Function
End Class

在 XAML 中使用这个值转换器的示例如下:

<Window.Resources>
    <local:BooleanToStringConverter x:Key="BooleanToStringConverter" />
</Window.Resources>
<TextBlock Text="{Binding IsEnabled, Converter={StaticResource BooleanToStringConverter}}" />

在这个示例中, IsEnabled 属性的布尔值会被转换为 “是” 或 “否” 字符串显示在 TextBlock 中。

8. 综合应用示例

为了更好地理解上述内容,下面给出一个综合应用示例,包含错误处理、验证、格式化和转换的完整场景。

假设我们有一个 Product 类,包含 UnitCost ModelNumber IsAvailable 等属性:

Public Class Product
    Implements INotifyPropertyChanged, INotifyDataErrorInfo

    Private _unitCost As Decimal
    Public Property UnitCost As Decimal
        Get
            Return _unitCost
        End Get
        Set(ByVal value As Decimal)
            _unitCost = value
            OnPropertyChanged("UnitCost")
            ValidateUnitCost()
        End Set
    End Property

    Private _modelNumber As String
    Public Property ModelNumber As String
        Get
            Return _modelNumber
        End Get
        Set(ByVal value As String)
            _modelNumber = value
            OnPropertyChanged("ModelNumber")
            ValidateModelNumber()
        End Set
    End Property

    Private _isAvailable As Boolean
    Public Property IsAvailable As Boolean
        Get
            Return _isAvailable
        End Get
        Set(ByVal value As Boolean)
            _isAvailable = value
            OnPropertyChanged("IsAvailable")
        End Set
    End Property

    Private errors As New Dictionary(Of String, List(Of String))()

    Public Event ErrorsChanged As EventHandler(Of DataErrorsChangedEventArgs) Implements INotifyDataErrorInfo.ErrorsChanged

    Private Sub SetErrors(ByVal propertyName As String, ByVal propertyErrors As List(Of String))
        errors.Remove(propertyName)
        errors.Add(propertyName, propertyErrors)
        RaiseEvent ErrorsChanged(Me, New DataErrorsChangedEventArgs(propertyName))
    End Sub

    Private Sub ClearErrors(ByVal propertyName As String)
        errors.Remove(propertyName)
        RaiseEvent ErrorsChanged(Me, New DataErrorsChangedEventArgs(propertyName))
    End Sub

    Private Sub ValidateUnitCost()
        Dim errorList As New List(Of String)()
        If UnitCost < 0 Then
            errorList.Add("UnitCost 不能为负数。")
        End If
        If errorList.Count > 0 Then
            SetErrors("UnitCost", errorList)
        Else
            ClearErrors("UnitCost")
        End If
    End Sub

    Private Sub ValidateModelNumber()
        Dim valid As Boolean = True
        For Each c As Char In _modelNumber
            If Not Char.IsLetterOrDigit(c) Then
                valid = False
                Exit For
            End If
        Next
        Dim errorList As New List(Of String)()
        If Not valid Then
            errorList.Add("ModelNumber 只能包含字母和数字。")
        End If
        If errorList.Count > 0 Then
            SetErrors("ModelNumber", errorList)
        Else
            ClearErrors("ModelNumber")
        End If
    End Sub

    Public Function GetErrors(ByVal propertyName As String) As IEnumerable Implements INotifyDataErrorInfo.GetErrors
        If propertyName = "" Then
            Return errors.Values
        Else
            If errors.ContainsKey(propertyName) Then
                Return errors(propertyName)
            Else
                Return Nothing
            End If
        End If
    End Function

    Public ReadOnly Property HasErrors As Boolean Implements INotifyDataErrorInfo.HasErrors
        Get
            Return errors.Count > 0
        End Get
    End Property

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
    Private Sub OnPropertyChanged(ByVal propertyName As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    End Sub
End Class

在 XAML 中,我们可以这样绑定这些属性,并应用格式化和验证:

<Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:YourNamespace"
        Title="Product Details" Height="350" Width="525">
    <Window.Resources>
        <local:BooleanToStringConverter x:Key="BooleanToStringConverter" />
    </Window.Resources>
    <Grid Name="gridProductDetails" BindingValidationError="Grid_BindingValidationError">
        <Label Content="Unit Cost:" Margin="10,10,0,0" VerticalAlignment="Top" HorizontalAlignment="Left" />
        <TextBox Margin="100,10,0,0" Grid.Row="0" Grid.Column="1" x:Name="txtUnitCost"
                 Text="{Binding UnitCost, Mode=TwoWay, ValidatesOnExceptions=True, NotifyOnValidationError=True, StringFormat='C'}" />
        <Label Content="Model Number:" Margin="10,40,0,0" VerticalAlignment="Top" HorizontalAlignment="Left" />
        <TextBox Margin="100,40,0,0" Grid.Row="1" Grid.Column="1" x:Name="txtModelNumber"
                 Text="{Binding ModelNumber, Mode=TwoWay, ValidatesOnNotifyDataErrors=True, NotifyOnValidationError=True}" />
        <Label Content="Is Available:" Margin="10,70,0,0" VerticalAlignment="Top" HorizontalAlignment="Left" />
        <TextBlock Margin="100,70,0,0" Text="{Binding IsAvailable, Converter={StaticResource BooleanToStringConverter}}" />
        <TextBlock x:Name="lblInfo" Margin="10,100,0,0" VerticalAlignment="Top" HorizontalAlignment="Left" />
    </Grid>
</Window>

在代码背后,处理 BindingValidationError 事件:

Public Class MainWindow
    Private Sub Grid_BindingValidationError(ByVal sender As Object, ByVal e As ValidationErrorEventArgs)
        If e.Error.Exception IsNot Nothing Then
            lblInfo.Text = e.Error.Exception.Message
        Else
            lblInfo.Text = e.Error.ErrorContent.ToString()
        End If
        If TypeOf e.OriginalSource Is TextBox Then
            CType(e.OriginalSource, TextBox).Focus()
        End If
    End Sub
End Class

在这个示例中, UnitCost 属性应用了字符串格式化,显示为货币格式; ModelNumber 属性使用了 INotifyDataErrorInfo 接口进行验证; IsAvailable 属性使用了值转换器将布尔值转换为字符串显示。同时,通过 BindingValidationError 事件处理错误,当出现错误时会在 lblInfo 中显示错误信息,并将焦点转移到出错的文本框。

9. 总结

数据绑定是开发过程中的重要环节,而错误处理、验证、格式化和转换则是确保数据绑定能够正确、高效工作的关键因素。通过合理运用这些技术,可以提高用户体验,减少错误发生的概率,使应用程序更加健壮和易用。

  • 错误弹出框的使用需要注意空间问题,并且修改其显示方式可能会比较繁琐。
  • BindingValidationFailed 事件可以让开发者对错误处理有更多的控制权,结合 Validation 类可以随时检查控件的验证状态。
  • 创建具有内置验证的数据对象可以通过实现 IDataErrorInfo INotifyDataErrorInfo 接口来实现,避免了简单抛出异常的弊端。
  • 字符串格式化和值转换器为数据的显示和存储提供了强大的转换功能,根据不同的需求选择合适的方法可以更好地满足应用程序的要求。

在实际开发中,要根据具体的场景和需求,灵活运用这些技术,不断优化数据绑定的过程,提升应用程序的质量。

下面是一个简单的 mermaid 流程图,展示了数据绑定中验证和转换的基本流程:

graph TD;
    A[数据源] --> B[绑定到控件];
    B --> C{是否需要验证};
    C -- 是 --> D[执行验证逻辑];
    D -- 验证通过 --> E{是否需要转换};
    D -- 验证失败 --> F[显示错误信息];
    C -- 否 --> E;
    E -- 是 --> G[使用格式化或转换器];
    E -- 否 --> H[直接显示数据];
    G --> H;

通过这个流程图,可以更清晰地看到数据从源到显示在控件上的整个过程,以及验证和转换在其中的作用。

总之,掌握数据绑定中的错误处理、验证、格式化和转换技术,对于开发高质量的应用程序至关重要。希望本文的内容能够帮助开发者更好地理解和运用这些技术。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值