Click here to Skip to main content
Click here to Skip to main content

Exposing custom-made classes through a Webservice and binding them to a DataGrid.

, 26 Feb 2003
Rate this:
Please Sign up or sign in to vote.
When you want to expose your custom made business objects through a webservice interface, and you want them to bind with a DataGrid, you have a problem that the generated proxy class exposes fields instead of properties. A possible solution is to generate at runtime, a wrapper for your proxy class.

Sample Image - CodeProjectMonitor.jpg

Table of contents

  1. Revision history
  2. Introduction
  3. Explaining the problem
    1. The business layer
    2. The services layer (a.k.a. facade)
    3. The client application
    4. Exploring the problem
  4. A solution!
  5. Designing the solution
    1. Dynamic proxy class generation and compilation
    2. Caching compiled types for performance
    3. Providing thread safety
  6. Demo application: CodeProjectMonitor
  7. Recommended reading

Introduction

Creating and consuming XML Web services in Visual Studio.NET is really easy. But when you want to expose your custom build business entities through a Web service you will experience some problems (or at least I did), when you want to bind your objects to a DataGrid. This article shows a possible solution to overcome this by generating at runtime a wrapper class that encapsulates the original proxy class. Maybe this sounds like rocket science, but with the CodeDom namespace, this is pretty easy.

The first part of the article describes how to setup a solution to simulate this behavior and to explain what the problem exactly is. The second part is the most interesting part, here the solution code is explained. If you don't want to dive into the technical stuff, and just use the solution, the full working source code, and a compiled DLL can be downloaded.

Explaining the problem

To explain the problem, I will use a very simple example. Suppose I have to build a solution that provides information about the customers in a company. The solution needs to expose a services layer (facade) that uses web services to provide the needed functionality. The application architecture is a standard 3-tier architecture. For more information see the links at then end of the article. As documented in MSDN, the structure of such an application would look like this:

The business layer

In the business layer you will define your business entity classes. In this example (for keepings things simple), we will only need 1 business entity, our Customer class. This class will be our data carrier and contains some business logic. Besides this business entity, we will need a container that can contain multiple Customer objects, that will be the CustomerCollection. So the classes needed in the business layer can be coded like this:

Public Class Customer
    Private _name As String
    Private _email As String

    Public Property Name As String
        Get
            Return _name
        End Get
        Set (value As String)
            _name = value
        End Set
    End Property

    Public Property Email As String
        Get
            Return _email
        End Get
        Set (value As String)
            _email = value
        End Set
    End Property

    Public Readonly Property Display As String
        Get
            'Return a string for displaying data: a very simple business rule
            Return Name & " (" & Email & ")"
        End Get
    End Property
End Class

Public Class CustomerCollection
    Inherits CollectionBase

    Public Sub New()
        Mybase.New
    End Sub

    Public Sub Add(customer As Customer)
        MyBase.InnerList.Add(customer)
    End Sub

    Public Default Property Item(index As Integer) As Customer
        Get
            Return MyBase.InnerList(index)
        End Get
        Set (value As Customer)
            _MyBase.InnerList(index) = value
        End Set
    End Property        
End Class

The services layer (a.k.a. facade)

This layer will be used by other solutions to access our data. We will create a WebService class that has functionality to do the standard operations on our Customer object: retrieve, save, ... . For retrieval, we have 2 choices, first we can retrieve 1 customer (return value is a Customer object), secondly we can retrieve multiple customers (return value is a CustomerCollection object). For simplicity of this example I will provide only 2 methods that can be used to for retrieval. In a real life solution, there would be some methods to add and alter customers.

    'In our WebService class:
    <WebMethod()> Public Function GetCustomer(name As String) As Customer
        'In real life, you would retrieve the customer from a database,
        'for now I will return always the same customer...
        Dim c As New Customer
        c.Name = "Jan Tielens"
        c.Email = "jan@leadit.be"
        Return c
    End Function

    <WebMethod()> 
    Public Function GetCustomerCollection() As CustomerCollection
        'In real life, there would be passed 1 or 
        'more paramters that would be used
        'to retrieve a set of Customers from the database.
        Dim custColl As New CustomerCollection

        'Construct some sample customers
        Dim c1 As New Customer
        c1.Name = "Jan Tielens"
        c1.Email = "jan@leadit.be"
        Dim c2 As New Customer
        c2.Name = "Jan Tielens"
        c2.Email = "jan@leadit.be"

        'Add them to the database
        custColl.Add c1
        custColl.Add c2
    End Function

The client application

