Download JsonMVC.zip - 77.06 KB

Introduction

This article uses a simple example to answer some common questions when working on Json objects in jQuery and MVC.

Background

"JSON" (JavaScript Object Notation) is a lightweight text-based open standard designed for human-readable data interchange. When working together with "jQuery" and "ASP.net MVC" in building web applications, it provides an efficient mechanism to exchange data between the web browser and the web server.

JsonSerializationBetweenServerBrowser.jpg

At the browser side, the data is stored and manipulated as "javascript" "Json objects". At the server side, if ASP.net MVC is used, the data is stored and manipulated as ".Net objects".

  • When the browser loads data from the server, the .Net objects need to be "serialized" into "Json strings" and passed to the browser. The browser will "de-serialize" the "Json strings" into easy to use javascript "Json objects".
  • When the browser sends data to the server, the "Json objects" needs to be "serialized" into "Json strings" and then "de-serialized" into ".Net objects" at the server side.

This article is to answer the following questions in the context of "jQuery" and "ASP.net MVC" with an example "ASP.net MVC" project:

  • How to initiate a request from the browser to the server to ask for some data as javascript "Json objects"?
  • How does the server serialize the ".Net objects" and send it to the browser?
  • How does the browser serialize the javascript "Json objects" and send them to the server?
  • How does the server "de-serialize" the "Json strings" into ".Net objects"?

Besides answering these questions, this example project also shows how "session" can be used in "ASP.net MVC" outside the "controllers". The simple example project looks like the following in the "Solution Explorer":

SolutionExplorerFinal.JPG

The main building blocks of the application are the following:

  • The class definitions of the .Net objects, which will also serve as the templates for the "Json objects" are implemented in the "Models\StudentModel.cs" file.
  • The "ASP.net MVC" "ViewPage" "Index.aspx". It is the only "ViewPage" in the web application. All the client side javascript code is implemented in this page.
  • Two "controller" classes in the "Controllers" folder. The "HomeController" class is the "controller" that loads the "Index.aspx". The "JsonOperationsController" class is the center place at the server side to process the "Json objects" posted from the browsers and to deliver all the "Json objects" to the browsers.
  • The "StudentsRepository" class serves as the repository of the application's data using the web application's "session" state.

This example project is developed in Visual Studio 2010 and "MVC 2". The "jQuery" version is "1.4.2". To build the UI of the application in the "Index.aspx", the "jQuery treeview plugin" is used. This "plugin" is used to display the application's data in a "tree" structure in the browser.  This article assumes the readers having some basic knowledge on "MVC", "jQuery", and "Json". If you are new to these subjects, you should be able to easily find reference materials.

We will first take a look at the model classes in this application, so you will know the data being passed between the browser and the server. We will then take a look at the server side and the client side code. At the end, we will give answers to our questions to conclude this article.

The .Net "model" classes

In order to make the structure of the data being passed between the browser and the server complex enough, three .Net classes are implemented in the "StudentModel.cs" file:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
 
namespace JsonMVC.Repositories
{
    public class Student
    {
        public string Name { get; set; }
        public IList<Class> ClassesTaken { get; set; }
    }
 
    public class Class
    {
        public string Name { get; set; }
        public IList<Exam> ExamesTaken { get; set; }
    }
 
    public class Exam
    {
        public DateTime ExamTime { get; set; }
        public string Description { get; set; }
        public int Score { get; set; }
    }
}

These three classes are "Student", "Class", and "Exam". The data being passed between the browser and the server are "Lists" of "Student" objects. Each "Student" object can have multiple "Class" objects referenced by the public "property" "ClassesTaken", and each "Class" object can have multiple "Exam" objects referenced by the public "property" "ExamesTaken".

The "StudentsRepository" class

To demonstrate web "session" can be used outside "MVC" "controllers", the application data at the server side is kept in the "session" by the "StudentsRepository" class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using JsonMVC.Models;
 
