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

使用反射绑定对象数据到数据源

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.54/5 (14投票s)

2005年5月26日

9分钟阅读

viewsIcon

71436

downloadIcon

564

本文描述了可用于通过反射更新具有对象数据的数据源的类。

引言

我一直以来的一个困扰是,为了用数据源中的私有数据填充对象,你必须公开成员或创建一个类方法来接受数据并在内部用数据填充成员值。在前一种情况下,成员值会因开发人员的意外损坏而暴露,封装概念被破坏。在后一种情况下,数据与数据源耦合得太紧密。我想到,通过使用反射,我可以直接绑定到私有成员。

鉴于有这么多关于使用反射的文章,我惊讶地发现没有一篇与数据绑定相关。所以我决定自己编写类。在这个过程中,我开发了一个用于快速开发数据访问代码的框架的雏形。例如,使用这里提供的代码作为基础,我开发了一系列 DAL 对象,这些对象为对象数据的基本 C.R.U.D. 生成 SQL 命令。通过使用这个框架,为具有新属性的对象修改 DAL 就像为新属性添加一个属性一样简单。如果这篇文章引起了足够的兴趣,我将发布一篇“第二部分”文章来演示这一点。

警告!这是我的第一篇文章。我将尽力写出有用的东西。开始吧……

使用代码

如果你和我一样,你会直接跳到这篇文章的末尾,看看最终结果是否是你能用的。所以,我把结尾放在开头。这是一些从本文附带的示例项目中提取的代码片段。此代码片段用 DataRow (oRow) 中包含的值填充 Employee 类 (oLocalEmployee) 的实例。oBinderDataSourceBinder 类的一个实例(稍后会详细介绍这个类)。

oBinder.SetValues(oLocalEmployee, oRow)

一行代码。就是这样!这是从示例项目中获取的填充 DataRow 并包含来自对象的数据的代码。

oBinder.GetValuesIntoDataRow(oEmployee, oRowView.Row)

同样,只有一行代码。你可能会问,这怎么可能?继续阅读!

好的。我作弊了。Employee 类的成员用 DataField 属性进行了标记。此属性定义了成员在数据绑定期间如何处理,并应用于类的字段或属性,如下所示:

<DataField("EmployeeID")> Private iEmployeeID As Integer

DataSourceBinder 使用这些属性来构建一个名为 MemberBindingCollection 的集合,其中填充了(你猜对了)MemberBinding 实例。然后,DataSourceBinder 使用此 MemberBindingCollection 进行数据绑定。

实际上,DataSourceBinder 的方法是重载的,因此 MemberBindingCollection 可以作为其中一个参数。这样就可以绑定到一个没有成员使用 DataField 属性修饰的类。这完全将对象与数据源解耦,但将创建 MemberBindingCollection 的负担交给了程序员。

oBinder.SetValues(oLocalEmployee, oMemberBindings, oRow)

核心细节

让我们深入了解一些细节。

MemberBinding 类

整个框架的基本构建块是 MemberBinding 类。MemberBinding 类具有以下属性和方法:

<Serializable()>
Public Class MemberBinding

    Public ReadOnly Property Member() As MemberInfo
    Public ReadOnly Property FieldName() As String
    Public ReadOnly Property DefaultValue() As String
    Public ReadOnly Property DefaultValueType() As Type
    Public ReadOnly Property ConvertDefaultToDBNull() As Boolean
    Public ReadOnly Property ConvertDBNullToNull() As Boolean

    Public Function GetValue(ByVal obj As Object) As Object
    Public Sub SetValue(ByVal obj As Object, ByVal Value As Object)

End Class

Member 属性是必需的,包含一个 MemberInfo 实例。这是将由这个 MemberBinding 实例控制的字段或属性。

FieldName 属性是必需的,包含数据源中字段的名称。在大多数情况下,数据源将是 DataRow。但对此没有要求。