Our sample client application will have only 1 function: retrieve a collection of customers and display them in a DataGrid. Here will our problem start! We have a simple Windows Form with 1 Button and 1 DataGrid on it. When the Button is pressed, the client app. connects to the services interface (our WebService facade) and retrieves some customers.

    'Code called when the button is pressed:
    Dim eng As New Facade.CustomerEngine 
    'Facade is the name we gave our WebSerice reference
    'CustomerEngine is the name of the WebService class (see above)

    DataGrid1.DataSource = CustomerEngine.GetCustomerCollection

Exploring the problem

When you try to execute the steps as described above, or take a look at the included demo application, you will notice that the DataGrid does not display the data we wanted to display. In fact, it only shows the number of rows that there should be, but the Name and Email column are missing. Why is this??

The reason for this behavior is quite easy to understand if you know where to look for it. The result of the call to the web service (CustomerEngine.GetCustomerCollection) gives us as a return value an array of Customer objects. So far, so good, arrays can be displayed by DataGrids. So there must be something wrong with our Customer objects?

That's right, when you made a web reference (in the client app.) to the Webservice, Visual Studio generated a proxy class for your Customer class, based on the description available in the WSDL of the WebService. The code for that proxy class can be found in the Reference.vb file. When you explore the proxy class code, you will notice that your generated proxy class basically looks like this:

Public Class Customer
    '...
    Public Name As String
    Public Email As String
    '...
End Class

The proxy class exposes our properties as fields: Public Property Name ... End Property is replaced with Public Name As String. That is why the DataGrid does not show the Name and Email columns! The DataGrid only shows properties, so for our proxy class, there aren't any...

A solution!

Ok, the problem is explained. It's time for a possible solution. We need to convert those fields back to properties. It's possible to alter the Reference.vb file, and change the fields back to properties (for a link see the end of the article), but this gives you the drawback, that every time the Webservice changes, and you refresh your Web reference, the changes will be deleted and replaced by a new generated proxy class.

Programmers are lazy, so this is not a very handy solution. We could write a wrapper class that has Name and Email properties and internally has a Customer object (instance of the proxy class). Then it would be possible to build an array of instances of the wrapper class (with instances of proxy classes inside of them), and pass that array to the DataGrid. Like this, the DataGrid would be able to display the Name and Email columns and retrieve the values form the inner proxy class instance. Such a proxy class could look like this:

UML Diagram

Public Class CustomerWrapper
    'The original instance of the wrapper class
    Private _customer As Facade.Customer 

    Public Sub SetInternalObject(customer As Facade.Customer)
        _customer = customer
    End Sub

    Public Property Name As String
        Get
            Return _customer.Name
        End Get
        Set (value As String)
            _customer.Name = value
        End Set
    End Property

    Public Property Email As String
        Get
            Return _customer.Email
        End Get
        Set (value As String)
            _customer.Email = value
        End Set
    End Property
End Class

This will work, but as you see, doing this manually for each proxy class would be not only quite some work, but also quite boring and repetitive work. Knowing that programmers are lazy, this is a bad thing. So why not let .NET take care of this? The .NET framework has some built-in functionality to build code at runtime and even compile that code at runtime, so those classes could be used immediately! The technology used is the CodeDom.

As you can see in the schema, when you want to display a collection of customers in a DataGrid, the first thing is making a call to the Webservice. The result is an array of Customer object (from the generated proxy class). Then you would use the WebServiceWrapper that provides for each Customer in the array, a wrapper, and puts them in a new array of CustomerWrapper objects. This array finally can be passed to a DataGrid that will show all the properties correctly. This sounds pretty complicated, but when you want to use it, it's only one line of code:

'Suppose customers is the array of Customer objects
DataGrid2.DataSource = Leadit.Utils.WebServices._
        WebServiceWrapper.GetArrayList(customers)

Designing the solution

And now, finally, the fun part: coding the WebServiceWrapper class!! For an overview of the complete code, I suggest you download and take a look and the source code. For now, I will explain the most important parts of the WebServiceWrapper class.

Dynamic proxy class generation and compilation

As I said, the technology that we will use is the CodeDom, it's like an XMLDomDocument, but for code. If you want a tutorial for the CodeDom, I will provide some links at the end of the article to get you started.

The first thing we need is the base container: a CodeTypeDeclaration (root node in XML):

    'Create new codetype declaration for the new wrapper class
    Dim code As New CodeTypeDeclaration(typeToWrap.Name & "WithProperties")
    code.IsClass = True

    'Create constructor
    Dim constructor As New CodeConstructor()
    constructor.Attributes = MemberAttributes.Public
    code.Members.Add(constructor)

