Xamarin.Froms: Images in Lists

comments edit

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 worked through some bugs in XF’s binding and cleaned up the layout a bit. Today the plan is to add product photos to make the list of products and the product pages look prettier.

Recap and Code

This is the fourth 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)
  • Day 3: Images in Lists (blog / code)

The latest version of the code can always be accessed on the GitHub project page.

Bugs fixed

Last week I ran into a known bug where XF wasn’t properly responding to IPropertyChaged events. At the time there was a fix in a beta build of Xamarin.Forms but I had trouble getting the beta dlls to run in my app. Since then the fix has been officially released. So, I updated all my nuget packages; lo and behold binding now works as expected.

Long story short: Xamarin fixed the bug quickly and correctly. Good for them.

Cleaning the Data

A little nitpicky, but while playing with my app I noticed that the product names don’t look like product names. They are all lower cases and have odd punctuation. I wanted to clean this up quickly so I grabbed Humanizer and added some post processing on the client after loading the products.

item.Name = item.Name
                .Titleize()
                .Replace(".", string.Empty);
item.Description = item.Description
                       .Humanize(LetterCasing.Sentence);

Now, if I want to show “product pictures” I’ll need to get those photos from somewhere. From an unrelated undertaking I had a list of 300+ photos from Imgur. Imgur also has a nice feature where you can request the image as an icon or full size. My data lives in a single string of json which is deserialized at startup. I want to be lazy and not update each item in that list manually so I’m taking a shortcut. I added the Imgur ids to a queue and then in my post-process of the item, I add the icon and image urls. Note the “s” at the end of the IconUrl. That’s Imgur’s way to specify a “small square” icon.

private static void UpdateItem(Product item, string imgurId)
{
    item.ImageUrl = string.Format("http://i.imgur.com/{0}.jpg", imgurId);
    item.IconUrl = string.Format("http://i.imgur.com/{0}s.jpg", imgurId);
}

Now that I have all my data, I’ll start displaying the photos.

Inserting Images

I’m going to start with adding the image to the product page. it’s just one image on the page so it should be pretty simple.

<ScrollView>
  <StackLayout VerticalOptions="FillAndExpand" Padding="50">
 
    <Label Text="{Binding Product.Name}"
      HorizontalOptions="Center"
      Font="Bold,Large" />
 
    <Image Aspect="AspectFill"
            HorizontalOptions="FillAndExpand"
            Source="{Binding Product.ImageUrl}" />
 
    <Label Text="{Binding Product.Description}"
      HorizontalOptions="Start"
      Font="Medium" />
 
    <Label Text="{Binding Product.Price, StringFormat='{0:c}'}"
      HorizontalOptions="Start"
      Font="Bold,Medium" />
 
  </StackLayout>
</ScrollView>

All I needed to do was add an Image element and bind the source to the product’s ImagUrl property. The image will automatically be downloaded and cached for me. According to the documentation the cache lasts one day by default but is configurable. You can even turn the cache off.

This worked pretty well out of the box, everything loaded up very quickly. The only caveat is that some of my pictures are fairly long so the description and text are pushed off the page. Wrapping the entire StackLayout in a ScrollView allows me to scroll down and see the rest of my content.

Moving to the list view, all I do was bind against the IconUrl in the Product model class. Remember, this is the smaller thumbnail provided to us by Imgur.

<ListView ItemsSource="{Binding Products}" ItemSelected="OnItemSelected">
  <ListView.ItemTemplate>
    <DataTemplate>
      <ViewCell>
        <StackLayout VerticalOptions="FillAndExpand" Orientation="Horizontal" Padding="10">
 
          <Image Aspect="AspectFill"
                 HeightRequest ="75" WidthRequest="75"
                 Source="{Binding IconUrl}" />
 
          <Label Text="{Binding Product.Name}" YAlign="Center" Font="Large" />
        </StackLayout>
      </ViewCell>
    </DataTemplate>
  </ListView.ItemTemplate>
</ListView>

I know I want the images to all be the same size so I set the width and height to be 75 pixels. The Aspect property will do its best to show as much of the image as possible, clipping it where needed. As you can see, I also needed to add a horizontal StackLayout because the data template now has two items in it. I’ve also upped the text size from before, it was hard to click an item in the list because it was too small.

Here’s the result:

It doesn’t look too bad, but there is a problem: it takes a long time to load. Since XF synchronously loads the images, the app is completely blank and unresponsive for 2 to 5 seconds.

Researching the Slow

