Использование AJAX в ASP.NET 

Рассматривая ту или иную новую технологию прежде всего необходимо разобраться для чего эта самая технология необходима и что именно она позволяет (или облегчает) делать, что не позволяют делать другие технологии. Чтобы более наглядно понять, какие предпосылки были к появлению AJAX, можно рассмотреть процесс эволюционного развития web-сайтов. 

На самой заре развития интернет сайты представляли собой набор простых статических страниц – пользователь запрашивал ресурс у сервера и сервер возвращал статическую страницу. Эта страница представляла собой простой HTML текст и хранилась как текстовый файл на сервере. Данная страница поставлялась пользователю as is и, кроме того, не имела никаких клиентских скриптов.

CGI

Первая попытка сделать страницы более динамичными был Common Gateway Interface (CGI). CGI позволял разработчику создавать исполняемые программы, которые генерировали страницу, что позволяло принимать параметры от пользователя. С учетом этих параметров можно было генерировать уникальную страницу. По большому счету, данный подход используется и сейчас в том же ASP.NET, PHP и т.д.

JavaScript

Генерация страниц на серверной стороне это конечно хорошо, но это вынуждало при каждом, даже самом незначительном действии пользователя, делать запрос к серверу с перегрузкой всей страницы. Чтобы не перегружать всю страницу в случаях, когда участие сервера абсолютно не нужно, стали использоваться клиентские скрипты.

Апплеты и Flash

В случае, если запрос к серверу все же необходим и одними клиентскими скриптами не обойтись – разработчики в web-страницах стали использовать т.н. апплеты а также flash.

Апплеты впервые были использованы в 1995 году, когда Sun представила миру на всеобщее обозрение свою новую платформу с новым языком – JAVA. По сути апплеты представляли собой программы, написанные на JAVA и которые могли запускаться в броузере как отдельные приложения. Для того, чтобы эта программа, написанная на неведомом для броузера языке JAVA, могла быть запущена внутри броузера, необходимо было установить JVM (Java Virtual Machine) – среду для выполнения программ JAVA. И хотя данный подход звучит очень заманчиво – он имел множество недостатков, прежде всего это проблемы безопасности (не каждый пользователь, путешествуя по интернет, разрешит запускать на своем компьютере программы непонятного происхождения) и необходимость громозкой VM. Несмотря на то, что помимо общения с сервером апплеты и флеш также позволяли реализовать возможности, недоступные JavaScript (скажем, более высокие требования к графике) – те сложности не дали апплетам прижиться настолько, чтобы окончательно решить проблемы с запросами к серверу.

DHTML

Dynamic HTML объединил в себе HTML, каскадные таблицы стилей (CSS) и JavaScript. Также ко всему этому набору добавился DOM – объектная модель броузера. Вся эта смесь позволяла (и позволяет) успешно создавать очень красивые, удобные и функциональные страницы «на лету». Но опять же, в случае, если нужно выполнить запрос к серверу – приходится перегружать весь документ.

AJAX

Решение этой проблемы пришло с появлением новой технологии, которая в 2004 году была названа AJAX (Asynchronous JavaScript + XML). Данная технология построена на принципе выполнения запроса к серверу с использованием JavaScript и получению результата опять же, с помощью JavaScript, что позволяет избежать перегрузки страницы и следовательно имеет несколько неоспоримых преимуществ:

1. На сервер отправляются не все элементы страницы (точнее не их значения), а только те минимальные данные, которые необходимы для выполнения того или иного запроса и в ответ принимается не вся страница, а только необходимые данные, что позволяет уменьшить трафик в десятки (а иногда и в сотни) раз.
2. Не происходит перегрузка страницы в броузере и у пользователя создается впечатление, что все происходит на его компьютере.

Об этой технологии и пойдет дальше речь.
Объектная модель броузера.

Если вы меня спросите на чем основан принцип работы технологии AJAX, то я вам наверное отвечу : «благодаря объектной модели броузера». Что же это за такая объектная модель броузера (DOM)?

Document Object Model (DOM) – это спецификация, стандартизированная W3C комитетом, которая является кроссплатформенной и описывает вызовы и описания, касающиеся действиям с самим документом, его структурой, HTML, XML и стилями. Как следует из названия, основой спецификации DOM являются объекты.

