-->

Thursday, February 14, 2013

Parse Json to c# in WinRT, Silverlight, WPF, Windows Phone

I never worked with Json before. But due to security limitations it’s the only simple way to get twitter data in Silverlight. So, I decided to do the same sample application for WinRT, Silverlight, WPF and Windows Phone platforms. As usual, there are many different ways to do the same thing. Jokingly, almost all Json stuff, included into the box, is different on different platforms. Perhaps MS guys don't like to re-use code. Finally I’ve found the single common thing, which can be used everywhere. It’s System.Runtime.Serialization.Json.DataContractJsonSerializer. In WinPhone and Silverlight applications it is included into System.Servicemodel.Web assembly, in WPF – into System.Runtime.Serialization, and in WinRT it’s the part of .NET for Windows Store apps.
This sample uses twitter search api 1.0 which doesn’t require any authentication (https://dev.twitter.com/docs/api/1/get/search ) .
To work with DataContractJsonSerializer you should generate the set of classes to which your Json can be deserialized. It might be tricky if you want to do it by yourself. Fortunately, you can use the public service like http://json2csharp.com/. It takes Json string and generates the set of classes. Then you can copy this classes to your project and use them. The easiest way to get Json string is to download it using the same uri which you are planning to use in your application. In Silverlight you can use WebClient.DownloadStringAsync:
var client = new WebClient();
            client.DownloadStringCompleted += (sender, args) =>
                {
                    string json = args.Result;
                };
            client.DownloadStringAsync(new Uri(yourUri));

My search uri looks like this one:
http://search.twitter.com/search.json?q=from:ComponentOne&rpp=20&include_entities=true

Twitter returns quite long json string for my query, I won’t post it here. Just copy the string and ask json2csharp to convert it. For my case it gave me the next set of classes:

public class Hashtag
    {
        public string text { get; set; }
        public List<int> indices { get; set; }
    }

    public class Url
    {
        public string url { get; set; }
        public string expanded_url { get; set; }
        public string display_url { get; set; }
        public List<int> indices { get; set; }
    }

    public class Entities
    {
        public List<Hashtag> hashtags { get; set; }
        public List<Url> urls { get; set; }
        public List<object> user_mentions { get; set; }
    }

    public class Metadata
    {
        public string result_type { get; set; }
    }

    public class Result
    {
        public string created_at { get; set; }
        public Entities entities { get; set; }
        public string from_user { get; set; }
        public int from_user_id { get; set; }
        public string from_user_id_str { get; set; }
        public string from_user_name { get; set; }
        public object geo { get; set; }
        public object id { get; set; }
        public string id_str { get; set; }
        public string iso_language_code { get; set; }
        public Metadata metadata { get; set; }
        public string profile_image_url { get; set; }
        public string profile_image_url_https { get; set; }
        public string source { get; set; }
        public string text { get; set; }
        public object to_user { get; set; }
        public int to_user_id { get; set; }
        public string to_user_id_str { get; set; }
        public object to_user_name { get; set; }
    }

    public class RootObject
    {
        public double completed_in { get; set; }
        public long max_id { get; set; }
        public string max_id_str { get; set; }
        public int page { get; set; }
        public string query { get; set; }
        public string refresh_url { get; set; }
        public List<Result> results { get; set; }
        public int results_per_page { get; set; }
        public int since_id { get; set; }
        public string since_id_str { get; set; }
    }
 
Believe or not, it was the most complicated part. Note, if some part of returned Json is empty, for example, there is no any data for urls in the entities, json2csharp will not create Url class and will use object type instead. In such case you won’t be able to parse urls. So, check such things and try to get different json which contains needed data.  
You can use above classes directly, but I decided to use them for parsing only. Returned Json might change upon service developers decision, I don’t want to change my xaml every time. So, in my samples I use really simple class to represent twitter item:

public class TwitterItem
    {
        public DateTime PublishDate { get; set; }
        public string Handle { get; set; }
        public string Title { get; set; }
        public string Link { get; set; }
    }

And the last peace of work to get real data from server. Here is Silverlight version (the same exact code can be shared for WPF and Windows Phone platforms):
/// <summary>
/// Searches and gets the index of the first older item already in the list
/// </summary>
/// <param name="date">The date of the item being compared to the list</param>
/// <returns>The index of the first oldest item, otherwise the number of items in the list</returns>
public static int GetFirstIndexOfOlderTwitterItem(DateTime date, ObservableCollection<TwitterItem> tItems)
{
    if (tItems.Count > 0)
    {
        TwitterItem tfi = tItems.FirstOrDefault(d => d.PublishDate < date);
        if (tfi == null)
            return tItems.Count;
        else
            return tItems.IndexOf(tfi);
    }
    else
    {
        return 0;
    }
}

/// <summary>
/// Loads twitter items into the specified control.
/// </summary>
/// <param name="uri">The uri to load twitter data.</param>
/// <param name="control">If set, method uses it to load photos only with this specific tag.</param>
public static void Load(string uri, Control control)
{
    ObservableCollection<TwitterItem> result = new ObservableCollection<TwitterItem>();
    var client = new WebClient();

    client.OpenReadCompleted += (s, e) =>
        {
            #region ** parse twitter data
            using (System.IO.Stream stream = e.Result)
            {
                // parse Json data
                DataContractJsonSerializer rootSer = new DataContractJsonSerializer(typeof(RootObject));

                RootObject root = (RootObject)rootSer.ReadObject(stream);
                foreach (Result res in root.results)
                {
                    TwitterItem ti = new TwitterItem();
                    ti.Title = res.text;
                    DateTimeOffset dateOffset;
                    if (DateTimeOffset.TryParse(res.created_at, out dateOffset))
                    {
                        ti.PublishDate = dateOffset.UtcDateTime;
                    }
                    ti.Handle = res.from_user;
                    if (res.entities.urls != null && res.entities.urls.Count > 0)
                    {
                        ti.Link = res.entities.urls[0].url;
                    }
                    result.Insert(GetFirstIndexOfOlderTwitterItem(ti.PublishDate, result), ti);
                }
                if (control is ItemsControl)
                {
                    ((ItemsControl)control).ItemsSource = result;
                }
            }
            #endregion
        };
    client.OpenReadAsync(new Uri(uri));
}

And a bit different code for WinRT platform:

/// <summary>
/// Loads twits.
/// </summary>
/// <param name="uri">The uri to load twitter data.</param>
/// <returns></returns>
public static async Task<ObservableCollection<TwitterItem>> Load(string uri)
{
    ObservableCollection<TwitterItem> result = new ObservableCollection<TwitterItem>();
    try
    {
        var client = WebRequest.CreateHttp(uri);
        var response = await client.GetResponseAsync();

        #region ** parse twitter data
        using (System.IO.Stream stream = response.GetResponseStream())
        {
            // parse Json data
            DataContractJsonSerializer rootSer = new DataContractJsonSerializer(typeof(RootObject));

            RootObject root = (RootObject)rootSer.ReadObject(stream);
            foreach (Result res in root.results)
            {
                TwitterItem ti = new TwitterItem();
                ti.Title = res.text;
                DateTimeOffset dateOffset;
                if (DateTimeOffset.TryParse(res.created_at, out dateOffset))
                {
                    ti.PublishDate = dateOffset.UtcDateTime;
                }
                ti.Handle = res.from_user;
                if (res.entities.urls != null && res.entities.urls.Count > 0)
                {
                    ti.Link = res.entities.urls[0].url;
                }
                result.Insert(GetFirstIndexOfOlderTwitterItem(ti.PublishDate, result), ti);
            }
        }
        #endregion
    }
    catch
    {
    }
    return result;
}

Thanks to people who keep posting on http://stackoverflow.com/, the most of problems got solved after reading answers.