Tuesday, May 17, 2011

Using ICustomTypeProvider in Silverlight 5 to Display JSON Data

By now you've likely heard the buzz surrounding the release of the Silverlight 5 beta and some of the great new features. Developers have been waiting some time for a dynamic type that can participate in data-binding for Silverlight. Silverlight 5 introduces this ability, but not in the form of Expando objects or the dynamic keyword as most expected. Instead, it comes in the form of ICustomTypeProvider.

The custom type provider interface declares a single method:

Type GetCustomType();

That's it! Sounds easy, but there is actually quite a bit of work that goes into providing a custom type. The actual "work" behind that method could be something like the dynamic types that I described in the linked post. In this post, however, I'll focus on the helper method that Microsoft employee Alexandra Rusina was kind enough to provide in her post, binding to dynamic properties with ICustomTypeProvider.

The key to ICustomTypeProvider is reflection. In anticipation of focusing more on the reflection engine for dynamic types, Silverlight 5 provides overrides for many of the base reflection classes. Alexandra's CustomTypeHelper class takes advantage of this to help construct types on the fly and provide for setting dynamic property values. I'll let you read her post to understand the details, and then return here for an example of a practical application of the concept.

For our example we're going to display a sortable grid that derives from JSON data. The example will assume the JSON data is an array of a simple (only one level deep) object graph. Here is a JSON file with information about the planets:

[
   { 
 "Name" : "Mercury",
 "Mass" : 0.05,
 "Diameter" : 4876
   },
   { 
 "Name" : "Venus",
 "Mass" : 0.81,
 "Diameter" : 12107
   },
   { 
 "Name" : "Earth",
 "Mass" : 1.0,
 "Diameter" : 12755
   },
   { 
 "Name" : "Mars",
 "Mass" : 0.1,
 "Diameter" : 6794
   },
   { 
 "Name" : "Jupiter",
 "Mass" : 317,
 "Diameter" : 142983
   },
   { 
 "Name" : "Saturn",
 "Mass" : 95,
 "Diameter" : 120536
   },
   { 
 "Name" : "Uranus",
 "Mass" : 14.6,
 "Diameter" : 51117
   },
   { 
 "Name" : "Neptune",
 "Mass" : 17,
 "Diameter" : 4876
   },
   { 
 "Name" : "Pluto",
 "Mass" : 0.0002,
 "Diameter" : 2390
   }
]

To start with, create a basic class that encapsulates the custom type. It will have a unique identifier and implement Equals and GetHashCode based on the unique identifier. It will also expose some helper methods that pass through to the CustomTypeHelper class in order to dynamically wire the type:

public class CustomType : ICustomTypeProvider 
{
    public CustomType()
    {
        Id = Guid.NewGuid();
    }

    [Display(AutoGenerateField = false)]
    public Guid Id { get; set; }        

    readonly CustomTypeHelper<CustomType> _helper = new CustomTypeHelper<CustomType>();

    public static void AddProperty(String name)
    {
        CustomTypeHelper<CustomType>.AddProperty(name);
    }

    public static void AddProperty(String name, Type propertyType)
    {
        CustomTypeHelper<CustomType>.AddProperty(name, propertyType);
    }

    public static void AddProperty(String name, Type propertyType, List<attribute> attributes)
    {
        CustomTypeHelper<CustomType>.AddProperty(name, propertyType, attributes);
    }


    public void SetPropertyValue(string propertyName, object value)
    {
        _helper.SetPropertyValue(propertyName, value);
    }

    public object GetPropertyValue(string propertyName)
    {
        return _helper.GetPropertyValue(propertyName);
    }

    public PropertyInfo[] GetProperties()
    {
        return _helper.GetProperties();
    }

    public Type GetCustomType()
    {
        return _helper.GetCustomType();
    }

    public override bool Equals(object obj)
    {
        return obj is CustomType && ((CustomType) obj).Id.Equals(Id);
    }

    public override int GetHashCode()
    {
        return Id.GetHashCode();
    }
}

Notice it internally stores an instance of the helper for the type and passes through to it to implement the ICustomTypeProvider interface. As much as I loathe data annotations in clean data models, here I'm using one to suppress the inclusion of the Id in the grid because I will let it auto-generate columns to show the dynamic type.

