At Mix Essentials in Belgium, I delivered a session on working with Silverlight and data and services. Since the session was very well received and also the way the session was constructed, I decided to create a tutorial from it.
This tutorial covers several scenario's for having Silverlight connect with data and services: varying from WCF, REST, POX... all the way up to RSS. So let's take a look and I'm sure that by the end of this tutorial, you'll have a solid understanding on the topic. I did re-use some graphics from my slide deck to make some things more clear.
There are actually 4 parts to this tutorial. In this first part, I'll start with a little intro and then we'll create the first end-to-end application using Silverlight and WCF.
Silverlight and data: why?
We all grew accustomed with web applications that use data that comes from services "somewhere out there". For example, finding the available books in a bookstore: we enter a value in a search field, the browser sends that data to the server/service that in return will send some data down the wire. It's the model that tons and tons of applications on the web use.
The way that Silverlight has to interact with data is nothing different from the above model. In our Silverlight applications, we will also have to call out to services on the outside and process that information in our application. Silverlight will have to be able to connect to all kinds of services: images and video (which are after all, data), RSS, public services (Yahoo, Flickr...), WCF services...
In Silverlight 1.0, it wasn't easy to do all this. In fact, the only kind of data that you could easily connect to, was images and video's. The cause of this was of course the programming model based on JavaScript: connecting to services, parsing XML..., it is possible in JavaScript, but it's far from easy.
Silverlight 2 however offers a programming model based on managed code (C#, VB.net). We all know that using the full .net framework, it's easy to connect to a service and it's easy to parse XML. In fact, connecting to services in .net 3.5 is not very different than doing it in Silverlight.
Demo 1: A simple bookstore
In the first example we'll be taking a look at, I'm going to show you how to build a service that's accessible from Silverlight and then we'll build the client (Silverlight) application that will connect to the service. The sample mimics a simple book store in which I can search for books, based on a title or description.
Below is a screenshot of the result of the application working.
We'll start by creating a new Silverlight solution in Visual Studio 2008 (ProductCatalog). You should see something that resembles the screenshot of the Solution Explorer below.
We'll start by adding a service to the solution. This service should of course go in the web application (ProductCatalog_Web). Right-click on the project and select "Add new Item". The preferred way of building services that Silverlight can connect with, is using WCF (Windows Communication Foundation). In the Add new Item-dialog window, we see 2 WCF templates: the Ajax-Enabled WCF Service and the default WCF Service.
WCF relies heavily on configuration code and actually, the difference between both of these templates is the actual configuration code. Note that there is no Silverlight-enabled WCF service... yet! Probably in beta 2, Microsoft will add this template. For now, we'll go with the default WCF template and we'll do some tweaking manually.
Select "WCF Service" and call it ShoppingService. Visual Studio will now add the service: ShoppingService.svc as well as 2 files in the App_Code directory will be added (IShoppingService.cs and ShoppingService.cs).
We'll start by doing the tweaking of the service first, in order to make it accessible from Silverlight. Open the web.config and scroll all the way down, straight to the "system.serviceModel" tag. In there, the endpoint to which the client will be connecting, is specified and it has "wsHttpBinding" as value for the "Binding" property. Change this to be a "basicHttpBinding".
Now let's go ahead and actually build the service. We'll start by defining the contract in the IShoppingService.cs. Visual Studio already generated some code:
1: [ServiceContract]
2: public interface IShoppingService
3: {
4: [OperationContract]
5: void DoWork();
6: }
The ServiceContract attribute specifies that this class defines what the service should look like. When adding the OperationContract on a method, we are saying that that method will be callable on that service. We'll know add our own code that contains a method that will get Products based on the search value.
1: [ServiceContract]
2: public interface IShoppingService
3: {
4: [OperationContract]
5: List GetProducts(string searchValue);
6: }
We are using a type Product in this code. Let's add that type.
1: [DataContract]
2: public class Product
3: {
4: [DataMember]
5: public string title;
6: [DataMember]
7: public string imageUrl;
8:
9: public Product(string title, string imageUrl)
10: {
11: this.title = title;
12: this.imageUrl = imageUrl;
13: }
14: }
Since this type is marked with the DataContract attribute, it will be exchanged over the wire and will thus be available both in the service code but also in the client code. Every member of such a type marked with the DataMember attribute will actually be included in what is sent over the wire.
Only thing left on the service side, is implementing the service. This will be done in the ShoppingService.cs.
The ShoppingService class implements the IShoppingService interface and will thus have to implement the GetProducts method. For the sake of simplicity, I will not actually be going to a database to get product information, I just hard-coded them in this method. I know it's not good practice, but hey, we want to make things as easy as possible.
Here's the actual implementation.
1: public class ShoppingService : IShoppingService
2: {
3: public List GetProducts(string searchValue)
4: {
5: List products = new List();
6: if (searchValue.Contains("basic"))
7: {
8: products.Add(new Product("Visual Basic", "img1.jpg"));
9: products.Add(new Product("Beginning ASP.Net 3.5 in VB 2008", "img10.jpg"));
10: products.Add(new Product("Visual Basic 2005 Step by Step", "img4.jpg"));
11: products.Add(new Product("Visual Basic 2008 For Dummies", "img8.jpg"));
12: products.Add(new Product("Visual Basic 2008 Express Edition", "img7.jpg"));
13: products.Add(new Product("Mastering Visual Basic 2008", "img9.jpg"));
14: }
15: else if(searchValue.Contains("visual"))
16: {
17: products.Add(new Product("Visual C# 2008 Step by Step", "img1.jpg"));
18: products.Add(new Product("Visual C# 2008 Express Edition", "img6.jpg"));
19: }
20: else if(searchValue.Contains("silverlight"))
21: {
22: products.Add(new Product("Silverlight 1.0", "img2.jpg"));
23: }
24: else
25: {
26: products.Add(new Product("Windows Communication Foundation Step by step", "img5.jpg"));
27: }
28:
29: return products;
30: }
31: }
This concludes our work on the service side. Build your project now and browse to the ShoppingService.svc. You should get the following result.
OK, that's it for the service side. Now, let's build the Silverlight client that will be connecting to our WCF service. First, we need to show our client Silverlight application where the service is located, so it can retrieve data from it. We can do so by adding a service reference. Click on the "Add service reference" item. The following dialog is now displayed.
The dialog has 2 buttons: the Go and the Discover button. The latter one is the one we'll use now, it searches for services available in the current solution. As you can see above, it found the WCF service hosted in the web application. Specify "ShoppingService" as the namespace.
In the background, Visual Studio will create a proxy: it's like a copy of the service, but without the actual implementations. These are replaced by invocations of the service methods. The proxy generation is responsible for giving you IntelliSense in your client applications (in this case, the Silverlight app).
I'll know add the XAML to create the UI. It's actually some very simple XAML code, shown below.
1: <UserControl x:Class="ProductCatalog.Page"
2: xmlns="http://schemas.microsoft.com/client/2007"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: >
5: <StackPanel Background="#333333">
6: <Grid x:Name="LayoutRoot" Background="#cccccc" Margin="10">
7: <Grid.RowDefinitions>
8: <RowDefinition Height="50">RowDefinition>
9: <RowDefinition Height="*">RowDefinition>
10: Grid.RowDefinitions>
11: <Grid.ColumnDefinitions>
12: <ColumnDefinition Width="200">ColumnDefinition>
13: <ColumnDefinition Width="*">ColumnDefinition>
14: Grid.ColumnDefinitions>
15: <TextBlock Text="Search for products" Margin="10">TextBlock>
16: <StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" Margin="10">
17: <TextBox x:Name="txtSearchValue" Width="200">TextBox>
18: <Button x:Name="btnSearch" Click="btnSearch_Click" Content="Search" Margin="10 0">Button>
19: StackPanel>
20: <StackPanel Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="2" Background="#cccccc" Margin="5">
21: <ItemsControl x:Name="resultsList">
22: <ItemsControl.ItemTemplate>
23: <DataTemplate>
24: <StackPanel Background="WhiteSmoke" Orientation="Horizontal" Margin="2">
25: <Image Width="100" Height="100" Source="{Binding imageUrl}">Image>
26: <TextBlock Text="{Binding title}" Margin="0 20 0 0">TextBlock>
27: StackPanel>
28: DataTemplate>
29: ItemsControl.ItemTemplate>
30: ItemsControl>
31: StackPanel>
32: Grid>
33: StackPanel>
34: UserControl>
On line 18, you'll see the click event for the button. Now, when the button is clicked, you want to call the service, passing in the search string. What will happen, is that you'll now need to instantiate the proxy.
1: private void btnSearch_Click(object sender, RoutedEventArgs e)
2: {
3: ShoppingService.ShoppingServiceClient proxy = new ProductCatalog.ShoppingService.ShoppingServiceClient();
4:
5: }
What we now want, is do something like proxy.GetProducts. But wait a minute... It's not in the IntelliSense options. There is however a "GetProductsAsync". The fact is that in Silverlight, every call to a service, will be done asynchronously. This is done so the UI thread would not block. As with every asynchronous call, we need to specify which code is to be called whenever the service returns. This is done using the GetProductsCompleted.
Below is the complete code for the click event.
1: private void btnSearch_Click(object sender, RoutedEventArgs e)
2: {
3: ShoppingService.ShoppingServiceClient proxy = new ProductCatalog.ShoppingService.ShoppingServiceClient();
4: proxy.GetProductsCompleted += new EventHandler(proxy_GetProductsCompleted);
5: proxy.GetProductsAsync(txtSearchValue.Text);
6: }
The only thing left, is actually implementing the "completed" event handling code. To get access to the results (in our case, a generic list of Products), we call the e.Result. If needed, we can loop through these results using code similar to e.Result[0].
1: void proxy_GetProductsCompleted(object sender, ProductCatalog.ShoppingService.GetProductsCompletedEventArgs e)
2: {
3: resultsList.ItemsSource = e.Result;
4: }
Here, I'm binding the list to the ItemsSource property of the resultsList. resultsList is an ItemsControl, earlier specified in the XAML code. When setting the ItemsSource this way, we can use databinding on the textblock and the image specified in the ItemTemplate (see XAML).
To recap, these are the necessary steps to build a service, ready for Silverlight and the Silverlight client:
- Add a WCF Service
- Define the contracts (ServiceContract, OperationContract, DataContract, DataMember)
- Implement the service
- Connect the client Silverlight app using the Add Service Reference
- Instantiate the proxy
- Issue an async call to the service using the proxy
- Write code that will execute whenever the service returns
- Eventually use databinding with the returned result from the service (on a ListBox, ItemsControl...)
So far, we have seen how to build our own services and how to connect with those. Now what about services we didn't write ourselves? Examples of this can include REST APIs, ASMX services... created by some other party, but we have to write code that can connect with them. In the next part, I'll be connecting to a self-describing service (SOAP), in other words, a public service that exposes a WSDL for me to connect with.
Demo 2: Connecting to self-describing services
Services that describe themselves can be of very different kinds. The one we used and wrote ourselves in Demo 1 actually is one. Other examples can include SOAP services in the enterprise (including java, ASP.NET asmx webservices, BEA...) and SOAP enabled public accessible services, as from Live, Amazon and ebay.
In this demo, we'll be using the Live Search API, which exposes a WSDL for us to connect to. You'll see that the implementation is actually very similar to the previous example. The biggest difference is of course the fact that we won't be writing any service code here, we'll be connecting to an existing one, the Live Search on that is.
Create a new Silverlight solution, let's call it LiveSearchApplication. In the Silverlight application, let's create a service reference. The URL we need to specify is "http://soap.search.live.com/webservices.asmx?wsdl" . Based on the meta-data in this WSDL endpoint, VS will create again a proxy.
Below is the XAML for the application. It's very similar to the XAML of the previous app.
1: <UserControl x:Class="LiveSearchApplication.Page"
2: xmlns="http://schemas.microsoft.com/client/2007"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: Width="800" Height="600">
5: <StackPanel Background="#333333">
6: <Grid x:Name="LayoutRoot" Background="#cccccc" Margin="10">
7: <Grid.RowDefinitions>
8: <RowDefinition Height="50">RowDefinition>
9: <RowDefinition Height="*">RowDefinition>
10: Grid.RowDefinitions>
11: <Grid.ColumnDefinitions>
12: <ColumnDefinition Width="200">ColumnDefinition>
13: <ColumnDefinition Width="*">ColumnDefinition>
14: Grid.ColumnDefinitions>
15: <StackPanel>
16: <Image Source="livesearch.png" Margin="3" Width="100">Image>
17: StackPanel>
18: <StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" Margin="10">
19: <TextBox x:Name="txtSearchValue" Width="200">TextBox>
20: <Button x:Name="btnSearch" Click="btnSearch_Click" Content="Search" Margin="10 0">Button>
21: StackPanel>
22: <StackPanel Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="2" Background="#cccccc" Margin="5">
23: <ItemsControl x:Name="liveResultsList">
24: <ItemsControl.ItemTemplate>
25: <DataTemplate>
26: <StackPanel Background="WhiteSmoke" Orientation="Horizontal" Margin="2">
27: <TextBlock Text="{Binding Title}" >TextBlock>
28: <HyperlinkButton NavigateUri="{Binding Url}" Content="More info" HorizontalAlignment="Right" Margin="30 5 0 0">HyperlinkButton>
29: StackPanel>
30: DataTemplate>
31: ItemsControl.ItemTemplate>
32: ItemsControl>
33: StackPanel>
34: Grid>
35: StackPanel>
36: UserControl>
Now, since VS has created the proxy, we have full IntelliSense. The call to the Live Search service will also be asynchronous, so we'll need to specify which code will be executed when the service returns. The "SourceRequest" and the "SearchRequest" are needed to perform a search on Live, and as you can see, having IntelliSense to write code like this can really come in handy!
1: private void btnSearch_Click(object sender, RoutedEventArgs e)
2: {
3: var proxy = new MSNSearchPortTypeClient();
4: proxy.SearchCompleted += new EventHandler(proxy_SearchCompleted);
5:
6: SourceRequest[] sources = new SourceRequest[1];
7: sources[0] = new SourceRequest();
8: sources[0].Source = SourceType.Web;
9: sources[0].ResultFields = ResultFieldMask.All;
10:
11: SearchRequest sr = new SearchRequest();
12: sr.AppID = liveSearchKey;
13: sr.Query = txtSearchValue.Text;
14: sr.CultureInfo = "en-US";
15: sr.SafeSearch = SafeSearchOptions.Strict;
16: sr.Requests = sources;
17:
18: proxy.SearchAsync(sr);
19: }
Only thing left is writing some code to handle the completion of the service. Again, we get an enumerable list of results. On this list, we're doing some LINQ query to get the results in the way we want them.
1: void proxy_SearchCompleted(object sender, SearchCompletedEventArgs e)
2: {
3: liveResultsList.ItemsSource =
4: from r in e.Result.Responses[0].Results
5: select (new Item(r.Title, new Uri(r.Url)));
6: }
And the result is shown on the following screenshot.
CrossDomain calls
One very important thing related to this matter is the CrossDomain call. Earlier, I mentioned you can access any SOAP service out there from your Silverlight application. In fact, this is only partly true. Silverlight will by default, only let you access services in the same domain, because of security restrictions. You might wonder, how then did we access the Live service in the previous example?
Well, in fact, the service can opt-in to allow cross-domain calls. So, truth is that Silverlight will always go out to the service, but then a check will be done to see whether or not the service allows to be called from other domains. The service can therefore publish a so-called Cross Domain Policy file (XML format) in the root of the domain. In this file, it can specify whether every call is legit or only call from particular domains.
There are 2 possible file that Silverlight will check for: the Flash "crossdomain.xml" file and the Silverlight "clientaccesspolicy.xml". Large API, such as from Flickr and Live, publish at least one of those files and can therefore be accessed from Silverlight.
Here's a Flash crossdomain.xml file:
1: xml version="1.0" ?>
2: <cross-domain-policy>
3: <allow-access-from domain="*" />
4: cross-domain-policy>
And here's a Silverlight clientaccesspolicy.xml example:
1: xml version="1.0" encoding="utf-8" ?>
2: <access-policy>
3: <cross-domain-access>
4: <policy>
5: <allow-from>
6: <domain uri="*" />
7: allow-from>
8: <grant-to>
9: <resource path="/" include-subpaths="true" />
10: grant-to>
11: policy>
12: cross-domain-access>
13: access-policy>
In the next part, we'll take a look at services that do not describe themselves.
Demo 3: Read the *** manual
Not all services are so kind to give us a WDSL to connect with. In fact, there are a lot of services out there, for example REST services, that only have a "human readable" documentation. They do contain a URL that you have to send a request to, with some parameters specified, and most of the time, they give you an example of the XML you can expect back from them.
Examples include YouTube, Flickr, FaceBook, Yahoo, Google... Here you can read the YouTube API docs.
We are going to use Flickr to request some geotagged images. For this, the Flickr API gives us a URL to which we should send a request and it specified which XML we'll get back.
Thus, 3 steps are needed to access these kind of services:
- Build the URL
- Send a request to that URL
- Process the XML we retrieve from the service
Let's go ahead and build this demo. The application is called FlickrSearchApplication. I won't include the XAML here, it's the same as in the previous sample. Let's take a look at the code.
As said, we need to add the URL to which we need to send the request. In this case, the URL is the following:
1: private const string SearchUrlFormat =
2: "http://api.flickr.com/services/rest/?
3: method=flickr.photos.search&api;_key={0}&tags;=geotagged%2C{1}
4: &tag;_mode=all&sort;=interestingness-desc&safe;_search=1&extras;=
5: geo%2Co_dims%2Cdate_taken&per;_page=100";
When doing a search, we need to add the search value as parameter {1}. The code for the click event is different now: we are not using a proxy now (we didn't add a service reference either, did we). To get the data into our app, we need to send a request a URL. This can be done using the WebClient class, a type also available in the full .net 3.5 framework. The WebClient's request is also asynchronous, therefore, we need to add a completed event handler.
1: public void SearchPhotos(string tag)
2: {
3: string url = String.Format(SearchUrlFormat, _apiKey, tag);
4: Uri uri = new Uri(url, UriKind.Absolute);
5:
6: _searchQuery = new WebClient();
7: _searchQuery.DownloadStringCompleted += OnSearchCompleted;
8: _searchQuery.DownloadStringAsync(uri, tag);
9: }
The following code will be executed when the request is done and the XML is retrieved. We are going to add some LINQ to do the parsing of the XML.
1: private void OnSearchCompleted(object sender, DownloadStringCompletedEventArgs e)
2: {
3: XDocument xdoc = XDocument.Parse(e.Result);
4: var photos = (from photo in xdoc.Descendants("photo")
5: select new Photo {
6: Title = photo.Attribute("title").Value,
7: ThumbnailUrl = String.Format(ThumbnailUrlFormat,
8: photo.Attribute("server").Value,
9: photo.Attribute("id").Value,
10: photo.Attribute("secret").Value),
11: PhotoUrl = String.Format(PhotoUrlFormat,
12: photo.Attribute("server").Value,
13: photo.Attribute("id").Value,
14: photo.Attribute("secret").Value),
15: PageUrl = String.Format(PageUrlFormat,
16: photo.Attribute("owner").Value,
17: photo.Attribute("id").Value),
18: Latitude = Double.Parse(photo.Attribute("latitude").Value),
19: Longitude = Double.Parse(photo.Attribute("longitude").Value),
20: ShotOn = DateTime.SpecifyKind(DateTime.Parse(photo.Attribute("datetaken").Value), DateTimeKind.Utc)
21: }).Take(100);
22: flickrResultsList.ItemsSource = photos;
23: }
The last line will take care of the databinding being done. The result can be seen below.
Before we continue, let's take a look at some points of interest in this example.
The URL we need to send a request to can be HTTP or HTTPS. We can't connect to file:// or FTP protocols. The same cross-domain restrictions apply as seen in the previous example.
To send the request, we used the WebClient class. This is a lightweight class and sufficient for most scenario's. Should you need more fine grained control, you can use the HttpWebRequest, also available in the full .net 3.5 framework.
Finally, the XML parsing can be done using LINQ to XML as in the example, but also the XmlReader/XmlWriter and the XmlSerializer are available.
The only thing we can now improve in this example is the XML parsing.
Demo 4: RSS and Silverlight
If we are connecting to RSS feeds, the resulting XML will always be the same (how else would an RSS feed reader work?). In .net 3.5, some new classes have been added in the System.ServiceModel namespace, the SyndicationFeed classes. Using these classed, we can eliminate the parsing of the XML when using RSS 2.0 or Atom 1.0 feeds.
The final demo is called RssApplication and will connect to 2 RSS feeds. Using the same code, these feeds will be displayed. The XAML is again almost the same, only now that I have 2 buttons to trigger the download for either of the two feeds.
In the Click event handler, I'll try to figure out what button was clicked.
1: private void btnSnowballRSS_Click(object sender, RoutedEventArgs e)
2: {
3: Button b = sender as Button;
4: if (b.Tag.Equals("Snowball"))
5: strRss = strSnowball;
6: else if (b.Tag.Equals("Codeflakes"))
7: strRss = strCodeFlakes;
8: GetRssFeed(strRss);
9: }
We now need to send a request to a URL, the RSS URL that is. This is again done like in the previous example, using the WebClient.
1: private void GetRssFeed(string uri)
2: {
3: WebClient w = new WebClient();
4: w.DownloadStringCompleted += new DownloadStringCompletedEventHandler(w_DownloadStringCompleted);
5: w.DownloadStringAsync(new Uri(uri));
6: }
So far, not a lot of change. Now, let's look at the completed event code. In there, we won't be doing any parsing. Instead, we load the retrieved XML into a SyndicationFeed and using its Items property, we get access to all the information in the RSS feed. As you can see, much less code!
1: void w_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
2: {
3: SyndicationFeed sf = SyndicationFeed.Load(XmlReader.Create(new StringReader(e.Result)));
4: rssList.ItemsSource = sf.Items;
5: }
The result can be seen below.
A few final notes on the RSS connection: it can be used to access RSS 2.0 and Atom 1.0 feeds and also respects the crossdomain restrictions.
Summary
I hope that by now you have a clear understanding of what types of data-access you can do in Silverlight. There are a lot of opportunities here, making Silverlight ready for LOB (Line of Business) applications.
The demo's can be downloaded here: http://www.codeflakes.net/blog/post/Mix-Essentials-slide-deck-and-demos.aspx