Click here to Skip to main content
15,868,016 members
Articles / Programming Languages / Visual Basic

Exposing Custom-made Classes through a Webservice and Binding them to a DataGrid.

Rate me:
Please Sign up or sign in to vote.
4.84/5 (35 votes)
26 Feb 200311 min read 344.1K   1K   124   44
How to expose custom-made classes through Webservice and bind them to datagrid
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. Introduction
  2. Explaining the Problem
    1. The Business Layer
    2. The Services Layer (a.k.a. Facade)
    3. The Client Application
    4. Exploring the Problem
  3. A Solution!
  4. Designing the Solution
    1. Dynamic Proxy Class Generation and Compilation
    2. Caching Compiled Types for Performance
    3. Providing Thread Safety
  5. Demo Application: CodeProjectMonitor
  6. Conclusion
  7. Recommended Reading
  8. Revision History

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 the end of the article. As documented in MSDN, the structure of such an application would look like this:

Image 2

The Business Layer

In the business layer, you will define your business entity classes. In this example (to keep things simple), we will only need one 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:

VB
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 two 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 two methods that can be used for retrieval. In a real life solution, there would be some methods to add and alter customers.

VB
'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 one function: retrieve a collection of customers and display them in a DataGrid. Here, our problem will start! We have a simple Windows Form with one Button and one DataGrid on it. When the Button is pressed, the client app connects to the services interface (our WebService facade) and retrieves some customers.

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

VB
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

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

Image 4

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:

VB
'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):

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

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

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

VB
'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):

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

VB
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 the following function:

VB
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 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 do? Well, this application connects to the CodeProject webservice and retrieves the latest articles. The results are bound to two 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!

VB
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 image 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:

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

  • 20th February, 2003: First release
  • 20th February, 2003: Added sample project: CodeProjectMonitor
  • 27th February, 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.


Written By
Web Developer U2U
Saudi Arabia Saudi Arabia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralProblem using it Pin
tony888815-Jul-03 15:47
tony888815-Jul-03 15:47 
GeneralRe: Problem using it Pin
Ryan_Roberts21-Aug-03 6:46
Ryan_Roberts21-Aug-03 6:46 
GeneralRe: Problem using it Pin
jimshatt9-May-04 23:27
jimshatt9-May-04 23:27 
GeneralProblem in Web Form Pin
Godless23-Jun-03 7:42
Godless23-Jun-03 7:42 
GeneralRe: Problem in Web Form Pin
dmdj28-Sep-04 11:11
dmdj28-Sep-04 11:11 
GeneralRe: Problem in Web Form Pin
c_marius23-Nov-04 3:58
c_marius23-Nov-04 3:58 
GeneralRe: Problem in Web Form Pin
John Z17-May-05 4:17
John Z17-May-05 4:17 
GeneralRe: Problem in Web Form Pin
Allen _ Wang20-May-05 5:51
professionalAllen _ Wang20-May-05 5:51 
GeneralRe: Problem in Web Form Pin
casey barnes7-Aug-07 10:17
casey barnes7-Aug-07 10:17 
GeneralSome more praise Pin
Sameer Khan6-May-03 5:33
Sameer Khan6-May-03 5:33 
Generalhi, Do you have C# version Pin
Stoney Tian19-Apr-03 17:01
Stoney Tian19-Apr-03 17:01 
GeneralRe: hi, Do you have C# version Pin
Jan Tielens19-Apr-03 21:20
Jan Tielens19-Apr-03 21:20 
GeneralRe: hi, Do you have C# version Pin
Shawn Wall23-Sep-03 4:45
Shawn Wall23-Sep-03 4:45 
GeneralRe: hi, Do you have C# version Pin
19-May-04 6:41
suss19-May-04 6:41 
GeneralMore praise Pin
Richard Johnn1-Mar-03 19:13
Richard Johnn1-Mar-03 19:13 
GeneralDatagrid binding Pin
Rgw27-Feb-03 1:31
Rgw27-Feb-03 1:31 
GeneralRe: Datagrid binding Pin
Jan Tielens27-Feb-03 21:49
Jan Tielens27-Feb-03 21:49 
GeneralArticle Diagrams Pin
MStanbrook20-Feb-03 4:57
MStanbrook20-Feb-03 4:57 
GeneralRe: Article Diagrams Pin
Jan Tielens20-Feb-03 5:59
Jan Tielens20-Feb-03 5:59 

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

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