Потребление веб-служб Web API

190

Итак, в предыдущей статье мы создали веб-службу Web API и провели ее базовое тестирование. Теперь мы собираемся построить более сложную веб-форму и воспользоваться ею для расширенного потребления веб-службы. Мы создали веб-форму Data.aspx с контентом, представленным в примере ниже:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Data.aspx.cs" Inherits="ClientDev.Data" %>

<!DOCTYPE html>
<html>
<head runat="server">
    <title></title>
    <style>
        th { text-align: left; border-bottom: thin solid black;}
        input[type=text][name=Price] { width: 75px;}
        input[type=text][name=Category] { width: 100px;}
        .error { color: red;}
    </style>
    <%: System.Web.Optimization.Scripts.Render("~/bundle/jquery") %>
    <script src="Scripts/Data.js"></script>
    <script type="text/template" id="rowTemplate">
        <tr>
            <td>{GameId}</td>
            <td><input type="text" name="Name" Value="{Name}"></td>
            <td><input type="text" name="Category" Value="{Category}"></td>
            <td><input type="text" name="Price" Value="{Price}"></td>
            <td>
                <button data-id={GameId} data-action="update">Обновить</button>
                <button data-id={GameId} data-action="delete">Удалить</button>
            </td>
        </tr>
    </script>
</head>
<body>
    <form id="form1" runat="server">
        <table id="dataTable">
            <thead>
                <tr>
                    <th>ID</th><th>Название</th><th>Категория</th><th>Цена</th>
                    <th></th>
                </tr>
            </thead>
            <tbody></tbody>
        </table>
    </form>
</body>
</html>

Эта веб-форма содержит элемент <table>, в который будут вставляться строки для всех элементов данных, получаемых от веб-службы. Имеется также ссылка на пакет сценариев для библиотеки jQuery и элемент <script> для файла Data.js, который будет содержать код JavaScript, специфичный для страницы этой веб-формы.

Кроме того, определен элемент <script>, атрибут type которого установлен в text/template. Это позволяет определить разметку, создаваемую для каждого объекта данных, так, чтобы браузер не пытался интерпретировать или выполнять содержащиеся внутри <script> элементы. В шаблоне предусмотрена разметка для строки таблицы, в которой определены ячейки для каждого свойства. Ячейка для свойства GameId допускает только чтение, но для остальных свойств используются элементы <input>, что дает возможность редактировать данные и обновлять хранилище. Контент ячеек устанавливается с помощью имен свойств в фигурных скобках, например {Name}, и они будут заменены значениями данных, полученными от веб-службы.

В целях упрощения примера мы создадим собственную базовую систему шаблонов для этой статьи. Соответствующий код будет помещен в файл Data.js. Обычно мы не поступаем так в реальных проектах и рекомендуем вам найти для своих приложений более эффективную и гибкую систему шаблонов. Доступно несколько великолепных пакетов шаблонов на JavaScript, одним из которых является jQuery Templates.

История, связанная с этим пакетом, довольно странная - в Microsoft содействовали данному проекту в jQuery, и он должен был стать официальным пакетом шаблонов jQuery. Однако "за кулисами" возникли какие-то разногласия и в jQuery решили создать собственный пакет, который все еще находится на стадии бета-тестирования. Как следствие, этот пакет не только вяло разрабатывается, но даже не обновляется и не исправляется, однако работает он нормально и нас вполне устраивает. Если вы не хотите иметь дело с неподдерживаемым кодом (что достаточно разумно), то есть много других альтернативных средств.

Мы вынесли код, который будет работать с веб-службой и заполнять данными HTML-разметку, генерируемую веб-формой, в отдельный файл по имени Data.js и разместили его в папке Scripts. Содержимое этого файла с кодом JavaScript приведено в примере ниже:

(function () {

    String.prototype.format = function (dataObject) {
        return this.replace(/{(.+)}/g, function (match, propertyName) {
            return dataObject[propertyName];
        });
    };

    function getData() {
        $.getJSON("/api/game", null, displayData);
    };

    function displayData(data) {
        var target = $("#dataTable tbody");
        target.empty();
        var template = $("#rowTemplate");
        data.forEach(function (dataObject) {
            target.append(template.html().format(dataObject));
        });
        $(target).find("button").click(function (e) {
            $("*.errorMsg").remove();
            $("*.error").removeClass("error");
            var index = $(e.target).attr("data-id");
            if ($(e.target).attr("data-action") == "delete") {
                deleteData(index);
            } else {
                var gameData = { gameID: index };
                $(e.target).closest('tr').find('input')
                    .each(function (index, inputElem) {
                        gameData[inputElem.name] = inputElem.value;
                    });
                updateData(index, gameData);
            }
            e.preventDefault();
        });
    }

    function deleteData(index) {
        $.ajax({
            url: "/api/game/" + index,
            type: 'DELETE',
            success: getData
        });
    }

    function updateData(index, gameData) {
        $.ajax({
            url: "/api/game/" + index,
            type: 'PUT',
            data: gameData,
            success: getData,
            error: function (jqXHR, status, error) {
                var errorRow = $("button[data-id=" + index + "]").closest("tr");
                errorRow.find("*").addClass("error");
                var errData = JSON.parse(jqXHR.responseText);
                for (var i = 0; i < errData.length; i++) {
                    errorRow.after("<tr class='errMsg error'><td/><td colspan=3>"
                        + errData[i] + "</td></tr>");
                }
            }
        });
    }

    $(document).ready(function () {
        getData();
    });

})();

Мы начинаем с определения функции format() для строкового типа, которая будет применяться для замены разделов {Name} в шаблоне значениями свойств объекта. Именно так будет создаваться экземпляр шаблона внутри веб-формы.

Функция getData() с помощью Ajax вызывает метод Get(), определенный в классе GameController. Функция displayData() получает данные и использует шаблон для их отображения в элементе <table>, который определен внутри веб-формы. Функция deleteData() посредством Ajax отправляет HTTP-запрос DELETE (нацеленный на метод Delete() контроллера), а функция updateData() выполняет HTTP-запрос PUT (нацеленный на метод Put() контроллера). Функции deleteData() и updateData() вызывают getData(), когда их запросы Ajax проходят успешно, чтобы выполнить обновление с учетом полученных данных. Подобное не должно делаться в реальном проекте, т.к. при этом серверу отправляется дополнительный запрос, но мы хотим сохранить пример простым и обеспечить аккуратное представление HTML-разметкой данных из хранилища.

Полученный результат можно увидеть, запустив приложение и запросив веб-форму Data.aspx. После загрузки HTML-разметки, сгенерированной веб-формой, браузер запустит наш код JavaScript. Это приведет к вызову функции getData() и отображению возвращенных данных в простом табличном шаблоне, как показано на рисунке ниже:

Использование веб-службы для получения данных приложения

Данные можно удалять из хранилища, щелкая на кнопках "Удалить", или вносить в них изменения, редактируя значения в элементах <input> и щелкая на кнопках "Обновить". Внесенные изменения в данных остаются в хранилище только до останова или перезапуска приложения, после чего отменяются.

Обработка ошибок проверки достоверности моделей

Внимательно просмотрев код JavaScript в файле Data.js, вы заметите, что в функции updateData() с помощью конфигурационного свойства error для запроса Ajax указывается функция, которая должна быть вызвана в случае возврата ошибки. Этот код позволяет продемонстрировать влияние, которое привязка моделей может оказывать на веб-службу, а также возможность применения веб-службы для возврата клиенту сообщений об ошибках, когда данные, предоставленные пользователем, не проходят проверку достоверности.

В примере ниже представлены изменения, внесенные в класс GameController:

// ...
public HttpResponseMessage Put(int id, [FromBody] GameView value)
{
    if (ModelState.IsValid)
    {
        Repository repository = new Repository();
        Game game = repository.Games
            .Where(p => p.GameId == id).FirstOrDefault();
        if (game != null)
        {
            game.Name = value.Name;
            game.Price = value.Price;
            game.Category = value.Category;
        }
        repository.SaveGame(game);
        return Request.CreateResponse(HttpStatusCode.OK);
    }
    else
    {
        List<string> errors = new List<string>();
        foreach (var state in ModelState)
            foreach (var error in state.Value.Errors)
               errors.Add(error.ErrorMessage);

        return Request.CreateResponse(HttpStatusCode.BadRequest, errors);
    }
}
// ...

Возвращая объект HttpResponseMessage, можно контролировать код состояния, отправляемый обратно методом контроллера Web API. Экземпляры этого класса создаются посредством метода Request.CreateResponse(), которому передается значение перечисления HttpStatusCode и необязательный объект, предназначенный для отправки в виде данных JSON внутри тела ответа.

Свойство ModelState.IsValue используется для выяснения, имеются ли ошибки привязки. Если ошибки возникали, мы возвращаем код состояния BadRequest (с числовым значением 400) и включаем массив ошибок привязки модели. Мы должны строить массив ошибок самостоятельно, поскольку форматер JSON не умеет сериализировать объект ModelState.

Код ошибки 400 запускает обработчик, указанный в конфигурационном свойстве error внутри функции updateData() из файла Scripts\Data.js, который предупреждает пользователя о наличии проблемы. Мы не применяли атрибуты проверки достоверности к классу модели представления GameView, так что единственной причиной возникновения ошибки привязки модели в этом примере является предоставление нечислового значения для свойства "Цена".

