Mobile Zone is brought to you in partnership with:

Den is a DZone Zone Leader and has posted 460 posts at DZone. You can read more from them at their website. View Full User Profile

Building an Imgur Client for Windows Phone - Part 1 - Core & Main Gallery

01.07.2013
| 6572 views |
  • submit to reddit

Windows Phone does not yet have a comprehensive Imgur client, so I thought about developing one, that will provide a great user experience when it comes to browsing and sharing images through the service. This article series documents the development process from A to Z, showing how to use the API and how to represent the returned data in an efficient way in the application itself.

Getting Service Approval

First of all, you need to make sure that your application can access the Imgur endpoints. To do that, you need to sign up for a free account and register your application. This process is free for non-commercial applications, which means that you will not:

  • distribute your app as a paid product
  • display in-app advertisements

There is a way to do this for commercial applications through a paid endpoint, which I am not going to focus on in this series. Once you register the application, you will have two values in your hands - the client ID and the client secret.

The interesting part is that even if your applications accesses public data, such as the images from the main gallery, it still needs to send the authorization header with the client ID, in order to ensure that each application is respecting it's API rate limits.

The default rate limits are set to:

  • 1,250 uploads per day or
  • ~12,500 requests per day

This is more than enough for a simple application, and there is a way to keep track of how much your app has left from the quota, which I will be discussing in one of the next articles.

Starting Building The App

Open Visual Studio and create a new Windows Phone application. We will start with a 7.1 app, and will later extend it with capabilities from 8.0, so make sure you select the correct target operating system.

First and foremost, we need to create the code core - the Imgur service client. To do this, I created a new folder in my solution called ImgurAPI. Inside it, I create a class named ImgurClient - this will be the central connection point for any requests ran against the Imgur API.

As I mentioned that there are two constants for your app - the client ID and the client secret, you need to somehow tie those to the ImgurClient class. I've done this through a constructor:

private string _clientID;
private string _clientSecret;

public ImgurClient(string clientID, string clientSecret)
{
    _clientID = clientID;
    _clientSecret = clientSecret;
}

This way, a single instance can be used for the entire application. Now let's look at how we can get images from the main gallery. The main gallery is the public image repository. When users upload a public image, it is being placed in the main gallery. You can sort images by the time added and by how popular those are, so what you see by default is the top stack.

Look at the Gallery Image model to see what data you will get about each image returned after successfully executing the request. Based on the available description, I built a C# class that will represent the entity:

public class ImgurImage
{
  public string ID { get; set; }
  public string Title { get; set; }
  public Int64 DateTime { get; set; }
  public string Type { get; set; }
  [JsonProperty(PropertyName = "animated")]
  public bool IsAnimated { get; set; }
  public int Width { get; set; }
  public int Height { get; set; }
  public Int64 Size { get; set; }
  public Int64 Views { get; set; }
  [JsonProperty(PropertyName = "account_url")]
  public string AccountUrl { get; set; }
  public string Link { get; set; }
  public string Bandwidth { get; set; }
  public int Ups { get; set; }
  public int Downs { get; set; }
  public int Score { get; set; }
  [JsonProperty(PropertyName = "is_album")]
  public bool IsAlbum { get; set; }
}

There are a few things to mention here. You probably noticed that in the official description, some of the data types for properties such as DateTime are marked as integers. What the documentation doesn't tell you is what kind of integer is necessary. For example, for DateTime, a standard integer (Int32) would not suffice and you need to use Int64 (long) instead. Not doing so will result in an overflow exception.

Also some of the properties are marked with a JsonProperty attribute. I am using Json.NET for JSON data handling and deserialization. Add it to the project by right clicking on References in Solution Explorer and selecting Manage NuGet Packages.

By default, the JSON deserializer will associate each property with a field that has the same name in the JSON string. Having is_album as a C# property is not something commonly used, so that's where I can include an attribute that will override the default link between the field and property names.

Let's implement a function in the ImgurClient class that will retrieve the JSON data and will return it to the invoker.

