Формы Html, Ajax, ASP.NET MVC и вы

Дино Эспозито (Dino Esposito) | 24 июня 2010 г.

В Интернете клиенты большей частью взаимодействуют с веб-сервером двумя способами: отправляя содержимое формы HTML и размещая запрос GET для контента, на который указывает данный URL-адрес. Оба типа запросов обычно обрабатываются веб-браузером и приводят к обновлению страницы целиком. Появление Ajax не изменило этот базовый факт, но изменило наше восприятие ожидаемого результата. Вкратце, пользователям и разработчикам больше не нужно ждать обновления всей страницы, чтобы увидеть отображение результатов работы сервера.

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

Сегодня у нас есть два основных подхода к настройке взаимодействия «клиент/веб-сервер». Одним является чистый подход «клиент/сервер» в котором клиентом является код JavaScript, выполняемый в браузере, а сервером — слой служб, специально созданных для обслуживания запросов Ajax.  Другой подход — что-то, лежащее между этой моделью с интенсивным использованием JavaScript и классической моделью веб-программирования на стороне сервера. Второй подход сводится к использованию обычной формы HTML, которая отправляет запрос, но этот запрос, переданный формой, перехватывается некоторым внешним скриптом, который поглощает первоначальный запрос и заменяет его собственным асинхронным запросом.

В чем различие? Что каждый из вариантов означает для разработчика? Давайте разберемся.

Одна парадигма Ajax, две основных модели

Новые подходы сводятся к двум фундаментальным моделям Ajax: модель BST (Browser-side Template, шаблон на стороне браузера) и модель HM (HTML Message, HTML-сообщение). Дополнительные сведения об этих и других, более специфических моделях можно найти на веб-сайте http://ajaxpatterns.org

Модель BST практически отправляет асинхронные запросы серверу, чтобы получить неформатированные данные для клиента. Пользователь инициирует удаленный вызов, взаимодействуя с пользовательским интерфейсом текущей страницы. Это действие загружает некоторые данные в клиент. Важно отметить, что эта модель рекомендует отправлять по проводу только данные — никакой HTML-разметки и никаких сведений о макете.

Затем для управления полученными данными используется обратный вызов JavaScript. Функция обратного вызова JavaScript отвечает за создание экземпляра компонента нового вида — построителя разметки. Построитель разметки получает ссылку на один или несколько шаблонов HTML в модели DOM страницы. Он также получает загруженные данные, выполняет свою работу и возвращает строку HTML. Наконец, обратный вызов вставляет строку в текущую объектную модель документа (document object model, DOM) страницы.

Вместо этого модель HM предполагает, что конечная точка сервера возвращает ответ, готовый к вставке в текущую модель DOM. Другими словами, этот ответ не требует никакой обработки на стороне клиента за исключением использования клиентского кода для отображения полученной разметки. В сценарии HM конечная точка возвращает HTML-сообщение, не всю страницу, но заметную часть для замены или вставки в какое-то место существующей модели DOM страницы.

Сегодня платформы веб-разработки строятся вокруг одной из этих моделей или в какой-то степени поддерживают обе модели. Сегодня мы находим, что подход BST лежит в основе популярных библиотек, таких как jQuery. Если нужно отправлять форму, получать какие-то свежие данные, а затем выполнять любую нужную клиентскую логику для соответствующего обновления пользовательского интерфейса, понадобится библиотека, подобная jQuery (и множество ее подключаемых модулей). В этом случае уровень представления частично перемещается в веб-браузер и делится между слоем на основе JavaScript, работающим только на клиенте, и слоем ASP.NET, размещаемым на стороне сервера и содержащим логику контроллера.

Для тех, кто обладает опытом работы с ASP.NET, этот подход заметно отличается, и его внедрение в компании может быть связано с трудностями. С другой стороны, этот подход действительно оказывается эффективным при реализации умелыми руками.

