Главная страница   /   7.3. Использование Ajax с JSON и клиентскими шаблонами (ASP.NET MVC 4 в действии

ASP.NET MVC 4 в действии

ASP.NET MVC 4 в действии

Джеффри Палермо

7.3. Использование Ajax с JSON и клиентскими шаблонами

Во всех предыдущих примерах в этой главе мы получали фрагменты HTML-разметки от действия контроллера в ответ на запрос Ajax. Наш пример со ссылкой возвращал фрагмент разметки с информацией о политике конфиденциальности, а отправка формы возвращала комментарий в теге <li />.

Хотя в этом подходе нет ничего неправильного, действия, вызываемые Ajax, не ограничены просто возвращением HTML. Они могут вернуть множество форматов, включая простой текст, XML и JSON.

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

Ajax с JSON

JSON (произносится как "Джейсон") обозначает JavaScript Object Notation и представляет собой очень сжатый способ представления данных. Он часто используется в приложениях, в которых много Ajax, поскольку строки JSON требуют очень мало анализа в JavaScript - можно просто передать строку JSON в функцию eval, и JavaScript преобразует его в граф объекта.

Если вы уже знакомы с литералами объектов JavaScript, структура строки JSON будет для вас знакомой. Листинг 7-11 содержит данные о докладчике на нашей фиктивной конференции в формате XML, а в листинге 7-12 показаны те же данные в формате JSON.

Листинг 7-11: Данные о докладчике в формате XML
<Speaker>
	<Id>5</Id>
	<FirstName>Jeremy</FirstName>
	<LastName>Skinner</LastName>
	<PictureUrl>/content/jeremy.jpg</PictureUrl>
	<Bio>Jeremy Skinner is a C#/ASP.NET software developer in the UK.</Bio>
</Speaker>
Листинг 7-12: Данные о докладчике в формате JSON
{
	"Id":5,
	"FirstName":"Jeremy",
	"LastName":"Skinner",
	"PictureUrl":"/content/jeremy.jpg",
	"Bio":"Jeremy Skinner is a C#/ASP.NET software developer in the UK."
}

Формат JSON легко понять после того, как вы усвоите основные правила. По сути, объект представляется так, как показано на рисунке 7-6.

Рисунок 7-6: Схема объекта JSON для простого понимания формата

Как вы можете видеть, объекты JSON гораздо меньше, чем объекты XML благодаря отсутствию угловых скобок, что может существенно сократить размер загрузки, особенно для больших документов.

Чтобы продемонстрировать JSON в действии, добавим в приложение SpeakersController. Действие Index будет отображать список докладчиков на фиктивной конференции, причем пользователь может кликнуть по имени докладчика. Если он это сделает, мы отправим запрос Ajax к действию, которое будет возвращать подробные данные о докладчике в формате JSON. Конечный результат будет просто отображать имя докладчика в диалоговом окне, как показано на рисунке 7-7.

Рисунок 7-7: Отображение имени докладчика в ответ на запрос Ajax

Базовая реализация приведена в листинге 7-13.

Листинг 7-13: SpeakersController
public class SpeakersController : Controller
{
	private SpeakerRepository _repository
		= new SpeakerRepository();
	public ActionResult Index()
	{
		var speakers = _repository.FindAll();
		return View(speakers);
	}
	public ActionResult Details(int id)
	{
		var speaker = _repository.FindSpeaker(id);
		return Json(speaker,
			JsonRequestBehavior.AllowGet);
	}
}

Строки 3-4: Создать хранилище

Строка 7: Получить список докладчиков

Строка 8: Передать список докладчиков в представление

Строки 13-14: Преобразовать данные о докладчике в формат JSON

Контроллер содержит ссылку на объект SpeakerRepository, который можно использовать для получения объектов Speaker, которые содержат данные о докладчиках на конференции.

Примечание

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

Действие контроллера Index использует SpeakerRepository, чтобы получить список всех докладчиков и передать их в представление.

Действие Details принимает ID конкретного докладчика и извлекает соответствующий объект из хранилища. Затем оно преобразует этот объект в формат JSON, вызывая метод контроллера Json, который возвращает JsonResult. JsonResult является реализацией ActionResult, который при исполнении просто преобразует объект в формат JSON, а затем записывает его в поток результата.

ASP.NET MVC, JSON и запросы GET

В листинге 7-13 вы можете заметить, что мы передаем значение enum JSONRequestBehavior.AllowGet в метод контроллера JSON. По умолчанию в ASP.NET MVC JsonResult будет работать только в ответ на запрос HTTP POST. Если мы хотим вернуть JSON в ответ на запрос GET, мы должны явно выбрать это поведение.

Это поведение необходимо для предотвращения атаки JSON hijacking, что является одной из форм межсайтового скриптинга.

Если сайт должен вернуть конфиденциальные данные в формате JSON в ответ на запрос GET, вредоносный сайт может заставить неосведомлённого пользователя раскрыть эти данные, если будет добавлена скриптовая ссылка в уязвимое место на странице.

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

В данном примере, мы не возвращаем конфиденциальные данные, так что совершенно безопасно включить ответы JSON на запросы GET.

Далее, мы создаем представление Index.

Листинг 7-14: Страница со списком докладчиков
@model IEnumerable<ajaxexamples.models.speaker>

<link rel="Stylesheet" type="text/css" href="@Url.Content("~/content/speakers.css")" />

<script type="text/javascript" src="@Url.Content("~/scripts/Speakers.js")"></script>

<h2>Speakers</h2>
<ul class="speakers">
@foreach (var speaker in Model) {
	<li>
		@Html.ActionLink(speaker.FullName, "Details", new { id = speaker.Id })
	</li>
}
</ul>

<img id="indicator" src="@Url.Content("~/content/load.gif")" alt="loading..." style="display:none" />

<div class="selected-speaker" style="display:none"></div>

Строка 1: Строго типизированное представление

Строка 3: Ссылка на CSS

Строка 5: Ссылка на пользовательский скрипт

Строки 8-14: Генерирует список докладчиков

Строка 16: Выводит индикатор прогресса

Строка 18: Контейнер результатов

Мы начнем с указания, что наше представление строго типизировано и завязано на IEnumerable<Speaker>, который соответствует списку докладчиков, переданному от контроллера в представление. Далее мы включаем ссылку на файл стилей CSS и ссылку на скриптовый файл, который будет содержать наш клиентский код.

Затем мы проходим циклом по всем докладчикам, создавая неупорядоченный список, содержащий их имена в гиперссылке.

После этого мы добавляем на страницу изображение, которое будет отображаться во время обработки запроса Ajax (также известно как spinner).

Наконец мы создаем элемент <div />, в котором будут отображаться данные о докладчике после того, как они будут получены с сервера. Пока мы не будем его использовать, но займемся им далее.

Теперь, когда представление создано, мы можем создать наш клиентский код в файле Speakers.js.

Листинг 7-15: Клиентский скрипт для страницы со списком докладчиков
$(document).ready(function () {
	$("ul.speakers a").click(function (e) {
		e.preventDefault();
		$("#indicator").show();
		var url = $(this).attr('href');
		$.getJSON(url, null, function (speaker) {
			$("#indicator").hide();
			alert(speaker.FirstName);
		});
	});
});

Строка 4: Показывает индикатор прогресса

Строка 5: Извлекает URL

Строка 6: Вызывает запрос Ajax

Строка 8: Выводит результат

Как обычно при работе с JQuery, сначала мы ждем загрузки DOM, а затем прикрепляем функцию к событию click для ссылок в списке докладчиков. В ней первым делом мы отображаем индикатор загрузки.

После этого мы извлекаем URL из гиперссылки, на которую нажал пользователь, и сохраняем ее в переменной url. Эта переменная затем используется, чтобы отправить запрос на сервер. На этот раз мы используем функцию JQuery $.getJSON, передавая в URL, который мы вызываем, любые дополнительные данные (в этом случае таких нет, поэтому мы передаем null), и функцию обратного вызова, которая будет вызвана после отправки запроса. Эта функция автоматически десериализирует строку JSON, полученную от сервера, и преобразует ее в объект JavaScript. Затем этот объект будет передан в функцию обратного вызова.

Функция обратного вызова принимает в качестве параметра объект, десериализированный из ответа сервера в формате JSON (в данном случае, наш объект Speaker). В функции обратного вызова мы скрываем индикатор загрузки, а затем отображаем свойство FirstName в окне сообщения.

Отображение модального диалогового окна с именем докладчика - не самый полезное поведение. Было бы гораздо лучше, если бы мы подгружали на страницу разметку, содержащую данные о докладчике и его фото. Для этого используются клиентские шаблоны.

Клиентские шаблоны

Мы можем создавать клиентские шаблоны так же, как и серверные шаблоны в виде файлов Razor.cshtml.

Клиентские шаблоны позволяют нам генерировать разметку на лету в браузере, без необходимости обращаться к серверу или вручную создавать элементы с помощью JavaScript. Существует несколько библиотек клиентских шаблонов, но мы будем использовать jQuery-tmpl, библиотеку шаблонов jQuery, которая была создана Microsoft и способствовала развитию jQuery как проекта с открытым исходным кодом.

Мы изменим страницу со списком докладчиков так, что после клика по имени докладчика будут отображаться его биография и фото, как показано на рисунке 7-8.

Рисунок 7-8: Отображение шаблона рядом со списком докладчиков

Чтобы обращаться к jQuery-tmpl, мы можем либо скачать его с https://github.com/jQuery/jQuery-tmpl и разместить в каталоге Scripts в нашем приложении, либо использовать его напрямую из Microsoft CDN по адресу http://ajax.microsoft.com/ajax/jQuery.templates/beta1/jQuery.tmpl.js. Поставив ссылку на jQuery-tmpl, мы можем добавить шаблон для нашего представления.

Листинг 7-16: Использование клиентских шаблонов
<script type="text/javascript"
	src="@Url.Content("~/scripts/jquery.tmpl.js")">
</script>
<script id="speakerTemplate" type="text/x-jquery-tmpl">
	<img src="${PictureUrl}"
		alt="Speaker image" class="speaker-pic" />
	<p class="speaker-bio">
		${Bio}
	</p>
	<br style="clear:both;" />
</script>

Строки 1-3: Ссылка на jQuery Templates

Строка 4: Определение раздела шаблона

Строки 5-6: Шаблон фото

Строка 8: Шаблон биографии

Для начала мы подключаем скрипт jQuery-tmpl из папки скриптов, затем создаем шаблон. Шаблоны на странице находятся внутри тега script, для них указывается тип text/x-jQuery-tmpl. Заключение разметки шаблона в тег script гарантирует, что элементы шаблона не будут отображаться на странице.

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

Далее нам нужно изменить код в Speakers.js для рендеринга шаблона. Вот новый код.

Листинг 7-17: Изменяем скрипт для рендринга шаблона
$(document).ready(function () {
	$("ul.speakers a").click(function (e) {
		e.preventDefault();
		$(".selected-speaker").hide().html('');
		$("#indicator").show();
		var url = $(this).attr('href');
		$.getJSON(url, null, function (speaker) {
			$("#indicator").hide();
			$("#speakerTemplate")
				.tmpl(speaker)
				.appendTo('.selected-speaker');
			$('.selected-speaker').show();
		});
	});
});

Строка 4: Скрыть информацию о докладчике

Строка 9: Отобразить шаблон с данными

Этот код является во многом таким же, как и код в листинге 7-15, но с несколькими отличиями. Во-первых, если мы уже отображаем данные о докладчике, то скрываем их до того, как отправить новый запрос на сервер. Во-вторых, вместо того, чтобы отображать диалоговое окно с ответом на запрос Ajax, мы теперь отображаем шаблон. Для этого мы сначала даем jQuery инструкцию найти тег шаблона, а затем вызываем метод tmpl для его рендеринга. Этот метод принимает объект, который должен быть передан в шаблон и которым в данном случае является ссылка на докладчика. Представленный шаблон затем добавляется к тегу <div /> на нашей странице с CSS классом для selected-speaker.

В конечном результате после клика по имени докладчика шаблон будет отображаться напротив списка, как показано на рисунке 7-8. Обратите внимание, что были добавлены дополнительные стили, чтобы сделать страницу более презентабельной. Эти дополнительные стили можно найти в образцах кода к данной главе.

Завершающие штрихи

Наш страница со списком докладчиков в основном готова, но у нее есть один недостаток. Если в браузере будет отключен JavaScript, то после клика по имени докладчика соответствующий объект JSON будет загружен как текстовый файл и не будет отображаться как шаблон.

Чтобы обойти эту проблему, мы можем использовать метод, подобный методу из листинга 7-4, в котором мы выводили представление, если действие не было запрошено через Ajax.

Листинг 7-18: Использование действия Details, если оно не было запрошено через Ajax
public ActionResult Details(int id)
{
	var speaker = _repository.FindSpeaker(id);
	if(Request.IsAjaxRequest())
	{
		return Json(speaker,
			JsonRequestBehavior.AllowGet);
	}
	return View(speaker);
}

Строка 4: Вернуть JSON на запрос Ajax

Строка 9: Вывести представление для обычных запросов

Чтобы не полагаться на условие if, здесь мы можем использовать селектор метода действия для разграничения запросов Ajax и обычных запросов. В главе 2 мы узнали, как используются селекторы метода действия, и теперь мы можем создать AcceptAjaxAttribute путем наследования от атрибута ActionMethodSelector, как показано здесь.

Листинг 7-19: Реализация AcceptAjaxAttribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AcceptAjaxAttribute : ActionMethodSelectorAttribute
{
	public override bool IsValidForRequest(
		ControllerContext controllerContext, MethodInfo methodInfo)
	{
		return controllerContext.HttpContext
			.Request.IsAjaxRequest();
	}
}

AcceptAjaxAttribute просто возвращает значение true от метода IsValidForRequest, если текущее действие было запрошено через Ajax. Теперь мы можем использовать этот атрибут из SpeakersController, определив два действия: одно - для отправки запросов Ajax, другое - для обычных запросов.

Листинг 7-20: Использование AcceptAjaxAttribute
[AcceptAjax]
public ActionResult Details(int id)
{
	var speaker = _repository.FindSpeaker(id);
	return Json(speaker, JsonRequestBehavior.AllowGet);
}
[ActionName("Details")]
public ActionResult Details_NonAjax(int id)
{
	var speaker = _repository.FindSpeaker(id);
	return View(speaker);
}

Строка 1: Доступен только для запросов Ajax

Строка 7: Создание альтернативного действия с помощью ActionName

Первый перегруженный вариант действия Details содержит AcceptAjaxAttribute, что гарантирует, что оно будет вызываться только для запросов Ajax. Этот вариант действия возвращает данные о докладчике, преобразованные в JSON.

Другой перегруженный вариант не имеет AcceptAjaxAttribute и, следовательно, будет вызываться для остальных запросов. Это действие просто передает Speaker в представление. Обратите внимание, что, поскольку в C# нельзя определить два метода с одним и тем же именем и сигнатурой, второй вариант действия называется Details_NonAjax, но он также доступен по URL/Speakers/Details, потому что имеет атрибут ActionName.

Примечание

AcceptAjaxAttribute также содержится в ASP.NET MVC Futures DLL, которая может быть скачана с http://aspnet.codeplex.com.

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

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

Листинг 7-21: Отображение информации о докладчике без Ajax
@model AjaxExamples.Models.Speaker
<h2>Speaker Details: @Model.FullName</h2>
<p class="speaker">
	<img src="@Model.PictureUrl"
		alt="@Model.FullName" />
	<span class="speaker-bio">@Model.Bio</span>
</p>
<br style="clear: both" />
@Html.ActionLink("Back to speakers", "index")

Строки 4-5: Отображает фото

Строка 6: Отображает биографию

Теперь после клика по имени докладчика в браузере с отключенным JavaScript загрузится новая страница, как показано на рисунке 7-9.

Рисунок 7-9: Информация о докладчике отображается без Ajax