Xamarin.Froms: Frames, Event Handlers, and Binding Bugs
To recap, I’m writing a shopping cart app for Windows Phone, Android, and iOS. The purpose of the app is primarily to let me use Forms. Each post will build on top of the previous one.
Last time I got some basic binding setup and navigation working. Today I plan on cleaning up the views a bit so that they look “nice”. Well nicer. After that I’d like to show a list of products and let the user drill in and look at details.
Recap and Code
This is the third post in the series, you can find the rest here:
- Day 0: Getting Started (blog / code)
- Day 1: Binding and Navigation (blog / code)
- Day 2: Frames, Event Handlers, and Binding Bugs (blog / code)
The latest version of the code can always be accessed on the GitHub project page.
Cleaning up Login and Main Page
Last time I was focused on functionality. I was happy to just get the pages to do what I want. Now i want to take a little time and play with the layout a bit. First on the docket is “MainPage”. It really isn’t the main page, so I’ll rename that to “WelcomePage”. This includes the the view model as well.
With that done I want to add some space around all of the text box and button. Problem is: there’s no “margin” property on any of the controls. After a little digging, it seems that the only way to add spacing is to wrap each control in its own ContentView and set the Padding property on that. A slightly simpler approach is to use a Frame instead. It inherits directly from ContentView and has a default padding of 20. Despite the fact that this only saves me from setting one property, the fact that it’s in a Frame helps me remember why I’m wrapping the control in the first place. Let’s wait a few weeks and see if I continue using Frames.
The WelcomePage (né MainPage) now looks like this:
<?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="ShoppingCart.Views.WelcomePage"
xmlns:local="clr-namespace:ShoppingCart;assembly=ShoppingCart"
BindingContext="{x:Static local:App.WelcomeViewModel}">
<StackLayout
VerticalOptions="Center">
<Frame>
<Label Text="Welcome to The Store" Font="Bold, Large" HorizontalOptions="Center" />
</Frame>
<Label Text="Login to start shopping" HorizontalOptions="Center" />
<Frame>
<Button Text ="Log In" Command="{Binding GoToLoginPageCommand}" HorizontalOptions="Center" />
</Frame>
</StackLayout>
</ContentPage>
I also tweaked the welcome text making it bigger and bold as well as adding a call to action to help the user navigate to their next step. There’s a fair bit you can do with the Font (size, style) property just by providing a comma separated list of values.
The login page got the same spacing treatment, including a nice fat margin around the entire page just so the text boxes don’t sit flush against the right side. It’s still a little stark so I’ll throw in a touch of color on the title text, just because I can.
<StackLayout VerticalOptions="FillAndExpand" Padding="50">
<Frame Padding="75">
<Label Text="Login"
Font="Bold,Large"
TextColor="Yellow"
HorizontalOptions="Center" />
</Frame>
<Label Text="User name"
HorizontalOptions="Start" />
<Entry Text ="{Binding Username}"
Placeholder ="User name goes here"
HorizontalOptions="FillAndExpand" />
<Label Text="Password"
HorizontalOptions="Start" />
<Entry Text ="{Binding Password}"
Placeholder ="Password goes here"
HorizontalOptions="FillAndExpand" />
<Button Text ="Log In"
Command="{Binding LoginCommand}"
HorizontalOptions="CenterAndExpand" />
</StackLayout>
Now that looks a little bit nicer.
Data
Now that the two pages I have look reasonable, I’ll add another. In order to show some data, I actually need data. To keep it simple, I start off by creating a list of hard coded C# data. I have to admit that I got a bit silly here. At first I tried to hand craft a back log of data. That got old really fast. In fact I only got one item defined before I realized I was wasting a lot of time. Next I decided to grab a list of products (books) from the web and just tweak the data to my needs. This too proved onerous. Then I broke down and went to the web to generate all of my data. I found a great site that even outputs the data in JSON. It was the first hit on Google. To process the JSON i nuget and install Newtonsoft’s Json.NET.
I add a ProductLoader and a ProductService class. The loader simply stores the literal string of JSON and deserializes it on request. In the future I want to create another implementation that reads the data from disk or the web. The ProductService doesn’t care where the data comes from, it provides the view models with an interface to query and filter the data. Because the underlying data will eventually come from a web request, both of these services asynchronously return Tasks. I use Stephen Cleary’s NotifyTaskCompletion in my view model to consume these services. For a detailed explanation of what’s going on here, take a look at his Patterns for Asynchronous MVVM Applications series.
The data object itself is pretty simple.
public class Item
{
public string Category { get; set; }
public string Description { get; set; }
public string ImageUrl { get; set; }
public string Name { get; set; }
public double Price { get; set; }
public string ProductCode { get; set; }
public int Rating { get; set; }
public List<string> Tags { get; set; }
}
The Category property lets us show a short list to the user once they log in. Once they pick a category they see all of the items in that category and then drill down into a specific item. To accommodate this flow, I’ll add three more pages with corresponding view models:
- CategoriesListPage/ViewModel
- ProductsListPage/ViewModel
- ProductPage/ViewModel
With a bigger app I’d lean towards single instances of each of these pages and using a message broker to update one from the other. I..e, when a category is clicked on in the CategoriesList page I’d send an “Update List of Products” message and then navigate to the ProductsPage. But since I already have the convenient App.cs handling all of my interactions between pages, I’ll just squash it into there. Not ideal for a larger app that I’d like to keep decoupled, but fine for the five pages I currently have.
Lists and DataTemplates
The first thing to tackle is to show the list of categories. This is similar to traditional two step process in Windows XAML. Step one: bind the ItemsSource property to the list. A quick reminder that the Categories property is a [NotifyTaskCompletion<List
<ListView ItemsSource="{Binding Categories.Result}" ItemSelected="OnItemSelected">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding .}" BackgroundColor = "Red" YAlign="Center" Font="Medium" />
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Handling Events
You’ll notice in the above definition of my ListView that I’m setting the ItemsSelected event handler. I’m not binding against the view model here, I’m calling into code behind which then calls into my view model.
public partial class CategoriesListPage
{
public CategoriesListPage()
{
InitializeComponent();
}
private void OnItemSelected(object sender, SelectedItemChangedEventArgs e)
{
var param = e.SelectedItem as string;
var command = ((CategoriesListViewModel)BindingContext).NavigateToCategory;
if (command.CanExecute(param))
{
command.Execute(param);
}
}
}
This is a lot of boilerplate code for handling a click. The problem is that the ListView doesn’t expose a command I can bind against when an item is selected. In a traditional Xaml app I’d use the EventToCommand pattern except that it is built on Blend behaviors which aren’t PCL compatible and therefore not Xamarin compatible.
Another option was to subclass the ListView class and expose the command logic that I need. I might eventually go this route, but I’ll probably need more than a few list boxes in my app to justify it.
Binding Bug in XF
The command for navigating to a category’s page gets a list of all the items for that category and calls into the static App class to get the page to navigate to.
var page = App.GetProductsListPage(items, categoryName);
await _navi.PushAsync(page);
Originally App.cs just updated the properties on the view model and returned the single instance of the ProductsListPage.
public static Page ProductsListPage { get; private set; }
public static Page GetProductsListPage(List<Item> items, string title)
{
if (string.IsNullOrWhiteSpace(title)) title = "Products";
ProductsListViewModel.Products = items;
ProductsListViewModel.Title = title;
return ProductsListPage;
}
This relies on the binding to update the view when something has changed and INotifyProperty.PropertyChanged is raised. The first time this is called it works just fine. It fails on all subsequent calls. After a lot of debugging and assuming that I was wrong I found a recent post on Xamarin’s forums explaining that there is a bug where the UI is not updated when a bound value is changed. Note that this only effects values that are updated in the view model updating on the view; the other way works just fine. Updating a value in the view (like a text entry) correctly updates the bound value in the view model. This is why my login page worked just fine.
Xamarin has released a fix for this bug, but as of writing this it is in a pre build of XF. I tried to use it but kept getting runtime DLL errors. I tried several times before having to give up on this as an immediate solution. I will say that this may have just been an issue with user error since it was close to 1 AM at this point.
In my specific case, there was no reason that I had to reuse the same view. Simply recreating the view every time I wanted to display it was simple enough.
public static Page GetProductsListPage(List<Item> items, string title)
{
if (string.IsNullOrWhiteSpace(title)) title = "Products";
ProductsListViewModel.Products = items;
ProductsListViewModel.Title = title;
}
Summary
Today I was able to clean up some of the views using the Frame control to provide margins. I had to resort to two workarounds, one for routing events to commands in code behind the other for updating the view when a bound value has changed.
I’m not sure what I want to tackle next week. Perhaps reading from the file stream? Maybe fleshing out the products view a bit so I display an image to go with the product?
Until then, happy coding.
this post was originally on the MasterDevs Blog