DefaultValue 属性是可选的。当存在时,它表示当数据源值为 DBNull 时将插入到成员中的默认值的 String 表示。如果您的成员无法处理 DBNull,则需要设置此值。

DefaultValueType 属性是可选的。它是 System.Type 的实例。通常,MemberMemberType 属性在内部用于将默认值转换为有用值。但是,如果您的 MemberTypeSystem.Object,则这是不可能的。在这种情况下将使用 DefaultValueType

ConvertDefaultToDBNull 设置为 True 时,当 Member 的值等于 DefaultValue 时,DBNull 将插入到数据源中。此属性默认为 False

ConvertDBNullToNull 设置为 True 时,数据源 DBNull 值在插入到 Member 的值之前会转换为 Nothing (null)。当 Member 的值为 Nothing (null) 时,在将值传输到数据源时,Member 值总是转换为 DBNull。此属性默认为 False

你会注意到所有这些属性都是 ReadOnly。它们是通过 MemberBinding 类的各种重载设置的。

GetValue 返回 Member 的值。该值将根据上面列出的属性设置进行转换。例如,假设您的 Member 代表一个 IntegerGetValue 将返回 MemberInteger 值。现在假设 DefaultValue 属性设置为“0”,并且 ConvertDefaultToDBNull 属性为 True。如果 Member 的值为 0,则 GetValue 将返回 DBNull。这是 GetValue 的代码:

Public Function GetValue(ByVal obj As Object) As Object
    
    Dim oMemberType As Type
    Dim oValue As Object
    Dim bIsIComparable As Boolean
    
    'Check for parameter validity.
    If obj Is Nothing Then Throw New ArgumentNullException("obj")
    If Not (Me.Member.DeclaringType Is obj.GetType) Then
        Throw New ArgumentException("The passed object is not of " &_
                 "the same type as the member's declaring type.", "obj")
    End If
    
    
    'Get the value from the member.
    If Member.MemberType = MemberTypes.Field Then
    
        'MemberInfo is a FieldInfo object. Get the data out of the 
        'FieldInfo and save it into the value.
        Dim oField As FieldInfo
        oField = DirectCast(Member, FieldInfo)
        oValue = oField.GetValue(obj)
    
        'Use the default value type for comparison and
        'conversion. If there is no default type then use the
        'member's type.
        oMemberType = Me.DefaultValueType
        If oMemberType Is Nothing Then
            oMemberType = oField.FieldType
        End If
    
    
    ElseIf Member.MemberType = MemberTypes.Property Then
    
        'MemberInfo is a PropertyInfo object. 
        'Get the data out of the 
        'PropertyInfo and save it into the value.
        Dim oProperty As PropertyInfo
        oProperty = DirectCast(Member, PropertyInfo)
        oValue = oProperty.GetValue(obj, Nothing)
    
        'Use the default value type for comparison and
        'conversion. If there is no default type then use the
        'member's type.
        oMemberType = Me.DefaultValueType
        If oMemberType Is Nothing Then
            oMemberType = oProperty.PropertyType
        End If
    
    End If
    
    'Check if the member type implements IComparable.
    bIsIComparable = 
        (Not oMemberType.GetInterface("System.IComparable") Is Nothing)
    
    'If the value is nothing then use the default value defined
    'in this MemberBinding instance.
    If (oValue Is Nothing) AndAlso (Not Me.DefaultValue Is Nothing) Then
        If Me.DefaultValueType Is Nothing Then
            oValue = Convert.ChangeType(Me.DefaultValue, oMemberType)
        Else
            oValue = 
               Convert.ChangeType(Me.DefaultValue, Me.DefaultValueType)
        End If
    End If
    
    
    'Check if this MemberBinding instances' ConvertDefaultToDBNull value
    'is set. If it is then check if the value is equal to the
    'default value. If so, then convert the value to DBNull.
    If Me.ConvertDefaultToDBNull Then
    
        'Default value is nothing...
        If Me.DefaultValue Is Nothing Then
    
            'If the current value is also nothing then convert to DBNull.
            If (oValue Is Nothing) = True Then
                oValue = DBNull.Value
            End If
    
        ElseIf Not IsDBNull(oValue) Then
    
            'The default value is *Not* nothing and *Not* DBNull...
            If bIsIComparable Then
    
                'If the current value is equal to the default value then
                'Convert it to DBNull.
                Dim oTestValue As Object = 
                         Convert.ChangeType(Me.DefaultValue, oMemberType)
                If CType(oTestValue, IComparable).CompareTo(oValue) = 0 Then
                    oValue = DBNull.Value
                End If
    
            End If
    
        End If
    
    End If
    
    'If the value is *STILL* nothing then convert it to DBNull.
    If oValue Is Nothing Then oValue = DBNull.Value
    
    'Return the value.
    Return oValue
    