Чтобы увидеть, как все работает, запустите приложение и запросите веб-форму Data.aspx. Введите строку в одном из полей "Цена" и щелкните на связанной с ним кнопке "Обновить". Веб-служба сообщит об ошибке привязки; результат можно видеть на рисунке:

Отображение ошибок привязки модели для пользователя

Обработка проверки событий

Одним из самых распространенных применений данных, получаемых от веб-службы, является заполнение элементов управления. Это может вызвать проблему с функциональным средством ASP.NET, которое называется проверкой событий, в случае выполнения для многофункциональных элементов управления пользовательского интерфейса и элементов управления HTML серверной стороны. Чтобы продемонстрировать проблему, мы создали веб-форму по имени EventValidationDemo.aspx, контент которой приведен в примере ниже:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="EventValidationDemo.aspx.cs" Inherits="ClientDev.EventValidationDemo" %>

<!DOCTYPE html>
<html>
<head runat="server">
    <title></title>
    <style> 
        div { margin-bottom: 10px; }
    </style>
    <%: System.Web.Optimization.Scripts.Render("~/bundle/jquery") %>
    <script>
        $(document).ready(function () {
            var targetElem = $("#nameSelect");
            targetElem.attr("disabled", "true");
            $.ajax({
                url: "/api/game",
                type: "GET",
                success: function (data) {
                    for (var i = 0; i < data.length; i++) {
                        $("<option>" + data[i].Name
                            + "</option>").appendTo("#nameSelect");
                    }
                    targetElem.removeAttr("disabled");
                }
            });
        });
    </script>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            <asp:DropDownList ID="nameSelect" runat="server">
                <asp:ListItem>Все</asp:ListItem>
            </asp:DropDownList>
            <button type="submit">Отправить</button>
        </div>
        <div>Значение из элемента управления: <span id="controlValue" runat="server"></span></div>
        <div>Значение из формы: <span id="formValue" runat="server"></span></div>
    </form>
</body>
</html>

Эта веб-форма содержит элемент управления DropDownList, который будет использоваться для того, чтобы предоставить пользователю возможность выбора товара. Элемент управления DropDownList изначально содержит единственный элемент, а с помощью запроса Ajax мы дополняем его значениями, полученными от веб-службы. Мы определили два элемента <span> серверной стороны, которые применяются для отражения выбора внутри DropDownList и в данных формы, отправленных как часть запроса.

В примере ниже показано содержимое файла отделенного кода, в котором устанавливается внутренний текст элементов <span>:

using System;

namespace ClientDev
{
    public partial class EventValidationDemo : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            controlValue.InnerText = nameSelect.SelectedValue;
            formValue.InnerText = Request.Form["nameSelect"];
        }
    }
}

Чтобы увидеть проблему, запустите приложение и запросите веб-форму EventValidationDemo.aspx. Загрузится HTML-разметка, сгенерированная веб-формой, после чего выполнится наш код jQuery для отправки запроса Ajax и заполнения элемента <select> полученными значениями. (Элемент <select> генерируется элементом управления DropDownList.)

Удостоверьтесь, что выбран вариант "Все", и щелкните на кнопке "Отправить". Все работает нормально, и элементы <span> сообщают, что значение формы и значение в элементе управления оба равны "Все". Теперь повторите процесс с любым другим значением, отображаемым в элементе <select>. Возникнет сообщение об ошибке, подобное представленному на рисунке ниже:

Возникновение проблемы с проверкой событий

Средство проверки событий призвано предотвращать попытки злоумышленников внедрять непредвиденные значения в приложение, самостоятельно формируя запросы, содержащие такие значения. Элемент управления DropDownList был сконфигурирован только с одним значением "Все", поэтому любое другое значение выглядит для среды ASP.NET как попытка внедрения. Вместо передачи такого значения элементу управления и созданию риска для приложения ASP.NET отображает сообщение об ошибке, приведенное на рисунке.

Отключение проверки событий

Первый способ решения этой проблемы предусматривает отключение проверки событий, что делается в директиве Page:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="EventValidationDemo.aspx.cs" 
    Inherits="ClientDev.EventValidationDemo"
    EnableEventValidation="false" %>
	
...

Установка атрибута EnableEventValidation в false отключает проверку для всех элементов управления внутри веб-формы и тем самым предотвращает отображение сообщения об ошибке, когда получено непредвиденное значение. Однако это лишь частичное решение, поскольку многофункциональные элементы управления пользовательского интерфейса не могут выражать элементы, с которыми они не были сконфигурированы. Снова запустите приложение, запросите веб-форму EventValidationDemo.aspx и выберите любое значение кроме "Все". После отправки формы будет получен результат, показанный на рисунке ниже:

Результат выбора значения, с которым элемент управления не был сконфигурирован

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

Замена элемента управления

Сама идея проверки событий хороша, однако ее реализация досталась из эпохи намного более простого кода клиентской стороны. Если вы хотите модифицировать контент элементов формы и получать новые значения при отправке форме, то должны использовать обычные HTML-элементы. В примере ниже приведено модифицированное содержимое файла EventValidationDemo.aspx, из которого удалены элемент управления DropDownList и элемент <span>, отображающий значение свойства SelectedValue:

...
<form id="form1" runat="server">
        <div>
            <select id="nameSelect" runat="server"> 
                <option>Все</option>
            </select>
            <button type="submit">Отправить</button>
        </div>
        <div>Значение из формы: <span id="formValue" runat="server"></span></div>
</form>
...

Ниже показан измененный класс отделенного кода для учета модификации веб-формы:

using System;

namespace ClientDev
{
    public partial class EventValidationDemo : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            formValue.InnerText = Request.Form["nameSelect"];
        }
    }
}

Теперь пользовательский выбор отражает дополнения, сделанные с помощью Ajax - хотя и ценой отключения встроенной защиты против подделки пользователями собственных запросов с целью внедрения непредвиденных значений в приложение.

Использование атрибутов привязки моделей в Web API

В завершение этой статьи мы продемонстрируем, как применять Ajax, чтобы воспользоваться атрибутами привязки моделей при работе со средством Web API. В примере ниже приведен класс GameController с еще одним определением метода Get(). Этот новый метод принимает аргумент, аннотированный атрибутом Form, и применяет его для фильтрации товаров по категории:

public class GameController : ApiController
{
        // ...

        public IEnumerable<GameView> Get([System.Web.ModelBinding.Form] string categoryFilter)
        {
            if (categoryFilter == null || categoryFilter == "Все")
            {
                return Get();
            }
            else
            {
                return new Repository().Games
                    .Where(p => p.Category == categoryFilter)
                    .Select(p => new GameView(p));
            }
        }

        // ...
}

Обратите внимание, что аргумент categoryFilter не был сделан необязательным. Причина в том, что каждый метод в классе контроллера Web API должен иметь индивидуальную сигнатуру, которая является комбинацией имени метода и его аргументов. Необязательный аргумент создал бы конфликт с методом Get(), не принимающим аргументов. Это привело бы к ошибке, связанной с невозможностью определения средой ASP.NET, для какого метода предназначен поступивший запрос.

Воспользоваться атрибутом привязки моделей можно с помощью конфигурационного свойства data, указав значение формы categoryFilter для запросов Ajax к веб-службе. Чтобы продемонстрировать это, мы создали веб-форму под названием ModelBinding.aspx, контент которой показан ниже:

<%@ Page Language="C#" AutoEventWireup="true" 
   CodeBehind="ModelBinding.aspx.cs" Inherits="ClientDev.ModelBinding" %>

<!DOCTYPE html>
<html>
<head runat="server">
    <title></title>
    <%: System.Web.Optimization.Scripts.Render("~/bundle/jquery") %>
    <script>

        function getData() {
            $.ajax({
                url: "/api/game",
                type: "GET",
                data: {
                    categoryFilter: $("#category").val()
                },
                success: function (data) {
                    var list = $("#list");
                    list.empty();
                    for (var i = 0; i < data.length; i++) {
                        list.append("<li>" + data[i].Name + "</li");
                    }
                }
            });
        }

        $(document).ready(function () {
            getData();
            $("#category").change(getData);
        });
    </script>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            Категория: 
            <select id="category">
                <option>Все</option>
                <option>Симулятор</option>
                <option>Шутер</option>
                <option>RPG</option>
            </select>
        </div>
        <div>
            <ol id="list"></ol>
        </div>
    </form>
</body>
</html>

Внутри веб-формы определен элемент <select>, содержащий ряд категорий, и элемент <ol>, который применяется для отображения списка значений свойства Name объектов GameView, получаемых кодом jQuery из хранилища. Значение формы categoryFilter предоставляется из элемента <select> как часть запроса Ajax. Это позволяет фильтровать отображаемые данные.

Чтобы увидеть результат, запустите приложение, запросите веб-форму ModelBinding.aspx и выберите значение в элементе <select>. (Кнопки отправки в этом примере отсутствуют. Для обновления отображаемых данных при изменении значения, выбранного в элементе <select>, используется библиотека jQuery.)

Использование привязки моделей как части запроса Ajax

Данные изменяются настолько быстро, что создается впечатление об их фильтрации на стороне клиента. Воспользовавшись приемом профилирования запроса (инструменты разработчика в веб-браузере), вы сможете увидеть, что при каждом изменении значения, выбранного в элементе <select>, отправляется новый запрос Ajax.

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