ASP.NET AJAX - обратные вызовы сервера

83

Первый пример ASP.NET AJAX, который мы рассмотрим - измененная версия страницы клиентского обратного вызова, представленной в статье «Использование Ajax с клиентскими обратными вызовами». Эта страница включает в себя два раскрывающихся списка. Первый отображает список регионов, и второй - территории в выбранной области. Фокус в том, что второй список заполняется каждый раз, когда пользователь делает выбор в первом. Процесс заполнения списка требует обращения к серверу, который выполняет поиск в базе данных и предоставляет список:

Измененный пример динамического списка

Чтобы эта страница могла работать, используя функцию ASP.NET клиентского обратного вызова, нужно реализовать несколько громоздкий интерфейс ICallbackEventHandler. В ASP.NET AJAX применяется другой подход. В ASP.NET AJAX обратные вызовы всегда адресуются отдельному серверному методу, который формально представляет собой веб-службу. Этот подход улучшает разделение логики, облегчая организацию кода. Но что более важно, веб-служба берет на себя работу по сериализации. Это означает, что не нужно разрабатывать собственный метод для отправки сложных данных (подобный системе разбиения строки, рассмотренной ранее, которая довольно неуклюже разделяла значения символом вертикальной черты).

В последующих разделах будет показано создание требуемой веб-службы и рассмотрено несколько вариантов для ее использования.

Веб-службы в ASP.NET AJAX

При выполнении обратного вызова сервера с помощью ASP.NET AJAX клиентский код JavaScript вызывает метод в серверной веб-службы.

Веб-служба - это набор одного или более серверных методов, которые могут вызываться удаленными клиентами. Для обращения к веб-службе этот клиент отправляет сообщение запроса через HTTP-соединение. Это подобно процессу обратной отправки веб-страницы, за исключением того, что тело запроса содержит аргументы, которые передаются методу. Затем ASP.NET создает объект веб-службы, выполняет код в соответствующем веб-методе, возвращает результат и уничтожает объект веб-службы.

Существуют различные форматы сообщений запроса и ответа. Традиционно применяется стандарт на основе XML, называемый SOAP, но в ASP.NET AJAX, в основном, по соображениям совместимости браузеров, применяется упрощенная альтернатива на основе текста, получившая название JSON (JavaScript Object Notation - объектная нотация JavaScript).

Веб-службы .asmx и WCF

В .NET поддерживаются две технологии веб-служб: .asmx и WCF.

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

В качестве внутреннего интерфейса страницы ASP.NET AJAX можно применять также службу WCF (Windows Communication Foundation). Концептуально этот подход совпадает с использованием обычной веб-службы .asmx. Во многих отношениях WCF является преемницей веб-служб .asmx - она представляет собой более универсальную платформу, охватывающую ряд сценариев, которые веб-службы .asmx не принимают во внимание. Однако ни один из этих усовершенствованных сценариев неприменим к страницам ASP.NET AJAX. С практической точки зрения обе технологии веб-служб предоставляют совершенно одинаковые возможности странице ASP.NET AJAX.

Службы WCF несколько более трудоемки в реализации, поскольку они должны быть правильно зарегистрированы в файле web.config. Это регистрационное действие конфигурирует службу WCF так, чтобы она применяла сериализацию JSON. (Это же действие по конфигурации выполняется веб-службами .asmx посредством атрибута.) Простейший способ создания службы WCF с необходимыми параметрами конфигурирования - выбор пункта меню Website --> Add New Item в Visual Studio и последующий указание шаблона AJAX-enabled WCF Service (Служба WCF с поддержкой AJAX). Однако вполне допустимо выбрать службу .asmx, чтобы избежать излишней работы с конфигурированием в файле web.config, как было сделано в примерах этой статьи.

Важно понять, что хотя механизм обратного вызова ASP.NET AJAX использует веб-службы, это представляет собой специализированную реализацию. Те, кто знаком с веб-службами, найдут, что ASP.NET AJAX налагает некоторые дополнительные ограничения:

Эти ограничения не препятствуют использованию функции обратного вызова ASP.NET AJAX по ее прямому назначению - в качестве механизма выполнения страницей задач серверного приложения. Если ранее вам приходилось использовать веб-службы для представления серверной функциональности "толстым" клиентам, сторонним разработчикам и приложениям на платформах, отличных от .NET, вы увидите, что применение веб-служб в ASP.NET AJAX несколько проще.

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

Создание веб-службы

