coderox

Bing Maps with custom pushpin and popup interaction

Tags: Windows Phone,UX,Bing Maps

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.

Add a Comment