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

0 comments: