Tuesday, November 3, 2009

Silverlight Communication: Three Ways to Connect

Silverlight has myriad ways to connect to other systems and retrieve information for your applications. It is a common question people ask ("How do I get my data from the database to Silverlight?") and each method has its own pros and cons. The purpose of this project, "Silverlight Communicator," is to provide a simple, easy reference project that demonstrates three sample methods for communication in Silverlight 3. While these certainly aren't all of the ways Silverlight can obtain data, it does summarize more commonly used methods.

Download Source Now (44 Kb)

Silverlight Communicator

The main page is straightforward and uses the MVVM (Model-View-ViewModel) pattern to bind to the selections (no code-behind). I kept it simple and sweet: just a grid with two listboxes, which are bound to the view model that is instantiated as a static resource.

<UserControl x:Class="SilverlightCommunication.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    xmlns:local="clr-namespace:SilverlightCommunication"
    mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
    <UserControl.Resources>
        <local:ViewModel x:Key="ViewModel"/> 
    </UserControl.Resources>
        <Grid x:Name="LayoutRoot" DataContext="{StaticResource ViewModel}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*"/>
            <ColumnDefinition Width="1*"/>
        </Grid.ColumnDefinitions>
        <ListBox Grid.Column="0" ItemsSource="{Binding CommunicationTypes}" DisplayMemberPath="Name"
                 SelectedItem="{Binding Path=Communication,Mode=TwoWay}"/>
        <ListBox Grid.Column="1" ItemsSource="{Binding Months}" DisplayMemberPath="Name"/>
  </Grid>
</UserControl>

The view model exposes a collection of CommunicationType. This could probably be an enum but for the contrived example, it is loaded up and wired in the view model constructor and maps to the options for choices of the communication type. The view model also exposes a collection of Month, which is a class that contains just one property: Name (you guessed it: the name of the month). Again, a contrived example but it made more sense to bind to a class than a simple string.

If you notice in the code behind, I wired in SelectedItem="{Binding Path=Communication,Mode=TwoWay}". This will data bind the selected item of the list box to the property called Communication on the view model. This allows me to intercept the setter and work some magic. The "magic" looks like this:

set
{
    _communicationType = value;
    IMonthService service = ServiceFactory.GetService(value);
    if (service != null)
    {
        service.MonthsLoaded += new EventHandler<MonthArgs>(service_MonthsLoaded);
        service.RequestMonths();
    }
}

As you can see, when a communication type is selected, the view model goes to a factory and retrieves a service that honors the IMonthService contract. It then wires into a loaded event (where it will insert the months into the Months property) and requests the month list. Because Months is an ObservableCollection, it will fire an event to notify the UI that it has changed and the result will be a month list populated in the list box on the right side.

The month service contract is simple, but this is where we will implement three different ways to retrieve our list of months:

namespace SilverlightCommunication
{
    public interface IMonthService
    {
        void RequestMonths();

        event EventHandler<MonthArgs> MonthsLoaded;
    }
}

Before we jump into the specific methods, here's a peek at the factory. Again, the focus here is the communication methods, not a full-blown enterprise application. You'll see a lack of exception handling and other "good coding practices" here so we can focus on the meat of the project. Based on the type, the factory just returns the appropriate instance.

I also just want to call this out: I know you won't be spending all day on the application, so I don't mind newing up the instances. In a production application, these would probably use the singleton pattern to provide a single instance, or perhaps be configured in an inversion of control container with a lifetime manager.

public static IMonthService GetService(CommunicationType type)
{
    if (type.ID.Equals(1))
    {
        return new WCFMonthServiceImpl();
    }
    else if (type.ID.Equals(2))
    {
        return new RESTMonthService();
    }
    else if (type.ID.Equals(3))
    {
        return new CallbackMonthService();
    }
    else return null; 
}

Method One: WCF Service

WCF is one of the more popular methods. It is also fairly straightforward with Silverlight. On the server side, we define a service that simply returns a list of strings for each month. The contract looks like this:

[ServiceContract]
public interface IMonthService
{
    [OperationContract]
    List<string> GetMonths();
}

