This is the 11th article in our "Bring the clouds together: Azure + Bing Maps" series. You can find a preview of live demonstration on http://sqlazurebingmap.cloudapp.net/. For a list of articles in the series, please refer to http://blogs.msdn.com/b/windows-azure-support/archive/2010/08/11/bring-the-clouds-together-azure-bing-maps.aspx.

Introduction

In our previous posts, we have created a complete web application, with a cloud service hosted in our own cloud server, and a HTML client consuming our own service as well as an external cloud service (Bing Maps). We can say our own solution is complete. However, third party developers should also be able to create their own clients to consume our service. Otherwise we can only say we've created a web application, not a cloud service. In this post, let's add a Silverlight client that consumes both our own cloud service and the Bing Maps service. Let's examine if we need to modify the service code, and thus if our service is ready to be consumed by all kinds of clients.

Using Bing Maps Silverlight Control

Display a map

Bing Maps Silverlight SDK offers a full-featured Silverlight control. It is defined in the Microsoft.Maps.MapControl namespace in the Microsoft.Maps.MapControl.dll assembly. There's also a utility class library defined in the Microsoft.Maps.MapControl namespace in the Microsoft.Maps.MapControl.Common.dll assembly. So to use the types in Silverlight XAML, you treat them as normal Silverlight types.

First import the namespaces:

    xmlns:Microsoft_Maps_MapControl="clr-namespace:Microsoft.Maps.MapControl;assembly=Microsoft.Maps.MapControl"

    xmlns:Microsoft_Maps_MapControl_Common="clr-namespace:Microsoft.Maps.MapControl;assembly=Microsoft.Maps.MapControl.Common"

Then define the control:

        <Microsoft_Maps_MapControl:Map x:Name="map" Margin="8,21,36,23" Loaded="Map_Loaded" ZoomLevel="4" MouseClick="map_MouseClick" d:LayoutOverrides="GridBox" Grid.Column="1">

            <Microsoft_Maps_MapControl:Map.Center>

                <Microsoft_Maps_MapControl_Common:Location AltitudeReference="Ground" Altitude="0" Longitude="121" Latitude="31"/>

            </Microsoft_Maps_MapControl:Map.Center>

            <Microsoft_Maps_MapControl:MapItemsControl x:Name="mapItems" ItemTemplate="{StaticResource MapItemDataTemplate}"/>

        </Microsoft_Maps_MapControl:Map>

And in code behind, we provide the map's credential you obtained when registering on the Bing Maps portal. If you haven't registered yet, please refer to the previous post.

this.map.CredentialsProvider = new ApplicationIdCredentialsProvider(this._mapCredential);

In the above code, there're two interesting points. First, the Map control exposes a Center property, whose type is Location. This property defines the center of the map. Second, the Map class ultimately derives from UserControl, so you can add custom contents to the map. In most cases, you want to add shapes (such as pushpin) to the map, so let's use a MapItemsControl as the Content property of the Map class. The MapItemsControl class derives from ItemsControl (similar to ListBox), so any knowledge you have about ItemsControl works for MapItemsControl as well. For example, you can bind its data to a list, and define an ItemTemplate to display the data.

Add pushpins

In our sample, we define the ItemsSource of the MapItemsControl to be an ObservableCollection whose data is obtained from our own WCF Data Services:

private ObservableCollection<Travel> _travelItems = new ObservableCollection<Travel>();

 We put a Pushpin control in the MapItemsControl's ItemTemplate. So a pushpin will be rendered for each item in the data source. This is somewhat similar to jQuery Templates we used in our HTML client.

        <DataTemplate x:Key="MapItemDataTemplate">

               <Microsoft_Maps_MapControl:Pushpin Cursor="Hand" Content="{Binding Place}" Microsoft_Maps_MapControl:MapLayer.Position="{Binding Converter={StaticResource locationConverter}}" Template="{StaticResource PushpinControlTemplate}" Style="{StaticResource PushpinStyle}"/>

        </DataTemplate>

Just like a normal Silverlight control, you can modify the ControlTemplate of the Bing Maps controls, if you want to perform custom rendering. For example, you can add videos for each pushpin. Anyway, when working with Bing Maps Silverlight control, you can take advantage of all mighty of Silverlight.

Working with Bing Maps cloud service

We have demonstrated how to consume Bing Maps REST services in our HTML client. So this time, let's demonstrate SOAP services. To consume the SOAP service, you add a service reference to the proper URI such as http://dev.virtualearth.net/webservices/v1/geocodeservice/GeocodeService.svc/mex, just like referencing a normal WCF SOAP service.