The typeToWrap variable contains the Type of the original Customer proxy class. The name of our new type will be this original type's name with WithProperties added. As you can see there is also a simple constructor provided.

    'Create declaration for inner object
    Dim declaration As New _
       CodeMemberField(typeToWrap, "_" & _
       LCase(typeToWrap.Name))
    declaration.Attributes = MemberAttributes.Private
    code.Members.Add(declaration)

    'Create function to set inner object
    Dim setInnerObject As New CodeMemberMethod()
    setInnerObject.Name = "SetInnerObject"
    setInnerObject.Parameters.Add(_
    New CodeParameterDeclarationExpression(typeToWrap,_
                  LCase(typeToWrap.Name)))
    setInnerObject.Statements.Add(New CodeAssignStatement(_ 
           New CodeVariableReferenceExpression("_" & _
           LCase(typeToWrap.Name)), _
           New CodeVariableReferenceExpression_
           (LCase(typeToWrap.Name))))
    setInnerObject.Attributes = MemberAttributes.Public
    code.Members.Add(setInnerObject)

    'Create function to get inner object
    Dim getInnerObject As New CodeMemberMethod()
    getInnerObject.Name = "GetInnerObject"
    getInnerObject.ReturnType = New CodeTypeReference(typeToWrap)
    getInnerObject.Statements.Add(New CodeMethodReturnStatement(_
        New CodeVariableReferenceExpression("_" _
        & typeToWrap.Name)))
    getInnerObject.Attributes = MemberAttributes.Public
    code.Members.Add(getInnerObject)

The block code above, will add the SetInnerObject and GetInnerObject functions to our wrapper class. These functions are used to assign the original instances of the proxy class to instances of the wrapper class. There is also a declaration added to store an instance of the proxy class.

Now, we have a basic layout for our wrapper class, with functionality to get, set and store an instance of the proxy class. Next we will add for each field of the proxy class, a property to the new wrapper class. To loop through all the fields of the proxy class type, we use the GetFields method of the type.

        'Add for each public field, a public property to the new class
        Dim fieldinfo As System.Reflection.FieldInfo
        For Each fieldinfo In typeToWrap.GetFields()
            'Create property
            Dim prop As New CodeMemberProperty()
            prop.Name = fieldinfo.Name
            prop.Attributes = MemberAttributes.Public
            prop.HasGet = True
            prop.HasSet = True
            prop.Type = New System.CodeDom._
               CodeTypeReference(fieldinfo.FieldType)

            'Create getter for property
            Dim getter As New CodeMethodReturnStatement_
                   (New CodeVariableReferenceExpression(_
                   "_" & typeToWrap.Name & _
                   "." & fieldinfo.Name))
            prop.GetStatements.Add(getter)

            'Create setter for property
            Dim setter As New CodeAssignStatement(_
             New CodeVariableReferenceExpression("_" & _
                typeToWrap.Name & "." & fieldinfo.Name), _
                New CodeVariableReferenceExpression("value"))
            prop.SetStatements.Add(setter)

            'Add the propery to the class
            code.Members.Add(prop)
        Next

That's it! Our new wrapper class is defined in memory. To compile the class, so we can use it, we need to do the following steps:

    'Create and add namespace
    Dim ns As New CodeNamespace(typeToWrap.Name & "Wrapper")
    ns.Types.Add(code)

    'Create codeunit and add the namespace
    Dim codeUnit As New System.CodeDom.CodeCompileUnit()

    codeUnit.Namespaces.Add(ns)

A new Namespace is declared and the class is added to that Namespace. Then a new CodeUnit is instantiated, and will be used to compile the class. The Namespace is added to the CodeUnit. So the CodeUnit contains the namespace with our wrapper class.

To compile the CodeUnit we have to use the VBCodeProvider (or you can choose for the CSharpCodeProvider):

        'Set compiler parameters
        Dim compilerParams As New compiler.CompilerParameters()
        compilerParams.GenerateInMemory = True
        Dim assemblyFileName As String = Reflection.Assembly._
                       GetEntryAssembly.GetName.CodeBase
        If assemblyFileName.IndexOf("file:///") > -1 Then
            compilerParams.ReferencedAssemblies.Add_
                (assemblyFileName.Substring(Len("file:///")))
        End If

        'Compile the codeunit
        Dim compiler As compiler.ICodeCompiler = New _
                       VBCodeProvider().CreateCompiler
        Dim results As compiler.CompilerResults = _
            compiler.CompileAssemblyFromDom(compilerParams, _
            codeUnit)
        If results.Errors.HasErrors Then
            'There went something wrong: 
            'construct a nice error message
            Dim errorString As String = _
                "Compilation errors: " & vbCrLf
            Dim err As System.CodeDom.Compiler.CompilerError
            For Each err In results.Errors
                errorString = err.ToString & vbCrLf
            Next
            Throw New ApplicationException(errorString)
        End If

First I set some compilerparameters and add a reference to our solution. This is done by getting the EntryAssembly's filename and adding it to the ReferencedAssemblies. Then we get a Compiler from the VBCodeProvider and use it to compile our CodeUnit. If something goes wrong, an error is constructed and thrown, that contains all the compilation errors. To use the compiled class we return the newly compiled type:

  Return results.CompiledAssembly.GetType(typeToWrap.Name _
       & "Wrapper." & typeToWrap.Name & "WithProperties")

Now we have a fresh compiled type for our wrapper class, and we need to construct an array of wrapper instances that contain our original instances of the array of proxy classes. Therefore we will use following function:

    Public Shared Function GetArrayList(ByVal _
          arrayToConvert() As Object) As ArrayList
        SyncLock GetType(Leadit.Utils.Webservices.WebServiceWrapper)
            If arrayToConvert.Length > 0 Then
                'Add instances of the new class to a new ArrayList
                Dim oldObj As Object
                Dim newType As Type = GetWrapperType_
                          (arrayToConvert(0).GetType)
                Dim newArray As New ArrayList()
                For Each oldObj In arrayToConvert
                    Dim newObj As Object = Activator_
                             .CreateInstance(newType)
                    newObj.SetInnerObject(oldObj)
                    newArray.Add(newObj)
                Next

                'Return the constructed arraylist
                Return newArray
            Else
                Return Nothing
            End If
        End SyncLock
    End Function

Don't worry about the SyncLock, I will explain it later. As you can see, we will use the Activator class to get instances of our compiled wrapper class. Also notice that the return type is an ArrayList that is build by looping through all of the elements of the arrayToConvert that contains the proxy class instances, and generating a wrapper class instance for each of them. That is the result we wanted!

Caching compiled types for performance

As you will see in the complete code, there is a simple caching mechanism. So once a wrapper class for a specific type is generated and compiled, it will be stored in a HashTable. If the WebServiceWrapper is called again, it first checks if the type was already compiled. If so, the wrapper class can be used immediately, from the cache. To provide the cache mechanism, the Singleton design pattern is used.

Providing thread safety

To implement the Singleton Design Pattern, and for the ease of use, most of the functions are implemented as Shared. By doing this, the danger exists that 2 threads access the the same function at the same time, and that is bad! To avoid this, a SyncLock is used to provide Thread safety.

Demo application: CodeProjectMonitor

If you will take a look at the code of this project, you will see that it's very simple... What does this application? Well, this application connects to the CodeProject webservice and retrieves the latest articles. The results are bound to 2 DataGrids, first without using the WebServiceWrapper, then using the WebServiceWrapper. As you will see (screen shot at the top of this article), in the second DataGrid, all the columns are displayed with all of the data. The first DataGrid only displays the number of rows, without the data columns.

When you press the button on the form, the following code will be executed. As you see, using the WebServiceWrapper is only a matter of adding a few statements!

    Private Sub Button1_Click(ByVal sender As_
                 System.Object, ByVal e As System.EventArgs)_
                 Handles Button1.Click
        Dim latestBrief As New com.codeproject.www.LatestBrief()

        'Do not use the WebServiceWrapper
        DataGrid1.DataSource = latestBrief.GetLatestArticleBrief(10)

        'Use the WebServiceWrapper
        DataGrid2.DataSource = WebServiceWrapper._
               GetArrayList(latestBrief.GetLatestArticleBrief(10))
    End Sub

Conclusion

As you can see in the picture at the top of this article, by using the WebServiceWrapper, we get the functionality that was needed: the Name and Email properties are displayed in the DataGrid. The beauty of this solution is (in my opinion), that it's very easy to use, and provides great flexibility. As you can see below, using the WebServiceWrapper is very transparent:

'wsResult is the webservice result (array of objects)
'Do NOT use the wrapper
DataGrid1.DataSource = wsResult
'Do use the wrapper
DataGrid2.DataSource = Leadit.Utils.WebServices_
       .WebServiceWrapper.GetArrayList(wsResult)

On the other hand all of the problems would be avoided if we use strong typed DataSets instead of our custom build business entities. If you live in a Microsoft only world, using strong typed DataSets, could be an advantage, but using custom build business entities gives you greater flexibility. A very good article about this is provided at the end of this article.

