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