Модель HM ближе к классической разработке ASP.NET и представляет собой эволюцию модели программирования ASP.NET для интеграции парадигмы Ajax. Мы находим реализацию модели HM в интерфейсе API частичной визуализации веб-форм ASP.NET. Мы также находим реализацию этой модели в интерфейсе API Ajax, предлагаемом ASP.NET MVC.

Посмотрим, как добавить возможности Ajax в приложение MVC ASP.NET.

Определение формы Ajax в ASP.NET MVC

Если принято решение добавить возможности Ajax в приложение ASP.NET, то первая возможность, которая нужна — это отправка данных из формы и частичное обновление (в отличие от полного) текущей страницы. Чтобы это выполнялось, код должен перехватывать управления процессом отправки формы. Этого можно добиться только с помощью скриптов. Но, к счастью, разработчику не придется самостоятельно писать этот кусок кода скрипта снова и снова. Платформа ASP.NET MVC предоставляет определенные средства, позволяющие использовать более знакомую модель.

В ASP.NET MVC классическая форма HTML определяется следующим образом:

<% Html.BeginForm(actionName, controllerName) { %>



    <input type="text" id="Name" ... />

    <input type="text" id="Date" ... />

    <hr />

    <input type="submit" value="Save" />



<% } %>

Окружающий блок кода только выводит открывающий и закрывающий теги <form> с программно формируемым URL-адресом, связанным с указанными контроллером и действием. Для разработчика ASP.NET MVC этот код не представляет собой ничего нового и выглядит вполне знакомо. Вот что нужно сделать, чтобы этот код стал поддерживать Ajax.

<% Ajax.BeginForm(actionName, controllerName, ...); %>



    <input type="text" id="Name" ... />

    <input type="text" id="Date" ... />

    <hr />

    <input type="submit" value="Save" />



<% Ajax.EndForm(); %>

Самым большим изменением является переключение на другой базовый объект, который и создает всю необходимую разметку. Объект Ajax создаст фрагмент HTML-разметки, похожей на следующую. Предположим, что Home — это имя контроллера, а GetCustomerDetails — имя действия:

<form action="/Home/GetCustomerDetails" 

      method="post" 

      onclick="Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));" 

      onsubmit="Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { 

                          insertionMode: Sys.Mvc.InsertionMode.replace, 

                          loadingElementId: 'lblWait', 

                          updateTargetId: 'pnlDetails' });">



    <input type="text" id="Name" ... />

    <input type="text" id="Date" ... />

    <input type="submit" value="Save" />



</form>

Функция JavaScript — handleSubmit — перехватывает отправку формы. Ее первое действие заключается в предотвращении запуска обработчика события по умолчанию. Таким образом, классический процесс отправки формы, выполняемый браузером, останавливается и заменяется настраиваемым процессом, работающим асинхронно. Затем эта же функция handleSubmit выполняет классические запросы Ajax, имитирующие типичное поведение отправки формы. Эта функция определена в файле MicrosoftMvcAjax.js, который среда Visual Studio 2010 заботливо и автоматически добавляет в каждый создаваемый новый проект ASP.NET MVC.

Запрос, выполняемый Ajax, передается серверному приложению ASP.NET и разрешается путем выполнения заданного действия на указанном контроллере. А что с ответом? Метод действия, предназначенный для вызова из запроса Ajax, не может возвращать объект ViewResult. Например, следующий метод работать не будет.

public ActionResult GetCustomerDetails( ... )

{

   :

   return View();

}

Этот код не вызовет никакого исключения, но вернет всю HTML-страницу, которую клиентский скрипт попытается вставить в конкретное место текущей модели DOM. Окончательный результат, скорее всего, будет сильно отличаться от ожидаемого. Поэтому в первую очередь метод контроллера, вызываемый с помощью Ajax, должен возвращать частичное представление, являющееся некоторой формой контента, создаваемого пользовательским элементом управления.

[AjaxOnly, HttpPost]

public ActionResult GetCustomerDetails( ... )

{

   :

   return PartialView();

}

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

public class AjaxOnlyAttribute : ActionMethodSelectorAttribute 

{

    public override Boolean IsValidForRequest(

             ControllerContext controllerContext, MethodInfo methodInfo)

    {

        return controllerContext.HttpContext.Request.IsAjaxRequest();

    }

}

Обновление пользовательского интерфейса

Если вернуться к разметке, используемой для определения формы Ajax, можно заметить, что несколько ее частей остались неопределенными. Вот что я предлагал совсем недавно:

<% Ajax.BeginForm(actionName, controllerName, ...); %>

Два параметра — actionName и controllerName — служат цели определения URL-адреса действия. Но некоторые другие данные пропущены. В частности, какая часть текущей страницы должна обновляться, чтобы отразить результаты серверной операции? Этот фрагмент данных необходимо добавить в форму, чтобы он использовался кодом скрипта, обрабатывающим отправку формы.

Чтобы определить данные и любое дополнительное поведение, которое будет реализовано во время выполнения запроса, в ASP.NET MVC используется класс AjaxOptions. Члены класса AjaxOptions перечислены в таблице 1.

Свойство Описание
Confirm Указывает функцию JavaScript, вызываемую для получения подтверждения перед выполнением запроса.
HttpMethod Указывает метод HTTP, используемый для запроса.
InsertionMode Указывает режим вставки для любого загруженного контента, который нужно добавить в текущую модель DOM. Возможными значениями являются Replace (заменить), InsertBefore (вставить перед) и InsertAfter (вставить после).
LoadingElementId Указывает ID элемента модели DOM, отображаемого во время выполнения запроса. Управление видимостью элемента выполняется автоматически, и элемент появляется, когда запрос начинается, и исчезает после завершения запроса.
OnBegin Указывает функцию JavaScript, вызываемую перед выполнением запроса. Если функция возвращает значение false, запрос отменяется.
OnComplete Указывает функцию JavaScript, вызываемую, когда запрос выполнен, но до того, как будет определено, успешно или нет закончилось выполнение запроса.
OnFailure Указывает функцию JavaScript, вызываемую, когда запрос выполнен неудачно. Неудача обнаруживается с помощью кода состояния ответного пакета.
OnSuccess Указывает функцию JavaScript, вызываемую, когда запрос выполнен успешно.
UpdateTargetId Указывает ID элемента модели DOM, который необходимо обновить, добавив загруженный HTML-контент.
Url Указывает конечный URL-адрес запроса, если он еще не задан в разметке, при использовании ссылки или формы.

Таблица 1. Члены класса AjaxOptions

В качестве самого минимума понадобится задать значение члена UpdateTargetId. Этот член указывает часть модели DOM, которая будет заменена контентом полученного HTML-сообщения или в которую будет вставлен этот контент.

Этот аспект отмечает ключевое отличие от сценария без использования Ajax. Форма с поддержкой Ajax должна указывать, куда нужно поместить любой полученный объект. Интерфейс API ASP.NET MVC требует, чтобы вызываемый метод возвращал фрагмент HTML-разметки. Это в значительной степени автоматическое поведение упрощает добавление возможностей Ajax в форму HTML.

На основе личного опыта я должен признать, что когда я начал изучать интерфейс API Ajax в ASP.NET MVC, я не был впечатлен им и считал, что он не станет столь гладким, чтобы его можно было использовать во всем объеме приложения. Но когда я использовал этот интерфейс в реальном приложении, ощущения были ошеломляющими. Он показал себя гладким и эффективным. Он сохраняет удовольствие написания методов (вместо обработчиков событий postback) для обслуживания запроса и до определенной степени сохраняет возможность повторного использования и гибкость разметки, заставляя определять ответ в пользовательском элементе управления.

