Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Using Reflection to Bind Object Data to Data Sources

0.00/5 (No votes)
26 May 2005 1  
This article describes classes that can be used to update data sources with object data via reflection.

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
    
    '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 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

        '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

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

    '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

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 'A class with DataFieldAttributes defined.


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


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

Or

  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)

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here