Data caching in Windows 8 apps

Data caching in Windows 8 apps

When working with a lot of data in apps based on web services (JSON return format being quite common), you’d normally want to cache it in order to make your apps faster and make your users spend less Internet traffic on refreshing which actually isn't necessary. Also, if the users opens your app while having no Internet connectivity, it’s nice to show him something, even though it’s cached data. It’s always possible to implement your own caching framework, but there’s a nice library already out there called Q42.WinRT which makes it very easy to do the caching.

In your Windows 8 project, add a library from NuGet called Q42.WinRT

q42

Working with Flickr API

For this demo, we will be using the public Flickr API and the following request:

http://api.flickr.com/services/rest/?method=flickr.photos.getRecent&apikey=YOURAPI_KEY&format=json

It's a public API, but you still need to register in order to get your API key. Then can you start making requests.

This returns the following JSON:

JSON

Using json2csharp we can generate model classes for this data. Beware, the JSON returned starts with jsonFlickrApi(…… and ends with ) so it needs to be removed before using with json2csharp.

public class Photo  
{
    public string id { get; set; }
    public string owner { get; set; }
    public string secret { get; set; }
    public string server { get; set; }
    public int farm { get; set; }
    public string title { get; set; }
    public int ispublic { get; set; }
    public int isfriend { get; set; }
    public int isfamily { get; set; }
}

public class Photos  
{
    public int page { get; set; }
    public int pages { get; set; }
    public int perpage { get; set; }
    public int total { get; set; }
    public List photo { get; set; }
}

public class Response  
{
    public Photos photos { get; set; }
    public string stat { get; set; }
}

The format of Photo URL is the following:

http://farm{farm-id}.staticflickr.com/{server-id}/{id}{o-secret}o.(jpg|gif|png)

We will use the following format:

http://farm{farm-id}.staticflickr.com/{server-id}/{id}{o-secret}b.jpg

So for one of the photos from the above response, the URL is:

http://farm9.staticflickr.com/8365/8524366069cce2bc7074b.jpg

Where b means large photo, 1024 on longer side. Obviously, we need to add another property which returns us the URL so we can bind it to the UI. Add this to the Photo class:

public string PhotoUri  
{
    get
    {
        return string.Format("http://farm{0}.staticflickr.com/{1}/{2}_{3}_b.jpg", farm, server, id, secret);
    }
}

Make a simple request to get the data and bind to the UI:

protected async override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)  
{
    var client = new HttpClient();
    var clientResult = await client.GetStringAsync(new Uri("http://api.flickr.com/services/rest/?method=flickr.photos.getRecent&amp;api_key=YOUR_API_KEY&amp;format=json", UriKind.Absolute));
    var result = clientResult.Substring(14, clientResult.Length - 15);
    var photosResult = await JsonConvert.DeserializeObjectAsync(result);
    var photos = photosResult.photos;

    this.DefaultViewModel["Groups"] = photos.photo;
}

Where the UI is defined as a page with simple resource and a GridView:

<Page.Resources>

        <!--
            Collection of grouped items displayed by this page, bound to a subset
            of the complete item list because items in groups cannot be virtualized
        -->
        <CollectionViewSource x:Name="groupedItemsViewSource"
                              IsSourceGrouped="False"
                              Source="{Binding Groups}" />
    </Page.Resources>

    <!--
        This grid acts as a root panel for the page that defines two rows:
        * Row 0 contains the back button and page title
        * Row 1 contains the rest of the page layout
    -->
    <Grid Style="{StaticResource LayoutRootStyle}">
        <Grid.RowDefinitions>
            <RowDefinition Height="140" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <!--  Horizontal scrolling grid used in most view states  -->
        <GridView x:Name="itemGridView"
                  Grid.RowSpan="2"
                  AutomationProperties.AutomationId="ItemGridView"
                  AutomationProperties.Name="Grouped Items"
                  IsItemClickEnabled="True"
                  IsSwipeEnabled="false"
                  ItemClick="ItemView_ItemClick"
                  ItemTemplate="{StaticResource Standard250x250ItemTemplate}"
                  ItemsSource="{Binding Source={StaticResource groupedItemsViewSource}}"
                  Padding="116,137,40,46"
                  SelectionMode="None">

        </GridView>

And Standard250x250ItemTemplate modified to show the photo and the title of that photo:

<DataTemplate x:Key="Standard250x250ItemTemplate">  
    <Grid Width="250"
          Height="250"
          HorizontalAlignment="Left">
        <Border Background="{StaticResource ListViewItemPlaceholderBackgroundThemeBrush}">
            <Image AutomationProperties.Name="{Binding Title}"
                   Source="{Binding PhotoUri}"
                   Stretch="UniformToFill" />
        </Border>
        <StackPanel VerticalAlignment="Bottom" Background="{StaticResource ListViewItemOverlayBackgroundThemeBrush}">
            <TextBlock Height="40"
                       Margin="15,0,15,0"
                       Foreground="{StaticResource ListViewItemOverlayForegroundThemeBrush}"
                       Style="{StaticResource TitleTextStyle}"
                       Text="{Binding title}" />
        </StackPanel>
    </Grid>
