Использование JSON

68

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

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

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

Инфраструктура ASP.NET MVC Framework существенно упрощает создание метода действия, который генерирует данные JSON. В примере ниже такой метод действия добавлен в контроллер People приложения HelperMethods, которое мы создали ранее:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using HelperMethods.Models;

namespace HelperMethods.Controllers
{
    public class PeopleController : Controller
    {
        private List<User> UserData = new List<User> {
            new User {FirstName = "Иван", LastName = "Иванов", Role = Role.Admin},
            new User {FirstName = "Петр", LastName = "Петров", Role = Role.User},
            new User {FirstName = "Сидор", LastName = "Сидоров", Role = Role.User},
            new User {FirstName = "Вася", LastName = "Васильев", Role = Role.Guest}
        };

        public ActionResult Index()
        {
            return View();
        }

        public IEnumerable<User> GetData(string selectedRole)
        {
            IEnumerable<User> users = UserData;
            if (selectedRole != "All")
            {
                Role selected = (Role)Enum.Parse(typeof(Role), selectedRole);
                users = UserData.Where(p => p.Role == selected);
            }
            return users;
        }

        public JsonResult GetPeopleDataJson(string selectedRole = "All")
        {
            IEnumerable<User> users = GetData(selectedRole);
            return Json(users, JsonRequestBehavior.AllowGet);
        }

        public PartialViewResult GetPeopleData(string selectedRole = "All")
        {
            return PartialView(GetData(selectedRole));
        }

        public ActionResult GetPeople(string selectedRole = "All")
        {
            return View((Object)selectedRole);
        }
    }
}

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

Также был добавлен новый метод действия по имени GetPeopleDataJson(), возвращающий объект JsonResult. Это специальный вид ActionResult, который сообщает механизму визуализации о том, что вместо HTML-разметки клиенту должны быть возвращены данные JSON. (Класс ActionResult и его роль в инфраструктуре ASP.NET MVC Framework подробно рассматривались в статье Генерация ответа из контроллеров.)

Объект JsonResult создается вызовом метода Controller.Json() внутри метода действия с передачей ему данных, которые необходимо преобразовать в формат JSON. В этом случае методу также передается значение AllowGet перечисления JsonRequestBehavior. По умолчанию данные JSON будут отправляться только в ответ на запросы POST, однако за счет передачи AllowGet в качестве параметра методу Json() мы сообщаем инфраструктуре ASP.NET MVC Framework о том, что необходимо также реагировать и на запросы GET.

Значение JsonRequestBehavior.AllowGet должно использоваться только в ситуации, когда возвращаемые данные не являются конфиденциальными. Из-за проблемы с безопасностью во многих браузерах посторонние сайты способны перехватывать данные JSON, которые возвращаются в ответ на запрос GET. Именно по этой причине JsonResult по умолчанию не реагирует на запросы GET. В большинстве случаев для извлечения данных JSON будет возможность пользоваться запросами POST, избегая указанной проблемы.

Обработка данных JSON в браузере

Чтобы обработать данные JSON, извлекаемые из сервера приложений ASP.NET MVC Framework, мы определяем JavaScript-функцию, используя свойство обратного вызова OnSuccess в классе AjaxOptions. В примере ниже показано модифицированное содержимое файла представления GetPeople.cshtml, из которого удалены функции обработчиков, определенные в предыдущей статье, и добавлен обратный вызов OnSuccess для обработки данных JSON:

@using HelperMethods.Models
@model string
@{
    ViewBag.Title = "Данные пользователей";
    AjaxOptions ajaxOptions = new AjaxOptions
    {
        UpdateTargetId = "tableBody",
        Url = Url.Action("GetPeopleData"),
        LoadingElementDuration = 1000,
        LoadingElementId = "loading"
    };
}

<script type="text/javascript">
    function processData(data) {
        var target = $("#tableBody");
        target.empty();
        for (var i = 0; i < data.length; i++) {
            var user = data[i];
            target.append("<tr><td>" + user.FirstName + "</td><td>"
                + user.LastName + "</td><td>" + user.Role
                + "</td></tr>");
        }
    }
</script>

<h2>Данные пользователей</h2>

...

<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>, необходимые для HTML-таблицы.

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

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

Работа с данными JSON вместо фрагментов HTML-разметки

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

В случае вызова метода Json() внутри метода действия GetPeopleDataJson() мы позволяем инфраструктуре ASP.NET MVC Framework выяснить способ кодирования объектов People в формате JSON. Инфраструктуре ASP.NET MVC Framework ничего не известно о типах моделей в приложении, поэтому она пытается выстроить наилучшее предположение относительно того, что должно быть сделано. Вот как ASP.NET MVC Framework выражает одиночный объект User в формате JSON:

Стандартные данные JSON

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

Иногда полезно просматривать данные JSON, возвращаемые методами действий, как показано в примере выше. Проще всего это сделать, введя в браузере URL, который нацелен на необходимое действие, например:

http://localhost:1228/People/GetPeopleDataJson?selectedRole=Admin

Поступать подобным образом можно практически в любом браузере, но большинство браузеров будут предлагать сохранить содержимое JSON в текстовом файле. Я отдаю предпочтение браузеру Google Chrome, т.к. он успешно отображает данные JSON в своем главном окне, ускоряя процесс, и не заставляет создавать и затем открывать десятки текстовых файлов. Кроме того, рекомендуется использовать Fiddler - великолепный прокси-сервер для веб-отладки, который позволяет просматривать все подробности данных, передаваемых между браузером и сервером.

Итак, инфраструктура ASP.NET MVC Framework предприняла неплохую попытку, но в конечном итоге браузеру отправляются свойства, которые впоследствии невозможно использовать, к тому же значение Role выражено неподходящим образом. Такая ситуация является типичной, если полагаться на стандартное кодирование JSON.

Обычно требуется провести определенную подготовку данных, которые необходимо отправлять клиенту. В примере ниже приведен пересмотренный код метода действия GetUserDataJson() в контроллере People, обеспечивающий подготовку данных перед их передачей методу Json():

// ...
public JsonResult GetPeopleDataJson(string selectedRole = "All")
{
    var users = GetData(selectedRole).Select(p => new { 
        FirstName = p.FirstName,
        LastName = p.LastName,
        Role = Enum.GetName(typeof(Role), p.Role)
    });
    return Json(users, JsonRequestBehavior.AllowGet);
}
// ...

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

{"FirstName":"Петр", "LastName":"Петров", "Role":"User"}

Новый вывод в браузере показан на рисунке ниже. Конечно, здесь нельзя сказать, что неиспользуемые свойства не отправляются, однако легко заметить, что столбец Role содержит корректные значения:

Результат подготовки объектов данных к кодированию JS0N

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

В настоящий момент контроллер People содержит два метода действий, так что можно поддерживать запросы для данных HTML и JSON. Обычно я так и строю свои контроллеры, поскольку предпочитаю иметь множество коротких и простых действий, но вы не обязаны поступать подобным образом. Инфраструктура ASP.NET MVC Framework предоставляет простой способ обнаружения запросов Ajax, а это значит, что можно создать единственный метод действия, который обрабатывает сразу несколько форматов данных.

В примере ниже приведен переделанный код контроллера People, содержащего одиночное действие, которое поддерживает как JSON, так и HTML:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using HelperMethods.Models;

namespace HelperMethods.Controllers
{
    public class PeopleController : Controller
    {
        private List<User> UserData = new List<User> {
            new User {FirstName = "Иван", LastName = "Иванов", Role = Role.Admin},
            new User {FirstName = "Петр", LastName = "Петров", Role = Role.User},
            new User {FirstName = "Сидор", LastName = "Сидоров", Role = Role.User},
            new User {FirstName = "Вася", LastName = "Васильев", Role = Role.Guest}
        };

        public ActionResult Index()
        {
            return View();
        }

        public IEnumerable<User> GetData(string selectedRole)
        {
            IEnumerable<User> users = UserData;
            if (selectedRole != "All")
            {
                Role selected = (Role)Enum.Parse(typeof(Role), selectedRole);
                users = UserData.Where(p => p.Role == selected);
            }
            return users;
        }

        public ActionResult GetPeopleData(string selectedRole = "All")
        {
            IEnumerable<User> users = GetData(selectedRole);
            if (selectedRole != "All")
            {
                Role selected = (Role)Enum.Parse(typeof(Role), selectedRole);
                users = UserData.Where(p => p.Role == selected);
            }

            if (Request.IsAjaxRequest())
            {
                var formattedData = users.Select(p => new
                {
                    FirstName = p.FirstName,
                    LastName = p.LastName,
                    Role = Enum.GetName(typeof(Role), p.Role)
                });
                return Json(formattedData, JsonRequestBehavior.AllowGet);
            }
            else
            {
                return PartialView(users);
            }
        }

        public ActionResult GetPeople(string selectedRole = "All")
        {
            return View((Object)selectedRole);
        }
    }
}

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

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

Для поддержки показанного выше единственного метода действия понадобится также внести два изменения в представление GetPeople.cshtml:

@using HelperMethods.Models
@model string
@{
    ViewBag.Title = "Данные пользователей";
    AjaxOptions ajaxOptions = new AjaxOptions
    {
        UpdateTargetId = "tableBody",
        Url = Url.Action("GetPeopleData"),
        LoadingElementDuration = 1000,
        LoadingElementId = "loading",
        OnSuccess = "processData"
    };
}

...

<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, то должны применять для обработки ответа JSON от сервера ту же самую функцию processData(), которая была создана для ссылок Ajax. Второе изменение связано со значением свойства Url объектов AjaxOptions, создаваемых для ссылок. Действие GetPeopleDataJson больше не существует; и вместо него в качестве цели указывается действие GetPeopleData.

Пройди тесты
Лучший чат для C# программистов