Объект XMLHttpRequest

Этот объект появился впервые в Internet Explorer 5.0 и был реализован как ActiveX компонент. Важно заметить, что этот объект не является стандартом W3C, хотя многое из его функциональности описано в спецификации «The DOM Level 3 Load and Save Specification». По этой причине его поведение может немного отличаться в различных броузерах. Но во всех броузерах он выполняет одну и ту же функциональность – он умеет посылать запросы к серверу и получать от него ответы. Как уже говорилось выше, данный объект не стандартизирован и создание его instance может отличаться в различных версиях, поэтому для «надежного» его создания лучше использовать код, который объединяет в себе создание instance в нескольких броузерах подобно коду ниже:

var xmlHttp;
  function createXMLHttpRequest() 
  {
  if (window.ActiveXObject) 
  {
  xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
  }
  else if (window.XMLHttpRequest) 
  {
  xmlHttp = new XMLHttpRequest();
  }
  }

XMLHttpRequest имеет ряд «стандартных» (стандартных в кавычках т.к. как писалось выше, данный объект не стандартизирован для всех броузеров) свойств и методов, которые позволяют данному объекту посылать запрос, проверять состояние запроса и получать результат выполнения запроса от удаленного сервера. Эти свойства и методы мы рассмотрим в следующих двух таблицах.

В таблице 1 представлены «стандартные» свойства XMLHttpRequest

Метод

Описание

abort()

Прерывает текущий запрос

getAllResponseHeaders()

Возвращает все заголовки Response в виде ключ/значение

getResponseHeader(header)

Возвращает значение определенного заголовка

open(method, url, asynch, username, password)

Устанавливает состояние запроса к серверу. Первый параметр указывает метод запроса – PUT, GET, POST, второй – url запроса, третий (необязательный) – тип запроса (синхронный или асинхронный), четвертый и пятый (также необязательные) – для защищенных страниц

send(content)

Посылает запрос серверу

setRequestHeader(header, value)

Устанавливает значение определенного заголовка. Перед вызовом этого метода необходимо вызвать метод open

Также XMLHttpRequest содержит ряд свойств, которые представлены ниже:

Свойство

Описание

onreadystatechange

Обработчик события, которое возникает при каждой смене состояния запроса

readyState

Состояние запроса. Доступны следующие значения: 0 – запрос неинициализирован, 1 – загрузка, 2 – загрузка окончена, 3 – interactive, 4 - complete

responseText

Ответ от сервера в виде строки

responseXML

Ответ от сервера в XML. Этот объект может быть обработан и проверен как DOM

status

Код статуса HTML.(например 200 – OK)

statusText

Название кода статуса HTML

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

Прежде всего напишем серверную часть, которая будет возвращать простую строку. Для этого (чтобы не было «посторонних» данных вроде тегов открытия закрытия html) наиболее рационально будет создать hanlder. Открываем web-проект в Visual Studio 2005 и создаем файл типа Handler. Содержимое будет примерно следующим:

<%@ WebHandler Language="C#" Class="MyHandler" %>
  using System;
  using System.Web;
  public class MyHandler: IHttpHandler {
      public void ProcessRequest (HttpContext context) {
          context.Response.ContentType = "text/plain";
          context.Response.Write("Hello World");
      }
      public bool IsReusable {
          get {
              return false;
          }
      }
  }

Т.е. при запросе данной страницы этот hanlder возвращает text/plain документ с единственной строчкой “Hello World”. Нас такой handler устраивает как нельзя лучше.

Теперь создадим обычную HTML – страницу, которая будет и выполнять запрос, используя XMLHttpRequest.

