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






4.54/5 (14投票s)
2005年5月26日
9分钟阅读

71436

564
本文描述了可用于通过反射更新具有对象数据的数据源的类。
引言
我一直以来的一个困扰是,为了用数据源中的私有数据填充对象,你必须公开成员或创建一个类方法来接受数据并在内部用数据填充成员值。在前一种情况下,成员值会因开发人员的意外损坏而暴露,封装概念被破坏。在后一种情况下,数据与数据源耦合得太紧密。我想到,通过使用反射,我可以直接绑定到私有成员。
鉴于有这么多关于使用反射的文章,我惊讶地发现没有一篇与数据绑定相关。所以我决定自己编写类。在这个过程中,我开发了一个用于快速开发数据访问代码的框架的雏形。例如,使用这里提供的代码作为基础,我开发了一系列 DAL 对象,这些对象为对象数据的基本 C.R.U.D. 生成 SQL 命令。通过使用这个框架,为具有新属性的对象修改 DAL 就像为新属性添加一个属性一样简单。如果这篇文章引起了足够的兴趣,我将发布一篇“第二部分”文章来演示这一点。
警告!这是我的第一篇文章。我将尽力写出有用的东西。开始吧……
使用代码
如果你和我一样,你会直接跳到这篇文章的末尾,看看最终结果是否是你能用的。所以,我把结尾放在开头。这是一些从本文附带的示例项目中提取的代码片段。此代码片段用 DataRow
(oRow
) 中包含的值填充 Employee
类 (oLocalEmployee
) 的实例。oBinder
是 DataSourceBinder
类的一个实例(稍后会详细介绍这个类)。
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
的实例。通常,Member
的 MemberType
属性在内部用于将默认值转换为有用值。但是,如果您的 Member
的 Type
是 System.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
代表一个 Integer
。GetValue
将返回 Member
的 Integer
值。现在假设 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 类
DataFieldAttribute
是 MemberBinding
类的属性表示。此属性可以添加到类的 Public
或 Private
字段和属性中。在您的类中使用此属性比尝试在代码中构建 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
属性的成员,并使用它们返回该 Type
的 MemberBindingCollection
实例。
这是 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
在内部使用 DataFieldAttribute
、MemberBinding
、MemberBindingCollection
和一个名为 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
。其理念是让 DataSourceBinder
在 DataValuesDictionary
和您的对象之间传输数据,并编写您自己的代码来在 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)
但是,如果您的 DataValuesDictionary
或 DataRow
中的字段少于 MemberBindingCollection
中的 MemberBinding
实例,会发生什么?这可以通过设置可选的 IgnoreWhenNotSet
属性来处理。
通常,如果 MemberBinding
没有匹配的 DataValuesDictionary
条目,则该条目的成员值将设置为 Nothing
(null
)。在正常情况下,确保每个 MemberBinding
都有一个条目是明智的。但有时您只想更新对象的一部分。将 IgnoreWhenNotSet
设置为 True
将导致所有没有匹配条目的成员值保持不变。
遗漏的内容
您可能已经注意到我没有提及 DataSourceBinder.GetValuesIntoParameterCollection
方法或您在源代码中找到的 IdentityFieldAttribute
。它们可以工作。但我认为涵盖它们超出了本文的范围。
未来展望
如前所述,我已基于此项目中的类编写了一个 SQL 命令生成器。我还更进一步,构建了一个通用数据访问层组件,该组件将创建、检索、更新和删除任何用 DataFieldAttribute
装饰的对象的数据。您只需提供连接字符串和对象,它就会完成所有其余工作。如果本文引起了足够的兴趣,我将在未来的文章中发布这些项目。
历史
- 2005年5月26日:1.0版发布。