Building Contoso Cookbook with Xamarin Forms

Using NavigationPage, TabbedPage, ListView and Other Goodies in Xamarin Forms to Build Rich Multipage Apps

In my previous posts introducing developers to Xamarin Forms, I presented an RPN calculator app that runs on multiple platforms, and then built on that to describe how to respond to orientation changes in Xamarin Forms apps. A calculator provides a reasonable starting point for an exploration of Xamarin Forms, but it lacks many of the characteristics of more contemporary mobile apps. For example, it comprises a single page only, its UI consists of little more than a Label control and set of Buttons, and while it binds to a view-model, it neither binds to a traditional data source nor demonstrates how to use data templates to render data items into XAML.

In this, the third installment in the series, we’ll tackle multipage apps. In it, you’ll get a first-hand look at TabbedPage, ListView, and other other cool components available in Xamarin Forms. You’ll also learn how to bind to JSON data sources and render those data sources in a platform-independent manner.

The app featured in this article is a Xamarin Forms version of Contoso Cookbook. Contoso Cookbook is a sample app that I originally wrote for Microsoft and MSDN to get developers jazzed about Windows 8. I later updated it for Windows 8.1, and then turned it into a universal app for Windows and Windows Phone. It seemed natural to make it a real universal app by porting it to Xamarin Forms. And it presents the perfect opportunity to go deeper into Xamarin Forms by discussing navigation models, list views, scroll views, value converters, and more.

The Xamarin Forms version of Contoso Cookbook features two pages. The main page hosts a TabbedPage control that displays a collection of recipes. You can scroll sideways to see additional recipe groups (Chinese, French, German, and so on), or scroll vertically to view all the recipes within a group. The TabbedPage renders native controls on the different platforms; specifically, it generates a Pivot control on Windows Phone, a tabbed ActionBar on Android, and a UITabBar on iOS. Here’s how the main page looks on Windows Phone, Android, and iOS with Chinese recipes displayed:

MainPage2

Tapping a recipe navigates to the app’s second page, which shows recipe details. That page displays its content inside a ScrollView control so the user can scroll through the entire recipe:

RecipePage

Feel free to download the source code and run it yourself. Be aware that there are a few rough edges arising from bugs in Xamarin Forms that will most likely be fixed in subsequent releases. For example, you can’t scroll sideways in iOS to see all six recipe groups, and in Windows Phone, you can’t tap a recipe, tap the Back button, and then tap the same recipe again to return to the recipe-detail page. For the most part, however, the app just works on all three platforms and is further testament to power of using XAML to build cross-platform UIs.

Building the Main Page

Contoso Cookbook’s main page is implemented in MainPage.xaml and MainPage.xaml.cs. Here’s the former:

<?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:local="clr-namespace:XFormsContosoCookbook.Components;assembly=XFormsContosoCookbook"
            x:Class="XFormsContosoCookbook.MainPage"
            ItemsSource="{Binding RecipeGroups}"
            Title="Contoso Cookbook">

  <TabbedPage.Resources>
    <ResourceDictionary>
      <local:LocalImagePathConverter x:Key="LocalImagePathConverter" />
    </ResourceDictionary>
  </TabbedPage.Resources>

  <TabbedPage.ItemTemplate>
    <DataTemplate>
      <ContentPage Title="{Binding Title}">
        <ListView ItemsSource="{Binding Recipes}" ItemTapped="OnItemTapped">
          <ListView.RowHeight>
            <OnPlatform x:TypeArguments="x:Int32" iOS="128" Android="136" WinPhone="144" />
          </ListView.RowHeight>
          <ListView.ItemTemplate>
            <DataTemplate>
              <ViewCell>
                <Grid Padding="8">
                  <Grid.ColumnDefinitions>
                    <ColumnDefinition>
                      <ColumnDefinition.Width>
                        <OnPlatform x:TypeArguments="GridLength" iOS="120" Android="144" WinPhone="144" />
                      </ColumnDefinition.Width>
                    </ColumnDefinition>
                    <ColumnDefinition Width="*" />
                  </Grid.ColumnDefinitions>
                  <Image Source="{Binding ImagePath, Converter={StaticResource LocalImagePathConverter}}" />
                  <Grid Grid.Column="1">
                    <Grid.Padding>
                      <OnPlatform x:TypeArguments="Thickness" iOS="4,8,0,8" Android="8,2,0,4" WinPhone="12,-2,0,-4" />
                    </Grid.Padding>
                    <Label Text="{Binding Subtitle}" FontSize="Large" LineBreakMode="WordWrap" />
                  </Grid>
                </Grid>
              </ViewCell>
            </DataTemplate>
          </ListView.ItemTemplate>
        </ListView>
      </ContentPage>
    </DataTemplate>
  </TabbedPage.ItemTemplate>