<html xmlns="http://www.w3.org/1999/xhtml" >
  <head>
      <title>Simple XMLHttpRequest page</title>
      <script type="text/javascript">
          var xmlHttp;
          function createXMLHttpRequest() 
          {
              if (window.ActiveXObject) 
              {
                  xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
              }
              else if (window.XMLHttpRequest) 
              {
                  xmlHttp = new XMLHttpRequest();
              }
          }
          function startRequest() 
          {
              createXMLHttpRequest();
              xmlHttp.onreadystatechange = handleStateChange;
              xmlHttp.open("GET", "MyHandler.ashx", true);
              xmlHttp.send(null);
          }
          function handleStateChange() 
          {
              if(xmlHttp.readyState == 4) 
              {
                  if(xmlHttp.status == 200) 
                  {
                      alert("Response: " + xmlHttp.responseText);
                  }
              }
          }
      </script>     
  </head>
  <body>
      <input type="button" value="Start Asynchronous Request"
          onclick="startRequest();"/>
  </body>
  </html>

Данный код довольно прост. При нажатии на кнопку “Start Asynchronous Request” вызывается клиентская функция startRequest, которая в свою очередь сначала вызывает рассмотренную нами ранее функцию createXMLHttpRequest для создания объекта XMLHttpRequest, после чего цепляет обработчик (клиентскую функцию handleStateChange) на событие ReadyStateChange для этого объекта, открывает и посылает запрос. Если запрашиваемая страница доступна и данные были получены, status меняет свое состояние на 200. Поэтому в функции handleStateChange мы проверяем значение этого свойства. При нужном значении мы с помощью alert выводим полученное значение. Пробуем как это работает:

В данном несложном коде по сути заложена вся фукциональность AJAX – получение данных от сервера без перегрузки страницы. Понимания этого механизма достаточно, чтобы понять суть AJAX, а также успешно использовать его в своих приложениях. Далее только дело техники и далее мы рассмотрим реализацию всего этого, но с использованием ASP.NET J
Использование AJAX в ASP.NET
Обратные вызовы страницы.

Обратные вызовы – это специальный вид возврата формы, т.е. страница проходит свой цикл событий, но данные формы возвращаются клиенту до начала процесса рендеринга формы, т.е. до перерисовки. Как и в любом AJAX исполнении запрос начинается на клиентской стороне в результате возникновения какого-либо события, при этом запускается клиентская встроенная функция под названием WebForm_DoCallback. Эта функция имеет следующий прототип:

WebForm_DoCallback(pageID, argument, returnCallback, context, errorCallback, useAsync);

Где:

pageID – ID страницы, которая выполняет вызов,

argument – строковый аргумент, передаваемый серверу,

returnCallback – клиентская функция или клиентский скрипт, который должен выполниться после того, как серверная сторона вернет управление

context – данные, которые передаются returnCallback.

errorCallback – клиентская функция или клиентский скрипт, выполняемый при возникновении ошибок

useAsync – устанавливает, будет ли запрос синхронным или асинхронным.

Следующий этап – серверная страница должна знать, что она должна поддерживать обратные вызовы (т.е. прежде всего возвращать данные до начала рендеринга страницы). Для этого эта страница должна реализовывать интерфейс System.Web.UI.IcallbackEventHandler.

Данный интерфейс содержит 2 метода:

public interface ICallbackEventHandler
  { 
  string GetCallbackResult();
  void RaiseCallbackEvent(string eventArgument); 
  }

Выполнение обратного вызова на серверной стороне состоит из 2-х этапов: подготовка и возвращение результата. Метод RaiseCallbackEvent вызывается первым и предназначен для подготовки удаленного выполнения кода. Метод GetCallbackResult выполняется позже, когда результат уже готов к отправке. Данное разделение было введено только в release версии .NET 2.0, в предыдущих версиях эти 2 методы были объединены в один (это было сделано с учетом асинхронной работы). Метод GetCallbackResult возвращает string, поэтому возвращаемые данные должны быть сериализированы тем или иным методом в строку, а на клиенте наоборот, десериализированы.

При запросе страницы с клиентского скрипта сначала выполняется Init, после чего стандартный цикл событий загрузки страницы до события Load, в Load свойство IsCallback устанавливается в true, по завершению Load выполняются методы интерфейса ICallbackEventHandler, после чего как уже говорилось выше выполнение прерывается, не переходя в стадию рендеринга. Прежде всего это говорит о том, что не происходит стадия сохранения ViewState, так что пытаться что-либо сохранить в ViewState стандартным способом бесполезно (оно и понятно, т.к. ViewState страницы не обновляется). Управляет процессом взаимодействия между страницей и сервером т.н. диспетчер обратных вызовов. Диспетчер обратных вызовов имеет в себе библиотеку клиентских сценариев. Эти клиентские сценарии формируют и отправляют запрос, получают и разбирают ответ от сервера и т.д. Посмотрев View Source любой из страниц можно увидеть строки вроде

<script src="/WebResource.axd?d=VWLFgbEM584QkLzy5eDwGw2&amp;t=632964616335443742" type="text/javascript"></script>

Скачав файл, который возвращает данный обработчик WebResource.axd с указанными параметрами, можно углубиться в изучения клиентских скриптов, которые отвечают за указанные выше действия J

Метод GetCallbackEventReference

Написание клиентского метода WebForm_DoCallback не является чем-либо сложным, однако связано с некоторыми трудностями в случае динамического генерирования или передачей параметров. Для этого в класс Page.ClientScript (System.Web.UI.ClientScriptManager) введен специальный метод – GetCallbackEventReference, который получает ряд параметров и, подобно например методу GetPostBackEventReference, генерирует соответствующий клиентский код. Я бы не сказал, что этот метод очень изящный, особенно когда необходимо передать параметры в клиенский скрипт (особенно наличие одинарных и двойных кавычек в контатенирующейся строке портит всю картину), но все поудобнее, чем в лоб писать WebForm_DoCallback.

Данный метод имеет следующий прототип:

public string GetCallbackEventReference(
  Control target,
  string argument,
  string clientCallback,
  string context,
  string clientErrorCallback,
  bool useAsync)

где:

target – страница или WebControl, который будет обрабатывать обратный вызов. Соответственно, эта страница или контрол должны реализовать интерфейс ICallbackEventHandler, иначе будет брошено исключение:

System.InvalidOperationException: The target '__Page' for the callback could not be found or did not implement ICallbackEventHandler.

Генерует первый параметр функции WebForm_DoCallback

argument – аргумент, передаваемый клиентской функции или скрипту. Соответствует второму параметру функции WebForm_DoCallback.

returnCallback – клиентская функция или клиентский скрипт, который должен выполниться после того, как серверная сторона вернет управление (3-й параметр WebForm_DoCallback)

context – данные, которые передаются клиентской returnCallback (4-й параметр WebForm_DoCallback).

errorCallback – клиентская функция или клиентский скрипт, выполняемый при возникновении ошибок (5-й параметр WebForm_DoCallback)

useAsync – устанавливает, будет ли запрос синхронным или асинхронным (6-й параметр WebForm_DoCallback).

Теперь мы можем создать нашу первую aspx страницу с использованием ajax. Допустим, довольно частая задача – есть 2 выпадающих списка – в одном из них данные более высокого уровня, нежели во втором. Т.е. при выборе значения в первом списке – второй список перегружается в зависимости от выбранного значения в первом. Например – первый SELECT содержит список производителей машин, второй – модели машин, которые выбранный производитель выпускает. Для решения этой задачи можно конечно загрузить все данные в javascript, а потом скриптом перебиндивать второй select. В нашем случае это в принципе подошло бы, но есть несколько минусов – первый это то, что данных может быть очень много (допустим, 100 производителей и для каждого из них указаны все модели, когда либо выпускавшиеся, т.е. может быть более 100. Итого грузить несколько десятков тысяч итемов в javascript не есть лучший вариант). Кроме того, прийдется писать не такой уж и простой яваскрипт. Плюс может быть ситуация, когда данные должны быть интерактивными.

Попробуем решить это с помощью AJAX.

Для этого создаем обычную ASPX страницу:

<form id="form1" runat="server">
  <div>
      <table>
          <tr>
              <td>
                  Please, select Manufacturer:
              </td>
              <td>
                  <asp:DropDownList runat="server" ID="ddlManufacturer"></asp:DropDownList>
              </td>
          </tr>
          <tr>
              <td>
                  Please, select Model:
              </td>
              <td>
                  <asp:DropDownList runat="server" ID="ddlModel"></asp:DropDownList>
              </td>
          </tr>
      </table>
  </div>
  </form>