Веб-службы, используемые с AJAX ASP.NET, состоят из двух частей: файла .asmx, который действует в качестве конечной точки веб-службы, и файла .cs, который содержит фактический код C#. Эти файлы необходимо добавить к веб-сайту, содержащему страницу AJAX ASP.NET, которая будет использовать веб-службу.

Самый быстрый способ создания веб-службы в среде Visual Studio - выбрать пункт меню Website --> Add New Item, указать шаблон Web Service, назначить имя файлу (в следующем примере - TerritoriesService) и щелкнуть на кнопке Add (Добавить). При создании веб-сайта без проекта файл .asmx будет помещен в каталог веб-приложения, а соответствующий файл .cs - в папку App_Code для автоматической компиляции.

Чтобы веб-службы можно было использовать с ASP.NET AJAX, веб-приложение не обязательно размещать в виртуальном каталоге IIS. Вместо этого для тестирования приложения можно применять встроенный веб-сервер Visual Studio. Это возможно потому, что код сценария, который автоматически вызывает веб-службу, использует относительный путь. В результате, независимо от порта, выбираемого веб-сервером Visual Studio, веб-страница сможет сформировать правильный URL-адрес.

Файл .asmx не представляет особого интереса - если его открыть, в нем обнаружится единственная строка с директивой WebService, которая определяет язык кода, расположение файла отделенного кода и имя класса:

<%@ WebService Language="C#" CodeBehind="~/App_Code/TerritoriesService.cs" 
   Class="TerritoriesService" %>

В этом примере создается веб-служба TerritoriesService.asmx с файлом отделенного кода TerritoriesService.cs. В файле отделенного кода определен класс TerritoriesService, который выглядит следующим образом:

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.Web.Script.Services.ScriptService]
public class TerritoriesService : System.Web.Services.WebService {

    // ...
    
}

По умолчанию атрибут ScriptService закомментирован. Чтобы создать веб-службу, которую можно будет вызывать из страницы ASP.NET AJAX, не забудьте удалить символы комментария.

Этот класс является производным от System.Web.Services.WebService, который служит традиционным базовым классом для веб-служб. Однако этот подход выбран лишь для удобства и не является обязательным. Наследование от WebService предоставляет доступ к ряду встроенных объектов (таких как Application, Server, Session и User) без необходимости обращения к статическому свойству HttpContext.Current.

Обратите также внимание, что объявление класса веб-службы содержит три атрибута. Два первых - WebService (устанавливает пространство имен XML, используемое в сообщениях веб-службы) и WebServiceBinding (указывает уровень соответствия стандартам, поддерживаемый веб-службой) - применяются только при вызове веб-службы с помощью сообщений SOAP и не имеют значения в страницах ASP.NET AJAX. Однако третий атрибут - ScriptService - значительно важнее. Он конфигурирует веб-службу, разрешая JSON-вызовы из клиентов JavaScript. Без этого веб-службу нельзя было бы применять в странице ASP.NET AJAX.

Создание веб-метода

После выполнения описанных действий можно приступать к написанию кода для своей веб-службы. Каждая веб-служба содержит один или более методов, которые помечены атрибутом WebMethod. Этот атрибут делает метод доступным для удаленного вызова. Если добавить метод, который не содержит атрибут веб-метода, серверный код сможет его использовать, но клиентский код JavaScript не сможет вызывать его непосредственно.

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.Web.Script.Services.ScriptService]
public class TerritoriesService : System.Web.Services.WebService 
{

    [WebMethod]
    public string HelloWorld() {
        return "Hello World";
    }
    
}

He обязательно делать метод общедоступным (как в этом примере), но обычно это делают согласно принятым соглашениям. На веб-методы накладываются определенные ограничения. Типы данных, используемые для значений параметров и возвращаемых значений, должны быть из числа тех, что описаны в таблице ниже:

Допустимые типы данных параметров и возвращаемых значений веб-службы
Тип данных Описание
Базовые типы

Базовые типы данных C#, такие как целые числа (short, int, long), целые без знака (ushort, uint, ulong), нецелочисленные числовые типы (float, double, decimal) и ряд других смешанных типов (bool, string, char, byte, DateTime)

Перечисления