</TabbedPage>

That’s a relatively small amount of XAML, but a huge bang for the buck. Each time the page loads, it retrieves a view-model from the Application if it doesn’t already have one, and assigns a view-model reference to the page’s BindingContext:

protected async override void OnAppearing()
{
    base.OnAppearing();

    if (this.BindingContext == null)
        this.BindingContext = await ((App)Application.Current).GetRecipeViewModelAsync();
}

The call is made from the OnAppearing override rather than the page constructor because GetRecipeViewModelAsync is an async (awaitable) method, and you can can’t currently call awaitable methods from constructors in C#. The first time GetRecipeViewModelAsync is called, App instantiates the view-model and loads a JSON data source describing the recipes and recipe groups. Each time it’s called thereafter, it returns a reference to the extant view-model.

The view-model class – RecipeViewModel – has a public property named RecipeGroups whose type is List<RecipeGroup>. (I used List<> rather than ObservableCollection<> because once the recipe data is loaded, it never changes.) Inside each RecipeGroup is a property named Recipes whose type is List<Recipe>. The following statement binds the TabbedPage’s ItemsSource property to the view-model’s RecipeGroups property to produce one “tab” per recipe group:

ItemsSource="{Binding RecipeGroups}"

The TabbedPage’s ItemTemplate property specifies how each recipe group should be rendered into XAML. Each group generates a ContentPage containing a ListView. The ListView’s ItemsSource property is bound to the RecipeGroup’s Recipes property to produce a list of recipes:

<ListView ItemsSource="{Binding Recipes}" ... >

The ListView’s ItemTemplate, in turn, specifies how each recipe is rendered into XAML. Each recipe becomes a ViewCell containing an Image and Label, with a StackLayout to position them side by side. Properties of the Image and Label are bound to properties of the corresponding Recipe object to produce a graphical representation of each recipe.

Building the Recipe Page

The recipe page, found in RecipePage.xaml and RecipePage.xaml.cs, is a relatively straightforward ContentPage that uses data binding to render recipe details. Here’s the 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"
             xmlns:local="clr-namespace:XFormsContosoCookbook.Components;assembly=XFormsContosoCookbook"
             x:Class="XFormsContosoCookbook.RecipePage"
             Title="{Binding Title}">

  <ContentPage.Resources>
    <ResourceDictionary>
      <local:ListConverter x:Key="ListConverter" />
      <local:LocalImagePathConverter x:Key="LocalImagePathConverter" />
      <Color x:Key="CaptionColor">#FFFF8300</Color>
    </ResourceDictionary>
  </ContentPage.Resources>

  <Grid x:Name="LayoutRoot" Padding="16">
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <Label Text="{Binding Title}" FontSize="Large" LineBreakMode="WordWrap">
      <Label.IsVisible>
        <OnPlatform x:TypeArguments="x:Boolean" iOS="False" Android="False" WinPhone="True" />
      </Label.IsVisible>
    </Label>

    <ScrollView Grid.Row="1">
      <StackLayout>
        <Image Source="{Binding ImagePath, Converter={StaticResource LocalImagePathConverter}}" Aspect="AspectFill" />
        <Label Text="{Binding Description}" FontSize="Medium" LineBreakMode="WordWrap" />
        <StackLayout Orientation="Horizontal" Padding="0,16,0,0">
          <Label Text="Preparation time:" FontSize="Medium" FontAttributes="Italic" />
          <Label Text="{Binding PrepTime}" FontSize="Medium" FontAttributes="Italic" />
          <Label Text="minutes" FontSize="Medium" FontAttributes="Italic" />
        </StackLayout>
        <StackLayout Padding="0,16,0,0">
          <Label FontSize="Large" TextColor="{StaticResource CaptionColor}" Text="Ingredients" />
          <Label FontSize="Small" Text="{Binding Ingredients, Converter={StaticResource ListConverter}}" LineBreakMode="WordWrap" />
        </StackLayout>
        <StackLayout Padding="0,16,0,0">
          <Label FontSize="Large" TextColor="{StaticResource CaptionColor}" Text="Directions" />
          <Label FontSize="Small" Text="{Binding Directions}" LineBreakMode="WordWrap" />
        </StackLayout>
      </StackLayout>
    </ScrollView>
  </Grid>
</ContentPage>