Члены OnXxx, приведенные в таблице 1, предлагают другой уровень управления всем процессом в Ajax. С помощью кода JavaScript можно перехватить три стадии процесса — его начало, его завершение и его результат (неудача или успех). В обработчике OnBegin можно инициировать некоторую проверку на стороне клиента и вернуть значение false, если проверка не выполняется.

// Пример обработчика OnBegin
function beginOperation() {
   if (!validateInput())
      return false;
   return true;
}

Обратите внимание, что из-за маленькой ошибки в MicrosoftMvcAjax.js, когда OnBegin возвращает значение false, операция прерывается корректно, но интерфейс выполнения — если он задан с помощью LoadingElementId — продолжает отображаться и не исчезает никогда. Единственным способом обхода этой проблемы является редактирование файла MicrosoftMvcAjax.js. Ниже приведена точка вмешательства:

Sys.Mvc.MvcHelpers._asyncRequest = function ... {
   :
   if (ajaxOptions.onBegin) {
        continueRequest = ajaxOptions.onBegin(ajaxContext) !== false;
    }
 
    // Здесь понадобится этот IF 
    if (!continueRequest)
        return;
 
    if (loadingElement) {
        Sys.UI.DomElement.setVisible(ajaxContext.get_loadingElement(), true);
    }
    if (continueRequest) {
        request.add_completed(Function.createDelegate(null, function(executor) {
            Sys.Mvc.MvcHelpers._onComplete(request, ajaxOptions, ajaxContext);
        }));
        request.invoke();
    }   
}

После этого исправления может понадобиться средство уменьшения размеров Ajax, чтобы сжать слишком большой в противном случае файл скрипта до разумного размера. Средство уменьшения размера для Ajax можно загрузить по адресу http://aspnet.codeplex.com/releases/view/40584.

В других случаях может быть полезно исправить URL-адрес, чтобы гарантировать отражение в нем некоторого выбора, возможно, сделанного пользователем. OnComplete вызывается сразу же после выполнения операции, но до обработки результатов. Это событие происходит независимо от следующего обработчика — будь это OnSuccess или OnFailure.

Работа с уведомлениями

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

В классическом сценарии HTML сервер просто отправляет две различных страницы. Более существенно, что пользователь оказывается на странице сообщения и должен явно выйти из нее, чтобы вернуться на предыдущую стадию. В Ajax метод контроллера должен вернуть некоторую HTML-разметку, которая будет отображена на текущей странице. Дальнейшие действия различны для неудачи и для успеха. В случае неудачи просто отображается любое сообщение об ошибке, которое остается в форме. В случае успеха можно отобразить сообщение на несколько секунд, а затем перейти  к следующей стадии. Можно закодировать эти различные фрагменты логики в обработчиках OnSuccess и OnFailure, но, чтобы все работало, важно, чтобы метод сервера сообщал, успешно или нет закончилась операция.

Как же добиться такого поведения метода, если ожидается, что он просто будет возвращать фрагмент HTML-разметки? Ответ достаточно очевиден — с помощью кодов HTTP. Метод запишет ответ HTML, но установит код состояния равным 500, если HTML представляет собой сообщение об ошибке. В своих приложениях я обычно использую специальный класс результата действия, расширяющий базовый класс PartialViewResult, чтобы задать конкретный код ошибки, если частичное представление представляет ошибку. Ниже приведен пример кода:

public class PartialViewWithErrorResult : PartialViewResult
{
    public Int32 StatusCode { get; set; }
 
    public PartialViewWithErrorResult() : this(HttpStatusCode.InternalServerError)
    {
    }
    public PartialViewWithErrorResult(Int32 statusCode)
    {
       StatusCode = statusCode;
    }
    public PartialViewWithErrorResult(HttpStatusCode statusCode)
    {
       StatusCode = (Int32) statusCode;
    }
 
