The How-To series demo Silverlight accessing Web Services in various configurations. In this post, we will demonstrate an RSS Service hosted on Apache Tomcat server consumed by a Silverlight client application. This scenario can be categorized as a D2D scenario with a cross domain policy.
Both the Silverlight Client Application and RSS service are available as an attachment to this post. Click here to download a completed version of this sample application.
We would first develop a simple RSS service feeder for publishing weather updates for select cities. Then we would develop a Rich Internet Application that consumes this RSS feed and displays weather information to the user in the browser.
For this How-To, we use the Apache
Axis 2 framework. Please install Web Tools Platform (WTP) in eclipse
IDE. This is by default available if you use Eclipse
IDE for Java EE Developers. Otherwise you need to install it from
http://download.eclipse.org/webtools/downloads/.
Now we would describe various steps needed to create and setup the RSS service on tomcat.
Configure Tomcat inside Eclipse
We need to tell Eclipse about which Tomcat we are using and set up the integration. Use following steps to configure this.
Following screenshots shows the tomcat server details after successful installation.
Install and Configure Apache Axis 2
Creating Dynamic Web Project
package com.weather; public class WeatherServices { public String getAllCitiesAsRSS() { return null; } public String getWeatherDetailsAsRSS(String cityName) { return null; } }
For demonstration purpose, we create RSS xml file (AllCities.xml) listing all cities, add this file to the project inside the same package com.weather. This file looks like.
<?xml version="1.0" encoding="UTF-8"?> <rss version="2.0"> <channel> <title>Weather details of all cities</title> <link>http://www.eclipse4sl.org/</link> <description>Weather details of all cities</description> <language>en-us</language> <pubDate>Tue, 01 Dec 2008 04:00:00 GMT</pubDate> <lastBuildDate>Tue, 10 Dec 2008 04:00:00 GMT</lastBuildDate> <docs>http://www.eclipse4sl.org/allcities.rss</docs> <generator>Weather Web Service</generator> <managingEditor>editor@eclipse4sl.org</managingEditor> <webMaster>webmaster@eclipse4sl.org</webMaster> <ttl>10</ttl> <item> <title>Seattle</title> <link>http://www.eclipse4sl.org/Seattle</link> <description></description> <pubDate>Tue, 01 Dec 2008 04:00:00 GMT</pubDate> <guid>http://www.eclipse4sl.org/Seattle</guid> </item> <!-- Add list of all cities here --> <item> <title>Redmond</title> <link>http://www.eclipse4sl.org/Redmond</link> <description></description> <pubDate>Tue, 01 Dec 2008 04:00:00 GMT</pubDate> <guid>http://www.eclipse4sl.org/Redmond</guid> </item> <item> <title>Rome</title> <link>http://www.eclipse4sl.org/Rome</link> <description></description> <pubDate>Tue, 01 Dec 2008 04:00:00 GMT</pubDate> <guid>http://www.eclipse4sl.org/Rome</guid> </item> </channel> </rss>
getAllCitiesAsRSS method would simply return contents of this file as xml string. Copy following code into this method.
public String getAllCitiesAsRSS() { try { // Read city list RSS xml from template InputStream stream = getClass().getClassLoader() .getResourceAsStream("/com/weather/AllCities.xml"); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse(stream); stream.close(); // No processing here for demo purpose. Return city list directly // Return xml string for RSS xml document return getXmlStringFromXMLDocument(doc); } catch (Exception e) { // Need proper error handling, OK demonstration return null; } }
For demonstration purpose, this method also uses a template RSS file (WeatherDetails.xml) for retuning weather information RSS feed for specified city. Add this file to the project inside the same package com.weather. This file looks like
<?xml version="1.0" encoding="UTF-8"?> <rss version="2.0"> <channel> <title>Seattle</title> <link>http://www.eclipse4sl.org/</link> <description>Weather details of Seattle</description> <language>en-us</language> <pubDate>Tue, 01 Dec 2008 04:00:00 GMT</pubDate> <lastBuildDate>Tue, 10 Dec 2008 04:00:00 GMT</lastBuildDate> <docs>http://www.eclipse4sl.org/</docs> <generator>Weather Web Service</generator> <managingEditor>editor@eclipse4sl.org</managingEditor> <webMaster>webmaster@eclipse4sl.org</webMaster> <ttl>10</ttl> <item> <title>10° F</title> <link>http://www.eclipse4sl.org/</link> <description>Current temperature</description> <pubDate>Tue, 01 Dec 2008 04:00:00 GMT</pubDate> <guid>http://www.eclipse4sl.org/</guid> </item> <item> <title>10-20° F</title> <link>http://www.eclipse4sl.org/</link> <description>Temperature Range for Today</description> <pubDate>Tue, 01 Dec 2008 04:00:00 GMT</pubDate> <guid>http://www.eclipse4sl.org/</guid> </item> <item> <title>Sunny</title> <link>http://www.eclipse4sl.org/</link> <description>Climate for Today</description> <pubDate>Tue, 01 Dec 2008 04:00:00 GMT</pubDate> <guid>http://www.eclipse4sl.org/</guid> </item> </channel> </rss>
For demonstration purpose, getWeatherDetailsAsRSS() method reads this template file, and modify following parts with some random data:
Copy following code into getWeatherDetailsAsRSS() method.
public String getWeatherDetailsAsRSS(String cityName) { try { // Read WeatherDetails RSS xml from template InputStream stream = getClass().getClassLoader() .getResourceAsStream("/com/weather/WeatherDetails.xml"); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse(stream); stream.close(); NodeList itemsTitle = doc.getElementsByTagName("title"); // Set the city name itemsTitle.item(0).setTextContent(cityName); // Set weather information, randomly for this demo application Random generator = new Random(); int mode = generator.nextInt(4) + 1; switch (mode) { case 1: itemsTitle.item(1).setTextContent("32° F"); itemsTitle.item(2).setTextContent("20°-40° F"); itemsTitle.item(3).setTextContent("Thunder Storm"); break; case 2: itemsTitle.item(1).setTextContent("45° F"); itemsTitle.item(2).setTextContent("40°-50° F"); itemsTitle.item(3).setTextContent("Rainy"); break; case 3: itemsTitle.item(1).setTextContent("63° F"); itemsTitle.item(2).setTextContent("50°-70° F"); itemsTitle.item(3).setTextContent("Cloudy"); break; case 4: itemsTitle.item(1).setTextContent("82° F"); itemsTitle.item(2).setTextContent("70°-90° F"); itemsTitle.item(3).setTextContent("Sunny"); break; } // Return xml string for the modified RSS xml document return getXmlStringFromXMLDocument(doc); } catch (Exception e) { // Need proper error handling, OK demonstration return null; } }
Also add following helper method to this class.
private String getXmlStringFromXMLDocument(Document doc) throws TransformerFactoryConfigurationError, TransformerException { StringWriter stw = new StringWriter(); Transformer serializer = TransformerFactory.newInstance() .newTransformer(); serializer.transform(new DOMSource(doc), new StreamResult(stw)); return stw.toString(); }
After adding these template xml files, the project structure now looks as follows
Now let us expose the WeatherServices class as web service. This is very easy using eclipse and can be done with following steps.
Deploy Web Service on Tomcat
Save all files, then Run the project on the Tomcat Server. Right click on the project node : Run As -> Run on Server.
The service is now hosted on Tomcat at
http://localhost:8080/WeatherServicesApplication/services/WeatherServices?wsdl
Since the SOAP service and the Silverlight application reside on different hosts, we need to overpass the Cross Domain Issue. Create at the root of the SOAP service host, a clientaccesspolicy.xml file with the following content. The root of service host for tomcat is typically the folder C:\Java\Apache\apache-tomcat-6.0.18\webapps\ROOT.
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <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>
The development of Silverlight Web client consists of three steps:
First create a new Silverlight Web Project in eclipse and call it as EclipseSLWeatherApplication. If you are not familiar to creating Silverlight projects in eclipse, please refer to Hello World sample.
Let us design UI for displaying weather details for individual city. The UI should look like follows.
Various parts of this UI are as follows.
To design this UI, let us add a xaml page called "WeatherDetails.xaml" to the project. Also add various images to the project. Following diagram shows all images used for displaying weather details. Image names are self descriptive. Image names starting with "big" are used to indicate climate and image name containing sky are for setting panel background.
Now open WeatherDetails.xaml file and add following code.
<UserControl x:Class="EclipseSLWeatherApplication.WeatherDetails" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Width="320" Height="180"> <UserControl.Resources> <ImageBrush ImageSource="blue-sky.png" x:Key="BlueSky" /> <ImageBrush ImageSource="gray-sky.png" x:Key="GraySky" /> <BitmapImage UriSource="big-sun.png" x:Key="BigSun" /> <BitmapImage UriSource="small-sun.png" x:Key="SmallSun" /> <BitmapImage UriSource="big-cloudy.png" x:Key="BigCloud" /> <BitmapImage UriSource="small-cloudy.png" x:Key="SmallCloud" /> <BitmapImage UriSource="big-rain.png" x:Key="BigRain" /> <BitmapImage UriSource="small-rain.png" x:Key="SmallRain" /> <BitmapImage UriSource="big-thunderstorm.png" x:Key="BigThunderStorm" /> <BitmapImage UriSource="small-thunderstorm.png" x:Key="SmallThunderStorm" /> </UserControl.Resources> <Canvas x:Name="LayoutRoot" Width="264" Height="180" Background="{StaticResource BlueSky}"> <Canvas Canvas.Top="13" Canvas.Left="13" Width="230" Height="110"> <StackPanel x:Name="ConditionsScreen" > <Canvas Height="90"> <Image x:Name="ConditionsOverlay" Margin="-13,-13,0,0" Source="{StaticResource BigSun}"/> <StackPanel> <TextBlock x:Name="Temperature" Width="225" Height="37" TextAlignment="Right" FontSize="34" Text="18° F" /> <TextBlock x:Name="Climate" Width="225" Height="14" TextAlignment="Right" Text="Sunny" /> <TextBlock x:Name="Range" Width="225" Height="14" TextAlignment="Right" Text="15°-29° F" /> </StackPanel> </Canvas> <Canvas Height="25"> <TextBlock x:Name="CityName" Text="Redmond" Width="230" Foreground="White" FontSize="30" TextAlignment="Center" Opacity="0.5" /> </Canvas> </StackPanel> </Canvas> </Canvas> </UserControl>
The instant preview for this xaml will be similar to the one displayed above.
Let us try to show weather details of multiple cities in a nice fashion using Carousel. We want to show it as follows
All Weather details panels will be arranged circular fashion and these panels will keep rotating continuously, changing details for the panel at the front. This way user can take look weather information about all cities.
To implement Carousel, we will make use of a sample application available at
http://rhizohm.net/irhetoric/blog/74/default.aspx. This sample is known as Yet Another Carousel (YAC). We particularly chose this carousel for demonstration because it supports addition on any type of items like buttons, grids, images, videos etc. Apart from this, almost all other carousel samples can display only image. Of course there are few limitations of YAC. It can display only 5 items at a time instead of displaying all items in a circle. Remaining items are present virtually and will appear correctly when carousel moves. Any way, we will not go into details of Carousel code. One can always refer above link to learn more.
We have isolated two classes from this sample, with minor modifications. Add following two classes to the project.
Now open Page.xaml and paste following code in xaml source window.
<UserControl x:Class="EclipseSLWeatherApplication.Page" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Width="800" Height="600" > <Canvas x:Name="LayoutRoot" Background="White" Width="600" Height="400" MouseLeftButtonUp="OnMouseLeftButtonUp"> </Canvas> </UserControl>
Here one event handler, OnMouseLeftButtonUp, is added to changes direction of carousel rotation. Every mouse click on this page will reverse the direction.
Next step is to get list of all cities from the RSS service, create WeatherDetail control for each of them and add it to the Carousel. To call the web service, we need web service reference in the project. Currently it is not possible using eclipse4sl plugin. Therefore this has to be done using Visual Studio 2008.
Now the RSS Service is exposed and we have a client proxy to consume the Web Service with Silverlight.
In Page.xaml.cs, add an import to web service reference and related library.
using EclipseSLWeatherApplication.WeatherApplication; using System.Xml; using System.ServiceModel; using System.ServiceModel.Syndication; using System.Windows.Media.Imaging; using System.Windows.Threading;
Call getAllCitiesAsRSS() method
Let us populate Carousel with all cities. Here we need to call getAllCitiesAsRSS() method of the web service. This is done by calling getAllCitiesAsRSS() method asynchroneously in the Page_Loaded() handler. Add the Page_Loaded() handler in the Page constructor. This code is as follows.
public Page() { InitializeComponent(); this.Loaded += new RoutedEventHandler(Page_Loaded); } private void Page_Loaded(object sender, RoutedEventArgs e) { // Get list of all cities from RSS service WeatherServicesPortTypeClient client = new WeatherServicesPortTypeClient(); client.getAllCitiesAsRSSCompleted += new EventHandler<getAllCitiesAsRSSCompletedEventArgs> (client_getAllCitiesAsRSSCompleted); client.getAllCitiesAsRSSAsync(); // Set timer for Carousel rotatio. It is set to 2 seconds DispatcherTimer dt = new DispatcherTimer(); dt.Interval = new TimeSpan(0, 0, 0, 0, 2000); dt.Tick += new EventHandler(dt_Tick); dt.Start(); }
Here we have registered an handler for completion of getAllCitiesAsRSS() service call. This handler code is as follows
void client_getAllCitiesAsRSSCompleted(object sender, getAllCitiesAsRSSCompletedEventArgs e) { // Load feed into SyndicationFeed XmlReader reader = XmlReader.Create(new System.IO.StringReader(e.Result.ToString())); SyndicationFeed feed = SyndicationFeed.Load(reader); // Create a new Carousel carousel = new Carousel(); // Add each city weather detail in the Carousel foreach (SyndicationItem item in feed.Items) { // Create weather detail widget WeatherDetails weatherDetail = new WeatherDetails(); weatherDetail.Width = 320; weatherDetail.Height = 180; weatherDetail.CityName.Text = item.Title.Text; carousel.CreateItem(weatherDetail, item.Title.Text); // Pouplate weather detail by calling RSS service WeatherServicesPortTypeClient client = new WeatherServicesPortTypeClient(); client.getWeatherDetailsAsRSSCompleted += new EventHandler<getWeatherDetailsAsRSSCompletedEventArgs> (client_getWeatherDetailsAsRSSCompleted); client.getWeatherDetailsAsRSSAsync(item.Title.Text, weatherDetail); } // Add Carousel to the main panel this.LayoutRoot.Children.Add(carousel); }
In this handler, we will get list of all cities as RSS feed in the e.Result variable. We use the SyndicationFeed class to load this RSS feed. We can iterate through each item inside the feed using foreach loop on feed.Items collection. Each feed item will contain the city name. We then create WeatherDetail control for each city and add this control to the Carousel. And finally we add Carousel created dynamically to the main panel.
To learn more about using SyndicationFeed class, please refer the
http://silverlight.net/quickstarts/syndicationfeedreader.aspx, a sample available on Silverlight site.
To populate each WeatherDeail control, we have to make call to RSS service method getWeatherDetailsAsRSS() method. Again this has to be done asynchronously using event handler
client_getWeatherDetailsAsRSSCompleted(). This event handler is shown bellow.
void client_getWeatherDetailsAsRSSCompleted(object sender, getWeatherDetailsAsRSSCompletedEventArgs e) { // Load feed into SyndicationFeed XmlReader reader = XmlReader.Create(new System.IO.StringReader(e.Result.ToString())); SyndicationFeed feed = SyndicationFeed.Load(reader); // Get associated carousel WeatherDetails weatherDetail = (WeatherDetails) e.UserState; // Get current temperature weatherDetail.Temperature.Text = feed.Items.ElementAt(0).Title.Text; // Get today's temperature range weatherDetail.Range.Text = feed.Items.ElementAt(1).Title.Text; // Get climate text weatherDetail.Climate.Text = feed.Items.ElementAt(2).Title.Text; // Asscociat various images for above climate text ImageBrush brush = null; switch (weatherDetail.Climate.Text) { case "Thunder Storm": brush = new ImageBrush(); brush.ImageSource = new BitmapImage(new Uri("gray-sky.png", UriKind.Relative)); weatherDetail.LayoutRoot.Background = brush; weatherDetail.ConditionsOverlay.Source = (BitmapImage)new BitmapImage(new Uri("big-thunderstorm.png", UriKind.Relative)); break; case "Rainy": brush = new ImageBrush(); brush.ImageSource = new BitmapImage(new Uri("gray-sky.png", UriKind.Relative)); weatherDetail.LayoutRoot.Background = brush; weatherDetail.ConditionsOverlay.Source = (BitmapImage)new BitmapImage(new Uri("big-rain.png", UriKind.Relative)); break; case "Cloudy": brush = new ImageBrush(); brush.ImageSource = new BitmapImage(new Uri("gray-sky.png", UriKind.Relative)); weatherDetail.LayoutRoot.Background = brush; weatherDetail.ConditionsOverlay.Source = (BitmapImage)new BitmapImage(new Uri("big-cloud.png", UriKind.Relative)); break; case "Sunny": brush = new ImageBrush(); brush.ImageSource = new BitmapImage(new Uri("blue-sky.png", UriKind.Relative)); weatherDetail.LayoutRoot.Background = brush; weatherDetail.ConditionsOverlay.Source = (BitmapImage)new BitmapImage(new Uri("big-sun.png", UriKind.Relative)); break; } }
The above handler gets the RSS feed returned by getWeatherDetailsAsRSS() web service into variable e.Result. It again uses class SyndicationFeed to process the RSS feed. This method knows various items in the RSS feed and extract data like
Depending on climate text, this handler sets various images in associated WeatherDetail control. Look at switch statement to understand these settings.
Finally two handlers are written to control the Carousel rotation. A timer is set in Page_Loaded() method. Every time timer is hit, carousel is rotate by one position towards left of right, depending on value of rotateDirection flag.
// Timer event handler Used to rotate items in Carousel by one frame void dt_Tick(object sender, EventArgs e) { if (carousel != null) { if (rotateDirection == 1) carousel.MoveRight(1); else carousel.MoveLeft(1); } }
Rotation direction can be modified by clicking on the main panel. Every time the Mouse left button is clicked and released, the rotation direction is reversed.
// Event handler used to toggle the direction of rotation private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (rotateDirection == 1) rotateDirection = 2; else rotateDirection = 1; }