在 jQuery 和 MVC 中处理 JSON 对象
本文使用一个简单的示例来回答在使用 jQuery 和 MVC 处理 JSON 对象时遇到的一些常见问题。
引言
本文使用一个简单的示例来回答在使用 jQuery 和 MVC 处理 JSON 对象时遇到的一些常见问题。
背景
"JSON" (JavaScript Object Notation) 是一种轻量级的、基于文本的开放标准,旨在实现人类可读的数据交换。在构建 Web 应用程序时,它与 "jQuery" 和 "ASP.NET MVC" 结合使用,提供了一种在 Web 浏览器和 Web 服务器之间高效交换数据的机制。
在浏览器端,数据被存储和操作为 "JavaScript" "JSON 对象"。在服务器端,如果使用 ASP.NET MVC,数据将被存储和操作为 ".NET 对象"。
- 当浏览器从服务器加载数据时,.NET 对象需要被 "序列化" 成 "JSON 字符串" 并传递给浏览器。浏览器将 "反序列化" 这些 "JSON 字符串" 以便于使用 JavaScript "JSON 对象"。
- 当浏览器将数据发送到服务器时,"JSON 对象" 需要被 "序列化" 成 "JSON 字符串",然后在服务器端被 "反序列化" 成 ".NET 对象"。
本文旨在结合一个示例 "ASP.NET MVC" 项目,在 "jQuery" 和 "ASP.NET MVC" 的上下文中回答以下问题:
- 如何从浏览器发起一个请求到服务器,以 JavaScript "JSON 对象" 的形式请求某些数据?
- 服务器如何序列化 ".NET 对象" 并将它们发送到浏览器?
- 浏览器如何序列化 JavaScript "JSON 对象" 并将它们发送到服务器?
- 服务器如何 "反序列化" JSON 字符串为 ".NET 对象"?
除了回答这些问题,这个示例项目还展示了如何在 "ASP.NET MVC" 的 "控制器" 之外使用 session。这个简单的示例项目在解决方案资源管理器中的样子如下:
应用程序的主要构建块包括:
- 将在 "Models\StudentModel.cs" 文件中实现的 .NET 对象的类定义,它们也将作为在 "Models\StudentModel.cs" 文件中实现的 JSON 对象的模板。
- Index.aspx 的 "ASP.NET MVC" 视图页面。这是 Web 应用程序中唯一的 视图页面。所有客户端 JavaScript 代码都实现了这个页面。
- Controllers 文件夹中的两个 控制器类。
HomeController
类是加载 Index.aspx 的 控制器。JsonOperationsController
类是服务器端处理从浏览器发布的 JSON 对象并将所有 JSON 对象发送到浏览器的中心位置。 StudentsRepository
类使用 Web 应用程序的 session 状态作为应用程序数据的存储库。
该示例项目是在 Visual Studio 2010 和 "MVC 2" 中开发的。 jQuery 版本是 "1.4.2"。要构建 Index.aspx 中的应用程序 UI,使用了 "jQuery treeview 插件"。这个 "插件" 用于在浏览器中以 树 结构显示应用程序数据。本文假设读者对 MVC、jQuery 和 JSON 有一些基本知识。如果您对这些主题不熟悉,应该可以很容易地找到参考资料。
我们将首先查看应用程序中的模型类,以便您了解在浏览器和服务器之间传递的数据。然后,我们将查看服务器端和客户端代码。最后,我们将回答我们的问题来结束本文。
.NET "模型" 类
为了使浏览器和服务器之间传递的数据结构足够复杂,在 StudentModel.cs 文件中实现了三个 .NET 类:
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; }
}
}
这三个类是 Student
、Class
和 Exam
。在浏览器和服务器之间传递的数据是 Student
对象的 List
。每个 Student
对象可以通过公共 "属性" ClassesTaken
引用多个 Class
对象,并且每个 Class
对象可以通过公共 属性 ExamesTaken
引用多个 Exam
对象。
StudentsRepository 类
为了演示 Web session 可以在 MVC "控制器" 之外使用,应用程序服务器端的数据由 StudentsRepository
类保存在 session 中。
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;
}
}
}
"StudentsRepository
" 类中实现了三个公共方法:
Initialize
方法用于根据输入参数,通过一些随机生成的数据初始化一个Student
对象List
。然后,随机生成的Student
"List
" 被保存在 session 状态中。GetStudents
方法返回从 session 中检索到的Student
"List
"。SetStudents
方法接受一个Student
"List
" 作为输入参数,并将其保存在 session 中。
"HomeController"
HomeController
是用于加载应用程序唯一 视图页面 Index.aspx 的 MVC "控制器"。
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();
}
}
}
它获取在 Web.config 中配置的应用程序标题,并通过 ViewData 的形式将其传递给 Index.aspx。
JsonOperationsController
JsonOperationsController
"控制器" 是本文的重点之一。所有与 Web 浏览器使用 JSON 进行通信的服务器端操作都在这里实现。
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);
}
}
}
三个公共 ActionResult
方法是:
GetStudents
方法接受一个整数输入,并向浏览器返回一个Student
对象List
。AddAnExamByJson
方法接受来自浏览器的Student
对象List
,并向学生所修的每个 "Class" 对象添加一个Exam
对象。它将List
保存到 session 中,然后将Student
对象List
返回给浏览器。AddAnExamBySession
方法不接受任何参数。它使用StudentsRepository
类从 Web session 中获取Student
对象List
。它向学生所修的每个 "Class" 对象添加一个Exam
对象,然后将Student
对象List
返回给浏览器。
所有这三个方法都使用 JsonResult
将 JSON 对象发送到浏览器。它们各自展示了接收来自浏览器数据的三种情况之一。
AddAnExamBySession
不接受来自浏览器的数据。GetStudents
接受一个标量输入数据。AddAnExamByJson
接受一个复杂对象的List
。
MVC 视图 Index.aspx
在客户端与 JsonOperationsController
中的方法进行通信的代码实现在 "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>
Index.aspx 的 HTML 部分非常简单,但您应该注意以下组件:
- 按钮控件
btnAddAnExamSession
。此 按钮 触发一个 "jQuery" AJAX 调用到JsonOperationsController
中的AddAnExamBySession
方法。 - 按钮控件 "
btnAddAnExamJson
"。此 按钮 触发一个 "jQuery" AJAX 调用到JsonOperationsController
中的AddAnExamByJson
方法。 StudentTree
div 是一个占位符,我们将使用 jQuery treeview 插件 在其中显示从服务器接收到的Student
对象List
。
Index.aspx 中的大部分逻辑是用 JavaScript 和 jQuery 实现的。
- 在
$(document).ready()
事件中,使用 jQuery 调用JsonOperationsController
中的GetStudents
方法,执行一次同步 AJAX 调用。返回的Student
对象List
已经被 jQuery "反序列化" 成 JSON 对象数组。这个 JSON 数组然后被保存在全局 JavaScript 变量StudentJson
中以供稍后处理。它也用于buildStudentTree
函数来构建一个树形视图。这个树形视图是基于StudentTree
div 使用 jQuery treeview 插件 构建的。 - 在
btnAddAnExamSession
按钮的点击事件中,会发起一个异步 AJAX 调用到JsonOperationsController
中的AddAnExamBySession
方法。然后,树形视图将使用新接收的学生列表进行刷新。 - 在
btnAddAnExamJson
按钮的点击事件中,会发起一个异步 AJAX 调用到JsonOperationsController
中的AddAnExamByJson
方法。然后,树形视图将使用新接收的学生列表进行刷新。
Index.aspx 中的 JavaScript 代码演示了以下内容:
- jQuery "AJAX" 调用可以是 "同步" 和 "异步" 的。默认是异步调用,但您可以通过指定 "async: false" 来使其同步。进行同步 jQuery "AJAX" 调用不是本文的重点,但在某些情况下可能有用。
- 三个 jQuery "AJAX" 调用中的每一个都演示了三种情况之一,即向服务器传递零数据、传递一个标量数据项,以及传递一个 JSON 对象数组到服务器。
对默认 MVC "Model Binder" 的补充
现在我们已经完成了客户端和服务器端的编码,展示了如何使用 jQuery 和 MVC 处理 JSON 对象。在大多数情况下,这将运行良好。但是,默认的 MVC "ModelBinder" 不会 反序列化 JSON 对象数组。这意味着我们将无法将 Student
JSON 对象数组发送到 AddAnExamByJson
方法。要解决此问题,我们需要对 "Global.asax.cs" 文件进行更改。
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();
}
}
}
在 Global.asax.cs 中,实现了一个名为 JsonModelBinder
的类。该类中的 BindModel
方法首先检查 HTTP 请求的 "内容类型"。如果它是 "application/json",则使用 JavaScriptSerializer 来 反序列化 发布的 内容。否则,将使用默认的 "序列化器",因此它不会改变正常的 MVC 行为。我从 这里 学到了这个技巧。如果您有兴趣,可以进一步了解。
运行应用程序
现在我们可以测试运行该应用程序了。当 Index.aspx 加载时,将通过调用 JsonOperationsController
中的 GetStudents
方法,发起一个同步 jQuery "AJAX" 调用来检索学生 JSON 对象列表。然后,该学生列表将由 jQuery treeview 插件 在树形视图中显示。
然后,我们可以点击 "Add an exam to students by posting JSON" 和 "Add an exam to students using session" 按钮。每次按钮点击都会发出一个异步 jQuery "AJAX" 调用,该调用会指向 JsonOperationsController
中的 AddAnExamByJson
方法或 AddAnExamBySession
方法。树形视图将使用新内容进行刷新。
从上面的屏幕截图可以看出, JSON 对象已成功发布到 JsonOperationsController
,并且由 StudentsRepository
类管理的 session 状态也正常工作。
结论
为了总结本文,我将回答开头列出的问题:
- 如何从浏览器发起一个请求到服务器,以 JavaScript JSON 对象的形式请求某些数据?
- jQuery "AJAX" 方法可以调用一个 MVC "控制器",以 JavaScript JSON 对象的形式请求数据。
- jQuery "AJAX" 调用既可以 "同步" 进行,也可以 "异步" 进行。
- 服务器如何序列化 ".NET 对象" 并将它们发送到浏览器?
- 在 MVC "控制器" 方法中,可以直接返回一个
JsonResult
作为ActionResult
。 MVC 框架将处理序列化工作。 - 语法很简单,即 "return Json(object)",其中 "object" 是要被序列化并发送到浏览器的 .NET 对象。
- 浏览器如何序列化 JavaScript JSON 对象并将它们发送到服务器?
- 服务器如何 "反序列化" JSON 字符串为 .NET 对象?
- 默认的 MVC "ModelBinder" 不会 反序列化 JSON 对象数组。如果您希望 MVC "控制器" 方法始终正确接收 JSON 对象,您可以使用 此处介绍的方法。
- 在使用修改后的 MVC "ModelBinder" 时,您需要将 内容类型指定为 "application/json",并在进行 jQuery "AJAX" 调用时使用
JSON.stringify
来序列化 JSON 对象。
关注点
- 本文使用了一个简单的示例来回答在使用 jQuery 和 MVC 处理 JSON 对象时遇到的一些常见问题。
- 除了回答问题之外,本文还演示了如何在 MVC "控制器" 之外使用 session 状态,并提供了一个示例来使用 "jQuery treeview 插件" 从 JSON 对象中的数据动态生成树形视图。
- 在浏览器和服务器之间交换 JSON 数据有很多方法。本文介绍的方法只是其中一种。在使用 jQuery 和 MVC 进行开发时,我认为这里介绍的方法是其中一个简单的选择。
- 希望您喜欢我的博文,并希望本文能以某种方式帮助到您。
历史
这是本文的第一个修订版。