Tuesday, March 31, 2009

JSON and C# using Generics and Delegates

JSON or JavaScript Object Notation, is a method for transferring data, similar to XML and other formats. There are many advantages to using this method. It is human readable, and it translates easily to objects on the client side of the browser.

With JSON, I can declare an array like this:

var myArray = ["this","that","the other"]; 

The object notation is even simpler. Imagine a C# "Person" class:

public class Person 
{
   public int ID { get; set; } 
   public string FirstName { get; set; }
   public string LastName { get; set; }
   public string SSN { get; set; }
}

If I want to express this data to the client, I can do something like this:

var person = { 
   "ID" : "1", 
   "FirstName" : "Jeremy",
   "LastName" : "Likness",
   "SSN" : "111-222-3333" 
};
alert("My name is " + person.FirstName + " " + person.LastName); 

One criticism of the WebForms view engine (the one most of you are using in .NET ... while other view engines such as NVelocity have existed as part of the MonoRail project, Microsoft only recently released the MVC release candidate which introduces the Model-View-Controller view engine) is that it overburdens the client when rendering simple tags. If you have compared <input type="text"> with an <asp:TextBox>, you get the point.

Furthermore, developers tend to get lost in the "ease" of the AJAX framework and often fail to consider the performance implications of wrapping, say, cascading drop downs in an update panel. A much easier method is to serialize the values and bind them using a third-party tool like JQuery instead.

While I was working on this framework I realized it would be quite simple to serialize my domain objects for JSON. I wanted something generic, that didn't rely on reflection for abstraction, and gave me full control over which attributes to render without having to dirty my domain model with attributes or other declarations for something that is entirely UI-related.

The result? A "JSONObject" with a Serialize() method that emits the JSON code. Here is the class:

namespace MyJSON 
{
    /// <summary>
    ///     Delegate to determine how a value is pulled from the object
    /// </summary>
    /// <param name="instance">The instance to pull the property from</param>
    /// <param name="property">The value of the instance</param>
    /// <returns>The string representation of the value for that property</returns>
    public delegate string PropertyValue(T instance, string property);

    /// <summary>
    ///     Class to help serialize objects to the client
    /// </summary>
    public class JSONObject<T> 
    {        
        /// <summary>
        ///     The object to serialize
        /// </summary>
        private readonly T _object;

        /// <summary>
        ///     A list of properties to serialize
        /// </summary>
        private readonly List _properties;

        /// <summary>
        ///     Reference to delegate to parse the value
        /// </summary>
        private readonly PropertyValue _propValue; 
               
        /// <summary>
        ///     Constructor for JSON object
        /// </summary>
        /// <param name="instance">The entity instance</param>
        /// <param name="properties">The list of properties to serialize</param>
        /// <param name="propValue">The method to extract the property value</param>
        public JSONObject(T instance, IEnumerable<string> properties, PropertyValue<T> propValue)
        {
            _object = instance;
            _properties = new List(properties);
            _propValue = propValue; 
        }

        /// <summary>
        ///     Serialize to the JSON representation
        /// </summary>
        /// <returns>The JSON representation</returns>
        public string Serialize()
        {
            StringBuilder json = new StringBuilder();

            json.Append("{");

            bool first = true;

            foreach(string prop in _properties)
            {
                if (first)
                {
                    first = false;
                }
                else
                {
                    json.Append(","); 
                }
                string value = _propValue(_object, prop);
                json.Append(string.Format("\"{0}\":{1}", prop, EncodeJsString(value))); 
            }

            json.Append("}"); 

            return json.ToString(); 
        }
   }
}

The JSON object takes a type of T which can be anything. The constructor takes in "T", along with an array and a delegate. The array is a list of strings describing the properties I am interested in. For example, I may only want to send id and last name because I don't use the other attributes in my client script. Finally, the delegate. The signature is simple: given the attribute and the entity, what is the value as a string? This allows me to inject the logic to map the UI attributes to the domain attributes. The easiest way to send the data is to realize it as a string and then manipulate it appropriately on the client. After all, JavaScript itself doesn't "know" about any of our complex objects.

The simplest use would be simply to send one of my Person objects and show the name. For example:

<b>ID: </b> <span id="personId"> </span>
<b>Name: </b> <span id="personName"> </span>
In my own script I simply make my JSON object and serialize it out:
JSONObject<Person> jsonPerson = new JSONObject<Person>(
   new Person { ID = 1, LastName="Likness" },
   new[] { "id", "name" }, 
   (entity,property) => property.Equals("id") ? entity.ID : entity.LastName
);
Page.ClientScript.RegisterClientScriptBlock(GetType(),GetType(),
   string.Format("var person={0}",jsonPerson.Serialize()),true);  

Finally, I need something to wire it in for me, I'll choose JQuery ...

<script type="text/javascript">
   $(document).ready(function(){
      $("#personId").html(person.id);
      $("#personName").html(person.name); 
   });
</script>

The script block takes a type (I'm just using the type of the page or control I'm in), a key (this should be unique per script block ... I'm using the type again here but could have it strongly typed or set as a const, etc), the JavaScript to emit (from our serialize method on the JSON object), and then the true tells it to wrap my script tags because I haven't done it myself.

These objects can come back as the result of a callback (just assign them to the window.var object so they are globally accessible and use an eval) for dynamic binding, or you might simply render an array of objects and then do something like this:

for (var x = 0; x < window.json_options.length; x++) {
            var option = window.json_options[x];
            $("#selBox").append("<option value=\"" +
                option.id + "\"" + +(first ? " checked=\"checked\"" : "") + ">" + option.value + "</option>");
        }

Which will bind the drop down list. If you are fighting with the urge to cry "hack" because we're wiring in HTML ... remember, this is what ASP.NET does under the covers for you. In the end, controls are complex text rendering engines that emit fragments the browser can manage.

Of course, the next step was to make a JSONList<T> and I purposefully left the encode script out, you can google some solutions for that.

Jeremy Likness

5 comments:

  1. Your code is broken, in lines 79 and 80

    string value = _propValue(_object, prop);
    json.Append(string.Format("\"{0}\":{1}", prop, EncodeJsString(value)));

    ReplyDelete
  2. I believe the issue is that I did not include the EncodeJsString method ... this is one you can Google and find some various ways, I believe there is a HttpUtility shipped with ASP.NET that will do the trick ... thanks!

    ReplyDelete
  3. This is code for EncodeJsString:

    public static string EncodeJsString(string s)
    {
    StringBuilder sb = new StringBuilder();
    sb.Append("\"");
    foreach (char c in s)
    {
    switch (c)
    {
    case '\'':
    sb.Append("\\\'");
    break;
    case '\"':
    sb.Append("\\\"");
    break;
    case '\\':
    sb.Append("\\\\");
    break;
    case '\b':
    sb.Append("\\b");
    break;
    case '\f':
    sb.Append("\\f");
    break;
    case '\n':
    sb.Append("\\n");
    break;
    case '\r':
    sb.Append("\\r");
    break;
    case '\t':
    sb.Append("\\t");
    break;
    default:
    int i = (int)c;
    if (i < 32 || i > 127)
    {
    sb.AppendFormat("\\u{0:X04}", i);
    }
    else
    {
    sb.Append(c);
    }
    break;
    }
    }
    sb.Append("\"");

    return sb.ToString();
    }

    ReplyDelete
  4. This is very moot now, however, as we can use the System.Json and the Json.Serialization namespaces.

    ReplyDelete
  5. My VS 2010 does not have Suystem.Json.

    ReplyDelete