Wednesday, September 18, 2013

When needing to bind to a platform specific class but the ViewModel is a PCL

This is a less than ideal scenario. I've made by ViewModel a PCL as an aid to making it easier to write "proper unit tests" (i.e. run within Visual Studio) but now I discover I need to add a map to the app and show a dynamic collection of locations (pins) on the map.

<disclaimer>I'm working on a proof of concept app and the requirements are very "flexible" at the moment. Of course I'd plan things better than this in advance under normal scenarios.</disclaimer>

Starting simply, I want to add an ObservableCollection<MyPin> to my VM. But there's a problem.

    public class MyPin
    {
        public GeoCoordinate Coordinate { getset; }
        public string Name { getset; }
    }

The problem is that The GeoCoordinate lives in System.Device.Location and so can't be referenced in the PCL. This means I won't be able to bind to it directly. Frustrating!

frustration

Upon reflection this makes sense. That I'm using a GeoCoordinate is an implementation detail of the UI. It could be displayed as anything. It just makes my life a little bit more complicated.
In reality my class should be more generic:
    public class MyPin
    {
        public double Longitude { getset; }
        public double Latitude { getset; }
        public string Name { getset; }
    }

But this doesn't help with my binding.
So, I need to be able to convert a Latitude and Longitude value into a GeoCoordinate. Sounds like a job for an IValueConverter.

I'm not a big fan of using ValueConverters. They have 2 issues in my mind.
1. They are a performance overhead.
2. They are hard to write coded tests for.

These aren't really issues any more though.

1. This was true in the early days of WP7, where having lots of converters could noticeably impact page loading, but this isn't really a problem any more. You'd have to use loads for this to be an issue on WP8 and I'm only using it where I have to.
2. In this instance I'm only using them for a presentation details though so a lack of automated tests for how locations are displayed on the map isn't a big deal.

So let's add a converter:

public class PinToGeoCoordinateConverter : IValueConverter
{
  public object Convert(object value, Type targetType, object para, CultureInfo culture)
  {
    var pin = value as MyPin;
 
    return pin != null ? new GeoCoordinate(pin.Latitude, pin.Longitude)
                       : new GeoCoordinate();
  }
 
  public object ConvertBack(object valu, Type targetType, object para, CultureInfo culture)
  {
    throw new NotImplementedException();
  }
}
Then I just define my resource:
    <phone:PhoneApplicationPage.Resources>
        <converters:PinToGeoCoordinateConverter x:Key="PinToGeo" />
    </phone:PhoneApplicationPage.Resources>

and then I can use it
    <map:Map x:Name="TheMap">
     <toolkit:MapExtensions.Children>
      <toolkit:MapItemsControl>
       <toolkit:MapItemsControl.ItemTemplate>
        <DataTemplate>
         <toolkit:Pushpin GeoCoordinate="{Binding Converter={StaticResource PinToGeo}}"
                          Content="{Binding Name}" />
        </DataTemplate>
       </toolkit:MapItemsControl.ItemTemplate>
      </toolkit:MapItemsControl>
     </toolkit:MapExtensions.Children>
    </map:Map>

Simples.

All in all this feels like a bit of a kludge. At first it felt like a very bad approach but as time goes on I'm feeling more comfortable with it. I don't know if this is just be getting used to the idea or if it's really not that bad after all.

If you have any better approaches to dealing with this scenario or there are some issues you see with the above I'd love to hear from you.



3 comments:

  1. On of my biggest painpoints with PCLs was not being able to use Color in VMs, I mean, how common is that...

    So yeah, as unnecessary as it sounds, bring on HexToColorConverter and HexToSolidColorBrushConverter :)

    ReplyDelete
  2. @Andrej I've been pointed to https://github.com/paulcbetts/splat as a solution for handling Colors, Points and Bitmaps. It's still not a perfect solution but worth a look.

    ReplyDelete
  3. pauliom11:01 pm

    Since GeoCoordinate is not Sealed you can refactor an IGeoCoordinate and RealGeoCoordinate : GeoCoordinate, IGeoCoordinate - this is the one you use at runtime and is interchangeable with GeoCoordinate. You can then create FakeGeoCoordinate : IGeoCoordinate and you can test.

    ReplyDelete

I get a lot of comment spam :( - moderation may take a while.