Next is a helper method to wire in the type. The constructor is given the "template" of a single JsonObject (I'll be using System.Json for this example). The class will parse the values try to infer the type (this is a very simple algorithm, obviously a more complex one is needed for more complex data types). It will then wire up a new property on the custom type and provide a function for conversion to the target type. Another method given a JsonValue and a delegate to the property setter will parse the value out and set the property.

public class JsonHelper<T> where T: ICustomTypeProvider
{
    private readonly IEnumerable<string> _keys = Enumerable.Empty<string>();
    private readonly Dictionary<string,Func<object,object>> _converters = new Dictionary<string, Func<object, object>>();

    public JsonHelper(IDictionary<string,JsonValue> template)
    {
        _keys = (from k in template.Keys select k).ToArray();

        foreach (var key in template.Keys)
        {
            int integerTest;
            double doubleTest;
            var value = template[key].ToString();
            if (int.TryParse(value, out integerTest))
            {
                CustomTypeHelper<T>.AddProperty(key, typeof(int));
                _converters.Add(key, obj => int.Parse(obj.ToString()));
            }
            else if (double.TryParse(value, out doubleTest))
            {
                CustomTypeHelper<T>.AddProperty(key, typeof(double));
                _converters.Add(key, obj => double.Parse(obj.ToString()));
            }
            else
            {
                CustomTypeHelper<T>.AddProperty(key, typeof(string));                    
                _converters.Add(key, obj =>
                                            {
                                                // strip quotes
                                                var str = obj.ToString().Substring(1);
                                                return str.Substring(0, str.Length - 1);
                                            });
            }
        }
    }

    public void MapJsonObject(Action<string,object> setValue, JsonValue item)
    {
        foreach (var key in _keys)
        {
            setValue(key, _converters[key](item[key]));
        }
    }
}

Now that we have the helpers in place to parse the JSON, it is possible to expose a collection and parse the JSON file:

public List<CustomType> Items { get; private set; }

public JsonViewModel()
{
    Items = new List<CustomType>();

    using (
        var stream =
            typeof (JsonViewModel).Assembly.GetManifestResourceStream(
                typeof (JsonViewModel).FullName.Replace
                ("ViewModels.JsonViewModel", 
                 "SampleData.Data.json")))
    {
        var jsonArray = JsonValue.Load(stream) as JsonArray;

        if (jsonArray == null) return;

        var template = jsonArray[0] as JsonObject;

        if (template == null) return;

        var jsonHelper = new JsonHelper<CustomType>(template);

        foreach (var item in jsonArray)
        {
            var customType = new CustomType();

            jsonHelper.MapJsonObject(customType.SetPropertyValue, item);                            
            Items.Add(customType);
        }
    }
}

The stream is an embedded resource and it simply infers from the location of the view model where the location of the sample data will be - you could easily use a constant or other means of locating the embedded resource. It then grabs the first entry, creates the helper class with the template, then iterates the items and loads the type.

Now the exposed list can be databound to a grid:

<Controls:DataGrid HorizontalAlignment="Center" VerticalAlignment="Center" AutoGenerateColumns="True" ItemsSource="{Binding Items}" Grid.Row="1"/>

And when we run the example, we get exactly what we want - the JSON data in nice, formatted columns that can be sorted, grouped, or otherwise manipulated as first class types:

Where's the Source?

I'll be giving a talk in Knoxville at CodeStock 2011 on Friday, June 3rd about Silverlight 5 for Line of Business Applications. If you're able to join me, please do - as of this post there were less than 100 tickets remaining to the event. Otherwise, keep your eyes on this blog. As part of that talk I've created a large application based on the Jounce framework that demonstrates most of the new Silverlight 5 features that are available as of the beta. I'll post the code and slides after. As a bonus, if you do attend, check out my colleague Rik Robinson's Introduction to Jounce for Silverlight.

Jeremy Likness

5 comments:

  1. WOW!
    Although there is not DLR support in SL, this is break news indeed.

    ReplyDelete
  2. Is there a project for this one?

    ReplyDelete
  3. Oops, I noticed your comment about the source code being in a bigger context later.

    When you connect this capability with implicit data templates, you're cooking with gas!

    ReplyDelete
  4. Last thing about this code.

    1. I put it together, but used your json as a string, and just parsed it into a JsonArray for testing.

    2. I wonder how this would handle dates, since they come across in a funny format.

    Finally, the code works for the most part, however, I can see that this isn't the code you're using, because this wouldn't compile,

    public static void AddProperty(String name, Type propertyType, List attributes){
    ...

    Of course, it should be List

    ReplyDelete
  5. Code works fine, but instead of using the delegate pattern for CustomTypeHelper, I derived from it:

    public class CustomType : CustomTypeHelper
    {
    public CustomType()
    {

    }

    [Display(AutoGenerateField=false)]
    public Guid Id { get; set; }

    public override bool Equals(object obj)
    {
    return obj is CustomType && ((CustomType)obj).Id.Equals(Id);
    }

    public override int GetHashCode()
    {
    return Id.GetHashCode();
    }

    ReplyDelete