The app hanging when loading a product list is unacceptable. So the first thing I did to counteract this was to take the network out of the equation. I added a loading image as a resource to the app. Since I wanted the same image on all platforms, I added it directly to the PCL project as an EmbeddedResource.

Just for testing, I modified my model to expose this image instead of the image from the web and bound my view to that instead.

public ImageSource IconSource
{
    get
    {
        string resource = "ShoppingCart.Resouces.placeholderImageSmall.png";
        return ImageSource.FromResource(resource);
    }
}
<Image Aspect="AspectFill"
       HeightRequest ="75" WidthRequest="75"
       Source="{Binding IconSource}" />

The resource path is separated by dots and includes the assembly name as well as any sub folders. In my example above, the file “placeholderImageSmall.png” is in the “Resources” folder (which I created) in the “ShoppingCart” project.

With this change, the page loads instantly, but doesn’t look quite as nice since every product is using the same placeholder image.

Since XF doesn’t do the load asynchronously, I need to figure out how to display the loading icon while the image is downloaded from the web in the background.

If I was in a pure WPF world, I’d use Priority Binding. This lets you set a list of locations to get the binding value from, marking some as asynchronous. Sadly, this is not supported in XF, so that’s right out.

So this means I need to roll it by hand.

Speeding Things Up

I already have a class that manages binding to the result of a task. I used this last time for my product list which is (theoretically) coming from a slow source. It is Stephen Cleary’s NotifyTaskCompletion. This is a good start, but it returns null as the result until the task completes. In my case I want it to return a default value (my place holder image). I tweak the constructor and the result property just a touch to get the implementation I want.

private readonly TResult _defaultResult;
 
public NotifyTaskCompletion(Task<TResult> task, TResult defaultResult = default(TResult))
{
    _defaultResult = defaultResult;
    Task = task;
 
    if (!task.IsCompleted)
    {
        var _ = WatchTaskAsync(task);
    }
}
 
public TResult Result
{
    get
    {
        return (Task.Status == TaskStatus.RanToCompletion)
          ? Task.Result
          : _defaultResult;
    }
}

The constructor now takes the default value, with a default value of default(TResult) so we don’t break any existing uses of the class.

Next a few helpers to make using this for images simple:

public static class AsyncImageSource
{
    public static NotifyTaskCompletion<ImageSource> FromTask(Task<ImageSource> task, ImageSource defaultSource)
    {
        return new NotifyTaskCompletion<ImageSource>(task, defaultSource);
    }
 
    public static NotifyTaskCompletion<ImageSource> FromUriAndResource(string uri, string resource)
    {
        var u = new Uri(uri);
        return FromUriAndResource(u, resource);
    }
 
    public static NotifyTaskCompletion<ImageSource> FromUriAndResource(Uri uri, string resource)
    {
        var t = Task.Run(() => ImageSource.FromUri(uri));
        var defaultResouce = ImageSource.FromResource(resource);
 
        return FromTask(t, defaultResouce);
    }
}

All this is doing, is creating (and running) a task to download the images in the background and feeding that into the NotifyTaskCompletion. One caveat here is that even if nothing ever binds to the image, it will still be downloaded. The ProductViewModel can now expose the IconUrl as an asynchronous wrapper around an ImageSource

public class ProductViewModel : BaseViewModel
{
    private const string _resource = "ShoppingCart.Resources.placeholderImageSmall.png";
    public ProductViewModel(Product product)
    {
        Product = product;
        IconSource = AsyncImageSource.FromUriAndResource(product.IconUrl, _resource);
    }
 
    public NotifyTaskCompletion<ImageSource> IconSource { get; private set; }
 
    public Product Product
    {
        get { return GetValue<Product>(); }
        set { SetValue(value); }
    }
}

The ProductsListViewModel will have to expose a list of the ProductViewModel instead of just the raw model which is a shame. I like binding to the model whenever possible just so I don’t have to worry about passing view models around. But what can you do? And now in the view, we bind to the result of the IconSource like so:

<Image Aspect="AspectFill"
       HeightRequest ="75" WidthRequest="75"
       Source="{Binding IconSource.Result}" />

Expectations and Realities

Running up the app my expectation would be to see the loading images when you first go to the product list pages and over the course of 2 to 5 seconds have them change to the correct product images as they are downloaded. In reality, when I navigated to a page all of the photos were present immediately. Right away. No delay. No loading images.

Happy coding.

this post was originally on the MasterDevs Blog

Comments