/// <summary>
/// Get the images from the main gallery.
/// This call DOES NOT require authentcation.
/// </summary>
/// <param name="section"></param>
/// <param name="sort"></param>
/// <param name="page"></param>
public void GetMainGalleryImages(ImgurGallerySection section, ImgurGallerySort sort, int page, 
    Action<ImgurImageData> onCompletion)
{
    string _sort = sort.ToString().ToLower();
    string _section = section.ToString().ToLower();
    
    WebClient client = new WebClient();
    client.Headers["Authorization"] = "Client-ID " + _clientID;

    client.DownloadStringAsync(new Uri(string.Format(ImgurEndpoints.MainGallery, _section, _sort, page)));
    client.DownloadStringCompleted += (c, s) =>
    {
        var imageData = JsonConvert.DeserializeObject<ImgurImageData>(s.Result);
        onCompletion(imageData);
    };
}

As I mentioned previously, even for public data I need to set an authorization header that will tell Imgur what application is trying to aggregate data from the catalog. The main gallery endpoint is obtained from a static ImgurEndpoints class.

public static class ImgurEndpoints
{
    public const string MainGallery = "https://api.imgur.com/3/gallery/{0}/{1}/{2}.json";
}

When I am deserializing the data, I am getting an ImgurImageData object instead of a generic list. You might ask - why is that? Take a look at the result that you are getting (raw). A great tool to format JSON is JSON Formatter & Validator.

What's wrong with this JSON? Per se, nothing, but in order for me to deserialize a string to a List<ImgurImage>, I would need to have a raw JSON array. Here, I don't have one, but I have a data container. That's why there is an ImgurImageData class:

public class ImgurImageData
{
    [JsonProperty(PropertyName = "data")]
    public IEnumerable<ImgurImage> Images { get; set; }
}

Again, because I am not using the default name-to-name deserialization association, I am overriding the name of the field that is being used. To test the function, go to the main application page and use this snippet:

ImgurClient client = new ImgurClient(ConstantContainer.IMGUR_CLIENT_ID,
    ConstantContainer.IMGUR_CLIENT_SECRET);

client.GetMainGalleryImages(ImgurGallerySection.Hot, ImgurGallerySort.Viral, 0, (s) =>
    {
        ImgurImageData data = s;
        Debug.WriteLine(s.Images.First().AccountUrl);
    });

ConstantContainer is a static class that contains the pre-defined client ID and client secret. If you set a breakpoint at the Debug.WriteLine line, you will get a nice view of what's in the resulting container:

It's looking good so far, but I want to actually display the images somewhere in the application, and not just visualize their models in Visual Studio. To do this, I am actually going to move the ImgurClient instance to App.xaml.cs.

The data has to be inserted somewhere, obviously, so that's why I created a ViewModels folder in my solution, with a MainPageViewModel class inside it - this will be used to bind all data that needs to be handled on the main application page, and the images from the main gallery are a part of that.

using Imagine.ImgurAPI;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;

namespace Imagine
{
    public class MainPageViewModel : INotifyPropertyChanged
    {

        static MainPageViewModel instance = null;
        static readonly object padlock = new object();

        public MainPageViewModel()
        {
            HomeImages = new ObservableCollection<ImgurImage>();
        }

        public static MainPageViewModel Instance
        {
            get
            {
                lock (padlock)
                {
                    if (instance == null)
                    {
                        instance = new MainPageViewModel();
                    }
                    return instance;
                }
            }
        }

        private ObservableCollection<ImgurImage> _homeImages;
        public ObservableCollection<ImgurImage> HomeImages
        {
            get
            {
                return _homeImages;
            }
            set
            {
                if (_homeImages != value)
                {
                    _homeImages = value;
                    NotifyPropertyChanged("HomeImages");
                }
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void NotifyPropertyChanged(String info)
        {
            if (PropertyChanged != null)
            {
                System.Windows.Deployment.Current.Dispatcher.BeginInvoke(
                    () =>
                    {
                        PropertyChanged(this, new PropertyChangedEventArgs(info));
                    });
            }
        }
    }
}

This is a view model implementation I talked about here. Since there is more than one view in the application, it makes sense to have the view models separated - that way as a developer you won't have much problems debugging possible data misplacement and errors.

Back in MainPage.xaml.cs, make sure that you add a MainPage_Loaded event handler. Inside it, you can insert the main gallery image loading code:

void MainPage_Loaded(object sender, RoutedEventArgs e)
{
    if ((MainPageViewModel.Instance.HomeImages == null) !=
        (MainPageViewModel.Instance.HomeImages.Count == 0))
    {
        App.ServiceClient.GetMainGalleryImages(ImgurGallerySection.Hot, ImgurGallerySort.Viral, 0, (s) =>
        {
            ImgurImageData data = s;
            MainPageViewModel.Instance.HomeImages =
                new System.Collections.ObjectModel.ObservableCollection<ImgurImage>(s.Images);
            System.Diagnostics.Debug.WriteLine("Main gallery images loaded in MainViewModel.");
        });
    }
}

Great, so now we have a collection that keeps the most up-to-date images from the main public Imgur gallery, however those are never displayed anywhere. Let's fix this. Open App.xaml and add a new namespace, pointing to the ViewModels folder:

xmlns:vms="clr-namespace:Imagine.ViewModels"

Inside the Application.Resources node, add a new node for the MainPageViewModel. Give it a unique key, as it will be referenced for binding:

<!--Application Resources-->
<Application.Resources>
    <vms:MainPageViewModel x:Key="MainPageViewModel"></vms:MainPageViewModel>
</Application.Resources>

Now we can do something with the underlying Images collection. In MainPage.xaml, add a new ListBox. For testing purposes, we can make it as simple as possible, with an image as the default ItemTemplate.

<ListBox ItemsSource="{Binding Path=Instance.HomeImages,
    Source={StaticResource MainPageViewModel}}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Image Height="240" Width="240" Source="{Binding Link}"></Image>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

The results will be OK:

Settling for OK is not interesting enough, so let's work with a WrapPanel, the images being displayed in small adjacent squares, taking the most of the screen estate. The control is not part of the stock SDK, so you will need to add the WP Toolkit package through NuGet:

Add a reference to the new namespace in MainPage.xaml:

xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Toolkit"

I am now able to modify the default ListBox ItemsPanel to be a WrapPanel:

<ListBox ItemsSource="{Binding Path=Instance.HomeImages,
    Source={StaticResource MainPageViewModel}}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Image Stretch="UniformToFill" Height="240" Width="240" Source="{Binding Link}"></Image>
        </DataTemplate>
    </ListBox.ItemTemplate>

    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <toolkit:WrapPanel ItemWidth="240" ItemHeight="240"/>
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
</ListBox>

This should work in theory, but in practice you will most likely get this when the application runs on a physical device:

Why is this happening? Because by default, the returned JSON will get 200+ image references. Keeping separate ImgurImage instances is not a problem, however when all 200+ images are being downloaded at the same time in Image controls (remember that you are referencing the URL as the source), this will cause an application crash.

The solution here would be lazy loading or simply having images pre-downloaded once the JSON data is obtained. I chose to go the second route, so I created an ImageDownloadHelper class, with a DownloadImages function:

/// <summary>
/// Downloads images one-by-one from a given collection.
/// </summary>
/// <param name="images">The collection that contains the images.</param>
/// <param name="startIndex">Starting index at which the download starts.</param>
/// <param name="items">The number of items to download.</param>
/// <param name="onCompletion">Action executed every time an image is downloaded.</param>
public static void DownloadImages(IEnumerable<ImgurImage> images, int startIndex, int items, 
    Action<ImgurImage> onCompletion = null)
{
    if ((images != null) && (images.Count() > 0))
    {
        int count = images.Count();

        if (startIndex < count)
        {
            ImgurImage currentImage = images.ElementAt(startIndex);

            WebClient client = new WebClient();
            if (currentImage.Link != null && !(currentImage.Link.EndsWith(".gif")))
            {
                string thumbnailLocation = currentImage.Link.Insert(currentImage.Link.LastIndexOf('.'), "s");

                client.OpenReadAsync(new Uri(thumbnailLocation));
                client.OpenReadCompleted += (s, e) =>
                    {
                        BitmapImage image = new BitmapImage();
                        image.SetSource(e.Result);
                        currentImage.Image = image;

                        onCompletion(currentImage);

                        ContinueDownloadIfNecessary(images, startIndex, items, onCompletion);
                    };
            }
            else
            {
                ContinueDownloadIfNecessary(images, startIndex, items, onCompletion);
            }
        }
    }
}