namespace JsonMVC.Repositories
{
    public class StudentsRepository
    {
        public void Initialize(int NoOfStudents,
            int NoOfClasses, int NoOfExams)
        {
            List<Student> students = new List<Student>();
            Random rd = new Random();
            for (int iStudent = 0; iStudent < NoOfStudents; iStudent++)
            {
                Student aStudent = new Student();
                aStudent.Name = "Student Name No. " +
                    iStudent.ToString();
                aStudent.ClassesTaken = new List<Class>();
                for (int iClass = 0; iClass < NoOfClasses; iClass++)
                {
                    Class aClass = new Class();
                    aClass.Name = "Class No. " + iClass.ToString();
                    aClass.ExamesTaken = new List<Exam>();
                    for (int iExam = 0; iExam < NoOfExams; iExam++)
                    {
                        Exam aExam = new Exam();
                        aExam.ExamTime = System.DateTime.Now;
                        aExam.Description = "Exam No. " +
                            iExam.ToString();
                        aExam.Score
                            = Convert.ToInt16(60 + rd.NextDouble() * 40);
 
                        aClass.ExamesTaken.Add(aExam);
                    }
 
                    aStudent.ClassesTaken.Add(aClass);
                }
 
                students.Add(aStudent);
            }
 
            HttpContext.Current.Session["Students"] = students;
        }
 
        public IList<Student> GetStudents()
        {
            return (List<Student>)
                HttpContext.Current.Session["Students"];
        }
 
        public void SetStudents(IList<Student> students)
        {
            HttpContext.Current.Session["Students"] = students;
        }
    }
}

Three public methods are implemented in the "StudentsRepository" class:

  • The method "Initialize" is to initialize a "List" of "Student" objects by some randomly generated data according to the input parameters. The randomly generated "Student" "List" is then saved into the "session" state.
  • The method "GetStudents" returns the "Student" "List" retrieved from the "session".
  • The method "SetStudents" take a "Student" "List" from the input parameter and save it into the "session".

The "HomeController"

The "HomeController" is the "MVC" "controller" to load the application's single "ViewPage" "Index.aspx".

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Collections.Specialized;
using System.Configuration;
 
namespace JsonMVC.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            NameValueCollection appSettings
                = ConfigurationManager.AppSettings;
 
            ViewData["ApplicationTitle"] = appSettings["ApplicationTitle"];
            return View();
        }
    }
}

It takes the application's title configured in the "Web.config" and passes it to the "Index.aspx" in the form of "ViewData".

The "JsonOperationsController"

The "JsonOperationsController" "controller" is one of the focal points in this article. All the server actions related to communicating with the web browser using "Json" are implemented here.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using JsonMVC.Models;
using JsonMVC.Repositories;
 
namespace JsonMVC.Controllers
{
    public class JsonOperationsController : Controller
    {
        // Utility function to add a exam to each class for a list of students
        private void AddAnExamToStudents(IList<Student> students, string updateSource)
        {
            Random rd = new Random();
            foreach (Student aStudent in students)
            {
                foreach (Class aClass in aStudent.ClassesTaken)
                {
                    IList<Exam> exames = aClass.ExamesTaken;
                    Exam aExam = new Exam();
                    aExam.ExamTime = System.DateTime.Now;
                    aExam.Description = "Exam No. " +
                        exames.Count.ToString()
                        + " by " + updateSource;
                    aExam.Score
                        = Convert.ToInt16(60 + rd.NextDouble() * 40);
 
                    exames.Add(aExam);
                }
            }
        }
 
        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult GetStudents(int NoOfStudents,
            int NoOfClasses, int NoOfExams)
        {
            StudentsRepository repository = new StudentsRepository();
            repository.Initialize(NoOfStudents, NoOfClasses, NoOfExams);
 
            return Json(repository.GetStudents());
        }
 
        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult AddAnExamByJson(IList<Student> students)
        {
            StudentsRepository repository = new StudentsRepository();
            repository.SetStudents(students);
 
            AddAnExamToStudents(students, "json");
            return Json(students);
        }
 
        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult AddAnExamBySession()
        {
            StudentsRepository repository = new StudentsRepository();
            IList<Student> students = repository.GetStudents();
 
            AddAnExamToStudents(students, "session");
            return Json(students);
        }
    }
}

The three public "ActionResult" methods are "GetStudents", "AddAnExamByJson".

  • The "GetStudents" method takes an integer input and returns a "List" of "Student" objects to the browser.
  • The "AddAnExamByJson" method takes a "List" of "Student" objects from the browser and adds an "Exam" object to each "Class" object taken by the students. It saves the "List" into the "session" and then returns the "List" of the "Student" objects back to the browser.
  • The "AddAnExamBySession" method takes no parameter. It gets the "List" of "Student" objects from the web "session" using the "StudentsRepository" class. It adds an "Exam" object to each "Class" taken by the students and returns the "List" of the "Student" objects to the browser.

All the three methods send "Json" objects to the browser using "JsonResult". Each of them shows one of the three cases when receiving data from the browser.

  • The "AddAnExamBySession" takes no data from the browser.
  • The "GetStudents" takes a scalar input data.
  • The "AddAnExamByJson" takes a "List" of complex objects.