Upon loading, RecipePage retrieves a view-model – the same view-model instance used by MainPage – from the Application and initializes the page’s BindingContext. Rather than assign the view-model to BindingContext, however, it assigns the view-model’s Recipe property to BindingContext:

protected async override void OnAppearing()
{
    base.OnAppearing();
    this.BindingContext = (await ((App)Application.Current).GetRecipeViewModelAsync()).Recipe;
}

The Recipe property holds a reference to the Recipe object corresponding to the recipe that was tapped on the main page. (More on that, and how you navigate to the recipe page in the first place, in the next section.) Thus, the bindings you see in the recipe-page XAML refer to properties of the currently selected Recipe object: ImagePath, Description, PrepTime, and so on.

Since it’s virtually guaranteed that the entire recipe won’t fit on the screen of your smart phone (I say “virtually” because it’s only a matter of time until someone produces a phone with a 12” screen), RecipePage uses a Xamarin Forms ScrollView control to present most of the recipe content. That’s why you can scroll up or down as needed to view an entire recipe. Microsoft XAML developers are familiar with the ScrollViewer control, which serves the same purpose as ScrollView in Xamarin Forms.

You may have noticed that RecipePage uses a Label to show the recipe title, but that it hides the label on iOS and Android:

<Label Text="{Binding Title}" FontSize="Large" LineBreakMode="WordWrap">
  <Label.IsVisible>
    <OnPlatform x:TypeArguments="x:Boolean" iOS="False" Android="False" WinPhone="True" />
  </Label.IsVisible>
</Label>

That’s because the recipe title is shown automatically on iOS and Android, courtesy of the NavigationPage that wraps MainPage and RecipePage. What is NavigationPage? I’m glad you asked.

Navigating to the Recipe Page

To facilitate navigation between pages in a multipage app – something that tends to work very differently on different platforms – Xamarin Forms offers a handy abstraction in the form of the NavigationPage class. When a multipage app starts up, you new up a NavigationPage object and assign it to Application’s MainPage property. And you pass a reference to the app’s main page – the page you want to appear initially – to the NavigationPage constructor. Here’s the relevant code in Contoso Cookbook’s App.cs in the Portable project:

public App()
{
    this.MainPage = new NavigationPage(new MainPage());
}

If you later wish to navigate to another page, you call the awaitable PushAsync method through your page’s Navigation property, passing in a reference to the page you want to navigate to. Here’s the relevant code in MainPage.xaml.cs – specifically, the handler for the ItemTapped events that fire when someone taps an item in a ListView control:

private async void OnItemTapped(object sender, ItemTappedEventArgs args)
{
    ((RecipeViewModel)this.BindingContext).Recipe = (Recipe)args.Item;
    await this.Navigation.PushAsync(new RecipePage());
}

Xamarin obligingly displays the new page and updates the navigation stack to remember the page you navigated from. Clicking the Back button pops the previous page off the stack and takes you back to it. Incidentally, you can see in this code snippet how the view-model’s Recipe property gets initialized to refer to the recipe that was tapped. It’s done inside MainPage’s ItemTapped event handler before RecipePage is navigated to.

Most Windows phones have hardware Back buttons (those that do not, like the Lumia 635, have software Back buttons provided by the OS), so NavigationPage doesn’t render any UI of its own on Windows Phone. But on iOS and Android, it places an “action bar” at the top of the page containing a Back button and a page title. THAT’s why I hid the recipe title at the top of RecipePage on iOS and Android; rather than use a Label to show the recipe title, I assigned the recipe title to the page itself:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XFormsContosoCookbook.Components;assembly=XFormsContosoCookbook"
             x:Class="XFormsContosoCookbook.RecipePage"
             Title="{Binding Title}">

On iOS and Android, Xamarin displays the title in the action bar. No page title is shown on Windows Phone, so I used a Label to show the recipe title instead.

Text Sizes, Image Resources, Value Converters, and More

That’s the big picture. But there are numerous other lessons aspiring Xamarin Forms developers – especially those who are accustomed to slinging XAML on Microsoft platforms – can take away from Contoso Cookbook.

The first lesson regards text sizes. The Xamarin Web site contains an excellent document titled “Working with Fonts” dealing with text sizes, and Charles Petzold has recently published a chapter that addresses text sizes and much more. Rather than hard-code the font size in the various Labels I used in Contoso Cookbook, I took advantage of an enumerated type named NamedSize. NamedSize has four members:

  • Micro
  • Small
  • Medium
  • Large

Using these predefined values allows text sizes to be reasonably consistent across platforms as well as on screens of differing resolutions.

Lesson #2 regards images in Xamarin Forms apps – specifically, packaging images in PCLs and using those images at run-time. I wanted to make sure Contoso Cookbook worked even if I currently lacked an Internet connection. (As a presenter, I’ve learned the hard way that you’ll sometimes find yourself presenting in places that are connectivity-challenged.) So all the resources the app uses – the JSON data that defines the recipes and recipe groups, as well as the images depicting the various recipes – are packaged as embedded resources in the PCL. But that posed an interesting problem: how do you display an embedded-resource image in a Xamarin Image control? Believe it or not, it’s not very straightforward. In fact, you have to write some code to create an ImageSource object from a resource ID and assign the ImageSource to the Image’s Source property.

Rather than do that in the code-behind, I elected to do it in the XAML by utilizing a value converter. Value converters play an important role in just about every XAML UI. Value converters plug into Binding objects and allow you to see – and even change – data moving back and forth between a data source and a data target. They’re typically used to convert data from one format to another. A value converter is an instance of a class that implements an interface named IValueConverter. (This interface exists in Microsoft XAML and Xamarin XAML; it’s defined slightly differently on the two platforms, but the differences amount to no more than a couple of parameters passed to the interface’s methods.) IValueConverter has two methods: Convert and ConvertBack. Convert is called when data moves from the source to the target, and ConvertBack is called when (if) data moves from the target to the source. The latter only occurs in two-way data-binding scenarios.

Contoso Cookbook uses two value converters. The first one – LocalImagePathConverter – converts an image URL of the form “images/chinese/noodles.jpg” into a resource ID of the form “AssemblyName.images.chinese.noodles.jpg” and wraps it in an ImageSource object. Here’s the code for the converter:

class LocalImagePathConverter : IValueConverter
{
    private static string _assembly;

    static LocalImagePathConverter()
    {
        // Store assembly name (e.g. "XFormsContosoCookbook") with a trailing period
        _assembly = typeof(LocalImagePathConverter).AssemblyQualifiedName.Split(',')[1].Trim() + '.';
    }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        // Convert an image-path string (e.g. "images/chinese/photo.jpg" into a resource ID
        // (e.g. "XFormsContosoCookbook.images.chinese.photo.jpg") and return an ImageSource
        // wrapping that resource
        string source = _assembly + ((string)value).Replace('/', '.');
        return ImageSource.FromResource(source);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

And here’s the XAML in MainPage.xaml that declares an instance of LocalImagePathConverter as a page resource and uses it with an Image control:

<TabbedPage.Resources>
  <ResourceDictionary>
    <local:LocalImagePathConverter x:Key="LocalImagePathConverter" />
  </ResourceDictionary>
</TabbedPage.Resources>
    .
    .
    .
<Image Source="{Binding ImagePath, Converter={StaticResource LocalImagePathConverter}}" ... />

The second value converter – ListConverter – is used by RecipePage to convert an array of strings representing recipe ingredients into a single string with embedded carriage returns and line feeds that can be assigned to the Text property of a Label. It’s the secret to how RecipePage displays a list of ingredients. You’ll find the source code for ListConverter in the Portable project’s Components folder if you care to see how it works. Here’s the line in RecipePage.xaml that uses it:

<Label FontSize="Small" Text="{Binding Ingredients, Converter={StaticResource ListConverter}}" LineBreakMode="WordWrap" />

Another challenge I encountered in Contoso Cookbook was how to parse the JSON string I embedded in the PCL as a resource. Here’s the code in RecipeViewModel’s LoadRecipesAsync method that retries the JSON resource from the PCL and reads it into a text string:

// Read RecipeData.json from this PCL's DataModel folder
var name = typeof(RecipeViewModel).AssemblyQualifiedName.Split(',')[1].Trim();
var assembly = Assembly.Load(new AssemblyName(name));
var stream = assembly.GetManifestResourceStream(name + ".DataModel.RecipeData.json");

// Parse the JSON and generate a collection of RecipeGroup objects
using (var reader = new StreamReader(stream))
{
    string json = await reader.ReadToEndAsync();
      ...
}

The text string itself is huge: it comprises a nested arrays of JSON objects denoting recipes and recipe groups. But once you extract it from the PCL, how do you consume it? The types in .NET’s System.Json namespace aren’t available in a PCL, so you must resort to other means.

The “other means” I chose is the awesome (and free) Json.NET library from Newtonsoft. It literally reduced what would have been 30 or 40 lines of code using .NET to two simple lines:

var obj = new { Groups = new List<RecipeGroup>() };
var groups = JsonConvert.DeserializeAnonymousType(json, obj);

I grabbed the latest version of Json.NET from NuGet and plugged it right into the Portable project. It worked right out of the box. Now that’s value!

A final point regarding the design and implementation of Contoso Cookbook has to do with the ItemTapped event handler on the ListView control declared in the TabbedPage’s item template in MainPage.xaml:

<ListView ItemsSource="{Binding Recipes}" RowHeight="128" ItemTapped="OnItemTapped">

Normally, you don’t see events being wired to event handlers in code-behind in an MVVM app, and Contoso Cookbook is an MVVM app. But ListView lacks a Command property, so I either needed to handle the event in the code-behind or use an extension of some type to add commanding support to ListView.

A classic way to add commanding to controls that don’t support it out of the box is to write a behavior. Without going too deeply into behaviors (I’ll do that in a subsequent article), know that Xamarin Forms do support custom behaviors. So I wrote a custom behavior called EventToCommandBehavior and used it in the XAML like this:

<ListView ItemsSource="{Binding Recipes}" RowHeight="128">
  <ListView.Behaviors>
    <local:EventToCommandBehavior EventName="ItemTapped" Command="{Binding ItemTappedCommand}" />
  </ListView.Behaviors>
  <ListView.ItemTemplate>
    ...
  </ListView.ItemTemplate>
</ListView>

But it didn’t work, for the simple reason that on the ListView, BindingContext no longer pointed to the view-model containing the ItemTappedCommand property. This can be solved with yet another custom widget – this time, a custom markup extension – but I quickly realized that I was writing a ton of code simply to avoid embedding a simple event handler in the code-behind. And it didn’t feel right to do that.

MVVM purists will criticize me for this, but when it comes to writing code, I have always been a pragmatist. And when commanding isn’t pragmatic, then maybe you shouldn’t do it. So I wired ItemTapped events fired by ListView to a simple handler in MainPage.xaml.cs:

private async void OnItemTapped(object sender, ItemTappedEventArgs args)
{
    ((RecipeViewModel)this.BindingContext).Recipe = (Recipe)args.Item;
    await this.Navigation.PushAsync(new RecipePage());
}

That handler, which has already been referenced once in this article, initializes the view-model’s Recipe property with a reference to the recipe that was tapped, and then calls Navigation.PushAsync to navigate to the recipe-detail page. Simple, sweet, and direct – the best kind of code there is.

By the way, the source code for EventToCommandBehavior is still sitting in the Portable project’s Components folder. I’m sure I’ll use it in the future, and I intend to enhance it with new features, too. But in this instance, it simply wasn’t the right tool for the job.

Summary

Contoso Cookbook is more ambitious than the calculator app I presented previously, and it’s more representative of real-world apps, too. With Contoso Cookbook, you get to see NavigationPage, TabbedPage, ContentPage, ListView, ScrollView and other Xamarin Forms elements in action. You also get to see how they work together. Stay tuned for more articles on Xamarin Forms and more sample code highlighting key aspects of the programming model. We’ve accomplished a lot already, but there is much more to do.

Xamarin-Partner_thumb1

Need Xamarin Help?

Xamarin Consulting  Xamarin Training

Real time, continuous system and application monitoring for informed incident and problem management. Historical data monitoring for efficient capacity management. Choose Atmosera Monitoring Services to eliminate on-call worries. Atmosera can monitor your applications and/or hosting infrastructure, including:
• Network
• Physical systems
• Hypervisors
• Virtual systems
• Applications

A “single pane of glass” provides a holistic view — including colocation, cloud solutions, and equipment on client premises.
All technology teams can use the same data

Atmosera’s 24x7x365 Command Center staff validates, correlates and responds. Integrated with our ticketing system, incidents automatically open trouble tickets.
• Provides clear issue tracking over time
• Eliminates false positive pages in the middle of the night

Our vendor-agnostic monitoring capabilities supports a wide range of systems and vendors, allowing us to monitor almost anything our customers require.
• Monitor devices located on premises and/or in Atmosera’s facilities
• Continuously monitor performance metrics to ensure optimal network and equipment health
• Facilitate better planning and utilization for network upgrades or equipment reassignment
• Boost problem resolution with centralized control and drill-down capabilities

Stay Informed

Sign up for the latest blogs, events, and insights.

We deliver solutions that accelerate the value of Azure.
Ready to experience the full power of Microsoft Azure?

Atmosera is thrilled to announce that we have been named GitHub AI Partner of the Year.

X