Bing Maps with custom pushpin and popup interaction
Tags:
Windows Phone,UX,Bing Maps
apr 30 2012 10:57 by Johan Lindfors
When creating a solution which requires a map it's pretty common
to also display pushpins that inform the user of locations. This is
very easy to accomplish in Windows Phone, but when you also want to
respond to the user tapping one of these pushpins and display maybe
a popup, then it becomes a bit more difficult. Here is an approach
that I find both easy to implement and understand.
The solution is in short to expose a collection of
LocationViewModel's from the map's view model. This
LocationViewModel encapsulates a GeoCoordinate to position the
pushpin on the map, as well as two more properties, a Name property
(or at least some sort of descriptive text) and a boolean property
called IsSelected. Then I create a custom template for the pushpin
in the view to include both the actual pushpin as well as a custom
popup element that has its Visibility property bound to the
IsSelected property of the LocationViewModel, with a simple
converter from Boolean to Visibility. With some Expression Blend
magic and some custom margins the solution is quite simple. But
there are some gotchas…
Custom DataTemplate for the Pushpin and Popup:
<DataTemplate>
<maps:Pushpin Location="{Binding Item}" Tap="PushpinTapped">
<maps:Pushpin.Template>
<ControlTemplate>
<Grid Height="27" Width="400" Margin="-200,0,0,0">
<!-- Popup -->
<Border Background="Black" VerticalAlignment="Top" HorizontalAlignment="Center" Margin="0,-50,0,0" Visibility="{Binding IsSelected, Converter={StaticResource VisibilityConverter}}" Padding="24,12">
<TextBlock Text="{Binding Name}" Style="{StaticResource PhoneTextSmallStyle}" Foreground="White"/>
</Border>
<!-- Pushpin -->
<Image Width="48" Height="35" HorizontalAlignment="Center" Source="logo_48x35.jpg"/>
</Grid>
</ControlTemplate>
</maps:Pushpin.Template>
</maps:Pushpin>
</DataTemplate>
The LocationViewModel in it's simplicity:
public class LocationViewModel : ObservableObject<GeoCoordinate>
{
private string _name;
public string Name
{
get { return _name; }
set
{
if (_name == value)
return;
_name = value;
RaisePropertyChanged(() => Name);
}
}
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected == value)
return;
_isSelected = value;
RaisePropertyChanged(() => IsSelected);
}
}
public LocationViewModel(double latitude, double longitude, string name)
{
Item = new GeoCoordinate(latitude, longitude);
Name = name;
}
}
The ObservableObject<T> is a simple implementation of
INotifyPropertyChanged as well as an encapsulated instance of the
type T:
public class ObservableObject<T> : ObservableObject
{
protected T _item;
public T Item
{
get { return _item; }
protected set { _item = value; }
}
}
The ObservableObject is the following code:
public class ObservableObject : INotifyPropertyChanged
{
public void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public void RaisePropertyChanged<T>(Expression<Func<T>> propertyExpression)
{
if (propertyExpression.Body.NodeType == ExpressionType.MemberAccess)
{
var memberExpression = propertyExpression.Body as MemberExpression;
string propertyName = memberExpression.Member.Name;
this.RaisePropertyChanged(propertyName);
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
The view model of the map page then parses some sort of
collection of locations and populates the ObservableCollection, and
here's the gotcha. Make sure you populate the collection with the
locations ordered on Latitude DESCENDING!
var restaurants = from restaurant in xml.Descendants(gpx + "wpt")
select new LocationViewModel(
double.Parse(restaurant.Attribute("lat").Value),
double.Parse(restaurant.Attribute("lon").Value),
restaurant.Element(gpx + "name").Value);
foreach (var restaurant in restaurants.OrderByDescending(rest => rest.Item.Latitude))
{
Locations.Add(restaurant);
}
Why, you might ask yourself? If you don't you will most likely
notice that the selected Popup occasionally gets overdrawn by other
pushpin icons that are drawn in order. But making sure of the
ordering keeps this from happening, LINQ is so handy, don't you
agree?
Naturally the solution includes more source, and that's why I've
uploaded a small Swedish McDonalds restaurant
finder to my Skydrive, feel free to investigate. I had to
remove my Bing Maps Credentials so if you want to run the solution
properly you need to insert your own in MainPage.xaml.