Introduction
One of my ongoing frustrations has been that, in order to populate an object with private data from a data source you have to expose members publicly or create a class method that accepts the data and internally fills member values with the data. In the former case the member values are exposed to accidental corruption by developers and the concept of encapsulation is defeated. In the latter case the data gets too tightly coupled to the data source. It occurred to me that, by using reflection, I could bind to private members directly.
With all the articles about using reflection that are out there I was amazed that I could not find any that related to data binding. So I decided to write my own classes. And, in the process, I have developed the beginnings of a framework for rapid development of data access code. For example, using the code provided here as a foundation, I have developed a series of DAL objects that generate SQL commands for basic C.R.U.D. of object data. By using this framework, modifying the DAL for an object with a new property is as simple as adding an attribute to the new property. If this article generates enough interest I'll post a "part two" article that demonstrates this.
Warning! This my first article of any kind ever. I will do my best to produce something useful. Here it goes�
Using the code
If you�re like me you will go right to the end of this article to see if the final results are something you can use. So, instead, I�ll put the end at the beginning. Here is some code pulled from the sample project included with this article. This code snippet populates an instance of an Employee
class (oLocalEmployee
) with the values contained in a DataRow
(oRow
). oBinder
is an instance of the DataSourceBinder
class (more on this class later).
oBinder.SetValues(oLocalEmployee, oRow)
One line of code. That's it! Here is code from the sample project that fills a DataRow
with data from an object.
oBinder.GetValuesIntoDataRow(oEmployee, oRowView.Row)
Again, only one line of code. How can that be you say? Read on!
OK. I cheated. The members of the Employee
class are attributed with a DataField
attribute. This attribute defines how a member is treated during data binding and is applied to either fields or properties of a class like so:
<DataField("EmployeeID")> Private iEmployeeID As Integer
The DataSourceBinder
uses these attributes to build something called a MemberBindingCollection
populated with (you guessed it) MemberBinding
instances. The DataSourceBinder
then uses this MemberBindingCollection
to do the data binding.
In fact the DataSourceBinder
�s methods are overloaded so a MemberBindingCollection
can be one of the parameters. This way it is possible to bind to a class with no members decorated with the DataField
attribute. This completely decouples the object from the data source but puts the burden on the programmer to create the MemberBindingCollection
.
oBinder.SetValues(oLocalEmployee, oMemberBindings, oRow)
The Nitty Gritty
Let's delve into some of the details.
The MemberBinding class
The basic building block for the whole framework is the MemberBinding
class. The MemberBinding
class has the following properties and methods:
<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
The Member
property is required and contains a MemberInfo
instance. This is the field or property that will be controlled by this instance of the MemberBinding
.
The FieldName
property is required and contains the name of the field in the data source. In most cases the data source will be a DataRow
. But there is no requirement for this.
The DefaultValue
property is optional. When present it represents a String
representation of the default value that will be inserted into the member when the data source value is DBNull
. If your member can�t deal with DBNull
then you will want to set this value.
The DefaultValueType
property is optional. It is an instance of a System.Type
. Normally the Member
�s MemberType
property is used internally to convert the default value to a useful value. However, If the Type
of your Member
is System.Object
then this is not possible. DefaultValueType
will be used in this case.
When ConvertDefaultToDBNull
is set to True
, DBNull
will be inserted into the data source when the Member
�s value equals the DefaultValue
. This property is False
by default.
When ConvertDBNullToNull
is set to True
, data source DBNull
values are converted to Nothing
(null
) before being inserted into the Member
�s value. Member
values are always converted to DBNull
when transferring values to the data source if the Member
�s value is Nothing
(null
). This property is False
by default.
You'll notice all of these properties are ReadOnly
. They are set via various overloads of the MemberBinding
class.
GetValue
returns the value of the Member
. The value will be converted according to the property settings listed above. For example, Let's say your Member
represents an Integer
. GetValue
will return the Integer
value of the Member
. Now let's say the DefaultValue
property is set to �0� and the ConvertDefaultToDBNull
property is True
. If the value of the Member
is 0 then GetValue
will return DBNull
. Here is the code for GetValue
:
Public Function GetValue(ByVal obj As Object) As Object
Dim oMemberType As Type
Dim oValue As Object
Dim bIsIComparable As Boolean
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
Dim oField As FieldInfo
oField = DirectCast(Member, FieldInfo)
oValue = oField.GetValue(obj)
oMemberType = Me.DefaultValueType
If oMemberType Is Nothing Then
oMemberType = oField.FieldType
End If
ElseIf Member.MemberType = MemberTypes.Property Then
Dim oProperty As PropertyInfo
oProperty = DirectCast(Member, PropertyInfo)
oValue = oProperty.GetValue(obj, Nothing)
oMemberType = Me.DefaultValueType
If oMemberType Is Nothing Then
oMemberType = oProperty.PropertyType
End If
End If
bIsIComparable =
(Not oMemberType.GetInterface("System.IComparable") Is Nothing)
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
If Me.ConvertDefaultToDBNull Then
If Me.DefaultValue Is Nothing Then
If (oValue Is Nothing) = True Then
oValue = DBNull.Value
End If
ElseIf Not IsDBNull(oValue) Then
If bIsIComparable Then
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 oValue Is Nothing Then oValue = DBNull.Value
Return oValue
End Function
SetValue
is used to set the value of the Member
. Just as in GetValue
, the value that ultimately gets into the Member
depends on the property settings. Here is the code for SetValue
:
Public Sub SetValue(ByVal obj As Object, ByVal Value As Object)
Dim oField As FieldInfo
Dim oProperty As PropertyInfo
Dim oMemberType As Type
Try
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
oField = DirectCast(Member, FieldInfo)
oMemberType = oField.FieldType
ElseIf Member.MemberType = MemberTypes.Property Then
oProperty = DirectCast(Member, PropertyInfo)
oMemberType = oProperty.PropertyType
End If
If Value Is Nothing OrElse IsDBNull(Value) Then
If Not Me.DefaultValue Is Nothing Then
Dim oDefaultType As Type = Me.DefaultValueType
If oDefaultType Is Nothing Then
oDefaultType = oMemberType
End If
Value = Convert.ChangeType(Me.DefaultValue, oDefaultType)
End If
End If
If IsDBNull(Value) _
AndAlso Me.ConvertDBNullToNull Then
Value = Nothing
End If
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
The MemberBindingCollection class
The MemberBindingCollection
is nothing more than a collection of MemberBinding
s. An instance of this class will represent all the binding information needed to bind a data source to an object.
The DataFieldAttribute class
The DataFieldAttribute
is an attribute representation of the MemberBinding
class. This attribute can be added to Public
or Private
fields and properties of a class. Using this attribute in your classes is much, much simpler than trying to build MemberBinding
objects in code. I have shown you an example of it already. Here it is again:
<DataField("EmployeeID")> Private iEmployeeID As Integer
In this example The Private
iEmployee
field will be "bound" to the field called EmployeeID
in the data source.
Let me show you an example of some slightly more complex binding. In the sample project, the Employee
class has a private Integer
field called iReportsTo
. If the employee reports to no one (he's the boss), then the database field (called "ReportsTo
") should be set to DBNull
. Since an Integer
won't play well with DBNull
you have to check if the database value is DBNull
. If the value is not DBNull
then set iReportsTo
to the value in the database. If it is DBNull
then you have to set iReportsTo
to 0. When the time comes to send the data back to the database you have to check if iReportsTo
is equal to 0. If not then send the value of iReportsTo
to the database. If so, send DBNull
back to the database. How much code will it take you to do that? Here's how it's done using the DataFieldAttribute
:
<DataField("ReportsTo", "0", True)> Private iReportsTo As Integer
That's it!
Here is an outline of the DataFieldAttribute
class:
<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
As you can see, the DataFieldAttribute
�s properties mirror the MemberBinding
's properties. Their purpose is exactly the same.
The CreateMemberBinding
method is used to (oddly enough) create a MemberBinding
instance using the properties of the FieldAttribute
instance. Here is the code for 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
The GetMemberBindingsForType
shared method is used to find all the members attributed with DataFieldAttribute
for a given System.Type
and use them to return a MemberBindingCollection
instance for that Type
.
Here is the code for 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
If oDefinedTypes.ContainsKey(sTypeName) Then
oBindingArray = DirectCast(oDefinedTypes(sTypeName), _
MemberBinding())
Else
For Each oMember In [Type].GetMembers(BindingFlags.Public Or _
BindingFlags.NonPublic Or BindingFlags.Instance)
oDFAtt = DirectCast(Attribute.GetCustomAttribute(oMember, _
GetType(DataFieldAttribute)), DataFieldAttribute)
If Not oDFAtt Is Nothing Then
alBindings.Add(oDFAtt.CreateMemberBinding(oMember))
End If
Next
oBindingArray =
DirectCast(alBindings.ToArray(GetType(MemberBinding)), _
MemberBinding())
SyncLock oDefinedTypes
oDefinedTypes(sTypeName) = oBindingArray
End SyncLock
End If
Return New MemberBindingCollection(oBindingArray)
End Function
The DataSourceBinder class
Now you know the gritty details. The DataSourceBinder
class should help you to avoid them. The DataSourceBinder
internally uses the DataFieldAttribute
, the MemberBinding
, the MemberBindingCollection
, and something called a DataValuesDictionary
to fill objects with data and vice versa. Here is a summary of the DataSourceBinder
�s structure:
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
You will notice several of these methods reference something called a DataValuesDictionary
. The DataValuesDictionary
is nothing more than a specialized IDictionary
object that uses string values as keys. The key for each dictionary entry correlates to a field name for your data source. I created this class because I recognize there are more ways to transport data than just the DataRow
. The idea is to let the DataSourceBinder
transfer data between the DataValuesDictionary
and your object and to write your own code to transfer those values to and from the DataValuesDictionary
and your own data access mechanism. In fact, all the overloaded methods of the DataSourceBinder
internally use the DataValuesDictionary
to transfer data.
Because there are so many overloaded methods I am not going to go into the code details of the DataSourceBinder
. If you wish, you can look at the provided source code. Hopefully it is commented enough to understand. Instead, I am going to cover the use of some of the most common method calls.
The GetValues
method is used to return values from an object. The values are returned in a DataValuesDictionary
.
Dim MyObject as Widget
Dim oValues as DataValuesDictionary = _
MyDataSourceBinder.GetValues(MyObject)
Or
Dim MyObject as Widget
Dim MyBindings as New MemberBindingCollection
Dim oValues as DataValuesDictionary = _
MyDataSourceBinder.GetValues(MyObject, MyBindings)
The GetValuesIntoDataRow
method is used to transfer values from your object into a DataRow
.
Call MyDataSourceBinder.GetValuesIntoDataRow(MyDataValuesDictionary, _
MyDataRow)
Or
Call MyDataSourceBinder.GetValuesIntoDataRow(MyObject, MyDataRow)
Or
Call MyDataSourceBinder.GetValuesIntoDataRow(MyObject, _
MyDataRow, MyMemberBindings)
The SetValues
method is used to populate your object with values from your data source.
Call MyDataSourceBinder.SetValues(MyObject, MyDataValuesDictionary)
Or
Call MyDataSourceBinder.SetValues(MyObject, MyDataRow)
Or
Call MyDataSourceBinder.SetValues(MyObject, MyMemberBindings, _
MyDataValuesDictionary)
Or
Call MyDataSourceBinder.SetValues(MyObject, MyMemberBindings, _
MyDataRow)
But what happens if you have fewer fields in your DataValuesDictionary
or your DataRow
than there are MemberBinding
instances in your MemberBindingCollection
? This is handled by setting the optional IgnoreWhenNotSet
property.
Normally, if a MemberBinding
does not have a matching DataValuesDictionary
entry, the member value for that entry is set to Nothing
(null
). In normal circumstances, it would be wise to make sure you have an entry for every MemberBinding
. But there are occasions when you want to update only a portion of your object. Setting IgnoreWhenNotSet
to True
will cause all member values without matching entries to be left alone.
Things left out
You may have noticed that I made no mention of the DataSourceBinder.GetValuesIntoParameterCollection
methods or the IdentityFieldAttribute
you will find in the source code. They work. But I felt covering them was a little out of scope for this article.
The future
As I have previously mentioned, I have already written a SQL command builder based on the classes in this project. I have also gone a step further and built a generic data access layer component that will create, retrieve, update, and delete the data from any object that is decorated with the DataFieldAttribute
. All you do is give it a connection string and an object. It does all the rest. If this article generates enough interest I will post those projects in future articles.
History
- 26th May, 2005: Version 1.0 released into the wild.