In part one of this post series, I showed the working Silverlight application that calls a WCF service. If you would like to see this application then go to part one.
In this post we will show how one goes about creating this application. This post is divided into fout main sections;
- Creating the WCF service.
- Creating the Silverlight application that consumes the service.
- Deployment Issues
- Deploying the WCF service and Silverlight control.
Initial Setup
Let's start by creating a Silverlight application. Start VS2008 and then select File->New Project menu option. This will bring up the New Project dialog. This dialog is shown in Figure 1. I have decided to name this project WCF.
Figure 1: Creating a Silverlight Application.
In my case I have chosen the to create a Silverlight application using the VB.NET language. After clicking the OK button, the Add Silverlight Application dialog appears. As shown in Figure 2, accept the defaults and click the OK button.
Figure 2: The Add Silverlight Application Dialog.
VS2008 should have created a solution with two projects; the first an ASP.NET application used to host the Silverlight control and a second which is the Silverlight control. Figure 3 shows the solution window with these two projects.
Figure 3: The Initial Silverlight Solution.
We are now ready to create the WCF service.
WCF Service
The service we about to create will return a list of person objects. Begin by adding a new class to the web site project. To do this right click on the WCFWeb project and select the Add New Item menu option. This will result in the Add New Item dialog appearing as shown in Figure 4.
Figure 4: Adding the Person class
Name the class Person and select the OK button. This will cause an additional dialog to appear asking whether you want to add the code to the App_Code directory, select OK. Modify the generated Person class as per Listing 1.
Imports System.ServiceModel
Imports System.Runtime.Serialization
Imports Microsoft.VisualBasic
'This class needs to be serializable. The DataContract attribute is the WCF way
'of doing this.
<DataContract()> _
Public Class Person
'First name of the person. You must opt in to make a property serializable,
'hence the DataMember attribute.
Private _First As String
<DataMember()> _
Public Property First() As String
Get
Return _First
End Get
Set(ByVal value As String)
_First = value
End Set
End Property
'Last name of the person. You must opt in to make a property serializable,
'hence the DataMember attribute.
Private _Last As String
<DataMember()> _
Public Property Last() As String
Get
Return _Last
End Get
Set(ByVal value As String)
_Last = value
End Set
End Property
'Simple constructor.
Public Sub New(ByVal first As String, ByVal last As String)
Me.First = first
Me.Last = last
End Sub
End Class
Listing 1: Person Class
The Person class is rather simple containing just two properties and one constructor. The class is decorated with the <DataContract()> attribute which indicates it is serializable. The <DataMember()> attributes that decorate the two properties indicates that the members are part of the contract and are serializable. Unlike the <Serializable()> attribute, where by default properties are automatically serialized, with the <DataContract()> attribute you must explicitly chose which properties are included during serialization.
Next add a WCF Service template to the web project. Right click on the web project and select the Add New Item menu option. This will bring up the Add New Item dialog as shown in Figure 5. Call the service PersonService.
Figure 5: Add the WCF Service
After selecting the Add button, three new files are added to the web project; IPersonService.vb, PersonService.vb and PersonService.svc. In addition, the web.config file is modified with the addition of the <system.serviceModel> section.
Let's investigate each file.
As previously mentioned the web.config is modified whenever a WCF service is added to the project. Specifically, the section <system.serviceModel> is added. It is here that the configuration of the newly added WCF service exists. Two changes are required in order to get the service to properly working. Firstly, the binding wsHttpBinding needs to be changed to basicHttpBinding as Silverlight currently only supports the basicHttpBinding. The second change, although not strictly required, is to hard code the port for the dns. I like to do this to ensure that the application will always work regardless of which port the development web server decides to chose. An alternative approach would be to create a web application which automatically configures an IIS virtual directory. In order to hard code a port number select the web site project and go to the Properties window. Next set Use dynamic ports to False and then chose some port number. I like to use a port number of 666. Now you can update the web.config and hard code the dns value to localhost:666. Figure 6 shows the property window with the hard code port number and Listing 2 shows the <system.serviceModel> section with the binding and port number changes.
Figure 6: The Web Site Project Properties Window with the hard coded port number
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior name="PersonServiceBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false" />
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service behaviorConfiguration="PersonServiceBehavior" name="PersonService">
<endpoint address="" binding="basicHttpBinding" contract="IPersonService">
<identity>
<dns value="localhost:666" />
</identity>
</endpoint>
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
</service>
</services>
</system.serviceModel>
Listing 2: web.config
The change to the IPersonService.vb interface is small as all we need to do is change the generated function name to something more meaningful. Listing 3 shows the necessary changes to IPersonService.vb.
Imports System.ServiceModel
<ServiceContract()> _
Public Interface IPersonService
<OperationContract()> _
Function GetPeople() As List(Of Person)
End Interface
Listing 3: IPersonService.vb
Note the only change to the templated interface is the function name and that it returns a list of person objects. The interface is decorated with the <ServiceContract()> attribute indicating the interface defines a service contract in a WCF application. The <OperationContract()> attribute indicates that the GetPeople function defines an operation that is part of a service contract. The bottom line is that a client (Silverlight in our case) can call the function GetPeople across process boundaries.
The PersonService class needs to implement the IPersonService interface. Listing 4 contains the modifications to the class.
Imports System.ServiceModel
'Implementation of IPersonService.
<ServiceBehavior(IncludeExceptionDetailInFaults:=True)> _
Public Class PersonService
Implements IPersonService
'Return an in memory list of people.
Public Function GetPeople() As List(Of Person) Implements IPersonService.GetPeople
Dim people As New List(Of Person)
people.Add(New Person("John", "Smith"))
people.Add(New Person("Jane", "Summers"))
Return people
End Function
End Class
Listing 4: PersonService.vb
The implementation of the GetPeople function is rather simple. Firstly, we create a reference to a List of Person objects. Next two person objects are added to the list collection and finally this list of persons is returned.
The PersonService.svc represents the end point to the service. Provided we run the service within the development environment, this file does not require any modifications. Later, in the this post we will discuss deployment issues and we will see that modifications to this file and additional code is required when hosting the service on a web server that uses multiple host headers (i.e., more than one address). More on that later.
Right click on the PersonService.svc and select the View in Browser menu option. If all is well you should see the default service web page appearing (see Figure 7).
Figure 7: Navigating to the PersonService.svc service.
That's it we have created a WCF service. Now let's move on to consuming this service from within a Silverlight application.
Consuming the WCF Service from within Silverlight
We will start by adding a reference to the service. Right click on the Silverlight project and select the Add Service Reference menu item. This will bring up the Add Service Reference dialog. Select the Discover button. This will cause the IDE to search the solution for all services, displaying them in the list box. Since there is only one service in the solution the Services list box will only contain a single service, namely, PersonService.svc. Change the namespace to PersonProxy. After doing this the dialog should appear similar to Figure 8.
Figure 8: Adding a Service Reference to the Silverlight Project
Select OK. This will add the PersonProxy reference to the project and in addition will add the ServiceReferences.ClientConfig configuration file. This configuration file requires modification as it does not contain the fully qualified name of the service. It is missing the namespace, WCF (the name of the project). I'm not sure if this is a Beta 2 bug and perhaps it will be fixed for RTM. After this change the ServiceReferences.ClientConfig file should appear the same as in Listing 5.
<configuration>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_IPersonService"
maxBufferSize="65536"
maxReceivedMessageSize="65536">
<security mode="None" />
</binding>
</basicHttpBinding>
</bindings>
<client>
<endpoint address="http://localhost:666/WCFWeb/PersonService.svc"
binding="basicHttpBinding"
bindingConfiguration="BasicHttpBinding_IPersonService"
contract="WCF.PersonProxy.IPersonService"
name="BasicHttpBinding_IPersonService" />
</client>
</system.serviceModel>
</configuration>
Listing 5: ServiceReferences.ClientConfig
This configuration file is included as content into the Silverlight XAP deployment file. If you need to change the end point, as you would need to do so if you are changing the location of the WCF service, then you can rename the XAP extension to ZIP, unzip the contents, modify the endpoint, re-zip the file and rename the extension back to XAP.
Before we can write any code to call the service, we need to add some UI to the Page.xaml file. I have chosen to display the data in a DataGrid. To initiate the call to the sercice I have added a button control. Finally, in case there were any errors generated during the call to the service, I have included a TextBlock. Let's proceed with the UI.
Listing 6 contains the complete XAML for the UI. The default generated Grid has been replaced with a StackPanel. Within the StackPanel are the Button, DataGrid and TextBlock controls. It is best not to paste the code from this listing into your copy of Page.xaml as the DataGrid requires an addition reference which is automatically included when you drag and drop the control.
<UserControl
xmlns:my="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
x:Class="WCF.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="500" Height="250">
<StackPanel x:Name="LayoutRoot" Background="Bisque">
<Button Content="Call WCF Service" FontSize="24" Width="220" Height="50"
Margin="15" Click="btnWCF_Click" />
<my:DataGrid x:Name="grdPeople" AutoGenerateColumns="False"
FontSize="24" Width="500" RowHeight="50"
Visibility="Collapsed">
<my:DataGrid.Columns>
<my:DataGridTextColumn Header="First Name" FontSize="24" Width="230"
DisplayMemberBinding="{Binding First}" />
<my:DataGridTextColumn Header="Last Name" FontSize="24" Width="237"
DisplayMemberBinding="{Binding Last}" />
</my:DataGrid.Columns>
</my:DataGrid>
<TextBlock x:Name="txtError" TextWrapping="Wrap" Width="500" Height="350"
ScrollViewer.VerticalScrollBarVisibility="Auto" />
</StackPanel>
</UserControl>
Listing 6: Page.xaml
A couple of notes on the DataGrid. The AutoGenerateColumns is set to false so that we can control which items are data bound. We are declaratively binding the first and last name using the DisplayMemberBinding attribute. Later in the code behind we will call the service and set the ItemSource of the DataGrid.
That's it for the UI now let's concentrate on the code behind.
The code behind for Page.xaml is responsible for calling the WCF service and then setting the ItemSource of the DataGrid to the list of person objects. Listing 7 contains the complete code listing for page.xaml.vb.
Partial Public Class Page
Inherits UserControl
Public Sub New()
InitializeComponent()
End Sub
Private Sub btnWCF_Click( _
ByVal sender As System.Object, _
ByVal e As System.Windows.RoutedEventArgs)
Dim proxy As New PersonProxy.PersonServiceClient()
AddHandler proxy.GetPeopleCompleted, AddressOf onGetPeopleCompleted
proxy.GetPeopleAsync()
grdPeople.Visibility = Windows.Visibility.Collapsed
grdPeople.ItemsSource = Nothing
End Sub
Public Sub onGetPeopleCompleted( _
ByVal sender As Object, _
ByVal e As PersonProxy.GetPeopleCompletedEventArgs)
If e.Error Is Nothing Then
grdPeople.Visibility = Windows.Visibility.Visible
grdPeople.ItemsSource = e.Result
Else
txtError.Text = e.Error.ToString
End If
End Sub
End Class
Listing 7: Page.xaml.vb
The code consists of two methods. The btnWCF_Click method is wired to the button click event. This is where we instantiate the PersonProxy. Next an event handler for the GetPeopleCompleted event is added specifying the method to be called when the event is fired. We then initiate the call to the service by invoking GetPeopleAsync. Silverlight will only allow asynchronous calls to services.
The method onGetPeopleCompleted is responsible for binding the data to the DataGrid. As a precaution, we first check to see if an error has occurred and if so the error is displayed in a TextBlock. I have found that exceptions that happen during the call to the WCF service are by default hidden, i.e., they appear as (404) Not Found exception. This is a security feature as you don't necessarily want the consumers of the service to have knowledge of your code. Further investigation is required to know how to best handle these kinds of exceptions.
If no error is generated during the call to the WCF service then the returned data (e.Result) is is bound to the DataGrid via the ItemSource property.
That's it we are done. Let's try running the application. Right click the WCFTestPage.aspx file in the web site project and select View in Browser menu option. This should bring up the browser and hopefully the Silverlight control will appear. Click the Call WCF Service button and after a short period of time a DataGrid should appear, containing a list of people. Figure 9 shows the DataGrid with the list of people.
I also have a working copy of this Silverlight control embedded in my previous post.
Figure 9: Running the Silverlight Test Page.
Deployment Issues
I wanted to share with you some challenges I came across when I deployed this WCF service to my ISP. Hopefully this will save you some time.
The first problem I came across was using Silverlight for cross-domain communication, that is, Silverlight by default can only call services that are hosted on the same domain. This prevents cross-site request forgery and prevents a Silverlight control from making unauthorized calls to a third party service. In order for a Silverlight control to access a service in another domain the service must grant access. This can be done by installing one of two files on the web server. I will discuss only one of these files, the ClientAccessPolicy.xml. The contents of this file is shown in Listing 8.
<?xml version="1.0" encoding="utf-8" ?>
<access-policy>
<cross-domain-access>
<policy>
<allow-from http-request-headers="*">
<domain uri="*" />
</allow-from>
<grant-to>
<resource include-subpaths="true" path="/" />
</grant-to>
</policy>
</cross-domain-access>
</access-policy>
Listing 8: ClientAccessPolicy.xml
To add this file to the web site project, right click the project and select the Add New Item menu option. From the Add New Item dialog box select the XML file template and name it ClientAccessPolicy.xml. Finally, cut and paste the contents of the XML in listing 8 into this XML file.
As indicated by the <domain uri="*" /> node, this file allows requests from any domain. For further details on allowing cross-domain access visit MSDN Site. This file must be deployed to the root of the domain where the service is installed.
The second problem was a little more difficult to resolve. After I installed the the ClientAccessPolicy.xml file to the web server I received the following error:
This collection already contains an address with scheme http: There can be at most one address per scheme in the collection.
After a bit of searching I found a few posts that explained this multiple bindings issue. Out of the box .NET does not support multiple bindings per site and since I am hosting this service on my ISP the likelihood that it has multiple bindings is great. Thankfully there is a way around this that involves creating your own custom service host factory. This factory is responsible for choosing the appropriate base address. Listing 9 shown this custom service host factory.
Imports Microsoft.VisualBasic
Imports System.ServiceModel.Activation
Imports System.ServiceModel
Public Class CustomHostFactory
Inherits ServiceHostFactory
Protected Overrides Function CreateServiceHost( _
ByVal serviceType As System.Type, _
ByVal baseAddresses() As System.Uri) _
As System.ServiceModel.ServiceHost
Return New ServiceHost(serviceType, baseAddresses(0))
End Function
End Class
Listing 9: Custom Service Host Factory
To add this code right click the App_Code folder in the web site project and select the Add New Item menu option. Then from the Add New Item dialog select the Class template and name it CustomService.vb. Finally cut and paste the code from Listing 9 into CustomService.vb.
The code is rather simple. The class CustomHostFactory inherits from ServiceHostFactory and overrides the CreateServiceHost function. The implementation creates an instance of a ServiceHost class and chooses the first base address from the collection of addresses.
Next the PersonService.svc file must be changed so that the service is created using this custom service host factory. Listing 10 shows this modified PersonService.svc file. For further information please visit the following blog post.
<%@ ServiceHost Language="VB" Debug="true" Factory="CustomHostFactory"
Service="PersonService" CodeBehind="~/App_Code/PersonService.vb" %>
Listing 10: Adding Custom Service Host Factory to PersonService.svc
Deploying the WCF Service and Silverlight Application to a Web Server
WCF Service:
Copy the following files to the virtual directory where the service is to be hosted:
- App_Code/CustomService.vb
- App_Code/IPersonService.vb
- App_Code/Person.vb
- App_Code/PersonService
- PersonService.svc
- web.config (modify the DNS value and set it to the domain where the service is to hosted)
Silverlight Application:
Add the Silverlight control to whatever page you desire. If you need to change the location of the WCF Service then you can rename the XAP extension to ZIP, unzip the contents, modify the endpoint in the ServicesReferences.ClientConfig file, re-zip the file and rename the extension back to XAP.
Guess the movie
See that clock on the wall? In five minutes you are not going to believe what I've told you.