Azure + Bing Maps: Creating a Silverlight Client
This is the 11th article in our "Bring the clouds together: Azure + Bing Maps"
series. You can find a preview of live demonstration on
https://sqlazurebingmap.cloudapp.net/. For a list of articles in the series,
please refer to
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
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
https://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
https://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.