Главная страница   /   21.8. Работа с JSON (ASP.NET MVC 4

ASP.NET MVC 4

ASP.NET MVC 4

Адам Фриман

21.8. Работа с JSON

До сих пор в наших Ajax примерах сервер обрабатывал HTML фрагменты и отправлял их браузеру. Это вполне приемлемая техника, но она громоздка (потому что мы посылаем HTML элементы вместе с данными), и она ограничивает то, что мы можем делать с данными в браузере.

Одним из способов решения обеих этих проблем является использование формата JSON (JavaScript Object Notation), который является независимым от языка способом выражения данных. Он возник из языка JavaScript, но давно уже начал свою собственную жизнь и очень широко используется. В этом разделе мы покажем вам, как создать метод действия, который возвращает JSON данные, а также как обрабатывать эти данные в браузере.

Совет

В главе 25 мы описываем Web API, который является альтернативным подходом для создания веб-сервисов.

Добавление поддержки JSON в контроллер

MVC фреймворк делает создание метода действия, который генерирует JSON данные, а не HTML, очень простым. В листинге 21-18 показано, как мы добавили такой метод действия в контроллер People.

Листинг 21-18: Метод действия, который генерирует данные JSON
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using HelperMethods.Models;
namespace HelperMethods.Controllers
{
	public class PeopleController : Controller
	{
		private Person[] personData = {
			new Person {FirstName = "Adam", LastName = "Freeman", Role = Role.Admin},
			new Person {FirstName = "Steven", LastName = "Sanderson", Role = Role.Admin},
			new Person {FirstName = "Jacqui", LastName = "Griffyth", Role = Role.User},
			new Person {FirstName = "John", LastName = "Smith", Role = Role.User},
			new Person {FirstName = "Anne", LastName = "Jones", Role = Role.Guest}
		};
		public ActionResult Index()
		{
			return View();
		}
		private IEnumerable<Person> GetData(string selectedRole)
		{
			IEnumerable<Person> data = personData;
			if (selectedRole != "All")
			{
				Role selected = (Role)Enum.Parse(typeof(Role), selectedRole);
				data = personData.Where(p => p.Role == selected);
			}
			return data;
		}
		public JsonResult GetPeopleDataJson(string selectedRole = "All")
		{
			IEnumerable<Person> data = GetData(selectedRole);
			return Json(data, JsonRequestBehavior.AllowGet);
		}
		public PartialViewResult GetPeopleData(string selectedRole = "All")
		{
			return PartialView(GetData(selectedRole));
		}
		public ActionResult GetPeople(string selectedRole = "All")
		{
			return View((object)selectedRole);
		}
	}
}

Поскольку мы хотим представить одни и те же данные в двух различных форматах (HTML и JSON), мы переделали наш контроллер так, чтобы был общий (и private) метод GetData, который несет ответственность за выполнение фильтрации.

Мы добавили новый метод действий GetPeopleDataJson, который возвращает объект JsonResult. Это особый вид ActionResult, который говорит движку представления, что мы хотим вернуть клиенту JSON данные, а не HTML. (Вы можете узнать больше о классе ActionResult и роли, которую он играет в MVC фреймворке, в главе 15).

Мы создаем JsonResult при помощи метода Json в методе действия, передавая данные, которые мы хотим преобразовать в формат JSON:

...
return Json(data, JsonRequestBehavior.AllowGet);
...

В данном случае мы также передали в AllowGet значение из перечисления JsonRequestBehavior. По умолчанию данные JSON будут отправлены только в ответ на запрос POST, но, передавая это значение в качестве параметра методу Json, мы говорим MVC фреймворку также реагировать на запросы GET.

Внимание

Вы должны использовать только JsonRequestBehavior.AllowGet, если данные, которые вы возвращаете, не являются закрытыми. В связи с проблемой безопасности во многих веб браузерах для сторонних сайтов становится возможным перехватывать данные JSON, которые вы возвращаете в ответ на запрос GET, поэтому JsonResult не будет отвечать на запросы GET по умолчанию. Вместо этого в большинстве случаев вы сможете использовать POST запросы для получения JSON данных, избегая проблемы. Для дополнительной информации см. http://haacked.com/archive/2009/06/25/json-hijacking.aspx.

Обработка JSON в браузере

Для обработки JSON мы возвращаемся к серверу приложений MVC фреймворка, мы определяем функцию JavaScript, используя свойство обратного вызова OnSuccess в классе AjaxOptions. В листинге 21-19 вы можете увидеть, как мы обновили файл представления GetPerson.cshtml, чтобы удалить функции обработчика, которые мы определили в предыдущем разделе, и использовали OnSuccess для обработки данных JSON.

Листинг 21-19: Работа с данными JSON в представлении GetPerson
@using HelperMethods.Models
@model string
@{
	ViewBag.Title = "GetPeople";
	AjaxOptions ajaxOpts = new AjaxOptions {
		UpdateTargetId = "tableBody",
		Url = Url.Action("GetPeopleData"),
		LoadingElementId = "loading",
		LoadingElementDuration = 1000,
		Confirm = "Do you wish to request new data?"
	};
}
<script type="text/javascript">
	function processData(data) {
		var target = $("#tableBody");
		target.empty();
		for (var i = 0; i < data.length; i++) {
			var person = data[i];
			target.append("<tr><td>" + person.FirstName + "</td><td>"
				+ person.LastName + "</td><td>" + person.Role + "</td></tr>");
		}
	}
</script>
<h2>Get People</h2>
<div id="loading" class="load" style="display: none">
	<p>Loading Data...</p>
</div>
<table>
	<thead>
		<tr>
			<th>First</th>
			<th>Last</th>
			<th>Role</th>
		</tr>
	</thead>
	<tbody id="tableBody">
		@Html.Action("GetPeopleData", new {selectedRole = Model })
	</tbody>
</table>
@using (Ajax.BeginForm(ajaxOpts)) {
<div>
	@Html.DropDownList("selectedRole", new SelectList(
		new [] {"All"}.Concat(Enum.GetNames(typeof(Role)))))
	<button type="submit">Submit</button>
</div>
}
<div>
	@foreach (string role in Enum.GetNames(typeof(Role))) {
	<div class="ajaxLink">
		@Ajax.ActionLink(role, "GetPeople",
			new {selectedRole = role},
			new AjaxOptions {
				Url = Url.Action("GetPeopleDataJson", new {selectedRole = role}),
				OnSuccess = "processData"
		})
	</div>
	}
</div>

Мы определили новую функцию ProcessData, содержащую немного базового JQuery кода, который обрабатывает JSON объекты и использует их для создания элементов tr и td, необходимых для заполнения таблицы.

Совет

Мы не углубляемся в JQuery в данной книге, потому что он сам по себе является обширной темой. Хотя мы любим JQuery, и если вы хотите узнать о нем больше, то обратите внимание на книгу Адама Фримана Pro JQuery (Apress, 2012).

Обратите внимание, что мы удалили значение для свойства UpdateTargetId из объектов AjaxOptions, которые мы создали для ссылок. Если вы забыли это сделать, ненавязчивый Ajax постарается обрабатывать данные JSON, полученные от сервера, как HTML. Обычно это происходит потому, что содержание целевого элемента удаляется, но не заменяется новыми данными.

Вы можете увидеть результат перехода к JSON, запустив приложение, перейдя по URL /People/GetPerson и нажав на одну из ссылок. Как показано на рисунке 21-8, мы получаем не совсем правильный результат: в частности, информация, отображаемая в колонке Role таблицы, не является правильной. Мы объясним, почему это происходит, и покажем вам, как это исправить, в следующем разделе.

Рисунок 21-8: Работа с данными JSON вместо HTML

Подготовка данных для кодирования

Когда мы вызвали метод Json внутри метода действия GetPeopleDataJson, мы оставили MVC фреймворк выяснять, как кодировать наши объекты People в формате JSON. MVC фреймворк не имеет специального знания о нашей модели, и поэтому он очень старается предположить, что он должен делать. Вот как MVC фреймворк выражает один объект Person в JSON:

{"PersonId":0,"FirstName":"Adam","LastName":"Freeman",
	"BirthDate":"\/Date(62135596800000)\/","HomeAddress":null,"IsApproved":false,"Role":0}
...

Это похоже на кашу, но результат на самом деле довольно разумный: это просто не совсем то, что нам надо. Во-первых, все свойства, определенные классом Person, представлены в JSON, хотя мы и не присваивали им значения в контроллере People. В некоторых случаях используется значение по умолчанию для данного типа (например, false используется для IsApproved), а в других случаях используется null (например, для HomeAddress). Некоторые значения преобразуются так, чтобы их мог легко истолковать JavaScript, такие как свойство BirthDate, а другие обрабатываются не так хорошо, например, использование 0 для свойства Role, а не Admin.

Просмотр данных JSON

Полезно посмотреть, какие JSON данные возвращают методы действий, и самый простой способ сделать это – ввести URL, направленный на действие, в браузер:

http://localhost:57520/People/GetPeopleJson?selectedRole=Admin

Вы можете сделать это фактически в любом браузере, но большинство браузеров заставят вас сохранить и открыть текстовый файл, прежде чем вы сможете увидеть содержимое JSON. Нам нравится использовать для этого браузер Google Chrome, потому что он показывает JSON данные в главном окне браузера, что делает процесс быстрее, и обозначает, что в итоге вы не будете сидеть с десятками открытых окон текстового файла. Мы также рекомендуем Fiddler (www.fiddler2.com), который является отличной прокси веб отладки и позволяет просмотреть всю информацию о данных, передаваемых между браузером и сервером.

MVC фреймворк сделал хорошую попытку, но мы в конечном итоге отправили в браузер свойства, которые мы в дальнейшем не будем использовать, а значение Role не является полезным для нас. Это типичная ситуация, когда перед отправкой данных клиенту требуется немного подготовиться.

В листинге 21-20 можно увидеть, как мы переделали метод действия GetPersonDataJson в контроллере People для подготовки данных, которые мы передаем методу Json.

Листинг 21-20: Подготовка данных для JSON кодировки
...
public JsonResult GetPeopleDataJson(string selectedRole = "All") {
	var data = GetData(selectedRole).Select(p => new {
		FirstName = p.FirstName,
		LastName = p.LastName,
		Role = Enum.GetName(typeof(Role), p.Role)
	});
	return Json(data, JsonRequestBehavior.AllowGet);
}
...

Мы использовали LINQ для создания последовательности новых объектов, которые содержат только свойства FirstName и LastName нашего объекта Person и представили значение Role в строковом выражении. В результате этого мы получаем JSON данные, которые содержат только нужные нам свойства, более полезные для нашего jQuery кода:

...
{"FirstName":"Adam","LastName":"Freeman","Role":"Admin"}
...

На рисунке 21-9 показаны измененные выходные данные, отображаемые браузером. Естественно, неиспользованные свойства также отправлены, но вы видите, что столбец Role содержит правильные значения.

Рисунок 21-9: Результат подготовки данных для JSON кодировки

Совет

Возможно, вам нужно очистить историю в браузере, чтобы увидеть изменения.

Обнаружение AJAX запросов в методе действия

Наш контроллер в настоящее время содержит два метода действия, чтобы мы могли поддерживать запросы для HTML и JSON данных. Это, как правило, тот способ, которым мы строим наши контроллеры, потому что нам нравятся много коротких и простых действий, но вы не обязаны работать таким же образом. MVC фреймворк обеспечивает простой способ обнаружения Ajax запросов. Это обозначает, что вы можете создать один метод действия, который управляет разными форматами данных. В листинге 21-21 вы можете увидеть, как мы переделали контроллер Person, чтобы он содержал одно действие, которое обрабатывает как JSON, так и HTML.

Листинг 21-21: Создание одного метода действия, который обрабатывает запросы JSON и HTML
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using HelperMethods.Models;
namespace HelperMethods.Controllers
{
	public class PeopleController : Controller
	{
		private Person[] personData = {
			new Person {FirstName = "Adam", LastName = "Freeman", Role = Role.Admin},
			new Person {FirstName = "Steven", LastName = "Sanderson", Role = Role.Admin},
			new Person {FirstName = "Jacqui", LastName = "Griffyth", Role = Role.User},
			new Person {FirstName = "John", LastName = "Smith", Role = Role.User},
			new Person {FirstName = "Anne", LastName = "Jones", Role = Role.Guest}
		};
		public ActionResult Index()
		{
			return View();
		}
		public ActionResult GetPeopleData(string selectedRole = "All")
		{
			IEnumerable<Person> data = personData;
			if (selectedRole != "All")
			{
				Role selected = (Role)Enum.Parse(typeof(Role), selectedRole);
				data = personData.Where(p => p.Role == selected);
			}
			if (Request.IsAjaxRequest())
			{
				var formattedData = data.Select(p => new
				{
					FirstName = p.FirstName,
					LastName = p.LastName,
					Role = Enum.GetName(typeof(Role), p.Role)
				});
				return Json(formattedData, JsonRequestBehavior.AllowGet);
			}
			else
			{
				return PartialView(data);
			}
		}
		public ActionResult GetPeople(string selectedRole = "All")
		{
			return View((object)selectedRole);
		}
	}
}

Мы использовали метод Request.IsAjaxRequest для обнаружения Ajax запросов и передачи формата JSON, если результат является true. Есть несколько ограничений, которые вы должны знать прежде, чем будете следовать такому подходу.

Во-первых, метод IsAjaxRequest возвращает true, если браузер включил в свой запрос заголовок X-Requested-With и установил значение на XMLHttpRequest. Это широко используемое соглашение, но оно не является универсальным, и поэтому вы должны рассмотреть тот вариант, будут ли пользователи делать запросы, которые требуют JSON данных, без установки этого заголовка.

Вторым ограничением является то, что предполагается, что все AJAX запросы требуют JSON данных. Возможно, ваше приложение будет лучше поддерживаться, если вы разделите способ, которым был сделан запрос, от формата данных, которые ищет клиент. Мы предпочитаем этот подход, и по этой причине мы склонны определять отдельные методы действий для разных форматов данных.

Нам также нужно внести два изменения в наше представление GetPerson.cshtml для поддержки одного метода действия, как показано в листинге 21-22.

Листинг 21-22: Изменение представления GetPerson.cshtml для поддержки одного метода действия
@using HelperMethods.Models
@model string
@{
	ViewBag.Title = "GetPeople";
	AjaxOptions ajaxOpts = new AjaxOptions {
		Url = Url.Action("GetPeopleData"),
		LoadingElementId = "loading",
		LoadingElementDuration = 1000,
		OnSuccess = "processData"
	};
}
<script type="text/javascript">
	function processData(data) {
		var target = $("#tableBody");
		target.empty();
		for (var i = 0; i < data.length; i++) {
			var person = data[i];
			target.append("<tr><td>" + person.FirstName + "</td><td>"
			+ person.LastName + "</td><td>" + person.Role
			+ "</td></tr>");
		}
	}
</script>
<h2>Get People</h2>
<div id="loading" class="load" style="display: none">
	<p>Loading Data...</p>
</div>
<table>
	<thead>
		<tr>
			<th>First</th>
			<th>Last</th>
			<th>Role</th>
		</tr>
	</thead>
	<tbody id="tableBody">
		@Html.Action("GetPeopleData", new {selectedRole = Model })
	</tbody>
</table>
@using (Ajax.BeginForm(ajaxOpts)) {
<div>
	@Html.DropDownList("selectedRole", new SelectList(
		new [] {"All"}.Concat(Enum.GetNames(typeof(Role)))))
	<button type="submit">Submit</button>
</div>
}
<div>
	@foreach (string role in Enum.GetNames(typeof(Role))) {
	<div class="ajaxLink">
		@Ajax.ActionLink(role, "GetPeople",
			new {selectedRole = role},
			new AjaxOptions {
				Url = Url.Action("GetPeopleData", new {selectedRole = role}),
				OnSuccess = "processData"
		})
	</div>
	}
</div>

Первое изменение заключается в объекте AjaxOptions, который мы используем для Ajax формы. Поскольку мы уже не можем получить фрагмент HTML через Ajax запрос, мы должны использовать ту же функцию ProcessData для обработки JSON данных ответа сервера, которую мы создали для Ajax ссылок. Второе изменение заключается в значении свойства Url объектов AjaxOptions, которые мы создали для ссылок. Действия GetPeopleDataJson больше не существует, вместо этого и мы нацелены на действие GetPeopleData.