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.
Table of Contents
- Introduction
- Explaining the Problem
- The Business Layer
- The Services Layer (a.k.a. Facade)
- The Client Application
- Exploring the Problem
- A Solution!
- Designing the Solution
- Dynamic Proxy Class Generation and Compilation
- Caching Compiled Types for Performance
- Providing Thread Safety
- Demo Application: CodeProjectMonitor
- Conclusion
- Recommended Reading
- Revision History
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.
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:
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:
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 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
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.
<WebMethod()> Public Function GetCustomer(name As String) As 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
Dim custColl As New CustomerCollection
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"
custColl.Add c1
custColl.Add c2
End Function
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.
Dim eng As New Facade.CustomerEngine
DataGrid1.DataSource = CustomerEngine.GetCustomerCollection
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 DataGrid
s. 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...
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:
Public Class CustomerWrapper
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:
DataGrid2.DataSource = Leadit.Utils.WebServices._
WebServiceWrapper.GetArrayList(customers)
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.
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):
Dim code As New CodeTypeDeclaration(typeToWrap.Name & "WithProperties")
code.IsClass = True
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.
Dim declaration As New _
CodeMemberField(typeToWrap, "_" & _
LCase(typeToWrap.Name))
declaration.Attributes = MemberAttributes.Private
code.Members.Add(declaration)
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)
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.
Dim fieldinfo As System.Reflection.FieldInfo
For Each fieldinfo In typeToWrap.GetFields()
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)
Dim getter As New CodeMethodReturnStatement_
(New CodeVariableReferenceExpression(_
"_" & typeToWrap.Name & _
"." & fieldinfo.Name))
prop.GetStatements.Add(getter)
Dim setter As New CodeAssignStatement(_
New CodeVariableReferenceExpression("_" & _
typeToWrap.Name & "." & fieldinfo.Name), _
New CodeVariableReferenceExpression("value"))
prop.SetStatements.Add(setter)
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:
Dim ns As New CodeNamespace(typeToWrap.Name & "Wrapper")
ns.Types.Add(code)
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
):
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
Dim compiler As compiler.ICodeCompiler = New _
VBCodeProvider().CreateCompiler
Dim results As compiler.CompilerResults = _
compiler.CompileAssemblyFromDom(compilerParams, _
codeUnit)
If results.Errors.HasErrors Then
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 the following function:
Public Shared Function GetArrayList(ByVal _
arrayToConvert() As Object) As ArrayList
SyncLock GetType(Leadit.Utils.Webservices.WebServiceWrapper)
If arrayToConvert.Length > 0 Then
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 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!
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.
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 DataGrid
s, 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()
DataGrid1.DataSource = latestBrief.GetLatestArticleBrief(10)
DataGrid2.DataSource = WebServiceWrapper._
GetArrayList(latestBrief.GetLatestArticleBrief(10))
End Sub
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:
DataGrid1.DataSource = wsResult
DataGrid2.DataSource = Leadit.Utils.WebServices_
.WebServiceWrapper.GetArrayList(wsResult)
On the other hand, all of the problems would be avoided if we use strong typed DataSet
s instead of our custom build business entities. If you live in a Microsoft only world, using strong typed DataSet
s, 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.
- 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.
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.