Типы перечислений (определенные в C# с помощью ключевого слова enum) поддерживаются. Однако, веб-служба использует строковое имя значения перечисления (а не лежащее в основе целое число)

Специальные объекты

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

Массивы и коллекции

Можно использовать массивы любого поддерживаемого типа. Допускается также ArrayList (который просто преобразуется в массив), но более специализированные коллекции, такие как Hashtable, не разрешены. Можно применять базовые коллекции. Во всех этих случаях объекты в коллекции должны быть также сериализуемыми

XmlNode

Объекты, основанные на System.Xml.XmlNode, являются представлениями части XML-документа. Их можно применять для отправки произвольного XML-текста

DataSet и DataTable

DataSet и DataTable можно использовать для возврата информации из реляционной базы данных. Другие объекты данных ADO.NET, такие как DataColumns и DataRows, не поддерживаются. Применяемый объект DataSet или DataTable автоматически преобразуется в XML-фрагмент, подобно тому, как это происходит при использовании методов GetXml() или WriteXml()

Состояние сеанса в веб-службе

Атрибут WebMethod принимает ряд параметров, большинство из которых несет определенную нагрузку в странице ASP.NET. Одним исключением является свойство EnableSession, которое по умолчанию имеет значение false, в результате чего состояние сеанса визуализации веб-службе недоступно. Это значение по умолчанию имеет смысл в традиционной веб-службе, не использующей ASP.NET AJAX, поскольку какая-либо информация о сеансе может не существовать, а клиент может вообще не поддерживать cookie-набор сеанса. Но в случае веб-службы ASP.NET AJAX вызовы веб-службы всегда осуществляются из контекста веб-страницы ASP.NET, которая выполняется в контексте текущего пользователя веб-приложения, и у этого пользователя имеются действующий сеанс и cookie-набор сеанса, автоматически передаваемые вместе с вызовом веб-службы.

Ниже приведен пример предоставления веб-методу доступа к объекту Session:

[WebMethod(EnableSession=true)]
public void DoSomething() 
{
        if (Session["myObject"] != null)
        {
            // (Использовать объект в состоянии сеанса.)
        }
        else
        {
            // (Создать новый объект и сохранить его в состоянии сеанса.)
        }
}

Для примера с раскрывающимся списком веб-служба должна обеспечить способ получения регионов, расположенных на данной территории. Ниже приведен код веб-службы, содержащей веб-метод GetTerritoriesInRegion(), который извлекает регионы:

using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Services;
using System.Data;
using System.Data.SqlClient;
using System.Web.Configuration;

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.Web.Script.Services.ScriptService]
public class TerritoriesService : System.Web.Services.WebService 
{
    [WebMethod()]
    public List<Territory> GetTerritoriesInRegion(int regionID)
    {
        SqlConnection con = new SqlConnection(
          WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString);

        SqlCommand cmd = new SqlCommand(
            "SELECT * FROM Territories WHERE RegionID=@RegionID", con);

        cmd.Parameters.Add(new SqlParameter("@RegionID", SqlDbType.Int, 4));
        cmd.Parameters["@RegionID"].Value = regionID;

        List<Territory> territories = new List<Territory>();
        try
        {
            con.Open();
            SqlDataReader reader = cmd.ExecuteReader();

            while (reader.Read())
            {
                territories.Add(new Territory(
                  reader["TerritoryID"].ToString(),
                  reader["TerritoryDescription"].ToString()));
            }
            reader.Close();
        }
        catch
        {
            // Маскировать ошибки
            throw new ApplicationException("Ошибка данных");
        }
        finally
        {
            con.Close();
        }
        return territories;
    }   
}

Код метода GetTerritoriesInRegion() подобен коду, который применялся ранее для обслуживания клиентского обратного вызова. Однако у этого кода имеется основное отличие - вместо того, чтобы возвращать единственную длинную строку с результатами, он возвращает информацию, используя строго типизированный список объектов Territory. Это - намного более строгий подход, предотвращающий случайные ошибки.

Класс Territory охватывает две части строковой информации. В нем определены общедоступные переменные-члены, а не свойства, поскольку он призван играть роль исключительно контейнера данных, который транспортирует информацию по сети:

public class Territory
{
    public string ID;
    public string Description;

    public Territory(string id, string description)
    {
        this.ID = id;
        this.Description = description;
    }

    public Territory() { }
}

Это определение класса можно поместить в тот же файл кода, что и веб-служба, или в отдельный файл внутри каталога App_Code.

Вызов веб-службы

Теперь, когда нужная веб-служба создана, необходимо сконфигурировать страницу так, чтобы ей было известно о службе TerritoriesService. Для этого к странице нужно добавить элемент управления ScriptManager. Затем в дескриптор этого элемента управления потребуется добавить раздел <Services>.