Recommended reading

Revision history

  • 20 Feb. 2003: First release
  • 20 Feb. 2003: Added sample project: CodeProjectMonitor
  • 27 Feb. 2003: Added TOC and UML diagram, some minor text changes

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

Share

About the Author

Jan Tielens
Web Developer U2U
Saudi Arabia Saudi Arabia
No Biography provided

Comments and Discussions

 
GeneralMy vote of 1 PinmemberPeaceTiger7-Jan-11 3:27 
GeneralMy vote of 5 PinmemberWRHart18-Aug-10 11:40 
Nice Work ~ Nice Documentation!
QuestionCan we use Datasets Pinmemberksivasenthil15-Apr-08 23:48 
QuestionAren't we creating our business object once again..? Pinmembersekhar_nit23-Oct-07 23:10 
QuestionTools required for visualizing illustrated images Pinmemberhohuy3-Jun-07 21:52 
GeneralThis solution looks likeit will be some help Pinmembercodegalaxy24-May-07 10:06 
GeneralVery good Work! PinmemberDahan Abdo27-Mar-07 9:38 
GeneralHere's the C# equivelant PinmemberMartijn5-Jul-06 8:13 
GeneralI want to return Custom Data From Web Service PinmemberM. Saifullah5-Feb-06 20:56 
GeneralRe: I want to return Custom Data From Web Service PinmemberEmil Åström24-Mar-06 23:20 
GeneralRe: I want to return Custom Data From Web Service Pinmembersekhar_nit23-Oct-07 23:13 
GeneralDataGrid not sorting anymore Pinmemberlongrun20054-Feb-06 8:23 
GeneralC# version PinmemberSam Dahan26-Aug-05 7:01 
GeneralOption Strict on Pinmemberjohn palmer6-May-05 9:56 
Generalfor each loop Pinmemberztg10-Feb-05 13:00 
GeneralThere's another solution... PinmemberEmil Åström14-Jan-05 1:36 
GeneralRe: There's another solution... PinmemberJulian Slater24-Mar-06 16:51 
QuestionGreat Job.. but how can we access the inner class? Pinmemberphyst_e19-May-04 7:51 
QuestionHow would you change the HeaderText ? PinmemberLBrettsinclair17-May-04 13:17 
GeneralAbout the problem in WEBService Pinmember55515-Feb-04 23:03 
GeneralWebservicewrapper + DynwsLib PinmemberNSieger30-Jan-04 22:53 
GeneralComments and Suggestions Pinmemberxianuz6-Jan-04 2:07 
GeneralRe: Comments and Suggestions PinmemberJan Tielens10-Jan-04 1:51 
Generalmy test failed! Pinmembericeecho16-Jul-03 12:52 
GeneralProblem using it Pinmembertony888815-Jul-03 16:47 
GeneralRe: Problem using it PinmemberRyan_Roberts21-Aug-03 7:46 
GeneralRe: Problem using it Pinmemberjimshatt10-May-04 0:27 
GeneralProblem in Web Form PinmemberGodless23-Jun-03 8:42 
GeneralRe: Problem in Web Form Pinmemberdmdj28-Sep-04 12:11 
GeneralRe: Problem in Web Form Pinmemberc_marius23-Nov-04 4:58 
GeneralRe: Problem in Web Form PinmemberJohn Z17-May-05 5:17 
GeneralRe: Problem in Web Form PinmemberSmartAllen20-May-05 6:51 
GeneralRe: Problem in Web Form Pinmembercasey barnes7-Aug-07 11:17 
GeneralSome more praise PinmemberSameer Khan6-May-03 6:33 
Generalhi, Do you have C# version PinmemberStoney Tian19-Apr-03 18:01 
GeneralRe: hi, Do you have C# version PinmemberJan Tielens19-Apr-03 22:20 
GeneralRe: hi, Do you have C# version PinmemberShawn Wall23-Sep-03 5:45 
GeneralRe: hi, Do you have C# version Pinmemberc#lova19-May-04 7:41 
GeneralMore praise PinmemberRichard A. Johnn1-Mar-03 20:13 
GeneralDatagrid binding Pinsussrgw27-Feb-03 2:31 
GeneralRe: Datagrid binding PinmemberJan Tielens27-Feb-03 22:49 
GeneralArticle Diagrams PinmemberMStanbrook20-Feb-03 5:57 
GeneralRe: Article Diagrams PinmemberJan Tielens20-Feb-03 6:59 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.1411023.1 | Last Updated 27 Feb 2003
Article Copyright 2003 by Jan Tielens
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid