Потребление веб-служб Web API
190ASP.NET --- ASP.NET Web Forms 4.5 --- Потребление веб-служб Web API
Итак, в предыдущей статье мы создали веб-службу 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.)
Данные изменяются настолько быстро, что создается впечатление об их фильтрации на стороне клиента. Воспользовавшись приемом профилирования запроса (инструменты разработчика в веб-браузере), вы сможете увидеть, что при каждом изменении значения, выбранного в элементе <select>, отправляется новый запрос Ajax.