С помощью элементов ServiceReference этого раздела перечисляются все используемые страницей службы и их расположения. Добавление ссылки на ранее приведенный файл TerritoriesService .asmx выполняется следующим образом:

<asp:ScriptManager ID="ScriptManager1" runat="server">
    <Services>
        <asp:ServiceReference Path="~/TerritoriesService.asmx" />
    </Services>
</asp:ScriptManager>

При визуализации страницы на сервере ScriptManager будет генерировать прокси-объект JavaScript. В клиентском коде этот прокси-объект JavaScript можно применять для выполнения вызовов. Ниже приведен код двух списков, помещенных в веб-форму:

<asp:DropDownList ID="lstRegions" runat="server" Width="210px" DataSourceID="sourceRegions"
    DataTextField="RegionDescription" DataValueField="RegionID"
    onchange="GetTerritories(this.value);" />
<asp:DropDownList ID="lstTerritories" runat="server" Width="275px" />
<asp:SqlDataSource ID="sourceRegions" runat="server" ConnectionString="<%$ ConnectionStrings:Northwind %>"
    SelectCommand="SELECT 0 As RegionID, '' AS RegionDescription UNION SELECT RegionID, RegionDescription FROM Region" />

Первый список заполняется посредством обыкновенной привязки данных ASP.NET с помощью элемента управления источником данных SqlDataSource. Больший интерес представляет то, что он использует атрибут onchange для привязки к клиентскому обработчику события. В результате, когда пользователь выбирает новую территорию, JavaScript-функция GetTerritories() запускается, и текущее значение списка передается в качестве аргумента.

Формально весь код функции GetTerritories() можно было бы поместить непосредственно в атрибут события onchange, тем самым уменьшив количество создаваемых функций JavaScript. Однако отделение кода, который вызывает веб-службу, улучшает читабельность кода и облегчает его сопровождение.

Код JavaScript-функции GetTerritories() имеет следующий вид:

<script type="text/javascript">
    function GetTerritories(regionID)
    {
          TerritoriesService.GetTerritoriesInRegion(
               regionID, OnRequestComplete, OnError);
    }
</script>

Если вам ранее приходилось программировать с применением веб-служб ASP.NET, вы заметите, что синтаксис клиентского кода вызова веб-службы ASP.NET AJAX отличается от синтаксиса .NET. В приложении .NET сначала должен быть создан прокси-объект, а затем вызвана веб-служба на этом объекте. В странице ASP.NET AJAX используется готовый прокси-объект с тем же именем, что и у класса веб-службы.

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

Чтобы завершить пример, нужно предоставить клиентскую функцию, которая обрабатывает ответ. В данном примере это функция OnRequestComplete(). Она принимает возвращаемое значение в единственном параметре, а затем добавляет информацию во второй раскрывающийся список на веб-странице:

function OnRequestComplete(result) {
    var lstTerritories = document.getElementById("lstTerritories");
    lstTerritories.innerHTML = "";

    for (var n = 0; n < result.length; n++) {
        var option = document.createElement("option");
        option.value = result[n].ID;
        option.innerHTML = result[n].Description;
        lstTerritories.appendChild(option);
    }
}

Примечательной особенностью этого кода является то, что он в состоянии работать с результатом, возвращенным из веб-метода, без выполнения каких-либо дополнительных действий по десериализации. Еще больше впечатляет то, что веб-метод возвращает обобщенный список объектов Territory, который, очевидно, не имеет никакого эквивалента в коде JavaScript. Вместо этого ASP.NET AJAX создает определение для объекта Territory и возвращает полный список в массиве. Это позволяет коду JavaScript просматривать в цикле массив и проверять свойства ID и Description каждого элемента.

В данном случае можно воспользоваться одним небольшим ухищрением. Вместо метода document.getElementById() допускается применять псевдоним $get из ASP.NET AJAX, который выполняет ту же функцию и имеет следующий вид:

var lstTerritories = $get("lstTerritories");

Этот прием - обычное соглашение, применяемое в страницах ASP.NET AJAX.

Теперь этот пример работает совершенно так же, как версия клиентского обратного вызова, описанная ранее. Различие между ними состоит в том, что эта версия использует строго типизированный веб-метод без громоздкого кода сериализации строк. Кроме того, не нужно добавлять никакой серверный код для динамического приема ссылки обратного вызова и ее вставки. Вместо этого можно применять простой прокси-объект, который обеспечивает доступ к веб-службе.

