30 Aug
2011

Mapping an Address with the Bing Maps WPF Control (Beta)

Category:UncategorizedTag: , , , , :

In my last post I showed you how to get started using the Bing Maps WPF control by creating an earthquake application.  Now I want to introduce you to the Bing Maps SOAP Services.  The Bing Maps SOAP Services is a set of programmable SOAP services that allow you to integrate maps and imagery, driving directions, distance calculations, and other location intelligence into your applications, business processes, and Web sites.

Let?s start with the Geocode Service which is used to match addresses, places, and geographic entities to latitude and longitude coordinates on the map, as well as return location information for a specified latitude and longitude coordinate.  We will create a simple application that allows you to enter an address and place a pushpin at that location.  First thing is first:

  1. Get the Bing Maps WPF control.
  2. Get a Bing Maps API key.

You MUST have a Bing Maps API key in order to use any of the SOAP services.  Now that you have that out of the way let?s write an application. Create a new WPF application targeting the .NET 4.0 framework. Add a reference to the Microsoft.Maps.MapControl.WPF.dll. This will most likely be located in Program Files or Program Files (x86) ?> Bing Maps WPF Control ?> Beta ?> Libraries.

Open up your App.xaml.  You need to add an ApplicationIdCredentialsProvider as a resource and enter your ApplicationId:

<Application x:Class="BingMapsGeocodeDemo.App"
???????????? xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
???????????? xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
???????????? xmlns:bing="clr-namespace:Microsoft.Maps.MapControl.WPF;assembly=Microsoft.Maps.MapControl.WPF"
???????????? StartupUri="MainWindow.xaml">
????<Application.Resources>
????????<bing:ApplicationIdCredentialsProvider x:Key="MyCredentials" ApplicationId="Your API Key" />
????</Application.Resources>
</Application>

Now create a very simple UI that will accept an address and a button to perform the geocoding:

<Window x:Class="BingMapsGeocodeDemo.MainWindow"
??????? xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
??????? xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
??????? xmlns:bing="clr-namespace:Microsoft.Maps.MapControl.WPF;assembly=Microsoft.Maps.MapControl.WPF"
??????? Title="MainWindow" Height="600" Width="800">
????<Grid>
????????<Grid.RowDefinitions>
????????????<RowDefinition Height="Auto" />
????????????<RowDefinition Height="*" />
????????</Grid.RowDefinitions>

????????<StackPanel Orientation="Horizontal" Margin="10" >
????????????<TextBox x:Name="_txtAddress" MinWidth="250" />
????????????<Button Content="Map It" IsDefault="True"/>
????????</StackPanel>

????????<bing:Map Grid.Row="1" CredentialsProvider="{StaticResource MyCredentials}" Center="40,-95"
????????????????? ZoomLevel="4" AnimationLevel="Full" >
????????</bing:Map>
????</Grid>
</Window>

The next thing we need to do is add a new Service Reference to the project and point to the following service:

http://dev.virtualearth.net/webservices/v1/geocodeservice/geocodeservice.svc

Give your service a name (I called mine BingMapsService) and just for fun, in the ?Add Service Reference? dialog, click the ?Advanced? button and click ?Generate asynchronous operations?.

Next we need to create a ViewModel to support our View.  This is a WPF application after all.  This ViewModel will need a command for the button that will perform the geocoding, and a property that will represent the result of the service that the UI will use to map the address.  It should resemble something like this:

public class GeocodeViewModel : INotifyPropertyChanged
{
????public ICommand GeocodeAddressCommand { get; private set; }

????private BingMapsService.GeocodeResult _geocodeResult;
????public BingMapsService.GeocodeResult GeocodeResult
????{
????????get { return _geocodeResult; }
????????set
????????{
????????????_geocodeResult = value;
????????????OnPropertyChanged("GeocodeResult");
????????}
????}

????public GeocodeViewModel()
????{
????????GeocodeAddressCommand = new DelegateCommand<String>(GeocodeAddress);
????}

????private void GeocodeAddress(string address)
????{

????}

????public event PropertyChangedEventHandler PropertyChanged;
????protected virtual void OnPropertyChanged(string propertyName)
????{
????????if (PropertyChanged != null)
????????????PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
????}
}

I want to point out that the DelegateCommand<T> is a very simple ICommand implementation and should NOT be used in production applications.  Here is the code:

public class DelegateCommand<T> : ICommand
{
????private Action<T> _execute;

????public DelegateCommand(Action<T> execute)
????{
????????_execute = execute;
????}

????public bool CanExecute(object parameter)
????{
????????return true;
????}

????public event EventHandler CanExecuteChanged;

????public void Execute(object parameter)
????{
????????_execute.Invoke((T)parameter);
????}
}

Let?s hook our UI up to our command.  it should look something like this:

<StackPanel Orientation="Horizontal" Margin="10" >
????????????<TextBox x:Name="_txtAddress" MinWidth="250" />
????????????<Button Content="Map It" IsDefault="True"
??????????????????? Command="{Binding GeocodeAddressCommand}"
??????????????????? CommandParameter="{Binding Text,ElementName=_txtAddress}"/>
????????</StackPanel>

Notice that we are using the text of the TextBox as the CommandParameter.  Next, we need to implement the command:

private void GeocodeAddress(string address)
{
????using (BingMapsService.GeocodeServiceClient client = new BingMapsService.GeocodeServiceClient("CustomBinding_IGeocodeService"))
????{
????????client.GeocodeCompleted += (o, e) =>
????????{
????????????if (e.Error == null)
????????????{
????????????????GeocodeResult = e.Result.Results[0];
????????????}
????????};

????????BingMapsService.GeocodeRequest request = new BingMapsService.GeocodeRequest();
????????request.Credentials = new Credentials() { ApplicationId = (App.Current.Resources["MyCredentials"] as ApplicationIdCredentialsProvider).ApplicationId };
????????request.Query = address;
????????client.GeocodeAsync(request);
????}
}

In this command we are simply creating an instance of the service client, creating a GeocodeRequest, setting the credentials using the ApplicationId we have stored in the App.xaml, and then using the address as the Query for the request.  When the call to the service is completed we are setting the GeocodeResult property to the first element in the results.

Now the tricky part.  We want to place a Pushpin on the address that was entered and then move the map to that location.  We also need to make sure that when another address is entered, we clear any previous Pushpins that may exist on the map.  We also have to be aware that other elements my be on the map and we don?t want to accidentally alter those elements.  Oh and did I mention this was an MVVM application and this has to be done with just data binding?

The approach that I decided to go with is to use Attached Properties.  So I created two attached properties; one for the GeocodeResult and another for a MapLayer that will be dedicated to the GeocodeResult.  This will allow us to create a MapLayer that is specific to our GeocodeResut and not alter any other elements that may be on the map.  Let?s check out the code.

public class MapInteractivity
{
????#region GeocodeResult

????public static readonly DependencyProperty GeocodeResultProperty = DependencyProperty.RegisterAttached("GeocodeResult", typeof(BingMapsService.GeocodeResult), typeof(MapInteractivity), new UIPropertyMetadata(null, OnGeocodeResultChanged));
????public static BingMapsService.GeocodeResult GetGeocodeResult(Map target)
????{
????????return (BingMapsService.GeocodeResult)target.GetValue(GeocodeResultProperty);
????}
????public static void SetGeocodeResult(Map target, BingMapsService.GeocodeResult value)
????{
????????target.SetValue(GeocodeResultProperty, value);
????}
????private static void OnGeocodeResultChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
????{
????????OnGeocodeResultChanged((Map)o, (BingMapsService.GeocodeResult)e.OldValue, (BingMapsService.GeocodeResult)e.NewValue);
????}
????private static void OnGeocodeResultChanged(Map map, BingMapsService.GeocodeResult oldValue, BingMapsService.GeocodeResult newValue)
????{
????????Location location = newValue.Locations.Select(x => new Location(x.Latitude, x.Longitude)).First();

????????Pushpin pin = new Pushpin();
????????pin.Location = location;
????????pin.ToolTip = newValue.Address.FormattedAddress;

????????var locationLayer = GetGeocodeResultLayer(map);
????????if (locationLayer == null)
????????{
????????????locationLayer = new MapLayer();
????????????SetGeocodeResultLayer(map, locationLayer);
????????}

????????locationLayer.Children.Clear();
????????locationLayer.Children.Add(pin);

????????map.SetView(location, map.ZoomLevel);
????}

????#endregion //GeocodeResult

????#region GeocodeResultLayer

????public static readonly DependencyProperty GeocodeResultLayerProperty = DependencyProperty.RegisterAttached("GeocodeResultLayer", typeof(MapLayer), typeof(MapInteractivity), new UIPropertyMetadata(null, OnGeocodeResultLayerChanged));
????public static MapLayer GetGeocodeResultLayer(DependencyObject target)
????{
????????return (MapLayer)target.GetValue(GeocodeResultLayerProperty);
????}
????public static void SetGeocodeResultLayer(DependencyObject target, MapLayer value)
????{
????????target.SetValue(GeocodeResultLayerProperty, value);
????}
????private static void OnGeocodeResultLayerChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
????{
????????OnGeocodeResultLayerChanged((Map)o, (MapLayer)e.OldValue, (MapLayer)e.NewValue);
????}
????private static void OnGeocodeResultLayerChanged(Map map, MapLayer oldValue, MapLayer newValue)
????{
????????map.Children.Add(newValue);
????}

????#endregion //GeocodeResultLayer
}

Notice that when the GeocodeResult changes we get the Location and create a pushpin for that location.  We also set the tooltip to the FormattedAddress of the GeocodeResult.  So when we hover over the pushpin we will see a nice little tooltip of the address.  Next we try to get the GeocodeResultLayer from the map, if it is null this means it doesn?t exist on the map yet so we create one and assign it accordingly.  We clear the layer child elemnts and then add out pushpin.  The last thing we do is call the SetView method of the Map control passing in the location and the current ZoomLevel.

The final step is to use our AttachedProperty on the map.  Be sure to create a namespace for the MapInteractivity class and define it as follows:

<bing:Map Grid.Row="1" CredentialsProvider="{StaticResource MyCredentials}" Center="40,-95"
????????? core:MapInteractivity.GeocodeResult="{Binding GeocodeResult}"
????????? ZoomLevel="4" AnimationLevel="Full" >
</bing:Map>

One last thing.  Be sure to remember to set your DataContext of your View to the ViewModel we have created:

public MainWindow()
{
????InitializeComponent();
????DataContext = new GeocodeViewModel();
}

That?s it.  You now have an application the can map (geocode) an address using the Bing Maps WPF Control and the Geocoding SOAP Service.  Oh and we are doing this all with MVVM and data binding which is my favorite part.  Be sure to Download the Source and start playing around.