</DataTemplate>  

Which results in a simple app looking like this:

image

Caching

The photos are taking quite some time to load, especially because we chose to load the large versions of it. You’ll notice that nothing gets automatically cached, of course. You can check this location to see what’s happening in the local storage of your app:

C:Users{YourUserName}AppDataLocalPackages{PackageName}LocalState

In order to cache the downloaded JSON file, we need to add a few things to the LoadState method:

protected async override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)  
{
    var client = new HttpClient();
    string someUrl = "http://api.flickr.com/services/rest/?method=flickr.photos.getRecent&amp;api_key=YOUR_API_KEY&amp;format=json";
    var result = await JsonCache.GetAsync(CalculateMD5Hash(someUrl),
        async () =>
        {
            var tempResult = await client.GetStringAsync(new Uri(someUrl,UriKind.Absolute));
            var resultSubstring = tempResult.Substring(14, tempResult.Length - 15);
            var photosResult = await JsonConvert.DeserializeObjectAsync(resultSubstring);
            return photosResult;
        });

    this.DefaultViewModel["Groups"] = result.photos.photo;
}

JsonCache is a part of the Q42.WinRT.Data namespace (add it on the top of the file if you haven't already). The GetAsync method has the following parameters

  • string key
  • Func generate
  • expire date (optional)
  • force refresh (optional)

The string key is the name of the cached file under which your JSON file gets saved. In this simple case I could’ve defined a simple key myself, but in a much larger apps where users will be in charge of the requests that get called, you’ll want to have a better way of making sure that the keys are unique for every request. One way to accomplish that is to calculate the MD5 hash

public string CalculateMD5Hash(string input)  
{
    var md5h = HashAlgorithmProvider.OpenAlgorithm("MD5").CreateHash();
    var buff = CryptographicBuffer.ConvertStringToBinary(input,BinaryStringEncoding.Utf16BE);
    md5h.Append(buff);
    var buffHash = md5h.GetValueAndReset();
    return CryptographicBuffer.EncodeToBase64String(buffHash);
}

GetAsync method checks if there’s a JSON file in your storage that has the same name as the key. If there is, it loads that one and gets your results much faster. If it’s not there, or the cache expired, or you’re doing a forced refresh, the generate Func gets executed, downloading the results from the Internet.

The first time we run this app, there’s no cache saved, so the file gets downloaded and the cache is now saved in the local storage. The next time you run the app, it will get the data from the local storage and will skip the Func part. The cache is saved here:

C:Users{YourUserName}AppDataLocalPackages{PackageName}LocalState_jsonCache

We can also add the expireDate to the method call, like this:

            var result = await JsonCache.GetAsync(CalculateMD5Hash(someUrl),
                async () =>
                {
                    var tempResult = await client.GetStringAsync(new Uri(someUrl", UriKind.Absolute));
                    var resultSubstring = tempResult.Substring(14, tempResult.Length - 15);
                    var photosResult = await JsonConvert.DeserializeObjectAsync(resultSubstring);
                    return photosResult;
                },
                expireDate:DateTime.Now.AddMinutes(30));

The DateTime object will get saved inside the cached JSON file and will be checked the next time you make request. If the cache expired, again the generate Func will get executed.

If you wish, you can enable user to force refresh updates. You do that by setting the forceRefresh bool property to true:

            var result = await JsonCache.GetAsync(CalculateMD5Hash(someUrl),
                async () =>
                {
                    var tempResult = await client.GetStringAsync(new Uri(someUrl, UriKind.Absolute));
                    var resultSubstring = tempResult.Substring(14, tempResult.Length - 15);
                    var photosResult = await JsonConvert.DeserializeObjectAsync(resultSubstring);
                    return photosResult;
                },
                expireDate:DateTime.Now.AddMinutes(30),
                forceRefresh:true);

If you want to cache only images, you can change just the XAML Image part. Instead using Source property, do this:

<Image AutomationProperties.Name="{Binding Title}"  
       q42:ImageExtensions.CacheUri="{Binding PhotoUri}"
       Stretch="UniformToFill" />

q42 is a namespace defined as: xmlns:q42="using:Q42.WinRT.Controls"

This saves the images in the following folder:

C:Users{YourUserName}AppDataLocalPackages{PackageName}LocalState_webdatacache

I did notice it behaved a bit strange sometimes, so be careful.

If you wish to force clear all the cache (for example, user wants to clear it or user logs out from your app), do this:

await JsonCache.ClearAll();  

or just delete one particular cache file:

await JsonCache.Delete("someKEY");  

If you wish, you can only get the cache, without the ability to fall back to the HttpClient request, like this:

var result = await JsonCache.GetFromCache(CalculateMD5Hash(someUrl));  

Q42.WinRT is actually much more than just a caching library, but let's stay focused on that. We’ll see a slightly more enhanced way of using cache in the next blog post!

Igor Ralic

igor ralic

View Comments
Microsoft Certified Solutions Developer: Windows Store Apps in C#