Using Custom Business Objects with Windows Communication Foundation

By Rob Windsor
Visual Basic MVP, ObjectSharp Consulting

November 2007

In my previous article, Getting Started with Windows Communication Foundation, I described the basics of creating and consuming a simple WCF service. The services demonstrated used simple types like numbers and strings, but most real-world applications use more complex data like customers and invoices. In this article I’ll demonstrate how to work with these custom business objects in WCF.

Download the sample code here. To run the sample code, you'll need Visual Studio 2005, the .NET Framework 3.0, and the Visual Studio 2005 extensions for .NET Framework 3.0 (WCF and WPF) installed.

Setting Up

I’ll start with a solution that contains four projects.

Business is a class library project with a custom business class named Person. Person has a few simple properties (Id, Name, and BirthDate) and a ToString method.

PublicClass Person

    Private _id As Integer
    Private _name As String
    Private _birthDate As DateTime

    Public Sub New(ByVal id As Integer, _
        ByVal name As String, ByVal birthDate As DateTime)
        _id = id
        _name = name
        _birthDate = birthDate
    End Sub

   Public Property Id() As Integer
        Get
            Return _id
        End Get
        Set(ByVal value As Integer)
            _id = value
        End Set
    End Property

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

   Public Property BirthDate() As DateTime
        Get
            Return _birthDate
        End Get
        Set(ByVal value As DateTime)
            _birthDate = value
        End Set
    End Property

    Public Overrides Function ToString() As String
        Return String.Format( _
           "{0} [Id:{1}; Birth Date: {2}]", _
            _name, _id, _birthDate)
    End Function

End Class

Services is a class library project with a service class named PersonService. It exposes three operations: one to get a single Person object, one to get a group of Person objects, and one to update a Person object. Because the service uses an in-memory data store (i.e., the Dictionary), the instancing mode has been set to Single. This means that a single service object will handle all incoming requests, and those requests will access or modify the data inside the Dictionary.

Imports System.ServiceModel
Imports Business

<ServiceContract()> _
Public Interface IPersonService
    <OperationContract()>
    Function GetPerson(ByVal id As Integer) As Person
    <OperationContract()>
    Function GetPeople() As Person()
    <OperationContract()>
    Sub UpdatePerson(ByVal p As Person)
End Interface

<ServiceBehavior(InstanceContextMode:= _
    InstanceContextMode.Single)> _
