Web Services, Databinding, and properties of Child classes.
I’ve been doing some proof-of-concept testing for a web service that we’ll probably be using as the middle-tier of a web application we’re developing. When the web service creates the proxy class in Visual Studio, the web service class properties are all created as public fields instead of public properties. For the most part, who cares, right?
For some reason, the ASP .NET list controls (RadioButtonList and DropDownList) cannot find public fields when attempting to databind to an array of objects. Databinding to a DataGrid or DataRepeater doesn’t really offer much of a difficulty, but it really sucks not being able to bind directly to a list control.
Jan Tielens has show one possible way to overcome this problem. His solution compiles a new assembly in memory, at runtime, which “wraps” the proxy classes in a container class with public properties instead of fields. This is a fairly interesting solution, and taught me a lot about using reflection, but ultimately it failed as a viable solution for our needs.
One of the requirements of our project is that Option Strict be used—since Jan’s method effectively creates a class at runtime that doesn’t exist at design time, there’s no way (that I know of) to create an explicit reference to one of the wrapper classes because they don’t exist yet. This leads to compiler errors and such.
All was not lost however. I decided to use what I had learned from Jan Tielens to dynamically convert an array of classes into a bindable object to use as a datasource for my list controls.
Here is my first attempt:
' Specify the property name of a class, and this function will iterate through an array of said
' classes, creating an arraylist of property or field values.
'
Public Shared Function ArrayToArrayList(ByVal Arr As System.Array, _
ByVal PropertyOrFieldName As String) As ArrayList
Dim ret As New ArrayList
Dim T As Type = Arr.GetType.GetElementType
For Each o As Object In Arr
Dim PI As System.Reflection.PropertyInfo = T.GetProperty(PropertyOrFieldName)
Dim FI As System.Reflection.FieldInfo = T.GetField(PropertyOrFieldName)
If IsNothing(PI) And IsNothing(FI) Then Return Nothing
If Not IsNothing(PI) Then ret.Add(PI.GetValue(o, Nothing))
If Not IsNothing(FI) Then ret.Add(FI.GetValue(o))
Next
Return ret
End Function
There’s nothing overly difficult here. The function receives the name of the property that the developer is interested in, along with the array of objects containing the property. It finds the value of the property for each object, adds it to the arraylist, and returns the arraylist. Problem solved…
Except for one small thing. Databinding to the list controls entails more than just binding one field to the control: the list controls have a DataTextField and DataValueField properties, both of which need databinding support. What now?
At first I thought to simply extend what I had already written—but then I decided to write a new function from scratch that would return a datatable. Here’s what I came up with:
' Converts an array of like objects to a datatable object which can be bound to a list control.
Public Shared Function ArrayToDataSource(ByVal arr As System.Array, _
ByVal LC As ListControl) As DataTable
' Creates columns in the Datatable for the DataText and DataValue Fields
Dim DT As New DataTable
DT.Columns.Add(LC.DataTextField)
DT.Columns.Add(LC.DataValueField)
' Get the base type of the array.
Dim T As Type = arr.GetType.GetElementType
' For each item in the array, get the property values associated with
' specified field.
'
' Don't bother retrieving the property values if the Fields on the list control
' were left blank.
'
For Each o As Object In arr
Dim DR As DataRow = DT.NewRow
If Not LC.DataTextField.Length = 0 Then _
DR(LC.DataTextField) = GetFieldOrPropertyValue(o, LC.DataTextField)
If Not LC.DataValueField.Length = 0 Then _
DR(LC.DataValueField) = GetFieldOrPropertyValue(o, LC.DataValueField)
DT.Rows.Add(DR)
Next
Return DT
End Function
This function creates a datatable and adds two columns to it—one for the datatextfield, and one for the datavaluefield. It then loops through the array of objects and inserts the appropriate values into the table via a call to another function called GetFieldOrPropertyValue.
GetFieldOrPropertyValue receives an object and the name of some property which value you wish to retrieve. At this point I thought of still another frustration I have often had in doing databinding to list controls: I couldn’t databind to an object property of my source object. For example, suppose I had an array of classes called “Employee” with a property “ResidenceAddress” which was itself a class of type “AddressInfo”, and I was interested in binding the Employee.ResidenceAddress.State property to the list control. It seems apparent that we should be able to split the name of the property we’re interested in along the “.” Separator, and using reflection, retrieve each sub property until we reached the one we desired.
Here’s the source:
' Will get a field or property value from an object.
' The Name parameter can allude to properties of other objects which are properties of the
' source object. For instance, "Name" can be something like "Address.State". The function will
' recursively find the value of each element of the name string until it gets to the end.
'
Public Shared Function GetFieldOrPropertyValue(ByVal o As Object, ByVal Name As String) As Object
Dim PropertyNames() As String = Name.Split("."c) ' create an array of properties to look up
Dim result As Object = o ' Initialize res to be the source object
Dim PI As System.Reflection.PropertyInfo
Dim FI As System.Reflection.FieldInfo
For Each s As String In PropertyNames
' Get the type from res: this will change on each iteration.
Dim T As Type = result.GetType
PI = T.GetProperty(s)
FI = T.GetField(s)
' Set res to the value of the property we're lookin up.
' Using the above example, during the first iteration,
' res will be "Address", and the second time, it will
' be the "State".
'
If Not IsNothing(PI) Then result = PI.GetValue(result, Nothing)
If Not IsNothing(FI) Then result = FI.GetValue(result)
Next
' If we could not find a match, clear out res.
If IsNothing(PI) And IsNothing(FI) Then result = Nothing
Return result
End Function