    public override void ExecuteResult(ControllerContext context)
    {
       if (context == null)
          throw new ArgumentNullException("context");
 
       if (string.IsNullOrEmpty(ViewName))
          ViewName = context.RouteData.GetRequiredString("action");
 
       ViewEngineResult result = null;
       if (View == null)
       {
           result = FindView(context);
           View = result.View;
       }
 
       // Задайте код состояния
       context.HttpContext.Response.StatusCode = StatusCode;
 
       // Вывод
       var output = context.HttpContext.Response.Output;
       var viewContext = new ViewContext(context, View, ViewData, TempData, output);
       View.Render(viewContext, output);
       if (result != null)
          result.ViewEngine.ReleaseView(context, View);
    }
}

Класс PartialViewWithErrorResult работает рука об руку вместе с методом расширения для быстрого создания экземпляра.

public static PartialViewWithErrorResult PartialViewWithError(

     this Controller controller, String viewName, Object model, Int32 statusCode)

{

   if (model != null)

      controller.ViewData.Model = model;



    var result = new PartialViewWithErrorResult

    {

         StatusCode = statusCode,

         ViewName = viewName,

         ViewData = controller.ViewData,

         TempData = controller.TempData

    };

    return result;

}

Метод действия контроллера, вызываемый для запроса Ajax, будет использовать следующую структуру:

[AjaxOnly, HttpPost]

public ActionResult InsertBooking(BookingInputModel inputModel)

{

   var newBookingDto = new BookingDto {...};

   var response = _bookingService.InsertNewBooking(newBookingDto);

   if (!response.Success)

   {

      return this.PartialViewWithError(

              UserControls.Failed, 

              response, 

              (Int32) HttpStatusCode.InternalServerError);

   }



   // Success

   response.Success = true;

   response.ErrorMessage = AppResources.BookingSuccessful;

   return PartialView(UserControls.Inserted, response);

}

В этом примере физическое действие выполняется службой приложения, получающей объект передачи данных и возвращающей внутренний объект ответа, в который упакованы ответ типа Boolean и сообщение об ошибке. Как можно видеть, в случае ошибки и успеха возвращаются два различных фрагмента HTML-разметки. Единственная причина развилки между PartialView (частичное представление) и PartialViewWithError (частичное представление с ошибкой) — гарантировать возвращение HTTP-кода ошибки, который запустит на клиенте другую обработку JavaScript. Ниже приведены несколько типовых обработчиков JavaScript для операций с формой Ajax:

function handleFailure(context) {
    var markup = jQuery.trim(context.get_data());
    fnSetFeedback(markup);
    setTimeout(clearFeedbackAfterFailure, 4000);
}
 
function clearFeedbackAfterFailure() {
    fnSetFeedback("");
}
function handleSuccess() {
    setTimeout(clearFeedbackAfterFailure, 4000);
}
 
function clearFeedbackAfterFailure() {
    fnSetFeedback("");
 
    // Очистка формы (если необходимо)
    :
 
    // Обновление всего базового пользовательского интерфейса (если необходимо)
    $.ajaxSetup({ cache: false });
    $("#SomePieceOfUI").load("/refresh");
}

Функция handleFailure выполняется при получении HTTP-кода ошибки. Сначала она обновляет текущую страницу, используя разметку для ошибки (обычно значок ошибки и какой-нибудь текст), а затем устанавливает таймер. Таймер срабатывает через несколько секунд (4 секунды в этом коде) и очищает разметку для ошибки. Суммарный эффект состоит в том, что пользователь, прочитав об ошибке, возвращается к редактированию формы для следующей попытки.

Функция handleSuccess выполняется, если серверная операция выполняется успешно, и все, что нам нужно — вывести соответствующее подтверждение для пользователя. Разница между тем, как ASP.NET MVC обрабатывает успех и как — неудачу, состоит в том, что в случае успеха пользовательский интерфейс обновляется платформой, а в случае неудачи за отображение некоторой разметки отвечает разработчик. Вот почему handleSuccess устанавливает таймер — пользовательский интерфейс в момент вызова этой функции уже обновлен.

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

Выводы

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