Cross Platform C#
Xamarin.Forms -- Caching for the ListView
Similar to the UITableView in iOS and ListView in Android, the XF version can be used to cache images for use in other locations of an application.
I’ve always been concerned when working with images. I'm always careful with how my applications use them, as images can be relatively large and apps will typically download them via a cellular or slow Wi-Fi connection. So you might not be so concerned with image size when it takes only a few hundred milliseconds to download, but those milliseconds can add up when your app is trying to download a large number of them.
In a number of previous columns, I’ve focused on how to make a Xamarin Forms (XF) application look and act more like a native application. I'll continue along and this time I’ll look at how you can use the Xamarin.Forms ListView to cache images so that you can use the images in other locations in your application with application performance in mind.
Xamarin.Forms ListView is the equivalent of the UITableView in iOS and the ListView in Android. It allows you to present a simple grid of data to the user. The user is able to easily scroll through the data, and binding textual data to it is also a snap.
One of the problems that I see with XF ListView is that there has only been one way to easily bind the image in an ImageCell in a ListView, and that's by passing the URL of the image to the cell. ListView is then responsible for downloading the contents of the URL, handling the contents of the download, formatting the image contents and displaying the image.
The problem is that I would like to download the bytes of an image once, to a cache, and then have the cached bytes available whenever the application needs them. For example, the golf application I've been developing along with this article series displays a list of team pictures. I would like it to be able to touch the cell, have another screen open and display the image without going to the server to get it. To do this, I need to be able to cache the images. Because the application will cache the image content as a byte array, the application will need to convert from a byte array to an image, so there will need to be a Xamarin.Forms image converter to handle the binding.
So why is this an issue? Here's what I've come up with:
- If multiple people are taking pictures, there’s no reason to download images that the application already has. Yes, the application should see the new images, but the images that have already been downloaded do not need to be downloaded again.
- When moving to the content page that displays the image, the image should be pulled from the local image cache, not downloaded from the server, to save both on bandwidth and time. The bandwidth issue should be obvious. The time to download is also saved, because the data is already in the phone. It takes very few milliseconds to convert from a byte array to an image and display it versus a second or two to download the image and display it.
There are numerous ways to handle solve the problems here. I will present one way and add some thoughts on how to improve the code. In this scenario, you do this by checking the cache to see if you have the image already, downloading the necessary images, converting them to a byte array, storing the images for future use, and using the byte array. Once the program has the byte array, the application will need to convert the byte array into an image. To do this, the code breaks down into two areas:
- The caching. This will be the code that will cache the image locally.
- The byte array to image converter. This will be the code that will convert the byte array into an image. In Xamarin.Forms, this is a converter.
Caching Code
The caching code is fairly simple. The code stores its contents in an in-memory dictionary in a dictionary object that’s stored in the current App class. By storing it and accessing it via the App class, the contents are available all through the application. The following code holds the image dictionary in the Application class:
private Dictionary<String, byte[]> _dictionaryPics;
public void PicDictAdd(string Url, byte[] bytesToAdd)
{
lock (_dictionaryPics)
{
byte[] outPic;
if (!_dictionaryPics.TryGetValue(Url, out outPic))
{
_dictionaryPics.Add(Url, bytesToAdd);
}
}
}
The next step is to get the code to display the pictures of the teams. Teams and pictures are a simple relationship in a one-to-many relationship. The same is true with teams and tournaments. A single tournament will have multiple teams. On top of that, a single team can have multiple pictures associated with it. The code in Listing 1 displays a hierarchy of tournaments, teams and their pictures, by going through these steps:
- The first thing to do is to get the team pictures via a LINQ query.
- Next, iterate through the images.
- If there’s an image in the cache, the byte array is provided to the application.
- If there’s no image, a call is made to an HttpClient to get the contents of an image. The HttpClient provides an easy method that returns a byte array of the contents. The method on the HttpClient is GetByteArrayAsync.
Listing 1: Displaying Teams and Pictures
var data = app.Tournaments;
var DictOfPics = app.PicDict;
var pics = (from t in data select t).SelectMany(n => n.Teams).Where(m => m.TeamId == TeamId).Select(o => o.TeamPics).First();
foreach (var p in pics)
{
byte[] outPic = null;
if (DictOfPics.TryGetValue(p.ImageUrl, out outPic))
{
p.ImageBytes = outPic;
}
else
{
p.ImageBytes = await WebServices.ws.TeamPicture(p.ImageUrl);
app.PicDictAdd(p.ImageUrl, p.ImageBytes);
}
}
The Converter
The next step of the application is to convert the byte array into an image that can be displayed. This is the job of the converter, as shown in Listing 2. In this example, it takes a series of parameters and then converts it into an ImageSource, and the ImageSource is then returned. The ImageSource is created by converting the byte array by creating an in-memory stream, pointing the stream back to the beginning, and then using the .FromSource static method to convert the stream into an ImageSource.
Listing 2: Converting the Byte Array into Images
public class ByteArrayToImageSource : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
ImageSource imgsrc = null;
try
{
if (value == null)
return null;
byte[] bArray = (byte[])value;
imgsrc = ImageSource.FromStream(() =>
{
var ms = new MemoryStream(bArray);
ms.Position = 0;
return ms;
});
}
catch(System.Exception sysExc)
{
System.Diagnostics.Debug.WriteLine(sysExc.Message);
}
return imgsrc;
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
The final piece of the puzzle is the binding. The ListView, in this example, uses an ImageCell and image that is being bound in a ListView’s cell. With this as the basis, the following code will set the binding:
var cellTemplate = new DataTemplate(typeof(MyTeamPicCell));
var bnd = new Binding("ImageBytes", BindingMode.Default,
new Converters.ByteArrayToImageSource());
cellTemplate.SetBinding(ImageCell.ImageSourceProperty, bnd);
Please note that the application in Listing 3 does not specifically use an ImageCell. However, the cell that’s used inherits from the ImageCell and therefore will act like it. The binding information becomes interesting in that it has the cell delete the image on the server and then the app will re-query the Web service to get updated images.
Listing 3: Displaying Team Pics in ImageCell After Conversion
public class MyTeamPicCell : ImageCell
{
public MyTeamPicCell()
{
}
protected override void OnAppearing()
{
base.OnAppearing();
}
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
if (BindingContext != null)
{
var moreAction = new MenuItem { Text = "Delete", IsDestructive = true };
moreAction.SetBinding(MenuItem.CommandParameterProperty, new Binding("."));
moreAction.Clicked += async (sender, e) => {
await System.Threading.Tasks.Task.Run(() => {
var mi = ((MenuItem)sender);
var cp = (Models.TeamPicture)mi.CommandParameter;
var teamPicId = cp.TeamPictureId;
var myapp = (GolfGameClubApp.App)App.Current;
var ClubId = myapp.ClubId.Value;
var TournamentId = myapp.TournamentId.Value;
var TeamId = myapp.TeamId.Value;
WebServices.ws.TeamPictureDelete(ClubId, TournamentId, TeamId,
teamPicId).ContinueWith((task) => {
var app = (GolfGameClubApp.App)App.Current;
var token = app.UserToken;
WebServices.ws.TournamentsInClub(token, ClubId).ContinueWith((task1) =>
{
var data1 = task1.Result;
app.Tournaments = new ObservableCollection<Models.TournamentInfo>(data1);
MessagingCenter.Send<MyTeamPicCell>(this, "UpdateImages");
});
});
});
};
ContextActions.Add(moreAction);
}
}
}
What Could This Code Do Better?
This is some example code, and it works properly in the scenario that it’s used in, which is an application with a relatively small number of images that have already been compressed. If there were a large number of images or larger images, or this code ran on some older phones with a smaller amount of memory, it would run into memory problems. If the application crashed, images would need to be reloaded from the server. This code needs a durable caching location to store images. Thankfully, there is a solution to this. iOS and Android have the SQLite database embedded in each OS. By using the database, you get that durable mechanism to cache images between restarts of the application. SQLite has several datatypes that are suitable for this.
A second question is how to respond to low-memory issues. XF by itself doesn’t handle low-memory issues -- that’s the responsibility of the underlying OS. On iOS and Android, it would be possible to implement the necessary methods to catch low-memory messages in the underlying OS, use the MessagingCenter to send a message up to Xamarin.Forms, and then to handle getting rid of the necessary objects that are in memory.
Summary
In this article, it has looked at how to cache images, bind the cached byte array of images to the ListView, and look at some additional improvements to the application.
Thank you for your time.
About the Author
Wallace (Wally) B. McClure has authored books on iPhone programming with Mono/Monotouch, Android programming with Mono for Android, application architecture, ADO.NET, SQL Server and AJAX. He's a Microsoft MVP, an ASPInsider and a partner at Scalable Development Inc. He maintains a blog, and can be followed on Twitter.