Tuesday, December 9, 2008

Adding Properties to an object dynamically in VB.Net

In the current application that I am building, I came across this scenario where I had to add properties to an object during runtime.

Basically it is like this:

I had to display some data of a parent object on a grid – but the number of columns to display depended on the number of child objects the parent had. I usually build a façade object as a standard practice to bind any such modified/tweaked/manipulated data objects.
In order to explain the problem clearer, let me give an example.

Say we have “Product”, “ProductVersion” and “Order”. There could be multiple versions for the product and the relationship is one-to-many. “Order” and “ProductVersion” have a foreign key constraint.

In the middle tier we have Product class and ProductVersion class that map directly to each table. The requirement is to display in grid on the Products page that shows the following –

·         A side by side comparison of each version’s cost breakdown

·         Display is based on the Order Status

Product’s Order Status

Version 1

Version 2

Version 3

Shipped

$1024

$689

$60

Processing

$488

$843

$593

Pending Payment

$734

$1009

$978


One way is to get this whole thing done in a stored procedure. But just to make it happen in the middle tier, I dived into the world of TypeDescriptionProvider, partly because on an earlier project, when we encountered a similar problem, we chickened out of it by forcing the user  to change the functionality ( only due to lack of time).

Anyways, here goes the solution. You can get the actual code in zipped format here.
I am just explaining the most important parts:

The Class that I am going to build to bind with the grid is called – “ProductVersionFaçade”
The properties it will have – OrderStatus, and the rest of the properties would be Dynamically added.
Mark the façade class to use the TypeDescriptionProvider which we are about to build

GetType(ProductVersionFacadeTypeDescriptionProvider))> _

Public Class ProductVersionFacade

Let us have a dictionary inside this to hold our dynamic properties. You will see its usage later.

We then create our TypeDescriptionProvider as follows:

    Public Class ProductVersionFacadeTypeDescriptionProvider

        Inherits TypeDescriptionProvider

And override the GetTypeDescriptor function, which will return a CustmTypeDescriptor, which is also something we are going to build:

    Public Class ProductVersionFacadeTypeDescriptor

        Inherits CustomTypeDescriptor

This is the class where the most important GetProperties function will be overridden and inside that we add our new bunch of properties like this:

With propCollection

    Dim attArr() As Attribute

    ReDim attArr(MyBase.GetAttributes.Count - 1)

    MyBase.GetAttributes.CopyTo(attArr, 0)

 

    If TypeOf (_instance) Is ProductVersionFacade Then

        f = DirectCast(_instance, ProductVersionFacade)

        For Each i As Integer In f.DetailHourDict.Keys

            keyString = String.Format("ProductVersion_id{0}Cost", i)

            .Add(DirectCast(New ProductVersionFacadePropertyDescriptor( _

                    keyString, GetType(Integer), attArr), PropertyDescriptor))

        Next

    End If

 

End With

This is where we add the properties to the instance’s dictionary. And if you note we are adding a custom PropertyTypeDescriptor, we need that too. So we build the ProductVersionFacadePropertyDescriptor which among many other things overrides the GetValue function to return the value for your newly added properties.

Back to the Façade class, we overload the default property of it as follows:

        Default Public Property Item(ByVal fieldName As String) As Object

            Get

                Dim value As Object = Nothing

                If _customPropsDict.ContainsKey(fieldName) Then

                    value = _customPropsDict(fieldName)

                End If

 

                Return value

            End Get

            Set(ByVal value As Object)

                _customPropsDict(fieldName) = value

            End Set

        End Property

And a sample from the Page where it is bound:

Private Sub BindFacades()

 

   Dim list As List(Of ProductVersionFacade)

   list = ProductVersionFacadeSvc.BuildFacades(Me.Product)

 

   Dim keyString As String = String.Empty

   Dim idString As String = String.Empty

 

   For Each f As ProductVersionFacade In list

 

       For Each i As Integer In f.ProductVersionCostDict.Keys

           keyString = String.Format("ProductVersion_id{0}Cost", i)

           idString = String.Format("ProductVersion_id{0}", i)

 

           If f._customPropsDict.ContainsKey(keyString) Then

              f._customPropsDict(keyString) += f.ProductVersionCostDict(i)

           Else

              f._customPropsDict(keyString) = f.ProductVersionCostDict(i)

           End If

           f._customPropsDict(idString) = i

       Next

   Next

   Me.FormatFacadeGrid(Me.ProductVersion)

   With Me.ProductVersionFacadesGridViewCtl

        .DataSource = list

        .DataBind()

   End With

End Sub

3 comments:

Anonymous said...

Awesome work!!!

CruZ said...

May I know which gridview control u used? Why I cannot bind with my gridview. or can u provide the executable source.

thanks.

Karthik Padmanabhan said...

@CruZ
It was a simple ASP GridView control on an aspx page. The markup should be something like this:

<asp:GridView ID="ProductVersionFacadesGridViewCtl" CssClass="gridViewTable" AutoGenerateColumns="false" runat="server"/>

If you call the BindFacades() method(code already given), ideally from the Page_Load event, it should be able to bind the list returned by the FacadeSvc... Let me know if any further issues.