End Function

SetValue 用于设置 Member 的值。与 GetValue 一样,最终进入 Member 的值取决于属性设置。这是 SetValue 的代码:

    Public Sub SetValue(ByVal obj As Object, ByVal Value As Object)

        Dim oField As FieldInfo
        Dim oProperty As PropertyInfo
        Dim oMemberType As Type

        'Catch exceptions so they can be wrapped into 
        'something more descriptive.
        Try

            'Check for parameter validity.
            If obj Is Nothing Then Throw New ArgumentNullException("obj")
            If Not (Me.Member.DeclaringType Is obj.GetType) Then
                Throw New ArgumentException("The passed object is not " &_
                    "of the same type as the member's declaring type.", "obj")
            End If

            If Member.MemberType = MemberTypes.Field Then
                'Member is a Field, Cast as a Field and get it's type.
                oField = DirectCast(Member, FieldInfo)
                oMemberType = oField.FieldType

            ElseIf Member.MemberType = MemberTypes.Property Then
                'Member is a Property, Cast as a Property and get it's type.
                oProperty = DirectCast(Member, PropertyInfo)
                oMemberType = oProperty.PropertyType

            End If

            If Value Is Nothing OrElse IsDBNull(Value) Then

                '*The passed value is nothing or DBNull.
                '*Check if there is a default value defined. If so,
                '*then replace Value with the value of the default value.
                If Not Me.DefaultValue Is Nothing Then

                    'First get the type that will be used for the conversion.
                    'If no default type is defined then the member's 
                    'type will be used.
                    Dim oDefaultType As Type = Me.DefaultValueType
                    If oDefaultType Is Nothing Then
                        oDefaultType = oMemberType
                    End If

                    Value = Convert.ChangeType(Me.DefaultValue, oDefaultType)
                    'bValueFound = True

                End If
            End If

            If IsDBNull(Value) _
            AndAlso Me.ConvertDBNullToNull Then

                'If the value is DBNull and the DataField attribute's
                'ConvertDBNullToNull property is True then change the
                'value to Nothing.
                Value = Nothing

            End If

            'Set the value of the member.
            If Member.MemberType = MemberTypes.Field Then
                If Not Value Is Nothing Then
                    oField.SetValue(obj, Convert.ChangeType(Value, _
                                                    oField.FieldType))
                Else
                    oField.SetValue(obj, Nothing)
                End If

            ElseIf Member.MemberType = MemberTypes.Property Then
                If Not Value Is Nothing Then
                    oProperty.SetValue(obj, Convert.ChangeType(Value, _
                                         oProperty.PropertyType), Nothing)
                Else
                    oProperty.SetValue(obj, Nothing, Nothing)
                End If

            End If

        Catch x As Exception
            Throw New Exception("Error while setting value " &_
                "for """ & Me.FieldName & """: " & x.Message, x)

        End Try

    End Sub

MemberBindingCollection 类

MemberBindingCollection 只是 MemberBinding 的一个集合。此类的实例将表示将数据源绑定到对象所需的所有绑定信息。

DataFieldAttribute 类

DataFieldAttributeMemberBinding 类的属性表示。此属性可以添加到类的 PublicPrivate 字段和属性中。在您的类中使用此属性比尝试在代码中构建 MemberBinding 对象要简单得多。我已经向您展示了一个示例。这里再次展示:

<DataField("EmployeeID")> Private iEmployeeID As Integer

在此示例中,Private 字段 iEmployee 将“绑定”到数据源中名为 EmployeeID 的字段。

让我给您看一个稍微复杂一点的绑定示例。在示例项目中,Employee 类有一个名为 iReportsTo 的私有 Integer 字段。如果员工不向任何人汇报(他是老板),则数据库字段(名为“ReportsTo”)应设置为 DBNull。由于 Integer 不能很好地处理 DBNull,您必须检查数据库值是否为 DBNull。如果值不是 DBNull,则将 iReportsTo 设置为数据库中的值。如果是 DBNull,则必须将 iReportsTo 设置为 0。当需要将数据发送回数据库时,您必须检查 iReportsTo 是否等于 0。如果不是,则将 iReportsTo 的值发送到数据库。如果是,则将 DBNull 发送回数据库。您需要多少代码才能做到这一点?以下是使用 DataFieldAttribute 的方法:

<DataField("ReportsTo", "0", True)> Private iReportsTo As Integer

就是这样!

这是 DataFieldAttribute 类的概述:

<AttributeUsage(AttributeTargets.Field Or AttributeTargets.Property, _
 Inherited:=True, _
 AllowMultiple:=False), _
 Serializable()> _
Public Class DataFieldAttribute
    Inherits Attribute

    Public ReadOnly Property FieldName() As String
    Public ReadOnly Property DefaultValue() As String
    Public ReadOnly Property DefaultValueType() As Type
    Public ReadOnly Property ConvertDefaultToDBNull() As Boolean
    Public ReadOnly Property ConvertDBNullToNull() As Boolean

    Public Function CreateMemberBinding(ByVal Member As MemberInfo) _
                                                      As MemberBinding
    Public Shared Function GetMemberBindingsForType(ByVal [Type] As Type) _
                                                      As MemberBinding()

End Class

如您所见,DataFieldAttribute 的属性与 MemberBinding 的属性相对应。它们的用途完全相同。

CreateMemberBinding 方法用于(奇怪的是)使用 FieldAttribute 实例的属性创建 MemberBinding 实例。这是 CreateMemberBinding 的代码:

    Public Function CreateMemberBinding(ByVal Member As MemberInfo) As _
                                                           MemberBinding

        With Me
            Return New MemberBinding(.FieldName, _
                                     Member, _
                                     .DefaultValue, _
                                     .DefaultValueType, _
                                     .ConvertDefaultToDBNull, _
                                     .ConvertDBNullToNull)
        End With

    End Function

GetMemberBindingsForType 共享方法用于查找给定 System.Type 中所有带有 DataFieldAttribute 属性的成员,并使用它们返回该 TypeMemberBindingCollection 实例。

这是 GetMemberBindingsForType 的代码:

Public Shared Function GetMemberBindingsForType(ByVal [Type] As Type) _
                                           As MemberBindingCollection

    If [Type] Is Nothing Then Throw New ArgumentNullException("Type")

    Dim alBindings As New ArrayList()
    Dim oMember As MemberInfo
    Dim oDFAtt As DataFieldAttribute
    Dim oBindingArray As MemberBinding()

    Dim sTypeName As String = [Type].FullName

    'Check if the bindings for this Type is cached.
    If oDefinedTypes.ContainsKey(sTypeName) Then

        'Get the bindings from the hash table.
        oBindingArray = DirectCast(oDefinedTypes(sTypeName), _
                                          MemberBinding())

    Else

        'Build the bindings from the DataField attributes 
        'defined in the Type.

        'Iterate through all public and private instance members.
        For Each oMember In [Type].GetMembers(BindingFlags.Public Or _
                      BindingFlags.NonPublic Or BindingFlags.Instance)

            'Attempt to get a DataField attribute from the member.
            'If an attribute is retreived then add the member 
            'to the arraylist.
            oDFAtt = DirectCast(Attribute.GetCustomAttribute(oMember, _
                      GetType(DataFieldAttribute)), DataFieldAttribute)
            If Not oDFAtt Is Nothing Then
                alBindings.Add(oDFAtt.CreateMemberBinding(oMember))
            End If

        Next

        'Convert the arraylist to an array and 
        'save it in the cache.
        oBindingArray = 
           DirectCast(alBindings.ToArray(GetType(MemberBinding)), _
                                                   MemberBinding())
        SyncLock oDefinedTypes
            oDefinedTypes(sTypeName) = oBindingArray
        End SyncLock

    End If

    Return New MemberBindingCollection(oBindingArray)

End Function

DataSourceBinder 类

现在你已经了解了这些核心细节。DataSourceBinder 类应该能帮助你避免它们。DataSourceBinder 在内部使用 DataFieldAttributeMemberBindingMemberBindingCollection 和一个名为 DataValuesDictionary 的东西来填充对象数据以及反向操作。以下是 DataSourceBinder 结构的摘要:

Public Class DataSourceBinder

  Public Function GetValues(ByVal obj As Object, _
     ByVal Memberbindings As MemberBindingCollection) As DataValuesDictionary
  Public Function GetValues(ByVal InstanceObject As Object) As _
                                         DataValuesDictionary


  Public Sub GetValuesIntoDataRow(ByVal Values As DataValuesDictionary, _
                                    ByVal Row As DataRow)
                                    
  Public Sub GetValuesIntoDataRow(ByVal obj As Object, _
                        ByVal MemberBindings As MemberBindingCollection, _
                        ByVal Row As DataRow)

  Public Sub GetValuesIntoDataRow(ByVal InstanceObject As Object, _
                                    ByVal Row As DataRow)


  Public Sub SetValues(ByVal obj As Object, _
                         ByVal MemberBindings As MemberBindingCollection, _
                         ByVal Values As DataValuesDictionary, _
                         Optional ByVal IgnoreWhenNotSet As Boolean = False)

  Public Sub SetValues(ByVal InstanceObject As Object, _
                        ByVal Values As DataValuesDictionary, _
                        Optional ByVal IgnoreWhenNotSet As Boolean = False)

  Public Sub SetValues(ByVal obj As Object, _
                         ByVal MemberBindings As MemberBindingCollection, _
                         ByVal Values As DataRow, _
                         Optional ByVal IgnoreWhenNotSet As Boolean = False)

  Public Sub SetValues(ByVal InstanceObject As Object, _
                        ByVal Values As DataRow, _
                        Optional ByVal IgnoreWhenNotSet As Boolean = False)


  Public Sub GetValuesIntoParameterCollection(ByVal Values As _
                    DataValuesDictionary, _
                    ByVal Parameters As IDataParameterCollection, _
                    Optional ByVal ParamPrefix As String = Nothing, _
                    Optional ByVal IdentityKeyField As String = Nothing, _
                    Optional ByVal IdentityParameterName As String = Nothing)

  Public Sub GetValuesIntoParameterCollection(ByVal obj As Object, _
                    ByVal MemberBindings As MemberBindingCollection, _
                    ByVal Parameters As IDataParameterCollection, _
                    Optional ByVal ParamPrefix As String = Nothing, _
                    Optional ByVal IdentityKeyField As String = Nothing, _
                    Optional ByVal IdentityParameterName As String = Nothing)

  Public Sub GetValuesIntoParameterCollection(ByVal InstanceObject _
                    As Object, _
                    ByVal Parameters As IDataParameterCollection, _
                    Optional ByVal ParamPrefix As String = Nothing, _
                    Optional ByVal IdentityParameterName As String = Nothing)



  Public Function GetIdentityFieldName(ByVal [Type] As Type) As String
  Public Function GetIdentityFieldValue(ByVal InstanceObject As Object) _
                                                              As Object
  Public Function FixDBName(ByVal Name As String, _
                Optional ByVal NoBraces As Boolean = False) As String

End Class

您会注意到其中一些方法引用了一个名为 DataValuesDictionary 的东西。DataValuesDictionary 只是一个专门的 IDictionary 对象,它使用字符串值作为键。每个字典条目的键与数据源的字段名相关。我创建这个类是因为我认识到传输数据的方式不仅仅是 DataRow。其理念是让 DataSourceBinderDataValuesDictionary 和您的对象之间传输数据,并编写您自己的代码来在 DataValuesDictionary 和您自己的数据访问机制之间传输这些值。实际上,DataSourceBinder 的所有重载方法在内部都使用 DataValuesDictionary 来传输数据。

由于重载方法太多,我不会深入探讨 DataSourceBinder 的代码细节。如果您愿意,可以查看提供的源代码。希望注释足够清晰,以便理解。相反,我将介绍一些最常用方法调用的用法。

GetValues 方法用于从对象返回值。这些值以 DataValuesDictionary 的形式返回。

  Dim MyObject as Widget 'A class with DataFieldAttributes defined.

  '< code to fill the MyObject with data goes here... >

  Dim oValues as DataValuesDictionary = _
                 MyDataSourceBinder.GetValues(MyObject)

或者

  Dim MyObject as Widget 'A class *without* DataFieldAttributes defined.

  '< code to fill the MyObject with data goes here... >

  Dim MyBindings as New MemberBindingCollection

  '< code to fill the MyBindings with MemberBindings goes here... >

  Dim oValues as DataValuesDictionary = _
                 MyDataSourceBinder.GetValues(MyObject, MyBindings)

GetValuesIntoDataRow 方法用于将值从您的对象传输到 DataRow 中。

  Call MyDataSourceBinder.GetValuesIntoDataRow(MyDataValuesDictionary, _
                                                                MyDataRow)

或者

  Call MyDataSourceBinder.GetValuesIntoDataRow(MyObject, MyDataRow)

或者

  Call MyDataSourceBinder.GetValuesIntoDataRow(MyObject, _
                                  MyDataRow, MyMemberBindings)

SetValues 方法用于使用数据源中的值填充您的对象。

  Call MyDataSourceBinder.SetValues(MyObject, MyDataValuesDictionary)

或者

  Call MyDataSourceBinder.SetValues(MyObject, MyDataRow)

或者

  Call MyDataSourceBinder.SetValues(MyObject, MyMemberBindings, _
                                            MyDataValuesDictionary)

或者

  Call MyDataSourceBinder.SetValues(MyObject, MyMemberBindings, _
                                                         MyDataRow)

但是,如果您的 DataValuesDictionaryDataRow 中的字段少于 MemberBindingCollection 中的 MemberBinding 实例,会发生什么?这可以通过设置可选的 IgnoreWhenNotSet 属性来处理。

通常,如果 MemberBinding 没有匹配的 DataValuesDictionary 条目,则该条目的成员值将设置为 Nothing (null)。在正常情况下,确保每个 MemberBinding 都有一个条目是明智的。但有时您只想更新对象的一部分。将 IgnoreWhenNotSet 设置为 True 将导致所有没有匹配条目的成员值保持不变。

遗漏的内容

您可能已经注意到我没有提及 DataSourceBinder.GetValuesIntoParameterCollection 方法或您在源代码中找到的 IdentityFieldAttribute。它们可以工作。但我认为涵盖它们超出了本文的范围。

未来展望

如前所述,我已基于此项目中的类编写了一个 SQL 命令生成器。我还更进一步,构建了一个通用数据访问层组件,该组件将创建、检索、更新和删除任何用 DataFieldAttribute 装饰的对象的数据。您只需提供连接字符串和对象,它就会完成所有其余工作。如果本文引起了足够的兴趣,我将在未来的文章中发布这些项目。

历史

  • 2005年5月26日:1.0版发布。
© . All rights reserved.