And the implementation is simple:

public List<string> GetMonths()
{
    return new List<string> { "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", 
        "Diciembre" };
}

The months will return in Spanish to distinguish the results from the other modes of communication.

The first key to making the service useable by Silverlight is to make sure it has a simple binding. The Web.config most likely generated it as a wsHttpBinding, what we want is a basicHttpBinding. A quick change leaves a configuration that looks like this:

<system.serviceModel>
  <behaviors>
   <serviceBehaviors>
    <behavior name="SilverlightCommunication.Web.MonthServiceBehavior">
     <serviceMetadata httpGetEnabled="true" />
     <serviceDebug includeExceptionDetailInFaults="false" />
    </behavior>
   </serviceBehaviors>
  </behaviors>
  <services>
   <service behaviorConfiguration="SilverlightCommunication.Web.MonthServiceBehavior"
    name="SilverlightCommunication.Web.MonthService">
    <endpoint address="" binding="basicHttpBinding" contract="SilverlightCommunication.Web.IMonthService">
     <identity>
      <dns value="localhost" />
     </identity>
    </endpoint>
    <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
   </service>
  </services>
 </system.serviceModel>

In production, we'd probably secure the service and use an HTTPS endpoint, and define a specific fault contract to manage errors.

At this point you should be able to publish the service and browse to it locally.

If your computer complains that there is a "page not found" at the service endpoint (on my machine, at http://localhost/MonthService.svc), the services are most likely not registered.

To register them, follow these steps:

  1. Open a command line as administrator
  2. Browse to the Windows Communication Foundation folder, on most systems it will be C:\windows\microsoft.NET\v3.0\Windows Communicatoin Foundation
  3. Run "ServiceModelReg -i"

Now you should be registered and able to browse to your service.

On the Silverlight side, we add a service reference. In this project, I added it as WCFMonthService. This generates the ServiceReferences.ClientConfig file. You will most likely have to modify this file to point to the location of your service, unless you are hosting at the localhost root as I did. To see how you can manipulate your service bindings to automatically adjust to the install location, read my post Abstracting WCF Service Calls in Silverlight 3 (and read the caveat about HTTPS as well.)

Now the implementation of WCFMonthServiceImpl is straightforward. In the request, we create a client to connect to the service, register to the loaded event, and fire it off:

public void RequestMonths()
{
    WCFMonthService.MonthServiceClient client = new SilverlightCommunication.WCFMonthService.MonthServiceClient();
    client.GetMonthsCompleted += new EventHandler<SilverlightCommunication.WCFMonthService.GetMonthsCompletedEventArgs>(client_GetMonthsCompleted);
    client.GetMonthsAsync();
}

And when the service comes back, we load our args and fire the loaded event:

void client_GetMonthsCompleted(object sender, SilverlightCommunication.WCFMonthService.GetMonthsCompletedEventArgs e)
{
    WCFMonthService.MonthServiceClient client = sender as WCFMonthService.MonthServiceClient;
    client.GetMonthsCompleted -= client_GetMonthsCompleted;
    if (MonthsLoaded != null)
    {
        MonthArgs args = new MonthArgs();
        foreach (string month in e.Result)
        {
            args.Months.Add(new Month { Name = month }); 
        }
        MonthsLoaded(this, args); 
    }
}

(If you're bothered by the way I call the loaded event, read my note at the end of the callback method).

Traditional SOAP/WCF calls definitely have the most overhead of the methods we'll cover (although the ability to define the service as binary certainly helps). Your best friend when troubleshooting services is a proxy or sniffer like Fiddler. This will tell you if the plugin is requesting a clientaccesspolicy.xml (I've included one as an example, and if you host this in the root, it should satisfy the request) but also allow you to inspect the traffic.

For this method, our post contains a formatted SOAP request:

POST /MonthService.svc HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.4) Gecko/20091016 Firefox/3.5.4
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Content-Length: 135
Content-Type: text/xml; charset=utf-8
SOAPAction: "http://tempuri.org/IMonthService/GetMonths"

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
   <s:Body><GetMonths xmlns="http://tempuri.org/" />
   </s:Body></s:Envelope>

And the response is in an XML envelope:

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
   <s:Body>
      <GetMonthsResponse xmlns="http://tempuri.org/">
         <GetMonthsResult xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
        <a:string>Enero</a:string>
        <a:string>Febrero</a:string>
        <a:string>Marzo</a:string>
        <a:string>Abril</a:string>
        <a:string>Mayo</a:string>
        <a:string>Junio</a:string>
        <a:string>Julio</a:string>
        <a:string>Agosto</a:string>
        <a:string>Septiembre</a:string>
        <a:string>Octubre</a:string>
        <a:string>Noviembre</a:string>
        <a:string>Diciembre</a:string>
</GetMonthsResult></GetMonthsResponse></s:Body></s:Envelope>

Method Two: RESTful Service or Straight Web Stack

REST is a different type of service (Representational State Transfer). It uses the existing web (HTTP) protocol for communication, allowing "verbs" (you may be familiar with POST and GET, and REST adds more) and makes the assumption that a URL represents a certain "state." A URL might contain information about a product as /PRODUCT/1 and the expectation is that this URL always returns that product object in its current state. REST provides more flexibility than SOAP for the formatting of the response (it might be a string, a JSON object, a simple XML document, etc).

Many popular APIs like Twitter use the REST form of communication. ASP.NET provides support for these types of calls via the WebGet attribute. To simplify this example, however, I simply wired a simple handler that returns a list of months (in Italian, to distinguish them from the other methods). The handler code behind is very simple:

private const string MONTHS = "Gennaio,Febbraio,Marzo,Aprile,Maggio,Giugno,Luglio,Agosto,Settembre,Ottobre,Novembre,Dicembre";

public void ProcessRequest(HttpContext context)
{
    context.Response.ContentType = "text/plain";
    context.Response.Write(MONTHS);
}

As you can see, it literally spits out a list of months.

On the Silverlight side, we move from the concept of a WCF client to the more generic WebClient. This allows us to download strings or other content from the web, as well as upload content. It even includes support for credentials. In my case, I implemented the GetMonths with a simple request to download the string from the handler. Also, note I have hard-coded the URL. You may want to change this if your application is not hosted at the root.

public void RequestMonths()
{
    WebClient client = new WebClient();
    client.DownloadStringCompleted += new DownloadStringCompletedEventHandler(client_DownloadStringCompleted);
    client.DownloadStringAsync(new Uri("http://localhost/MonthHandler.ashx", UriKind.Absolute));
}

Once the string has been loaded, it's very easy to parse out and load (note that again, I'm not diving into error handling here, although one of the benefits of using the WebClient is vastly superior/simpler error handling over the WCF client).

void client_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
{
    WebClient client = sender as WebClient;
    client.DownloadStringCompleted -= client_DownloadStringCompleted;
    string[] months = e.Result.Split(',');
    MonthArgs args = new MonthArgs();
    foreach (string month in months)
    {
        args.Months.Add(new Month { Name = month });
    }
    if (MonthsLoaded != null)
    {
        MonthsLoaded(this, args);
    }
}

As you can see, it becomes simple string manipulation. We could just as easily retrieve an XML document and parse it using LINQ if we wanted to.

The post for this is simple, as the entire request is coded into the URL:

GET /MonthHandler.ashx HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.4) Gecko/20091016 Firefox/3.5.4
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive

The response? Event simpler ...

Gennaio,Febbraio,Marzo,Aprile,Maggio,Giugno,Luglio,Agosto,Settembre,Ottobre,Novembre,Dicembre

Method Three: Callback (via DOM Interaction)

This is probably the least known and used methods. Because Silverlight can interact with the host browser, it can also interact with traditional ASP.NET callbacks, postbacks, JQuery, and the AJAX framework. This method is useful when you have an existing application and you are integrating new Silverlight components. In one line of business application I was building, we used the existing callbacks to iterate fast proof of concepts and then slowly rebuilt the calls as services where we needed.

