Использование Ajax и Web API в ASP.NET Web Forms 4.5

142

В этой статье будет рассмотрено появившееся в версии ASP.NET 4.5 средство Web API, которое позволяет легко создавать веб-службы. Мы покажем, как создавать веб-службу и потреблять ее с использованием jQuery и запросов Ajax.

Ваших знаний ASP.NET должно быть вполне достаточно, чтобы разобраться в способе создания веб-служб Web API, но для понимания примеров потребления веб-служб внутри веб-форм понадобятся знания языка JavaScript и библиотеки jQuery.

Среда ASP.NET Framework обладает встроенной поддержкой запросов Ajax, которая главным образом основана на элементе управления UpdatePanel. Это по-настоящему неудачное средство, которое давным-давно устарело. Оно настолько плохо сочетается с принципами разработки современных веб-приложений, что мы решили здесь его не демонстрировать.

Пример проекта

Мы продолжим пользоваться проектом ClientDev, созданным в предыдущих статьях, посвященных оптимизации клиентских библиотек и таблиц стилей. Мы собираемся работать с данными, поэтому построим хранилище объектов Game, размещаемое в памяти, для тестирования примеров. Мы создали папку под названием Models и поместили в нее файл класса по имени Game.cs, содержимое которого приведено в примере ниже:

using System;

namespace ClientDev.Models
{
    [Serializable]
    public class Game
    {
        public int GameId { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public string Category { get; set; }
        public decimal Price { get; set; }
    }
}

Это тот же самый класс Game, который использовался в приложении GameStore. Мы создали папку Models/Repository и добавили в нее файл класса под названием Repository.cs, содержимое которого представлено в примере ниже:

using System.Collections.Generic;
using System.Linq;

namespace ClientDev.Models.Repository
{
    public class Repository
    {
        private static Dictionary<int, Game> data = new Dictionary<int, Game>();

        public IEnumerable<Game> Games
        {
            get
            {
                return data.Values;
            }
        }

        public void SaveGame(Game Game)
        {
            data[Game.GameId] = Game;
        }

        public void DeleteGame(Game Game)
        {
            if (data.ContainsKey(Game.GameId))
            {
                data.Remove(Game.GameId);
            }
        }

        public void AddGame(Game Game)
        {
            Game.GameId = Games.Select(p => p.GameId).Max() + 1;
            SaveGame(Game);
        }