The "MVC" View "Index.aspx"

The code that communicates with the methods in the "JsonOperationsController" at the client side is implemented in the "Index.aspx":

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<dynamic>" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <title>Working on Json Arrays in MVC</title>
    <link rel="SHORTCUT ICON"
        href="<%= Url.Content("~/Content/Images/rubik.ico") %>" />
    <link rel="stylesheet"
        href="<%= Url.Content("~/Content/Styles/Site.css") %>"
        type="text/css" />
    <link rel="stylesheet"
        href="<%= Url.Content("~/Content/Styles/jquery.treeview.css") %>"
        type="text/css" />
     
    <script src="<%= Url.Content("~/Scripts/jquery-1.4.2.min.js") %>"
        type="text/javascript">
    </script>
    <script src="<%= Url.Content("~/Scripts/jquery.treeview.js") %>"
        type="text/javascript">
    </script>
 
    <script type="text/javascript" language="javascript">
        var GetStudentsURL
            = '<%: Url.Action("GetStudents", "JsonOperations") %>';
        var AddAnExamBySessionURL
            = '<%: Url.Action("AddAnExamBySession", "JsonOperations") %>';
        var AddAnExamByJsonURL
            = '<%: Url.Action("AddAnExamByJson", "JsonOperations") %>';
        var StudentJson = null;
        var NoOfStudents = 3, NoOfClasses = 1, NoOfExams = 0;
 
        var prependZero = function (v) {
            v = v.toString();
            return (v.length == 1) ? "0" + v : v;
        };
 
        var dateDeserialize = function (dateStr) {
            var dt = new Date(parseInt(dateStr.substr(6)));
            return prependZero((dt.getMonth() + 1))
                + "/" + prependZero(dt.getDate())
                + "/" + dt.getFullYear()
                + " " + prependZero(dt.getHours())
                + ":" + prependZero(dt.getMinutes())
                + ":" + prependZero(dt.getSeconds());
        };
 
        var FixDateinJson = function (JsonStudents) {
            $.each(JsonStudents, function (i, JsonStudent) {
                $.each(JsonStudent.ClassesTaken, function (i, JsonClass) {
                    $.each(JsonClass.ExamesTaken, function (i, JsonExam) {
                        JsonExam.ExamTime = dateDeserialize(JsonExam.ExamTime);
                    });
                });
            });
 
            return JsonStudents;
        }
 
        var buildStudentTree = function (students) {
 
            var createTextNode = function (text) {
                var span = document.createElement("span");
                span.setAttribute("style", "margin-left: 2px");
                var tx = document.createTextNode(text);
                span.appendChild(tx);
 
                return span;
            };
 
            var root = document.createElement("ul");
            root.id = "StudentTreeRoot";
            root.setAttribute("style", "margin: 15px");
            root.className = "filetree";
            $.each(students, function (i, student) {
                var studentNode = document.createElement("li");
                //studentNode.className = "closed";
                var span = document.createElement("span");
                span.className = "folder";
                span.appendChild(createTextNode(student.Name));
                studentNode.appendChild(span);
 
                var classesNode = document.createElement("ul");
                $.each(student.ClassesTaken, function (i, aClass) {
                    var classNode = document.createElement("li");
                    //classNode.className = "closed";
                    span = document.createElement("span");
                    span.className = "folder";
                    span.appendChild(createTextNode(aClass.Name))
                    classNode.appendChild(span);
 
                    var examesNode = document.createElement("ul");
                    examesNode.className = "folder";
 
                    $.each(aClass.ExamesTaken, function (i, aExam) {
                        var examNode = document.createElement("li");
                        //examNode.className = "closed";
                        span = document.createElement("span");
                        span.className = "folder";
                        span.appendChild(createTextNode(aExam.Description));
                        examNode.appendChild(span);
 
                        var detailNode = document.createElement("ul");
                        var examTime = document.createElement("li");
                        span = document.createElement("span");
                        span.className = "file";
                        span.appendChild(createTextNode(aExam.ExamTime));
                        examTime.appendChild(span);
                        detailNode.appendChild(examTime);
 
                        var score = document.createElement("li");
                        span = document.createElement("span");
                        span.className = "file";
                        span.appendChild(createTextNode(aExam.Score));
                        score.appendChild(span);
                        detailNode.appendChild(score);
 
                        examNode.appendChild(detailNode);
 
                        examesNode.appendChild(examNode);
                    }); 
 
                    classNode.appendChild(examesNode)
 
                    classesNode.appendChild(classNode);
                });
 
                studentNode.appendChild(classesNode);
                root.appendChild(studentNode);
            });
 
            $("#StudentTree").html("").append(root);
            $("#StudentTreeRoot").treeview();
        };
 
        $(document).ready(function () {
            $("#StudentTree").html("");
            $.ajax({
                cache: false,
                type: "POST",
                async: false,
                url: GetStudentsURL
                    + "/?NoOfStudents=" + NoOfStudents
                    + "&NoOfClasses=" + NoOfClasses
                    + "&NoOfExams=" + NoOfExams,
                dataType: "json",
                success: function (students) {
                    StudentJson = FixDateinJson(students);
                    buildStudentTree(StudentJson);
                }
            });
 
            $("#btnAddAnExamJson").click(function () {
                $("#StudentTree").html("Loading ...");
 
                $.ajax({
                    cache: false,
                    type: "POST",
                    url: AddAnExamByJsonURL,
                    contentType: 'application/json',
                    dataType: "json",
                    data: JSON.stringify(StudentJson),
                    success: function (students) {
                        StudentJson = FixDateinJson(students);
                        buildStudentTree(StudentJson);
                    }
                });
            });
 
            $("#btnAddAnExamSession").click(function () {
                $("#StudentTree").html("Loading ...");
 
                $.ajax({
                    cache: false,
                    type: "POST",
                    url: AddAnExamBySessionURL,
                    dataType: "json",
                    success: function (students) {
                        StudentJson = FixDateinJson(students);
                        buildStudentTree(StudentJson);
                    }
                });
            });
        });
    </script>