Actually Bing Maps SOAP services are indeed WCF SOAP Services, and it's fully compatible with Silverlight. The cross domain access policy file has been configured, and it supports both BasicHttpBinding and a custom binding that uses Silverlight binary serialization.

In our sample, we handle the map's MouseClick event, and invoke the SOAP service to obtain detailed information of the clicked place. Unlike AJAX control, the Silverlight map's click event won't be invoked if the mouse moved when panning the map. So it's safe to use.

        private void map_MouseClick(object sender, Microsoft.Maps.MapControl.MapMouseEventArgs e)

        {

            ReverseGeocodeRequest request = new ReverseGeocodeRequest() { Location = map.ViewportPointToLocation(e.ViewportPoint) };

            request.Credentials = new Credentials() { Token = this._mapCredential };

            _geocodeClient.ReverseGeocodeAsync(request);

        }

After the response is returned, we can create a new Travel object according to the response, and add it to the MapItemsControl's ItemsSource. Since we're using ObservableCollection, Silverlight data binding system automatically picks up the new item, and displays it on the map.

        private void GeocodeClient_ReverseGeocodeCompleted(object sender, ReverseGeocodeCompletedEventArgs e)

        {

            if (e.Error != null)

            {

                MessageBox.Show(e.Error.Message);

            }

            else if (e.Result.Results.Count > 0)

            {

                var result = e.Result.Results[0];

                Travel travel = new Travel()

                {

                                 PartitionKey = "fake@live.com",

                                 RowKey = Guid.NewGuid(),

                    Place = result.DisplayName,

                    Time = DateTime.Now,

                    Latitude = result.Locations[0].Latitude,

                    Longitude = result.Locations[0].Longitude

                };

                this._travelItems.Add(travel);

                this._dataServiceContext.AddObject("Travels", travel);

            }

        }

As you can see, working with Bing Maps cloud service in Silverlight is very easy. To support our new client, the Bing Maps cloud service doesn't need any modification.

Consume WCF Data Services

The new client also needs to be integrated with our own cloud service, which is a WCF Data Services. We assume you already know how to consume WCF Data Services in Silverlight. If you don't have previous experience to work with WCF Data Services in Silverlight, please go through the tutorial on http://msdn.microsoft.com/en-us/library/ff650919(v=VS.95).aspx first.

When working with our own cloud service, we take the usual approach: First add a service reference to generate a client proxy. Then create a service context class. At the moment, we intend to host the Silverlight client in the same Web Role as the cloud service, so the best solution is to use a relative address:

this.LoginLink.NavigateUri = new Uri(Application.Current.Host.Source, "../LoginPage.aspx?returnpage=SilverlightClient.aspx");

By default, when running in browser to access a service on the same domain, WCF Data Services Silverlight library uses browser's xml HTTP stack. In other cases, it uses Silverlight client HTTP stack. One of the major limitation of browser's xml HTTP stack is it doesn't support HTTP verbs other than GET and POST. However, WCF Data Services allows client to send POST for most requests, and use the X-HTTP-Method-Override request header to indicate the actual verb. So generally it doesn't matter which HTTP stack to use when consuming data services. If you don't like this behavior, you can force the client library to use client HTTP stack:

            this._dataServiceContext.HttpStack = System.Data.Services.Client.HttpStack.ClientHttp;

Using client HTTP stack is recommended for many other cases because it supports more features. But you must manually deal with cookies. Since our sample doesn't require features offered by client HTTP stack, we'll continue to use the default browser xml HTTP stack.

Now you can perform CRUD operations as usual:

Query

        private void Map_Loaded(object sender, RoutedEventArgs e)

        {

            this._dataServiceContext.Travels.BeginExecute(result =>

            {

                this._travelItems = new ObservableCollection<Travel>(this._dataServiceContext.Travels.EndExecute(result).ToList().OrderBy(t => t.Time));

                this.Dispatcher.BeginInvoke(new Action(() =>

                {

                    this.mapItems.ItemsSource = this._travelItems;

                    this.placeList.ItemsSource = this._travelItems;

                }));

            }, null);

        }

Insert

Refer to the above GeocodeClient_ReverseGeocodeCompleted method.

Update

        private void DatePicker_SelectedDateChanged(object sender, SelectionChangedEventArgs e)

        {

            DatePicker datePicker = (DatePicker)sender;

            Travel travel = datePicker.DataContext as Travel;

            if (travel != null && travel.Time != datePicker.SelectedDate.Value)

            {

                travel.Time = datePicker.SelectedDate.Value;

                this._dataServiceContext.UpdateObject(travel);

            }

        }

