Monday, April 10, 2017

Applying MVVM to Difficult UI Elements

It’s worth repeating: the general principle in MVVM is to separate logic from presentation code. This separation makes it easy to swap out presentation elements and unit test our control logic. The generally accepted litmus test for “good mvvm” is the View should be XAML only and all code should be contained in the view-model. Ultimately it leads to more opportunities for code-reuse and higher unit testing code coverage.

However, living up to this ideal can be difficult to achieve, especially when rogue user controls weren’t designed with MVVM in mind. The MapControl is a great example of this – it offers so many complex features (layers, pushpins, custom-shapes, etc) that it would be difficult to produce a simple abstraction.

Caliburn.Micro has a Screen ViewModel that represents important view-lifecycle events. The OnViewAttached method provides an opportunity to obtain a reference to the view and perform logic that you would normally have to do in code-behind. It feels a bit hacky (it sort of is, I’ll show some alternatives in another post) – but it’s a good workaround for difficult scenarios. Keep in mind this is an exception to the rule, something that you might use once in a while.

This recipe uses these ingredients:

  • A named element in the View
  • An interface for the View (optional)
  • Override the OnViewAttached in the ViewModel

The View

We’ll start with a XAML View that uses the Xamarin.Forms Map control. There’s a bit of work to get the Map up and running, so I’ll refer you to Xamarin’s walk-through on how to configure your project. Though most examples from Xamarin show the Map control being used from code-behind, we should be able to do anything they do in those samples from our ViewModel.

In order to access the MapControl programmatically, we have to give it an x:Name.

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:maps="clr-namespace:Xamarin.Forms.Maps;assembly=Xamarin.Forms.Maps"             
             x:Class="XF.CaliburnMicro1.Views.Tab3View"            
             >

    <Grid VerticalOptions="FillAndExpand">
        
        <maps:Map x:Name="map"
                  IsShowingUser="True"
                  MapType="Street"
                  />
    </Grid>    
    
</ContentView>

Interface for the View

This step is optional and is largely because I don’t want to couple the View explicitly in the ViewModel. By defining an interface, I can use this technique with different Views. For example, you might subclass the Map control and use it different areas of your application. The interface approach makes it easy to access the control regardless of where it’s used in my app.

namespace XF.CaliburnMicro1.Views
{
    using Xamarin.Forms.Maps;

    public interface IMapAware
    {
        Map GetMap();
    }
}

Next we simply implement the interface on the View and return our named element.

namespace XF.CaliburnMicro1.Views
{
    using Xamarin.Forms;
    using Xamarin.Forms.Maps;

    public partial class Tab3View : ContentView, IMapAware
    {
        public Tab3View()
        {
            InitializeComponent();
        }

        public Map GetMap()
        {
            return this.map;
        }
    }
}

Access the Control from the ViewModel

The last step here assumes that we’re backing our Xamarin.Form Pages with ViewModels that derive from Caliburn.Micro’s Screen class. To access the control from the ViewModel, we override the OnViewAttached method and either cast the argument to our View or to the interface mentioned above.

In this example, I’m assigning the Control to a property on the ViewModel so that I can centralize initialization logic, such as subscribing to events and setting default properties.

namespace XF.CaliburnMicro1.ViewModels
{
   using Caliburn.Micro;
   using Xamarin.Forms.Maps;
   
   public class Tab3ViewModel : Screen
   {
      internal Map MapControl
      {
         get { return _map; }
         set
         {
            if (_map != null)
            {
               // unregister events
            }

            _map = value;

            if (_map != null)
            {
               // wire-up events
            }
         }
      }

      protected override void OnViewAttached(object view, object context)
      {
         var mapView = view as IMapAware;
         if (mapView != null)
         {
            MapControl = mapView.GetMap();
         }
      }

      protected override void OnActivate()
      {
         base.OnActivate();

         CenterMap();
      }
      
      internal void CenterMap()
      {
         var mapSpan = MapSpan.FromCenterAndRadius(
            new Position(43.6532, -79.3832),
            Distance.FromKilometers(10));
         MapControl.MoveToRegion(mapSpan);
      }
      
   }
   
}

Sweet. Now we can add our Pins to the Map from the ViewModel. In my next post, we'll look at a solution to apply our Pins using MVVM so that we don't need to manipulate the Map directly.

Happy coding.

0 comments: