Monday, March 13, 2017

Applying MVVM to Xamarin.Form’s TabbedPage (Updated)

My last post showed how to setup a TabbedPage with separate View/ViewModels for each tab using Caliburn.Micro. The post used a DataTemplateSelector to resolve the tab content which isn’t the preferred technique when working with Caliburn.Micro. Today we’ll use a strategy that is more aligned to Caliburn’s philosophy and is a tiny bit more extensible.

As per the previous post, we used a DataTemplateSelector because the default TabbedPage.ItemTemplate expects a ContentPage and we wanted to use existing ContentPage items as tabs. In reality, this was a situation that occurred because our team had originally developed these pages independently and then wanted to consolidate them into a TabbedPage after the fact. If you don’t plan on navigating to these pages outside of the TabbedPage, we can build up our ContentPage inside the DataTemplate and use Caliburn.Micro’s binding syntax. This approach will require us to change our existing ContentPage(s) into ContentView(s) and since we depended on the Page objects to provide us with Title and Icon information, we’ll need to push this information into our ViewModels.

Modify ViewModels

Let’s introduce a simple interface for our ViewModels that will be tabs in our TabbedPage:

public interface ITabViewModel
{
   string Title { get; }
   string Icon { get; }
   int SortOrder { get; }
}

The changes to the ViewModel are pretty trivial. We simply implement the ITabViewModel interface:

public class Tab1ViewModel : Screen, ITabViewModel
{
   public Tab1ViewModel()
   {
      Tab1Content = "Tab 1 Content";
   }

   public string Tab1Content { get; set; }

   public string Icon => "Tab1.png";

   public int SortOrder => 0;

   public string Title => "Tab1";
}

public class Tab2ViewModel : Screen, ITabViewModel
{
   public Tab2ViewModel()
   {
      Tab2Content = "Tab 2 Content";
   }

   public string Tab2Content { get; set; }

   public string Icon => "Tab2.png";

   public int SortOrder => 0;

   public string Title => "Tab 2";
}

Next, we register our ViewModels in the App using the ITabViewModel signature:

public class App : FormsApplication
{
   private readonly SimpleContainer container;

   public App(SimpleContainer container)
   {
      this.container = container;

      // TODO: Register additional viewmodels and services
      container
         .PerRequest<Main2ViewModel>()
         .PerRequest<ITabViewModel,Tab1ViewModel>()
         .PerRequest<ITabViewModel,Tab2ViewModel>()
         ;

      Initialize();

      DisplayRootViewFor<Main2ViewModel>();
   }
	
   // snip...
}

Lastly, we can change our Screen Conductor to be less coupled to the specific ViewModels:

public class Main2ViewModel : Conductor<Screen>.Collection.OneActive
{
   public Main2ViewModel(IEnumerable<ITabViewModel> tabs)
   {
      if (tabs.Any())
      {
         foreach(var tab in tabs.OrderBy(i => i.SortOrder))
         {
            Items.Add((Screen)tab);
         }

         ActivateItem(Items[0]);
      }
   }
}

Modify Views

The changes to the view are also very trivial. We’re simply changing them from ContentPage to ContentView. There are a few attributes that aren’t available (Title, Icon) so we’ll simply remove them if present.

And then finally, we can remove our DataTemplateSelector and use the View.Model attached property to resolve our ContentView:

Main2View.xaml
<?xml version="1.0" encoding="UTF-8"?>
<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms" 
            xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
            xmlns:cm="clr-namespace:Caliburn.Micro.Xamarin.Forms;assembly=Caliburn.Micro.Platform.Xamarin.Forms"
            x:Class="XF.CaliburnMicro1.Views.Main2View"
            ItemsSource="{Binding Items}"
            SelectedItem="{Binding SelectedItem}"
            >
    <TabbedPage.ItemTemplate>
        <DataTemplate>
            <ContentPage Title="{Binding Title}" Icon="{Binding Icon}">
                <ContentView cm:View.Model="{Binding}" />
            </ContentPage>
        </DataTemplate>
    </TabbedPage.ItemTemplate>
</TabbedPage>

Easy peasy.

Happy coding.

submit to reddit

Sunday, March 05, 2017

Applying MVVM to Xamarin.Forms's TabbedPage

For this post I thought we'd dig into an example that came up recently with Caliburn.Micro and the TabbedPage view, specifically how to apply MVVM using Caliburn.Micro.

The TabbedPage view that ships with Xamarin.Forms is a great cross-platform page structure that presents sets of content in different tabs. The layout is surprisingly very similar between iOS, Android and Windows 10. The biggest layout difference is seen in iOS where each tab optionally has an icon.

xamarinforms_tabbedpage

The above image, taken without permission from Xamarin’s documentation, shows iOS, Android and Windows 8.1 Phone. The Windows 10 version is closer to the Android version.

When looking at Xamarin’s documentation, most examples show a series of Pages defined as inline XAML, and applying an ItemTemplate assumes that each tab will have the same layout; there doesn’t seem to be a great way to swap in different pages per tab. Regardless of these short-comings, the most important point regarding these examples is that the TabbedPage view displays the the Title and Icon from the contained Page in the tab headers.

Setting up the ViewModel

The first step in setting up a TabbedPage view is to represent it as its own ViewModel. Caliburn.Micro offers a unique solution to this problem using a pattern it refers to as a Screen Conductor. To understand how this pattern works, Caliburn.Micro treats pages of your application as Screens and the base Screen class contains abstractions for the view lifecycle: OnInitialize, OnActivated, OnDeactivated. These methods, respectively, make it really easy to defer work that shouldn’t be in the constructor, to ensure the view always has the latest data, and to clean-up or prevent navigating away without saving changes. With regards to the TabbedPage, the ScreenConductor provides a simple mechanism to activate and deactivate ViewModels as you navigate between tabs.

Here we define our ScreenConductor as our Main2ViewModel, and Tab1ViewModel and Tab2ViewModel as the contained tabs.

namespace XF.CaliburnMicro1.ViewModels
{
    using Caliburn.Micro;

    public class Main2ViewModel : Conductor<Screen>.Collection.OneActive
    {
        public Main2ViewModel(Tab1ViewModel tab1, Tab2ViewModel tab2)
        {
            Items.Add(tab1);
            Items.Add(tab2);

            ActivateItem(Items[0]);
        }
    }

    public class Tab1ViewModel : Screen
    {
        public Tab1ViewModel()
        {
            DisplayName = "Tab 1";
            Tab1Content = "Tab 1 Content";
        }

        public string Tab1Content { get; set; }
    }

    public class Tab2ViewModel : Screen
    {
        public Tab2ViewModel()
        {
            DisplayName = "Tab 2";
            Tab2Content = "Tab 2 Content";
        }

        public string Tab2Content { get; set; }
    }
}

For completeness sake, these new ViewModels are registered in the App class (defined in my previous post):

public class App : FormsApplication
{
   private readonly SimpleContainer container;

   public App(SimpleContainer container)
   {
       this.container = container;

       // TODO: Register additional viewmodels and services
       container
          .PerRequest<Main2ViewModel>()
          .PerRequest<Tab1ViewModel>()
          .PerRequest<Tab2ViewModel>()
          ;

       Initialize();

       DisplayRootViewFor<Main2ViewModel>();
    }

    // snip...
}

The XAML definitions for these views are represented as a TabbedPage and two instances of ContentPage:

Main2View.xaml
<?xml version="1.0" encoding="UTF-8"?>
<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms" 
            xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
            xmlns:converters="clr-namespace:XF.CaliburnMicro1.Converters"
            x:Class="XF.CaliburnMicro1.Views.Main2View"
            ItemsSource="{Binding Items}"
            SelectedItem="{Binding SelectedItem}"
            >

</TabbedPage>
Tab1View.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XF.CaliburnMicro1.Views.Tab1View"
             Title="{Binding DisplayName}">
  <Label Text="{Binding Tab1Content}" VerticalOptions="Center" HorizontalOptions="Center" />
</ContentPage>
Tab2View.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XF.CaliburnMicro1.Views.Tab2View"
             Title="{Binding DisplayName}">
  <Label Text="{Binding Tab2Content}" VerticalOptions="Center" HorizontalOptions="Center" />
</ContentPage>

Fixing View / ViewModels for Tabs

If you run the solution as is, you’ll be disappointed. The reason for this is that while Caliburn.Micro can find the View/ViewModel for our MainPage, it doesn’t know how to resolve the View/ViewModels for the tabs. Now the solution I’m going to use leverages a DataTemplateSelector which isn’t something that is traditionally done with Caliburn.Micro. The correct approach with Caliburn.Micro is to take advantage of conventions and special attached properties (eg View.Model). However in this case, the approach I’m using allows you to use your ContentPage inside the TabbedPage or as a standalone page that you can navigate to directly. I’ll cover the other approach in an upcoming post.

We’ll define a DataTemplateSelector that can do the work of finding the View for our ViewModel:

namespace XF.CaliburnMicro1.Converters
{
    using System;
    using System.Collections.Generic;
    using Caliburn.Micro;
    using Caliburn.Micro.Xamarin.Forms;
    using Xamarin.Forms;    

    public class TabbedPageDataTemplateSelector : DataTemplateSelector
    {
        private readonly Dictionary<Type, DataTemplate> _selectors;

        public TabbedPageDataTemplateSelector()
        {
            _selectors = new Dictionary<Type, DataTemplate>();
        }

        protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
        {
            Type viewModelType = item.GetType();

            DataTemplate template = null;

            // check if we've already found the datatemplate for this view
            if (!_selectors.TryGetValue(viewModelType, out template))
            {
                // use caliburn to find the View for this viewmodel
                Type viewType = ViewLocator.LocateTypeForModelType(viewModelType, null, null);

                template = new DataTemplate(() =>
                {
                    var view = Activator.CreateInstance(viewType);

                    var bindableObject = view as BindableObject;

                    if (bindableObject != null)
                    {
                        // when the view's content changes...
                        bindableObject.BindingContextChanged += (sender, args) =>
                        {
                            var page = sender as Page;

                            // leverage a caliburn view lifecyle event
                            // if the viewmodel supports it
                            var viewAware = page?.BindingContext as IViewAware;
                            viewAware?.AttachView(page);
                        };
                    }

                    return view;
                });

                // cache the datatemplate
                _selectors.Add(viewModelType, template);
            }

            // return the correct view for the viewmodel
            return template;
        }
    }
}

Then associate into the view:

<?xml version="1.0" encoding="UTF-8"?>
<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms" 
            xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
            xmlns:converters="clr-namespace:XF.CaliburnMicro1.Converters"
            x:Class="XF.CaliburnMicro1.Views.Main2View"
            ItemsSource="{Binding Items}"
            SelectedItem="{Binding SelectedItem}"
            >
    <TabbedPage.ItemTemplate>
        <converters:TabbedPageDataTemplateSelector />
    </TabbedPage.ItemTemplate>
</TabbedPage>

Now when we run our solution, our tabs are correctly populated.

tabbedpage-content

Happy coding!

submit to reddit