Delete

        private void DeleteButton_Click(object sender, RoutedEventArgs e)

        {

            HyperlinkButton button = (HyperlinkButton)sender;

            Travel travel = button.DataContext as Travel;

            if (travel != null)

            {

                this._travelItems.Remove(travel);

                this._dataServiceContext.DeleteObject(travel);

            }

        }

WCF Data Services Silverlight library takes care of entity state tracking automatically. So we don't need to do any manual tracking. Finally, to save the changes, invoke Begin/EndSaveChanges method. Our service didn't implement the MERGE operation. This means we don't support partial update. So the client needs to specify SaveChangeOptions to ReplaceOnUpdate, which will issue PUT for update instead of MERGE.

        private void SaveButton_Click(object sender, System.Windows.RoutedEventArgs e)

        {

            this._dataServiceContext.BeginSaveChanges(SaveChangesOptions.ReplaceOnUpdate, new AsyncCallback((result) =>

            {

                var response = this._dataServiceContext.EndSaveChanges(result);

            }), null);

        }

Integrate with federated authentication

The final task for the Silverlight client is to integrate with federated authentication. At this stage, the task is easy. First create a SilverlightClient.aspx page that contains a code behind file, and move the Silverlight host from the generated Silverlight testing page to SilverlightClient.aspx. Remember authentication still must be performed on the server side so clients cannot send fake identities.

Now you can use the similar code to provide the login link and welcome message in HTML code. But you can also use a more Silverlight integrated solution. Silverlight supports hyperlink via the HyperlinkButton control. Since our Silverlight application is hosted in the same Web Role as the login page, once again we'll use a relative address:

<HyperlinkButton x:Name="LoginLink" Content="Login to manage your custom travel/>

this.LoginLink.NavigateUri = new Uri("../LoginPage.aspx?returnpage=SilverlightClient.aspx", UriKind.Relative);

Remember from our last post the LoginPage.aspx redirects the user to the identity provider of their choice, and after the sign in completes, it stores the user identity in session, and redirects the user back to the page specified in the returnpage query string. In our HtmlClient.aspx's code behind, we check for the user identity from session, and determine to display a welcome Label or a sign in link. For Silverlight, we can adopt the similar logic. But this time, instead of using an ASP.NET Label control, we talk to the Silverlight application using initial parameters.

First expose two properties so we can use them in the aspx page:

        public bool IsAuthenticated { get; set; }

        public string WelcomeMessage { get; set; }

 

        protected void Page_Load(object sender, EventArgs e)

        {

            if (Session["User"] != null)

            {

                this.IsAuthenticated = true;

                this.WelcomeMessage = "Welcome: " + (string)Session["User"] + ".";

            }

            else

            {

                this.IsAuthenticated = false;

                this.WelcomeMessage = null;

            }

        }

Then set the Silverlight initial parameters according to the properties value:

<param name="initparams" value="IsAuthenticated=<%= this.IsAuthenticated %>,WelcomeMessage=<%=this.WelcomeMessage %>" />

In Silverlight's App.xaml.cs, parse the initial parameters, and store them in application global scope:

        internal static bool IsAuthenticated = false;

        internal static string WelcomeMessage = null;

       

        private void Application_Startup(object sender, StartupEventArgs e)

        {

            IsAuthenticated = bool.Parse(e.InitParams["IsAuthenticated"]);

            if (IsAuthenticated)

            {

                WelcomeMessage = e.InitParams["WelcomeMessage"];

            }

            this.RootVisual = new MainPage();

        }

Finally, in MainPage.xaml.cs, we can once again determine to show the sign in hyperlink or the welcome message:

            if (App.IsAuthenticated)

            {

                this.LoginLink.Visibility = System.Windows.Visibility.Collapsed;

                this.WelcomeTextBlock.Visibility = System.Windows.Visibility.Visible;

                this.WelcomeTextBlock.Text = App.WelcomeMessage;

            }

            else

            {

                this.LoginLink.Visibility = System.Windows.Visibility.Visible;

                this.WelcomeTextBlock.Visibility = System.Windows.Visibility.Collapsed;

            }

Conclusion

This post described how to add an additional (Silverlight) client to our existing solution, without modifying any existing code. In reality, a true cloud service must be designed so it can be consumed by multiple first party and third party clients. Only then can we say it is a cloud service instead of a web application. The next post will introduce another client, a Windows Phone client.