|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
Table of contents
IntroductionCreating 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 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 problemTo 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 layerIn 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 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 aWebService 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 applicationOur sample client application will have only 1 function: retrieve a collection of customers and display them in a '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 problemWhen you try to execute the steps as described above, or take a look at the included demo application, you will notice that the 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 ( That's right, when you made a web reference (in the client app.) to the Webservice, Visual Studio generated a proxy class for your Public Class Customer
'...
Public Name As String
Public Email As String
'...
End Class
The proxy class exposes our properties as fields: 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
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 'Suppose customers is the array of Customer objects
DataGrid2.DataSource = Leadit.Utils.WebServices._
WebServiceWrapper.GetArrayList(customers)
Designing the solutionAnd 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 WebSe Dynamic proxy class generation and compilationAs I said, the technology that we will use is the CodeDom, it's like an The first thing we need is the base container: a '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 '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 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 '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 To compile the '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 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 Caching compiled types for performanceAs 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 Providing thread safetyTo implement the Singleton Design Pattern, and for the ease of use, most of the functions are implemented as Demo application: CodeProjectMonitorIf 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 When you press the button on the form, the following code will be executed. As you see, using the 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
ConclusionAs you can see in the picture at the top of this article, by using the '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 Recommended reading
Revision history
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||