В качестве последнего штриха можно задать лимит времени и функции обработки ошибок, например:

function OnError(result) {
    var lbl = document.getElementById("lblInfo");
    lbl.innerHTML = "<b>" + result.get_message() + "</b><br>";
    lbl.innerHTML += result.get_stackTrace();
}

Функция OnError() получает объект ошибки, содержащий метод get_message(), который извлекает текст ошибки, и метод get_stackTrace(), который возвращает подробный стек вызовов с указанием места возникновения ошибки. На рисунке ниже показано, что происходит, когда веб-методу не удается подключиться к базе данных, и он генерирует стандартное исключение ApplicationException:

Обработка серверных ошибок на клиенте

В этом примере продемонстрирована версия ASP.NET AJAX модели клиентского обратного вызова. Хотя она использует те же внутренние механизмы, что и функция клиентского обратного вызова ASP.NET, версия ASP.NET AJAX обеспечивает более прочный фундамент построенный на основе веб-служб. Тем не менее, оба подхода обладают одной общей чертой - независимо от применяемого подхода придется писать собственный код JavaScript для обновления страницы.

Помещение веб-метода в страницу

В большинстве случаев для обработки обратных вызовов ASP.NET AJAX целесообразно создавать отдельную веб-службу. Обычно этот подход ведет к созданию более четких страниц и облегчает отладку и совершенствование кода. Тем не менее, в некоторых ситуациях могут существовать один или более веб-методов, которые явно предназначены для использования на единственной странице и действительно не будут применяться в других частях приложения. В этом случае можно создать выделенную веб-службу для каждой страницы или же переместить код веб-службы в страницу.

Помещение кода веб-метода в страницу не представляет сложности - фактически, для этого достаточно простого перетаскивания. Для начала скопируйте свой веб-метод (дополненный атрибутом WebMethod) в класс отделенного кода страницы. Затем измените его на статический метод и добавьте атрибут System.Web.Script.Services.ScriptMethod. Ниже приведен пример помещения веб-метода (GetTerritoriesInRegion) в веб-страницу:

public partial class _Default : System.Web.UI.Page
{
    [WebMethod()]
    [System.Web.Script.Services.ScriptMethod]
    public static List<Territory> GetTerritoriesInRegion(int regionID)
    {
        // Передать работу классу веб-службы
        TerritoriesService service = new TerritoriesService(); 
        return service.GetTerritoriesInRegion(regionID);
    }
}

Установите свойство ScriptManager.EnablePageMethods в true и удалите ссылку в разделе <Services> кода ScriptManager (при условии, что не собираетесь использовать какие-либо веб-службы, не встроенные в страницу):

<asp:ScriptManager ID="ScriptManager1" runat="server" EnablePageMethods="true" />

И, наконец, измените код JavaScript так, чтобы он вызывал метод посредством объекта PageMethods, как показано в следующем примере:

function GetTerritories(regionID) {
    PageMethods.GetTerritoriesInRegion(regionID,
        OnRequestComplete, OnError);
}

Объект PageMethods представляет все веб-методы, добавленные в текущую веб-страницу.

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

Еще одна возможная причина кодирования веб-методов непосредственно в классе страницы - необходимость считывания значений из состояния представления или элементов управления, расположенных на странице. При вызове метода страницы запускается упрощенная версия жизненного цикла страницы, как это имело место при использовании функции клиентского обратного вызова ASP.NET. Конечно, попытка модификации деталей страницы лишена какого-либо смысла, поскольку страница не визуализируется и, следовательно, любые вносимые изменения будут просто отброшены.

С точки зрения безопасности приложения не имеет никакого значения, размещаются веб-методы в странице или в выделенной веб-службе. Размещение веб-метода в странице может скрыть его от случайных пользователей, но настоящий взломщик начнет с просмотра HTML-кода страницы, который включает в себя ссылку на прокси-объект JavaScript. Злоумышленники могут легко использовать прокси-объект JavaScript для выполнения подложных вызовов веб-метода. Для защиты от подобных угроз веб-методы должны всегда реализовывать те же самые меры безопасности, которые применяются в веб-страницах. Например, любой принимаемый ввод должен быть проверен на достоверность, код должен отказываться возвращать уязвимую информацию не аутентифицированным пользователям, а при доступе к базе данных должны применяться параметризованные команды для предотвращения атак внедрением SQL.

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