I am doing the cross-check to ensure that the item that I will try to download exists and that the index is in the valid range. To avoid downloading large images and blocking memory, I am downloading thumbnails instead, which can be obtained by appending a lowercase 's' to the image ID in the URL (you can see how I am forming the URL above).

Notice that the ImgurImage model now has an Image property. I modified the original model to include it:

[JsonIgnore]
public BitmapImage Image { get; set; }

Once an image is downloaded, I am able to proceed with the next download, if necessary, but I am also giving the user to perform a specific action for each separate download. ContinueDownloadIfNecessary is a helper function that increments the starting index and decrements the number of items to download:

private static void ContinueDownloadIfNecessary(IEnumerable<ImgurImage> images, int defaultStartIndex,
    int defaultItemsToLoad, Action<ImgurImage> onCompletion)
{
    int itemsToLoad = --defaultItemsToLoad;
    int newStartIndex = ++defaultStartIndex;

    if (itemsToLoad > 0)
        DownloadImages(images, newStartIndex, itemsToLoad, onCompletion);
}

By default, the Image control does not support GIF images, so I am making sure that I am not downloading unnecessary stuff either. I will show you how to mitigate this issue later in the series.

The loading routine in the main page effectively becomes this:

if ((MainPageViewModel.Instance.HomeImages == null) !=
    (MainPageViewModel.Instance.HomeImages.Count == 0))
{
    App.ServiceClient.GetMainGalleryImages(ImgurGallerySection.Hot, ImgurGallerySort.Viral, 0, (s) =>
    {
        ImgurImageData data = s;
        Debug.WriteLine("[JSON] Main gallery images loaded.");

        ImageDownloadHelper.DownloadImages(s.Images, 0, 10, (image) =>
            {
                MainPageViewModel.Instance.HomeImages.Add(image);
                Debug.WriteLine(string.Format("[{0}] Image added to HomeImages in MainPageViewModel.",
                    image.Type));
            });
    });
}

Here is a problem, though - once the download is complete, the ImgurImageData instance is gone, and so is the entire deserialized collection of JSON objects. To avoid this, I am going to create another List<ImgurImage> in my main page view model, specifically for storing the serialized items.

private List<ImgurImage> _deserializedHomeImages;
public List<ImgurImage> DeserializedHomeImages
{
    get
    {
        return _deserializedHomeImages;
    }
    set
    {
        if (_deserializedHomeImages != value)
        {
            _deserializedHomeImages = value;
            NotifyPropertyChanged("DeserializedHomeImages");
        }
    }
}

I can use a List because I am not binding it and I do not need to have an implementation of INotifyCollectionChanged.

Now your loading routine becomes much better:

if ((MainPageViewModel.Instance.HomeImages == null) !=
    (MainPageViewModel.Instance.HomeImages.Count == 0))
{
    App.ServiceClient.GetMainGalleryImages(ImgurGallerySection.Hot, ImgurGallerySort.Viral, 0, (s) =>
    {
        ImgurImageData data = s;
        MainPageViewModel.Instance.DeserializedHomeImages = 
            new System.Collections.Generic.List<ImgurImage>(s.Images);
        Debug.WriteLine("[JSON] Main gallery images loaded.");

        ImageDownloadHelper.DownloadImages(MainPageViewModel.Instance.DeserializedHomeImages, 0, 10, (image) =>
            {
                MainPageViewModel.Instance.HomeImages.Add(image);
                Debug.WriteLine(string.Format("[{0}] Image added to HomeImages in MainPageViewModel.",
                    image.Type));
            });
    });
}

If you run the application in its current state, you will get this result:

In the next article of the series, I will be talking about making the ListBox scrollable, loading images as we go, and viewing details about each of the images that is being loaded.