The first step is to implement ICallbackEventHandler, which Ido in the default page. When the callback is raised, Iignore any parameters and simply return a list of months in English. The important element of the code behind here is the Page.ClientScript.GetCallbackEventReference. This sets up the call so I know exactly how to execute the callback. A little known fact is that if you do not call this at least once (even if you discard the output), ASP.NET will not wire up a listener for the callback! It must be called in order to set that up.

In the page, there are two JavaScript functions to assist with the callback. They look like this:

// on the return from the callback, finds the control and injects the months
function callbackHandler(result,context) {
    var slControl = document.getElementById('silverlightControl');
    slControl.Content.SilverlightApp.LoadMonths(result); 
}

// just takes the callback reference and triggers the callback
function doCallback() {
    <%=CALLBACKREF%>;
}

The first method is the callback method that is referenced in the code-behind (Default.aspx.cs). It is called with the result (our months list) and the context (null). Note the call to get the Silverlight control itself (this is not the host DIV element, but the actual OBJECT tag). If you use the ASP.NET Control, you will grab this by the client id of the control. Note the syntax of "Content.SilverlightApp.LoadMonths(result)." We'll get to that in a minute.

The doCallback function is just a wrapper for the method we generated in the code behind. If you view the source, you'll see it generates like this:

function doCallback() {
   WebForm_DoCallback('__Page',null,callbackHandler,null,null,false);
}

Now let's jump into the Silverlight application and look at the CallbackMonthService implementation. The constructor wires up the class to be accessible in JavaScript:

public CallbackMonthService()
{
    HtmlPage.RegisterScriptableObject("SilverlightApp", this);
}

This is where the "SilverlightApp" in the JavaScript above came from.

When the RequestMonths method is called, it triggers the callback like this:

public void RequestMonths()
{
   HtmlPage.Window.Invoke("doCallback");
}

When the callback is completed, the JavaScript method will call back to the class and pass in the month list. To facilitate this, I provided a method labeled with the ScriptableMember attribute to make it callable from JavaScript. The method splits the string, loads the arguments, and then fires the loaded event.

[ScriptableMember]
public void LoadMonths(string monthList)
{
    string[] list = monthList.Split(',');

    MonthArgs args = new MonthArgs();

    foreach (string month in list)
    {
        args.Months.Add(new Month { Name = month });
    }

    if (MonthsLoaded != null)
    {
        MonthsLoaded(this, args); 
    }
}

For those of you new to programming, a good homework assignment would be to investigate the potential bug above. I want to call it out so I'm not accused of spreading bad coding habits, but doing it this way make help it stick. The way I raise the MonthsLoaded event is not the best practice, especially considering the multi-cast nature of the underlying delegate. If that doesn't make sense, do some research and it will help you to understand events more (and why I am always unregistering my events once they are fired).

The event was registered to by our view model, which will bind to the observable collection and show the months on the grid.

In the end, the request for this method is similar to a REST call, with a few parameters that the ASP.NET framework uses to track and route the request and response:

POST /default.aspx HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.4) Gecko/20091016 Firefox/3.5.4
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Referer: http://localhost/
Content-Length: 141
Pragma: no-cache
Cache-Control: no-cache

__EVENTTARGET=&__EVENTARGUMENT=&__VIEWSTATE=%2FwEPDwULLTExOTc0MjQyMjVkZOSAEQGSb7mMBhudDS%2FpDmv5GtNs&__CALLBACKID=__Page&__CALLBACKPARAM=null

But the response is just as simple:

0|January,February,March,April,May,June,July,August,September,October,November,December

(The 0 and pipe format help route multiple responses)

Conclusion

There you have it: a simple demonstration of three methods for getting data into your Silverlight application. I haven't touched upon local communication, sockets, or other more advanced methods, but this should provide a simple shell/skeleton for further exploration. In the future we'll talk more about securing those services and doing some more advanced things with messages on the client and server.

Download the Code (44 Kb)

Jeremy Likness

1 comment:

  1. Download File url is broken :(

    ReplyDelete