        static Repository()
        {
            Game[] dataArray = new Game[] {
                new Game { Name = "SimCity", Price = 1499, Category="Симулятор" },
                new Game { Name = "TITANFALL", Price=2299, Category="Шутер" },
                new Game { Name = "Battlefield 4", Price=899.4M, Category="Шутер" },
                new Game { Name = "The Sims 4", Price = 849, Category="Симулятор" },
                new Game { Name = "Dark Souls 2", Price=949, Category="RPG" },
                new Game { Name = "The Elder Scrolls V: Skyrim", Price=1399, Category="RPG" },
                new Game { Name = "FIFA 14", Price = 699, Category="Симулятор" },
                new Game { Name = "Need for Speed Rivals", Price=544, Category="Симулятор" },
                new Game { Name = "Crysis 3", Price=1899, Category="Шутер" },
                new Game { Name = "Dead Space 3", Price = 499, Category="Шутер" },
            };

            for (int i = 0; i < dataArray.Length; i++)
            {
                dataArray[i].GameId = i;
                data[i] = dataArray[i];
            }
        }
    }
}

В классе Repository определено свойство, предназначенное для извлечения доступных объектов Game, а также методы SaveGame(), DeleteGame() и AddGame(), которые позволяют обновлять, удалять и вставлять объекты Game. Хранилище заполняется с помощью статического конструктора. Это означает, что изменения, вносимые в данные, сохраняются только на период выполнения приложения, но будут сброшены в первоначальное состояние при перезапуске приложения.

Создание веб-служб с использованием Web API

С годами в ASP.NET Framework добавлялись различные технологии, предназначенные для создания веб-служб, и каждая из них учитывала сложившуюся на тот момент практику разработки. В ASP.NET 4.5 включено средство Web API, позволяющее создавать простые и легковесные веб-службы, которые точно воспроизводят природу протокола HTTP, используя различные типы HTTP-методов (GET, PUT, POST, DELETE и т.д.) для указания операций над данными. Это является фундаментом стиля REST (Representation State Transfer - передача состояния представления) в Web API, а результирующие веб-службы часто называются службами, поддерживающими REST. Операция в них указывается путем комбинирования URL и HTTP-метода, используемого для его запроса.

REST - это стиль API-интерфейса, а не строго определенная спецификация. Существуют разногласия по поводу того, как превратить веб-службу в службу, поддерживающую REST. Один из предметов спора связан с тем, что сторонники чистоты стиля не считают веб-службы, возвращающие данные в формате JSON, службами, которые поддерживают REST. Как и в случае любых разногласий, касающихся архитектурного шаблона, причины произвольны и скучны. Мы стараемся быть прагматичными относительно того, как применять шаблоны, поэтому считаем службы JSON поддерживающими REST.

Средство Web API упрощает создание веб-служб, которые могут быть вызваны с применением Ajax и возвращать данные в формате JSON, легко обрабатываемом с помощью JavaScript.

Работа с форматом JSON

Буква "x" в Ajax означает XML. Когда технология Ajax только набирала обороты, основным форматом данных был XML. В последние годы XML в значительной степени заменяется форматом JSON (JavaScript Object Notation - нотация объектов JavaScript), который является более простым и основан на способе представления данных в JavaScript. В качестве примера ниже показано, как объект Game может быть представлен с использованием JSON:

{"GameId":0, "Name":"SimCity", "Description":null, "Price":1499, "Category":"Симулятор"}

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

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

Описание цели

Мы будем применять средство Web API для создания веб-службы, которая предоставит доступ к объектам Game в хранилище, и затем использовать эти объекты с помощью Ajax и JavaScript внутри веб-формы. Выбранный для веб-службы URL будет следовать соглашению, принятому в Web API, которое выглядит как /api/<тип_данных>, что в этом случае означает отправку запросов Ajax на URL вида /api/game. Как уже объяснялось, HTTP-метод, применяемый в запросе, сообщает веб-службе, какую операцию необходимо выполнить. Операции описаны в таблице ниже:

Операции, которые будет поддерживать разрабатываемая веб-служба
Операция Описание
Получение всех объектов данных

Отправляет HTTP-запрос GET к /api/game

Удаление объекта

Отправляет HTTP-запрос DELETE, в котором в качестве сегмента URL указан уникальный идентификатор. Например, запрос DELETE к /api/game/3 будет запросом на удаление из хранилища объекта Game, свойство GameId которого имеет значение 3

Обновление объекта

Отправляет HTTP-запрос PUT, в котором в качестве сегмента URL указан уникальный идентификатор и который обновляет значения свойств с помощью данных формы. Например, запрос PUT к /api/game/3 будет запросом на обновление данными формы объекта Game в хранилище, свойство GameId которого имеет значение 3

Создание нового объекта

Отправляет HTTP-запрос POST с выраженными в виде данных формы - значениями для свойств нового объекта. Например, запрос POST к /api/game будет запросом на создание нового объекта с использованием значений данных формы

Возможно, вы еще не сталкивались с HTTP-методами PUT и DELETE, т.к. они не находят широкого применения за рамками веб-служб. Некоторые устаревшие браузеры не распознают такие HTTP-методы. В качестве обходного маневра можно отправлять запрос POST и указывать в заголовке x-Requested-with запроса имя HTTP-метода, который желательно было использовать.

Создание контроллера Web API

Средство Web API основано на инфраструктуре ASP.NET MVC Framework, в которой принят совершенно другой подход к разработке веб-приложений, отличающийся от используемого инфраструктурой Web Forms. Мы покажем, как создать веб-службу с применением средства Web API, но не будем объяснять детали работы MVC.

Для создания веб-службы необходимо добавить в проект новый элемент с использованием шаблона элементов Web API Controller Class (Класс контроллера Web API) в Visual Studio. Инфраструктура ASP.NET MVC Framework основана на принципе соглашения по конфигурации, при котором вместо применения сложных конфигурационных файлов разработчик должен следовать устоявшимся соглашениям, касающихся (среди прочих) именования классов и методов.

Одно из таких соглашений заключается в том, что имя класса Web API должно быть образовано из имени типа данных, с которым оперирует веб-служба, и слова Controller. В рассматриваемом примере это означает создание класса контроллера Web API по имени GameController. Первоначальный контент, помещаемый средой Visual Studio в файл GameController.cs, приведен в примере ниже:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;

namespace ClientDev
{
    public class GameController : ApiController
    {
        // GET api/<controller>
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }

        // GET api/<controller>/5
        public string Get(int id)
        {
            return "value";
        }

        // POST api/<controller>
        public void Post([FromBody]string value)
        {
        }

        // PUT api/<controller>/5
        public void Put(int id, [FromBody]string value)
        {
        }

        // DELETE api/<controller>/5
        public void Delete(int id)
        {
        }
    }
}

Контроллеры являются строительными блоками веб-приложений ASP.NET MVC Framework, а определенные в них методы используются для обслуживания HTTP-запросов. Контроллеры Web API относятся к специальному типу контроллеров, которые применяются для создания веб-служб.

Среда Visual Studio создает макет класса, который можно использовать в качестве отправной точки. В нем определены методы Get(), Post(), Put() и Delete(), которые соответствуют применяемым HTTP-методам, но из них реализованы только методы Get(). Метод Get(), не принимающий аргументов, предназначен для возвращения всех доступных объектов данных посредством запроса GET к /api/game, а метод Get() с аргументом id - для возвращения конкретного объекта через запрос GET к /api/game/id, где id уникальным образом идентифицирует требуемый объект. Вскоре мы реализуем остальные методы и изменим их типы данных на Game.

Атрибут FromBody, примененный к аргументам методов Post() и Put(), является особенностью Web API, которая гарантирует, что значения данных, используемые для привязки моделей, берутся из тела запроса, а не из сегментов маршрута URL. Его дополняет атрибут FromUri, который обеспечивает взятие значений из запрошенного URL.

Создание конфигурации маршрутизации

Контроллеры Web API по умолчанию не доступны, поэтому мы должны воспользоваться средством маршрутизации URL для отображения URL на нужный класс. Мы добавили в папку App_Start файл класса под названием RouteConfig.cs и определили в нем маршрут, требуемый для класса GameController:

using System.Web.Routing;
using System.Web.Http;

namespace ClientDev
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.MapHttpRoute(name: "WebApiRoute",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional });
        }
    }
}

Для добавления маршрута к веб-службе в коллекцию применяется расширяющий метод MapHttpRoute() из пространства имен System.Web.Http. Хотя расширяющий метод и новый, структура маршрута осталась такой же, как и для стандартных средств маршрутизации, за исключением того, что значение переменной controller будет автоматически дополняться словом Controller, т.е. запросы к /api/game отображаются на класс GameController.

Маршрут URL должен быть инициализирован при запуске приложения. В примере ниже можно видеть изменения, внесенные в глобальный класс приложения Global.asax.cs:

using System;
using System.Web.Optimization;
using System.Web.Routing;

namespace ClientDev
{
    public class Global : System.Web.HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            BundleConfig.RegisterBundles(BundleTable.Bundles);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
        }
    }
}

В этом примере мы создаем маршрут только для веб-службы, но внутри одного приложения могут без проблем сосуществовать и маршруты для веб-служб, и маршруты для веб-форм.

Тестирование веб-службы

