20 August 2015

Windows 10 maps part 3 - querying the map & attaching business objects to map shapes

Intro
In my previous post in this series I showed how to draw lines, polygons and icons on the map. What I did not show was how you could query the map for the shapes present at a location when you tap on it, and more importantly - how you find the business objects back that were used to create this shapes in the first place. In previous times, specifically the Visual Basic 6.0 era, people used to do something like putting something like an id in a Tag property of a UI element and use that go back into the database. I recently even encountered an approach using a Tag in a Window Store app *shudder* ;) Fortunately in these day and age things can be done quite differently.

To be honest - this article does not describe brand new knowledge, nor it is really specific to Windows 10 (I used this trick in Windows Phone 8.0 already), and if you dig through my blog and/or samples of the past year you might already distill it. Yet, I get the question about relating map shapes to business objects quite often, as map shapes (thankfully) do not have a Tag property, people apparently still wonder how to connect shapes and business objects so I thought it best to make this a separate article.

Attaching business objects to map shapes
This is actually pretty easy, and if you have been peeking at the sample app you might have seen that there is actually one extra statement in all three methods that draw a shape as I write in my previous post. For example, I take line drawing method:

private void DrawLines(object sender, RoutedEventArgs e)
{
  if (!DeleteShapesFromLevel(2))
  {
    var strokeColor = Colors.Green;
    strokeColor.A = 200;

    //Drawing lines, notice the use of Geopath. Consists out of BasicGeopositions
    foreach (var dataObject in PointList.GetLines())
    {
      var shape = new MapPolyline
      {
        StrokeThickness = 9,
        StrokeColor = strokeColor,
        StrokeDashed = false,
        ZIndex = 2,
        Path = new Geopath(dataObject.Points)
      };
      shape.AddData(dataObject);

      MyMap.MapElements.Add(shape);
    }
  }
}

There is an AddData method that you won't find in any Microsoft API documentation because it's an extension method I wrote myself. The key thing to understand is that a map shape descends from DependencyObject. And thus, while it does not have a Tag, it can support attached dependency properties.

So over in the MappingUtilities folder there is a class hosting a simple attached dependency property plus some helper methods:

using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls.Maps;

namespace MappingUtilities
{
  /// <summary>
  /// Helper class to attach data to map shapes. Well, to any 
  /// DependencyObject, but that's to make it fit into a PCL
  /// </summary>
  public static class MapElementData
  {
    #region Attached Dependency Property ObjectData
    public static readonly DependencyProperty ObjectDataProperty =
       DependencyProperty.RegisterAttached("ObjectData",
       typeof(object),
       typeof(MapElementData),
       new PropertyMetadata(default(object), null));

    public static object GetObjectData(MapElement obj)
    {
      return obj.GetValue(ObjectDataProperty);
    }

    public static void SetObjectData(
       DependencyObject obj,
       object value)
    {
      obj.SetValue(ObjectDataProperty, value);
    }
    #endregion

    public static void AddData(this MapElement element, object data)
    {
      SetObjectData(element, data);
    }

    public static T ReadData<T>(this MapElement element) where T : class
    {
      return GetObjectData(element) as T;
    }
  }
}

Attaching a business object to a shape is as simple as calling shape.SetObjectData(businessObject) and getting it back again from the shape is done by using ReadData<T>. Which brings me to the subject of

Querying the map
On any GIS (or simple map based system) you want to be able to tap an element on the map to review it's properties. On the Windows 10 map, it's not possible to select individual elements. In stead, you can tap the map and check what's there. I call that the "skewer method" - you basically stick a skewer in the map on the tap location and see what sticks :). Nothing wrong with that, by the way.

If you tap the map, you get either this message -  if you tap on a place where there are no shapes to be found
image

...or something like this when you tap on a place where there are map shapes indeed
image Either way, this method is called:

 private async void OnMapElementClick(MapControl sender, 
                                      MapElementClickEventArgs args)
{
  var resultText = new StringBuilder();

  // Yay! Goodbye string.Format!
  resultText.AppendLine($"Position={args.Position.X},{args.Position.Y}");
  resultText.AppendLine(
    $"Location={args.Location.Position.Latitude:F9},{args.Location.Position.Longitude:F9}");

  foreach (var mapObject in args.MapElements)
  {
    resultText.AppendLine("Found: " + mapObject.ReadData<PointList>().Name);
  }
  var dialog = new MessageDialog(resultText.ToString(),
    args.MapElements.Any() ? "Found the following objects" : "No data found");
  await Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
    async () => await dialog.ShowAsync());
}

This is great new event that makes searching for map elements on the location where is tapped or clicked. This is like actually applying the skewer - you stick it in the map and whatever is there, ends up in args.MapElements. Then it's simply a matter of processing all shapes in there and retrieve the business object using the ReadData method. The generic parameter conveniently casts it to the right type. Thus we can extract the Name property of every business objects from which the map shape originates and display it in a dialog. Or, presumably, do something more useful with it in a real life situation ;)

Hooking up the control to the click event is simple, like this

<maps:MapControl x:Name="MyMap" MapElementClick="OnMapElementClick" />

Concluding remarks
Using an attached dependency property to keep track of business objects and their map representation is pretty easy. So look no further for a tag (or some other weird trick) - just use the great things XAML offers us out of the box. Once again I refer to the sample app to show you how things work in real life.

3 comments:

Paul said...

Great explanation of all features available in MapControl and using them in an application.

The only thing that still bites me is that the phone emulator won't render any map content, so unfortunately it's not possible to use the nice location simulation.

Thanks a lot for your blog! Hopefully we will see the other parts soon.

Paul

Paul said...

Good explanation of the features of MapControl and how to use these in an uwp app!
Hopefully we will see the other parts soon.

The only thing that bites me is that I cannot test it in the phone emulator because the MapControl doesn't render any content on the emulator (version 10.0.10240.0). That's a pity because the phone emulator allows to simulate location changes or even test driving some route.

Thanks for your great blogpost!

Joost van Schaik said...

Paul, I sometimes have similar problems with the emulator. Can you verify the emulator has internet connection? You can easily find this out by starting the emulator, and then type for instance bing.com in the Edge browser on the emulator. If it has indeed network connection, then try the following:

* Start the map app.
* Click "yes" when it wants to accept your location
* Wait

With any luck, the map tiles will show up, and if they do, cherish this emulator. It does not make sense, it does not always work, but it helped me a couple of times.