</head>
 
<body>
    <div id="TitleContainer"><%= ViewData["ApplicationTitle"]%></div>
    <div id="MainContainer">
        <div id="StudentTree"></div>
        <div id="ButtonContainer">
            <button id="btnAddAnExamSession" class="ButtonStyle">
                Add an exam to students using session</button>
            <button id="btnAddAnExamJson" class="ButtonStyle">
                Add an exam to students by posting Json</button>
        </div>
    </div>
</body>
</html>

The "html" part of "Index.aspx" is very simply, but you should give some attention to the following components:

  • The "button" control "btnAddAnExamSession". This "button" triggers a "jQuery" "Ajax" call to the "AddAnExamBySession" method in the "JsonOperationsController".
  •  "The "button" control "btnAddAnExamJson". This "button" triggers a "jQuery" "Ajax" call to the "AddAnExamByJson" method in the "JsonOperationsController".
  • The "div" "StudentTree" is the place holder where we will be displaying the "List" of the "Student" objects received from the server using the "jQuery treeview plugin".

The majority of the logic in the "Index.aspx" is implemented in "javascript" with "jQuery":

  • In the "$(document).ready()" event, a "synchronous" "Ajax" call is made to the "GetStudents" method in the "JsonOperationsController" using "jQuery". The returned "List" of "Student" objects is already "de-serialized" into an array of "Json" objects by "jQuery". This "Json" array is then saved in the global "javascript" variable "StudentJson" to be processed later. It is also used by the function "buildStudentTree" to build a "treeview". The "treeview is built" on top of the "StudentTree" "div" using the "jQuery treeview plugin".
  • In the click event of the "btnAddAnExamSession" button, an "asynchronous" "Ajax" call is made to the "AddAnExamBySession" method in the "JsonOperationsController". The "treeview" is then refreshed by the newly received student list.
  • In the click event of the "btnAddAnExamJson" button, an "asynchronous" "Ajax" call is made to the "AddAnExamByJson" method in the "JsonOperationsController". The "treeview" is then refreshed by the newly received student list.

The "javascript" code in the in the "Index.aspx" demonstrated the following:

  • "jQuery" "Ajax" calls can be made "synchronous" and "asynchronous". The default is an "asynchronous" call, but you can specify "async: false" to make it a "synchronous". Making a "synchronous" "jQuery" "Ajax" call is not the focus of this article, but it may be useful under certain circumstances.
  • Each of the three "jQuery" "Ajax" calls demonstrates one of the three cases, i.e. passing no data to the server, passing a scalar data item to the server, and passing an array of "Json" objects to the server.

The Add-on to the default "MVC" "Model Binder"

Now we have finished the coding on both the client and server sides to show how to work on "Json" objects with "jQuery" and "MVC". For the most part, it would work fine. But the default "MVC" "ModelBinder" does not "de-serialize" arrays of "Json" objects. This means that we will fail to send the array of the "Student" "Json" objects to the "AddAnExamByJson" method. To address this problem, we need to make change to the "Global.asax.cs" file:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
 