Public Class PersonService
    Implements IPersonService

    Private _people As New Dictionary(Of Integer, Person)

    Public Sub New()
        _people.Add(1, _
           New Person(1, "Frodo Baggins", #1/1/1380#))
        _people.Add(2, _
           New Person(2, "Sam Gamgee", #2/2/1385#))
        _people.Add(3, _
           New Person(3, "Merry Brandybuck", #3/3/1390#))
        _people.Add(4, _
           New Person(4, "Pippin Took", #4/4/1395#))
    End Sub

   Public Function GetPerson(ByVal id As Integer) _
        As Person Implements IPersonService.GetPerson
        Dim result As Person = Nothing
        If _people.ContainsKey(id) Then
            result = _people(id)
        End If
        Return result
    End Function

   Public Function GetPeople() As Person() _
        Implements IPersonService.GetPeople
        Dim result(_people.Count - 1) As Person
        _people.Values.CopyTo(result, 0)
        Return result
    End Function

   Public Sub UpdatePerson(ByVal p As Person) _
        Implements IPersonService.UpdatePerson
        If _people.ContainsKey(p.Id) Then
            _people(p.Id) = p
        End If
    End Sub

End Class

Hosts is a console application that will be our service host. An endpoint has been configured to use an address of “https://localhost:8081/PersonService” and the basicHttpBinding.

Imports System.ServiceModel

Module Module1

    Sub Main()
        Using host As New ServiceHost( _
           GetType(Services.PersonService))
            host.Open()

            Console.WriteLine("Service started")
            Console.ReadLine()
        End Using
    End Sub

End Module

 

<? xml version ="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior name="MexEnabled">
          <serviceMetadata
           httpGetEnabled="true"  
           httpGetUrl=
              "https://localhost:8081/PersonService" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <services>
      <service
       behaviorConfiguration="MexEnabled"  
       name="Services.PersonService">
        <endpoint
          address="https://localhost:8081/PersonService"
          binding="basicHttpBinding"
          bindingConfiguration=""
          contract="Services.IPersonService"/>
      </service>
    </services>
  </system.serviceModel>
</configuration>

Finally, Client is a Windows Forms project that will be used to browse and edit data. The user interface elements have been added to the form, but none of the event handlers have been implemented.

Data Contracts

Once you have the solution set up and have a clean build, right-click the Hosts project and choose Debug > Start new instance. You should receive an error that looks like this:

Unlike ASMX Web Services, WCF does not allow automatic serialization of objects. All types you receive as parameters or return from methods must be serializable. While this can be done by having your type implement the IXmlSerializable interface, or by applying the Serializable attribute to your class, the preferred method is to use the new DataContact and DataMember attributes from the System.Runtime.Serialization namespace.

To address the InvalidDataContractException, we will add these attributes to our custom business object. Right-click the Business project and select Add reference, select System.Runtime.Serialization, and click OK.

Add an Imports statement for System.Runtime.Serialization to the top of the source code file for the Person class. Then add the DataContract attribute to the Person class declaration and the DataMember attribute to each of the properties.

Imports System.Runtime.Serialization

<DataContract()> _
Public Class Person

    Private _id As Integer
    Private _name As String
    Private _birthDate As DateTime

    Public Sub New(ByVal id As Integer, _
        ByVal name As String, ByVal birthDate As DateTime)
        _id = id
        _name = name
        _birthDate = birthDate
    End Sub

    <DataMember()> _
    Public Property Id() As Integer
        Get
            Return _id
        End Get
        Set(ByVal value As Integer)
            _id = value
        End Set
    End Property

    <DataMember()> _
    Public Property Name() As String
        Get
            Return _name
        End Get
        Set(ByVal value As String)
            _name = value
        End Set
    End Property

    <DataMember()> _
    Public Property BirthDate() As DateTime
        Get
            Return _birthDate
        End Get
        Set(ByVal value As DateTime)
            _birthDate = value
        End Set
    End Property

   Public Overrides Function ToString() As String
        Return String.Format( _
           "{0} [Id:{1}; Birth Date: {2}]", _
            _name, _id, _birthDate)
    End FunctionEnd Class

With these changes, our service should be ready to go. Right-click the Hosts project and select Debug > Start new instance. This time everything should work correctly: the console window should appear and there should be an indication that the service has started.

With the service host still running, right-click the Client project and select Add service reference. Enter “https://localhost:8081/PersonService” for the Service URI and PersonServiceProxy for the Service reference name, and then click OK. Once the service reference has been added to the client, close the service host console window.

Proxy Class

The Add Service Reference dialog does two things. It adds a client endpoint to the configuration for the project or web site, and it creates a service proxy class. To see the proxy class, select the Client project and click the Show All Files button in the Solution Explorer’s toolbar.

Double-click PersonServiceProxy.vb to open it in the text editor. The part we are interested in is the Person class. Not only does the proxy contain the code we need to call our service, it also has a client-side representation of our business class. When the data for a Person is sent from the service, it will be stored here. Also note that this class only represents the data, not the functionality. None of the methods from the original class will be included here.

System.Runtime.Serialization.DataContractAttribute( _
    [Namespace]:= "..."), _
System.SerializableAttribute()> _
Partial Public Class Person
    Inherits Object
    Implements IExtensibleDataObject

    <NonSerializedAttribute()> _
    Private extensionDataField As ExtensionDataObject

    <OptionalFieldAttribute()> _
    Private BirthDateField As Date

    <OptionalFieldAttribute()> _
    Private IdField As Integer

    <OptionalFieldAttribute()> _
    Private NameField As String

   Public Property ExtensionData() As ExtensionDataObject  
        Implements IExtensibleDataObject.ExtensionData
        Get
            Return Me.extensionDataField
        End Get
        Set(ByVal value As ExtensionDataObject)
           Me.extensionDataField = value
        End Set
    End Property

    <DataMemberAttribute()> _
    Public Property BirthDate() As Date
        Get
            Return Me.BirthDateField
        End Get
        Set(ByVal value As Date)
           Me.BirthDateField = value
        End Set
    End Property

    <DataMemberAttribute()> _
    Public Property Id() As Integer
        Get
            Return Me.IdField
        End Get
        Set(ByVal value As Integer)
           Me.IdField = value
        End Set
    End Property

    <DataMemberAttribute()> _
    Public Property Name() As String
        Get
            Return Me.NameField
        End Get
        Set(ByVal value As String)
           Me.NameField = value
        End Set
    End Property
End Class

Using the Service

Now that we have the proxy, we can add the logic to our client application. We will start by adding code to populate the list box when the form loads. Add event handlers for the form’s Load event and the list box's SelectedIndexChanged event. Leave these empty for now.

Just below this code, add the declaration for a private subroutine called LoadListBox. In this method we want to create an instance of our service proxy and then data bind the results of calling GetPeople to the list box. Once the list box is populated, we want to activate the event handler for the SelectedIndexChanged event. With this method complete, have the form’s Load event call LoadListBox.

PrivateSub Form1_Load(...) HandlesMyBase.Load
    LoadListBox()
End Sub

Private Sub LoadListBox()
    Using ws As New PersonServiceProxy.PersonServiceClient
        PeopleListBox.DataSource = ws.GetPeople()
        PeopleListBox.DisplayMember = "Name"
        PeopleListBox.ValueMember = "Id"
    End Using

    PeopleListBox_SelectedIndexChanged(Me, EventArgs.Empty)
    AddHandler PeopleListBox.SelectedIndexChanged, _
        AddressOf PeopleListBox_SelectedIndexChanged
End Sub

Now, when the user selects an item in the list box, we want to show the data for the associated Person object in the data entry area below. In the SelectedIndexChanged event hander, create an instance of the service proxy and call GetPerson, passing in the SelectedValue from the list box (cast to an integer) as a parameter. We’ll need a variable to store the result, so declare a private field (i.e., a class-level variable) called _person and use it. Then copy the values of the properties from the Person object into the text boxes.

Private_person As PersonServiceProxy.Person = Nothing

Private Sub PeopleListBox_SelectedIndexChanged(...)
    Using ws As New PersonServiceProxy.PersonServiceClient
        _person = ws.GetPerson(CInt(PeopleListBox.SelectedValue))
        IdTextBox.Text = _person.Id
        NameTextBox.Text = _person.Name
        BirthDateTextBox.Text = _
            _person.BirthDate.ToShortDateString()
    End Using
End Sub

Finally, in the Save button’s Click event, we want to reverse this process. Copy the values of the text boxes into the _person field’s properties, create an instance of the service proxy, and then call UpdatePerson to send the changes back to the service. The last line should be a call to LoadListBox to reload the data (with your changes) from the service.

PrivateSub SaveButton_Click(...) Handles SaveButton.Click
    _person.Id = IdTextBox.Text
    _person.Name = NameTextBox.Text
    _person.BirthDate = CDate(BirthDateTextBox.Text)
    Using ws As New PersonServiceProxy.PersonServiceClient
        ws.UpdatePerson(_person)
    End Using
    LoadListBox()
End Sub

With this done, you should now be able to test. We are going to need both the client and the service host running to use the application, so right-click the solution and select Properties. Select Multiple startup projects and then set the Hosts project to start first followed by the Client project.

You should now be able browse through the list of people (Hobbits actually), edit their data and save it back to the service.

Read-Only Properties

You may have noticed a small problem with our application while testing it: we can edit the Id property of the Person object. This is not a good idea, since we are using the property to identify the Person object in the service. If you edit the Id, your changes may be ignored or, worse yet, you may change the data for the wrong person.

To address this problem, go to the Person class and make the Id property read-only.

<DataMember()> _
Public ReadOnly Property Id() As Integer
    Get
        Return _id
    End Get
End Property

Get a clean build on the solution, and then right-click the Hosts project and select Debug > Start new instance.

Arggghhh!! This won’t work. Just like ASMX Web Services, only entities that can be read from and written to can be serialized. There are several possible solutions for this issue. I will describe one of them.

One Solution

Since read-only properties cannot be serialized, we are going to change the Person class so that the fields are included in the data contract instead of the properties. Open the Person class and remove the DataMember attribute from the three properties, and then add it to the declarations of the three fields (i.e. _id, _name, and _birthDate).

Imports System.Runtime.Serialization

<DataContract()> _
Public Class Person

    <DataMember()> Private _id As Integer
    <DataMember()> Private _name As String
    <DataMember()> Private _birthDate As DateTime

    Public Sub New(ByVal id AsInteger, _
        ByVal name As String, ByVal birthDate As DateTime)
        _id = id
        _name = name
        _birthDate = birthDate
    End Sub

   Public ReadOnly Property Id() As Integer
        Get
            Return _id
        End Get
    End Property

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

   Public Property BirthDate() As DateTime
        Get
            Return _birthDate
        End Get
        Set(ByVal value As DateTime)
            _birthDate = value
        End Set
    End Property

   Public Overrides Function ToString() As String
        Return String.Format( _
           "{0} [Id:{1}; Birth Date: {2}]", _
            _name, _id, _birthDate)
    End Function

End Class

Since we’ve changed how the Person object will look when it is serialized, the service proxy on the client needs to be modified. While you should be able to right-click PersonServiceProxy.map and select Update service reference to do this, I have found doing so problematic. Instead, right-click the Service References folder and select Delete. Do the same for the app.config file.

Get a clean build of the Hosts project, and then right-click and select Debug > Start new instance. With the service host running, right-click the client project and select Add service reference. Enter “https://localhost:8081/PersonService” for the Service URI and PersonServiceProxy for the Service reference name, and then click OK. Once the service reference has been added to the client, close the service host console window.

This hasn’t really solved our problem. The _id property of the client-side version of Person is still read/write. What we need to do to address this issue is to use the real Person object on the client side instead of the version in the proxy. Right-click the Client project and select Add reference, then from the Projects tab, select Business, and click OK.

Here comes the dodgy part. We need to manually edit the client-side proxy—even though the comment at the top of the proxy warns you against doing so. This is a problem because, if we need to regenerate our proxy, all of the changes we are about to make will be blown away. So, you will need to remember to make the edits every time you regenerate the proxy. If it’s any comfort, the Add Service Reference dialog in Visual Studio 2008 will address this issue.

Open PersonServiceProxy.vb in the code editor and remove the Person class from it. Then replace PersonServiceProxy.Person with Business.Person everywhere it appears. Now open Form1.vb in the code editor and replace PersonServiceProxy.Person with Business.Person everywhere it appears. Finally, in the Save button’s Click event hander, remove the line of code that sets the Id property. Since the Id cannot be changed, you may also want to make the text box that displays it read-only.

Imports System.ComponentModel

Public Class Form1

    Private _person As Business.Person = Nothing

   Private Sub Form1_Load(...) HandlesMyBase.Load
        LoadListBox()
    End Sub

   Private Sub LoadListBox()
        Using ws As New PersonServiceProxy.PersonServiceClient
            PeopleListBox.DataSource = ws.GetPeople()
            PeopleListBox.DisplayMember = "Name"
            PeopleListBox.ValueMember = "Id"
        End Using

        PeopleListBox_SelectedIndexChanged(Me, EventArgs.Empty)
        AddHandler PeopleListBox.SelectedIndexChanged, _
           AddressOf PeopleListBox_SelectedIndexChanged
    End Sub

   Private Sub PeopleListBox_SelectedIndexChanged(...)
        Using ws As New PersonServiceProxy.PersonServiceClient
            _person = ws.GetPerson( _
               CInt(PeopleListBox.SelectedValue))
            IdTextBox.Text = _person.Id
            NameTextBox.Text = _person.Name
            BirthDateTextBox.Text = _
                _person.BirthDate.ToShortDateString()
        End Using
    End Sub

   Private Sub SaveButton_Click(...) Handles SaveButton.Click
        _person.Name = NameTextBox.Text
        _person.BirthDate = CDate(BirthDateTextBox.Text)
        Using ws As New PersonServiceProxy.PersonServiceClient
            ws.UpdatePerson(_person)
        End Using
        LoadListBox()
    End Sub

End Class

You should now be able to run and test the completed application.

Another benefit of using the real Person type on the client is that you can now make use of its methods. To demonstrate this, we will add some code to call the ToString method, which the Person class has overridden. Add the button to the client form and then double-click it to add an event handler. In the event handler add the code to cast the SelectItem from the PeopleListBox to a Person and then show the results of calling ToString in a message box.

Private Sub ToStringButton_Click(...) Handles _
    ToStringButton.Click
    Dim p As Business.Person

    p = CType(PeopleListBox.SelectedItem, Business.Person)
    MsgBox(p.ToString())
End Sub

When you click the button, you should see results that look like this:

Dispose()

While there is more to the subject of data serialization, this article covers the fundamentals and the core issues. The most common challenge is dealing with read-only data, and I’ve shown you a technique to deal with this situation. Armed with this knowledge, you should now be able to build services that expose and consume custom business objects.

About Rob Windsor

Rob Windsor is a Senior Consultant and the Director of Training with ObjectSharp Consulting - a Microsoft Gold Partner based in Toronto, Canada. Rob focuses on the architecture, design and development of custom business applications using leading edge Microsoft technologies. In addition Rob is a top rated instructor - authoring and teaching courses on .NET development, SharePoint and software architecture. As a member of the MSDN Canada Speakers Bureau, Rob has had a chance to present at conferences, code camps, and user group meetings in Toronto and across North America. He is President of the Toronto Visual Basic User Group and has been recognized as a Microsoft Most Valuable Professional (MVP) for his involvement in the developer community.