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.
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" need 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 them 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 Solution Explorer:
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 that 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 loadsIndex.aspx. TheJsonOperationsController
class is the center place at the server side to process theJSON 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 Index.aspx, the "jQuery treeview plug-in" is used. This "plug-in" 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:
Hide Copy Code
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 List
s 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 publicproperty 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:
Hide Shrink Copy Code
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 aList
ofStudent
objects by some randomly generated data according to the input parameters. The randomly generatedStudent
"List
" is then saved into thesession state. - The method
GetStudents
returns theStudent
"List
" retrieved from the session. - The method
SetStudents
takes aStudent
"List
" from the input parameter and saves it into thesession.
The "HomeController"
The
HomeController
is the MVC "controller" to load the application's single ViewPage Index.aspx.
Hide Copy Code
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 ofViewData.
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.
Hide Shrink Copy Code
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:- The
GetStudents
method takes an integer input and returns aList
ofStudent
objects to the browser. - The
AddAnExamByJson
method takes aList
ofStudent
objects from the browser and adds anExam
object to each "Class" object taken by the students. It saves theList
into the session and then returns theList
of theStudent
objects back to the browser. - The
AddAnExamBySession
method takes no parameter. It gets theList
ofStudent
objects from the web session using theStudentsRepository
class. It adds anExam
object to each "Class" taken by the students, and returns theList
ofStudent
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.AddAnExamBySession
takes no data from the browser.GetStudents
takes a scalar input data.AddAnExamByJson
takes aList
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":
Hide Shrink Copy Code
<%@ 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 simple, but you should give some attention to the following components:
- The button control
btnAddAnExamSession
. This button triggers a "jQuery" AJAX call to theAddAnExamBySession
method inJsonOperationsController
. - The button control "
btnAddAnExamJson
. This button triggers a "jQuery" AJAX call to theAddAnExamByJson
method inJsonOperationsController
. - The div
StudentTree
is the place holder where we will be displaying theList
of theStudent
objects received from the server using the jQuery treeview plug-in.
The majority of the logic in Index.aspx is implemented in JavaScript with jQuery:
- In the
$(document).ready()
event, a synchronous AJAX call is made to theGetStudents
method inJsonOperationsController
using jQuery. The returnedList
ofStudent
objects is already "de-serialized" into an array of JSON objects by jQuery. This JSON array is then saved in the global JavaScript variableStudentJson
to be processed later. It is also used by the functionbuildStudentTree
to build a treeview. The treeview is built on top of theStudentTree
div using the jQuery treeview plug-in. - In the click event of the
btnAddAnExamSession
button, an asynchronous AJAX call is made to theAddAnExamBySession
method inJsonOperationsController
. 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 theAddAnExamByJson
method inJsonOperationsController
. The treeview is then refreshed by the newly received student list.
The JavaScript code in Index.aspx demonstrates the following:
- The jQuery "AJAX" calls can be made "synchronous" and "asynchronous". The default is an asynchronous call, but you can specify "async: false" to make it 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 withjQuery and MVC. For the most part, it would work fine. But the default MVC "ModelBinder" does not de-serializearrays of JSON objects. This means that we will fail to send the array of
Student
JSON objects to theAddAnExamByJson
method. To address this problem, we need to make changes to the "Global.asax.cs" file:
Hide Shrink Copy Code
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 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", 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 student JSON objects from the server by calling the
GetStudents
method inJsonOperationsController
. The list of students is then displayed in a treeview by the jQuery treeview plug-in:
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 JsonOperationsController
. The treeview is refreshed with the new content:
From the above screenshot, we can see that the JSON objects are successfully posted to
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?
- The jQuery "AJAX" method can initiate a call to an MVC "controller" to ask for data as JavaScriptJSON objects.
- The jQuery "AJAX" call can be made both "synchronously" and "asynchronously".
- How does the server serialize the ".NET objects" and send them to the browser?
- In the MVC "controller" method, it can simply return a
JsonResult
as theActionResult
. TheMVC framework will handle the serialization work. - The syntax is as simple as "return Json(object)", where "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?
- The jQuery "AJAX" method allows the browser to post JSON objects to an MVC "controller" in the server.
- How does the server "de-serialize" the JSON strings into .NET objects?
- The default MVC "ModelBinder" does not de-serialize arrays of JSON objects. If you want the MVC"controller" methods to always receive JSON objects properly, you can use the method introducedhere.
- When using the modified MVC "ModelBinder", you need to specify the content type as "application/json", and use
JSON.stringify
to serialize the JSON objects when making the jQuery"AJAX" call.
No comments:
Post a Comment