namespace JsonMVC
{
    public class JsonModelBinder : DefaultModelBinder
    {
        public override object BindModel(ControllerContext controllerContext,
            ModelBindingContext bindingContext)
        {
            // If the request is not of content type "application/json"
            // then use the default model binder.
            if (!IsJSONRequest(controllerContext))
            {
                return base.BindModel(controllerContext, bindingContext);
            }
 
            // Get the JSON data posted
            var request = controllerContext.HttpContext.Request;
            var jsonStringData =
                new System.IO.StreamReader(request.InputStream).ReadToEnd();
 
            return new System.Web.Script.Serialization.JavaScriptSerializer()
                .Deserialize(jsonStringData, bindingContext.ModelMetadata.ModelType);
        }
 
        private bool IsJSONRequest(ControllerContext controllerContext)
        {
            var contentType = controllerContext.HttpContext.Request.ContentType;
            return contentType.Contains("application/json");
        }
    }
 
    public class MvcApplication : System.Web.HttpApplication
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
            routes.MapRoute("Default", "{controller}/{action}/{id}",
                new
                {
                    controller = "Home",
                    action = "Index",
                    id = UrlParameter.Optional
                });
        }
 
        protected void Application_Start()
        {
            // Set the model binder
            ModelBinders.Binders.DefaultBinder = new JsonModelBinder();
            AreaRegistration.RegisterAllAreas();
            RegisterRoutes(RouteTable.Routes);
        }
 
        protected void Application_BeginRequest(object sender, EventArgs e)
        {
            // It is OK to cache the jQuery library, images, etc.
            string FilePath = HttpContext.Current.Request.FilePath;
            if (FilePath.Contains("jquery-1.4.2.min.js") ||
                FilePath.Contains("rubik.ico") ||
                FilePath.Contains(".jpg") ||
                FilePath.Contains(".css"))
                return;
 
            HttpContext.Current.Response.Cache.SetCacheability(HttpCacheability.NoCache);
            HttpContext.Current.Response.Cache.SetNoStore();
        }
    }
}

In the "Global.asax.cs", a class "JsonModelBinder" is implemented. The "BindModel" method in this class first checks the "content type" of the http "request". If it is "application/json", the "JavaScriptSerializer" is used to "de-serialize" the posted content. Otherwise, the default "serializer" is used, so it will not alter the normal "MVC" behavior. I learned this technique from here. If you are interested, you can take a further look at it.

Run the application

Now we are ready to test run the application. At the time when "Index.aspx" loads, a "synchronous" "jQuery" "Ajax" call is made to retrieve a list of students "Json" objects from the server by calling the "GetStudents" method in the "JsonOperationsController". The list of students is then displayed in a "treeview" by the "jQuery treeview plugin":

ApplicationStart.JPG

We can then click on the buttons "Add an exam to students by posting Json" and "Add an exam to students using session". Each button click will issue an "asynchronous" "jQuery" "Ajax" call either to the method "AddAnExamByJson" or the method "AddAnExamBySession" in the "JsonOperationsController". The "treeview" is refreshed with the new content:

ApplicationRun.JPG

From the above screenshot, we can see that the "Json" objects are successfully posted to the "JsonOperationsController" and the "session" state managed by the "StudentsRepository" class also works properly.

Conclusion

To conclude this article, I will be answering the questions listed at the beginning:

  • How to initiate a request from the browser to the server to ask for some data as javascript "Json objects"?
  • How does the server serialize the ".Net objects" and send it to the browser?
    • In a "MVC" "controller" method, it can simply return a "JsonResult" as the "ActionResult". The "MVC" framework will handle the "serialization" work.
    • The syntax is as simple as "return Json(object)", where the "object" is a .NET object to be "serialized" and sent to the browser.
  • How does the browser serialize the javascript "Json objects" and send them to the server?
  • How does the server "de-serialize" the "Json strings" into ".Net objects"?

Points of Interest

  • This article used a simple example to answer some common questions when working on Json objects in jQuery and MVC.
  • Besides answering the questions, this article also demonstrated using "session" state outside the "MVC" "controllers" and worked an example to use "jQuery treeview plugin" to dynamically generate "treeviews" from the data in the "Json" objects.
  • There are many ways to exchange data with "Json" between the browser and the server. The method introduced in this article is just one of them. When working with "jQuery" and "MVC", I believe that the method introduced here is a simple method among them.
  • I hope you like my postings and I hope this article can help you one way or the other.

History

This is the first revision of this article.