Adding WCF Services to Silverlight
Explore the nuances of using WCF Services with Silverlight, and learn troubleshooting tips for working with Visual Studio 2010 and the .NET Framework 4.
Q: An application with hard-coded data isn't very interesting. How do I display real data in the Silverlight applications from your earlier columns? (See "Mastering Silverlight," January 2010, and "Digging Deeper into Silverlight and MEF," February 2010.)
A: You're right. In order to focus on aspects of Silverlight, I hard-coded data in my last two columns. In limited cases you can store data in XML files in isolated storage on the user's local machine. There are size and other restrictions, but the real reason for caution with isolated Silverlight storage is that the user can delete these files in what appears to be a simple cleanup action.
For most data, you'll use a Windows Communication Foundation (WCF) service. Silverlight and WCF are an interesting combination because they force programmers who've avoided WCF into suddenly using it, and simultaneously force an asynchronous data model. WCF is an enormous topic and a key area of .NET specialization. If you're running a heavily loaded server or designing complex business-to-business service interactions, you may wish to bring in a specialist. But WCF as a data-access mechanism for Silverlight in less-extreme situations is not difficult. I'll show you how to use WCF, and I'll cover debugging a little later in this article.
I'll also show you how to interact with the server, but I'm still going to skip the actual database access. I want to avoid coupling my explanation to any particular data-access technique, so I'll still hard-code the data as a "fake" or simple approach to mocking. The critical difference is that I'll do this on the server, not the client. You can replace the hard-coded access with LINQ to SQL, ADO.NET, Entity Framework or any other data-access mechanism.
In my previous columns, I anticipated asynchronous server calls when I used databinding. Each step of the asynchronous return pipeline must notify the next that it's complete. Binding to an observable collection takes advantage of Silverlight's built-in support for the CollectionChanged event. This means loading a bound observable collection that automatically displays data in the UI. Data access merely needs to update this collection and the data will be displayed.
The generalized DataForm in the previous column created a ViewModelMany view model for the list and set this as the DataContext:
_viewModelMany = _retriever.GetItem( _
Of IViewModelMany)(targetType, False)
Me.DataContext = New PagedCollectionViewMef(
The code for this approach remains intact, although the behavior changes. Instead of immediately filling the list, it's filled at some later time over an asynchronous pipeline.
The view model accesses a local service to fill itself. Items must be copied in rather than assigning the collection to preserve binding.
Previously this was synchronous:
Public Sub RetrieveMany(ByVal filter As
For Each x In _service.RetrieveList(filter)
The asynchronous version calls a local service to retrieve data and then waits for an event indicating completion:
Public Sub RetrieveMany(ByVal filter As
The view model fills the internal list when it receives data back from the local service after that local service receives data back from the WCF services. Results can be passed along the pipeline with any combination of events and callback delegates. I use events in the sample application:
Private Sub service_RetrieveListCompletedEvent( _
ByVal sender As Object, _
ByVal e As Contracts. _
RetrieveListCompetedEventArgs(Of T)) _
Handles _internalService. _
For Each x In e.Result
The local service also reflects asynchronous behavior, calling a service wrapper and capturing the returned event. This change from a synchronous function to the asynchronous approach also requires parallel changes to the supporting interfaces.
Creating the WCF Service
Now that the client is nearly ready, it's time to create the WCF service. While you can get more control if you create all the pieces manually, the defaults work pretty well with a few modifications. In the Web application created as part of your Silverlight application, click "Silverlight Enabled WCF Service" from the Silverlight sub menu or the New Item dialog.
This template creates a Web service that defines its own server contract. Defining a server contract in an interface allows the contract to be placed into a separate assembly and reused. The contract I created for the customer WCF service uses ServiceContract and OperationContract attributes to define the service. Any members that aren't decorated with the OperationContract attribute won't be included in the WCF service:
Public Interface ICustomerService
Inherits ICrudService(Of Customer)
Public Interface ICrudService(Of T)
Function RetrieveList(ByVal filter As Filter) _
As List(Of T)
' Additional operations as needed
Adjusting the created service to use this service results in:
Public Class CustomerService
Public Function RetrieveList(ByVal filter As Filter) _
As List(Of Customer) _
The body of the service is any .NET code valid on the server and can include the Managed Extensibility Framework (MEF), data libraries like LINQ to SQL or Entity Framework (EF), and any other required code. Keeping the service simple and calling out to additional code in other assemblies for complex tasks keeps your application organized, decoupled and easier to maintain.
In addition to the code file, running the WCF-enabled WCF Service template added a .SVC file and updated web.config. Changing the service to use an interface forces a change to web.config. The default contract needs to be changed:
<endpoint address="" binding="customBinding"
To work correctly when serialized and deserialized across the WCF service boundary, classes that transfer data -- such as the Customer class -- must include DataContract and DataMember attributes. Any members not decorated with the DataMember attribute won't be serialized:
Public Class Customer
Private _customerName As String
Public Property CustomerName() As String Implements
' More properties
The Customer class and other data-transfer classes in the sample also include an InstanceState property indicating whether data is new, modified or deleted. Serializing an enum over WCF requires DataContract and EnumMember attributes:
<Flags(), DataContract()> _
Public Enum InstanceState
<EnumMember()> Unchanged = 0
<EnumMember()> Modified = 1
<EnumMember()> Deleted = 2
<EnumMember()> Created = 4
Hooking up the WCF Service
WCF is a bunch of plumbing controlled by configuration. Part of this configuration occurs on the server via attributes and web.config. The remainder happens on the client. The svcutil.exe tool allows automatic creation of the client-side plumbing, including proxies. You can access this tool through "Add Service Reference" in the context menu of Visual Studio. While this tool can be handy, it creates more artifacts than you need and is rather finicky.
The Add Service Reference dialog includes a button for discovering services in the solution. Once they're displayed in the tree, opening individual nodes displays the corresponding service and operation contracts. Certain types of errors occur when the node expands. Errors appearing at this point include incorrect contracts in web.config and invalid namespace URIs on ServiceContracts.
The proxy-creation tool is capable of creating new data classes. Deserializing to new classes forces you to copy the data from the generated data classes to your own. To avoid this, check "Reuse Types" after clicking "Advanced" in the Add Service Reference dialog.
To reuse types, they must have identical class signatures for the serialized aspects. The easiest way to accomplish this is to create a single class and use it for both the server and the client. If your main classes are in your Silverlight application, you'll create a .NET assembly to parallel this and select "Add Existing Item" from the context menu. After browsing and selecting the classes you wish to include, click the arrow that appears inside the Add button and select "Add as Link." The namespace-qualified name must be identical, so you'll need to ensure the VB default namespace matches in the two assemblies.
The final steps to creating the proxies are to enter a .NET namespace for the resulting classes and click OK. Checking the proxy output at this point narrows the problems that can occur later. The code you care about is in the References.vb or References.cs file below the References.svcmap node under the respective service entry on the client. Certain failures result in an empty file, and other failures result in creation of data classes instead of reusing your existing types.
The service proxy creation process is fragile. There are many ways for things to go wrong, and even simple errors are poorly reported. Start simple, add incrementally and test often. As long as you know which new piece caused a problem, tracking down the solution is generally easy. But if you add a dozen complex service classes before checking proxy generation, the challenge escalates.
Your local service classes could directly access the proxy classes. However, this creates unnecessary coupling between aspects of your application with interface contracts that could change at different rates. You can limit the impact of future change by creating a wrapper or adapter class to shield code from service layer changes.
This wrapper class can also expose the service via MEF. An example of a wrapper class is shown in Listing 1.
There are a lot of little pieces involved in building a pipeline for accessing WCF services from Silverlight using an MEF-based Model-View-ViewModel (MVVM) pipeline. You can adjust this pipeline as appropriate to your application, retaining the asynchronous access, data binding and well-designed WCF services.
Q: How do I debug a Silverlight application during development? I'm currently getting a 404 "not found" error, but I've checked and I think the service exists.
A: Incorporating WCF into apps requires learning new debugging techniques. 404 errors occur when almost anything goes wrong, which can make debugging a guessing game. You have tools at your disposal, but they're new and they require some time and experimentation.
The most important tool is service tracing. There are two parts of service tracing: setting up the trace and reading the trace result. You define your tracing request in web.config. You can change web.config directly, but this offers little help, validation or support. To make these edits easier, open web.config in the Service Configuration Editor. The graphical interface includes configuration-specific help for common tasks. The summary page under the diagnostics tab lets you set common values, including turning on tracing and logging, and specifying the file for trace output.
With tracing enabled, running your application will append events to the specified log file. Trust me, this file is ugly. While you can open it in any XML editor, it's a lot easier to use the Service Trace Viewer. As shown in Figure 1, this UI displays the trace information by activity and detail about each activity. You can also filter activities if you're collecting more information than currently seems relevant. On heavily used servers, this will be particularly important.
[Click on image for larger view.]
|Figure 1. The Service Trace Viewer offers standard and custom filters, search and coloration. The highlighted rows display exception details. |
You can use breakpoints, testing, tracing and utilities to exercise your services to determine whether the problem lies with the client prior to the server call, the WCF call to the service, the service itself, the WCF return pipeline or the client-side return pipeline. If the problem is in the WCF call or return pipeline, service tracing and the trace viewer are key to determining the problem. The problem will often be in serializing the return type. This requires careful checking and narrowing down the problem, often to a simple mistake such as missing a data contract or returning an interface when you intend to return a class.