После чего реализуем интерфейс ICallbackEventHandler для класса страницы:

public partial class CarList : System.Web.UI.Page, ICallbackEventHandler
  {
      protected string evArg;
      protected void Page_Load(object sender, EventArgs e)
      {
      }
      #region ICallbackEventHandler Members
      public string GetCallbackResult()
      {
          throw new Exception("The method or operation is not implemented.");
      }
      public void RaiseCallbackEvent(string eventArgument)
      {
          throw new Exception("The method or operation is not implemented.");
      }
      #endregion
  }

Нам необходимо получить eventArgument в методе RaiseCallbackEvent, сделать соответствующие действия и потом передать его в GetCallbackResult для возврата клиенту. Для этого мы вводим переменную evArg.

Далее нам необходимо прицепить клиентский обработчик для первого SELECT. Т.е. при смене значения должна вызываться функция WebForm_DoCallback. Так и пишем J

protected void Page_Load(object sender, EventArgs e)
  {
  string argClientFunction = "document.all['" + ddlManufacturer.ClientID + "'].options(document.all['" + ddlManufacturer.ClientID + "'].selectedIndex).value";
  string cScript = ClientScript.GetCallbackEventReference(this, argClientFunction, "CallbackFunction", "'CallbackContext'", "null", false);
  ddlManufacturer.Attributes.Add("onchange", cScript + ";return false;");
   }

Теперь напишем 2 метода, первый из которых нужен только один раз – для binding списка производителей, а второй - для списка моделей. Чтобы не мучаться с БД сделаем попроще следующим образом:

void BindManufacturers()
      {
          ddlManufacturers.Items.Add(new ListItem("Mercedes"));
          ddlManufacturers.Items.Add(new ListItem("BMW"));
          ddlManufacturers.Items.Add(new ListItem("Renault"));
          ddlManufacturers.Items.Add(new ListItem("Toyota"));
          ddlManufacturers.Items.Add(new ListItem("Daewoo"));
      }
      void BindModels(string manufacturer)
      {
          switch (manufacturer)
          {
              case "Mercedes":
                  ddlModel.Items.Clear();
                  ddlModel.Items.Add(new ListItem("S350"));
                  ddlModel.Items.Add(new ListItem("S500"));
                  ddlModel.Items.Add(new ListItem("S600"));
                  ddlModel.Items.Add(new ListItem("CLK"));
                  break;
              case "BMW":
                  ddlModel.Items.Clear();
                  ddlModel.Items.Add(new ListItem("model 3"));
                  ddlModel.Items.Add(new ListItem("model 5"));
                  ddlModel.Items.Add(new ListItem("model 7"));
                  ddlModel.Items.Add(new ListItem("X3"));
                  ddlModel.Items.Add(new ListItem("X5"));
                  break;
              case "Renault":
                  ddlModel.Items.Clear();
                  ddlModel.Items.Add(new ListItem("12"));
                  ddlModel.Items.Add(new ListItem("19"));
                  ddlModel.Items.Add(new ListItem("21"));
                  break;
              case "Toyota":
                  ddlModel.Items.Clear();
                  ddlModel.Items.Add(new ListItem("Aristo"));
                  ddlModel.Items.Add(new ListItem("Avalon"));
                  ddlModel.Items.Add(new ListItem("Avensis"));
                  ddlModel.Items.Add(new ListItem("Bandeirante"));
                  break;
              case "Daewoo":
                  ddlModel.Items.Clear();
                  ddlModel.Items.Add(new ListItem("Sens"));
                  ddlModel.Items.Add(new ListItem("Lanos"));
                  break;
          }
      }

И следовательно для того, чтобы заполнить список производителей, необходимо добавить в Page_Load:

if (!IsPostBack)
              BindManufacturers();

Теперь осталось сделать 2 вещи – написать клиентскую функцию, которая будет выполняться после возврата и написать код для функций RaiseCallbackEvent и GetCallbackResult.

Сначала напишем код для этих 2-х функций:

public string GetCallbackResult()
      {
          BindModels(evArg);
          evArg = String.Empty;
          for (int i = 0; i < ddlModel.Items.Count; i++)
          {
              evArg += ddlModel.Items[i