Мы добрались до точки, когда веб-службу можно протестировать. Проще всего для этого использовать браузер. Запустите приложение и измените URL так, чтобы браузер запрашивал /api/game. (Таким образом, например, если после запуска приложения браузер запрашивает http://localhost:32160/Default.aspx, приведите запрос к виду http://localhost:32160/api/game.) To, что произойдет дальше, зависит от применяемого браузера. В Internet Explorer 10 будет предложено открыть файл по имени game.json, если используется браузер Google Chrome, в его окне отобразятся следующие данные XML:

<ArrayOfstring xmlns:i="http://www.w3.org/2001/XMLSchema-instance" 
  xmlns="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
	<string>value1</string>
	<string>value2</string>
</ArrayOfstring>

Веб-службы, создаваемые с помощью Web API, способны возвращать данные в формате XML или JSON, а выбор кодирования осуществляется посредством значения заголовка Accept в запросе. Разные браузеры утверждают, что могут принимать различные типы данных, и средство Web API пытается под них адаптироваться. Мы можем реализовать более полные тесты, для чего создадим веб-форму и выполним запросы Ajax. В примере ниже представлен контент веб-формы под названием GameTest.aspx:

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

<!DOCTYPE html>
<html>
<head runat="server">
    <title></title>
    <style>
        div { margin-bottom: 10px; }
    </style>
    <%: System.Web.Optimization.Scripts.Render("~/bundle/jquery") %>
    <script>
        function GetObjectString(dataObject) {
            if (typeof dataObject === "string") {
                return dataObject;
            } else {
                var message = "";
                for (var prop in dataObject) {
                    message += prop + ": " + dataObject[prop] + "\n";
                }
                return message;
            }
        }

        $(document).ready(function () {
            $("button").click(function (e) {
                var action = $(e.target).attr("data-action");
                $.ajax({
                    url: action == "all" ? "/api/game" : "/api/game/1",
                    type: "GET",
                    dataType: "json",
                    success: function (data) {
                        if (Array.isArray(data)) {
                            var message = "";
                            for (var i = 0; i < data.length; i++) {
                                message += "Item " + [i] + "\n"
                                    + GetObjectString(data[i]) + "\n\n";
                            }
                            $("#results").text(message);
                        } else {
                            $("#results").text(GetObjectString(data));
                        }
                    }
                });
                e.preventDefault();
            });
        });
    </script>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            <button data-action="all">Все записи</button>
            <button data-action="one">Одна запись</button>
        </div>
        <textarea id="results" cols="40" rows="10"></textarea>
    </form>
</body>
</html>

Эта веб-форма содержит элементы button, которые будут применяться для вызова двух версий метода Get() класса GameController через запросы Ajax. Запросы Ajax можно выдавать с использованием объектов в API-интерфейсе модели HTML DOM, но библиотека jQuery позволяет делать это проще и эффективнее. Мы хотим подчеркнуть связь между веб-службой и HTTP-методами, поэтому собираемся применять низкоуровневую функцию ajax() библиотеки jQuery, однако доступны также и более лаконичные вспомогательные функции.

После загрузки HTML-документа мы используем jQuery для нахождения элементов button и регистрации функции обработчика для события click. Когда на одном из элементов button осуществляется щелчок, мы вызываем функцию ajax() библиотеки jQuery, передавая ей объект конфигурации с помощью свойств. Все детали конфигурации Ajax-запросов в jQuery описаны в статьях jQuery и Ajax и Использование Ajax.

В предыдущем примере мы определяем, на какой кнопке был выполнен щелчок, за счет просмотра атрибутов data-action. Атрибуты, имена которых начинаются с data-, называются атрибутами данных и позволяют отделить назначение HTML-элемента от его идентификатора или типа дескриптора (это очень похоже на шаблон команд, используемый многофункциональными элементами управления пользовательского интерфейса ASP.NET).

В настоящий момент поддерживаются только запросы GET, но уже сейчас можно взглянуть на данные, возвращаемые стандартными реализациями методов. Запустите приложение, запросите веб-форму GameTest.aspx и щелкните на кнопках; результат представлен на рисунке ниже:

Тестирование веб-службы для объектов Game

Реализация методов контроллера

Следующий шаг связан с реализацией методов в классе GameController, чтобы можно было оперировать с объектами Game в хранилище, как показано в примере ниже. Процесс подобен созданию методов данных в классах отделенного кода:

using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using ClientDev.Models;
using ClientDev.Models.Repository;

namespace ClientDev
{
    public class GameController : ApiController
    {
        public IEnumerable<Game> Get()
        {
            return new Repository().Games;
        }

        public Game Get(int id)
        {
            return new Repository().Games.Where(game => game.GameId == id)
                .FirstOrDefault();
        }

        public void Post([FromBody] Game value)
        {
            new Repository().AddGame(value);
        }

        public void Put(int id, [FromBody] Game value)
        {
            new Repository().SaveGame(value);
        }

        public void Delete(int id)
        {
            Repository repository = new Repository();
            Game game = repository.Games
                .Where(g => g.GameId == id).FirstOrDefault();
            if (game != null)
                repository.DeleteGame(game);
        }
    }
}

Мы изменили возвращаемые типы методов Get() на Game, равно как и аргументы для методов Post() и Put(). Средство Web API использует приемы привязки моделей, т.е. мы можем указывать объект Game в качестве аргумента, и он будет создан на основе значений, находящихся в данных формы.

Обратиться к реализациям метода Get() можно с помощью веб-формы GameTest.aspx - к другим методам мы еще вернемся. Запустите приложение, запросите веб-форму GameTest.aspx и щелкните на одной из кнопок. Вы увидите, что где-то возникла проблема. Вместо данных Game элемент <textarea> будет содержать данные, подобные показанным ниже:

Item 0
<GameId>k__BackingField: 0
<Name>k__BackingField: SimCity
<Description>k__BackingField: null
<Category>k__BackingField: Симулятор
<Price>k__BackingField: 1499

...

Проблема с данными, отправленными веб-службой, вызвана классом Web API, который отвечает за создание JSON-представлений для объектов и называется форматером JSON. Форматер JSON путается с атрибутом Serializable, примененным для декорирования класса модели Game, и генерирует странную смесь из JSON и сериализированного представления C# объекта.

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

Второй способ решения проблемы предусматривает указание форматеру JSON дополнительной информации за счет добавления других атрибутов в класс модели. Форматер JSON, применяемый Web API - это пакет Json.NET с открытым кодом, который поддерживает атрибуты, позволяющие управлять тем, как объекты преобразуются в формат JSON. Один из таких атрибутов применяется к классу Game в примере ниже:

using System;
using Newtonsoft.Json;

namespace ClientDev.Models
{
    [Serializable]
    [JsonObject]
    public class Game
    {
        public int GameId { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public string Category { get; set; }
        public decimal Price { get; set; }
    }
}

Для исправления проблемы с сериализацией используется атрибут JsonObject. Запустите приложение, запросите веб-форму GameTest.aspx и щелкните на одной из кнопок. На этот раз данные полученные выглядят следующим образом:

Item 0
GameId: 0
Name: SimCity
Price: 1499
Category: Симулятор

Мы исправили проблему, но только за счет применения средств пакета, который адаптирован Microsoft для Web API. Нам нравится пакет Json.NET, однако мы не хотим создавать зависимости от средств, которые Microsoft не открывает для широкого использования в рамках Web API.

В ситуациях подобного рода мы принимаем подход с созданием объектов модели представления, к которым не применяется атрибут Serializable. В примере ниже демонстрируется определение и применение класса модели представления для GameController:

using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using ClientDev.Models;
using ClientDev.Models.Repository;

namespace ClientDev
{
    public class GameView
    {
        public GameView() { }

        public GameView(Game Game)
        {
            this.GameId = Game.GameId;
            this.Name = Game.Name;
            this.Price = Game.Price;
            this.Category = Game.Category;
        }

        public int GameId { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
        public string Category { get; set; }

        public Game ToGame()
        {
            return new Game
            {
                GameId = this.GameId,
                Name = this.Name,
                Price = this.Price,
                Category = this.Category
            };
        }
    }

    public class GameController : ApiController
    {
        public IEnumerable<GameView> Get()
        {
            return new Repository().Games
                .Select(g => new GameView(g));
        }

        public GameView Get(int id)
        {
            return new Repository().Games.Where(game => game.GameId == id)
                .Select(g => new GameView(g)).FirstOrDefault();
        }

        public void Post([FromBody] GameView value)
        {
            new Repository().AddGame(value.ToGame());
        }

        public void Put(int id, [FromBody] GameView value)
        {
            Repository repository = new Repository();
            Game game = repository.Games
                .Where(g => g.GameId == id).FirstOrDefault();

            if (game != null)
            {
                game.Name = value.Name;
                game.Price = value.Price;
                game.Category = value.Category;
            }

            repository.SaveGame(game);
        }

        public void Delete(int id)
        {
            Repository repository = new Repository();
            Game game = repository.Games
                .Where(g => g.GameId == id).FirstOrDefault();
            if (game != null)
                repository.DeleteGame(game);
        }
    }
}

Использование для каждого запроса к веб-службе созданных объектов модели представления и затем их отбрасывание некоторые программисты считают неэффективным. Однако мы считаем, что объекты создаются и уничтожаются в огромном количестве для каждого запроса ASP.NET (HttpRequest, HttpResponse, HttpContext и т.д.), к тому же создание кратко существующих объектов отражает саму природу веб-приложений, а их очистка является работой сборщика мусора.

Класс модели представления GameView содержит подмножество свойств, определенных в классе модели Game. (В этой статье мы не работаем со свойством Description, поэтому оно не включено в класс модели представления.) Класс GameView также содержит конструктор и метод ToGame(), который упрощает перемещение между экземплярами класса Game.

Методы класса GameController были модифицированы для использования класса GameView. Вследствие таких изменений мы выполняем операции только над объектами Game, которые происходят из хранилища, а не теми, которые созданы процессом привязки моделей.

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