Skip to content

7. Впровадження Ajax у додаток ASP.NET MVC

7.1. Місце AJAX у веб-додатку

Наразі розглянуті навчальні приклади мають таку архітектуру:

Щоб перейти з подання [Vue1] до подання [Vue2], браузер:

  • надсилає запит до веб-додатку;
  • отримує вигляд [Vue2] і відображає його замість вигляду [Vue1].

Це класична схема:

  • запит від браузера;
  • створення веб-сервером сторінки у відповідь клієнту;
  • відображення цього нового виду браузером.

Існує ще один режим взаємодії між браузером і веб-сервером: AJAX (Asynchronous Javascript And Xml). Фактично це взаємодія між видом, що відображається браузером, та веб-сервером. Браузер продовжує робити те, що вміє, — відображати сторінку HTML, але тепер ним керує JavaScript, вбудований у відображену сторінку HTML. Схема така:

  • у [1] на сторінці, що відображається в браузері, відбувається подія (натискання кнопки, зміна тексту тощо). Ця подія перехоплюється JavaScript-кодом (JS), вбудованим у сторінку;
  • у [2] код JavaScript надсилає запит HTTP так, як це зробив би браузер. Запит є асинхронним: користувач може продовжувати взаємодіяти зі сторінкою, не затримуючись на очікуванні відповіді на запит HTTP. Запит проходить стандартний процес обробки. Нічим (або майже нічим) він не відрізняється від звичайного запиту;
  • у [3] клієнту надсилається відповідь JS. Замість повного подання HTML надсилається часткове подання HTML, потік XML або JSON (JavaScript Object Notation);
  • у [4] JavaScript отримує цю відповідь і використовує її для оновлення певної області відображеної сторінки HTML.

Для користувача відбувається зміна вигляду, оскільки те, що він бачить, змінилося. Однак повного перезавантаження сторінки не відбувається, а лише часткова модифікація відображуваної сторінки. Це сприяє плавності та інтерактивності сторінки: оскільки не відбувається повного перезавантаження сторінки, можна обробляти події, які раніше не оброблялися. Наприклад, пропонувати користувачеві список варіантів у міру того, як він вводить символи у поле введення. З кожним новим введеним символом надсилається запит AJAX на сервер, який потім повертає інші варіанти. Без Ajax такого роду допомога при введенні раніше була неможливою. Неможливо було перезавантажувати нову сторінку з кожним введеним символом.

7.2. Основи JQuery та JavaScript

Ми часто підключали бібліотеку JavaScript JQuery до наших сторінок. У ній міститься такий рядок:


  <script type="text/javascript" src="~/Scripts/jquery-1.8.2.min.js"></script>

Примітка: підберіть версію jQuery відповідно до вашої версії Visual Studio.

Технологія Ajax у ASP.NET MVC використовує JQuery. Ми самі напишемо кілька скриптів JQuery. Тому зараз розглянемо основи JQuery, які необхідно знати для розуміння скриптів цього розділу.

Створюємо новий проєкт [Exemple-04] у межах нашого рішення [Exemples]:

Щоб використовувати Ajax із ASP.NET MVC, у файлі конфігурації [Web.config] [1] має бути присутній такий рядок:


  <appSettings>
...
    <add key="UnobtrusiveJavaScriptEnabled" value="true" />
</appSettings>

Рядок 3 дозволяє використовувати Ajax у поданнях ASP.NET. Він присутній за замовчуванням.

Створюємо файл HTML [JQuery-01.html] у папці [Content] нового проєкту [2]:

Цей файл матиме такий вміст:


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>JQuery-01</title>
  <script type="text/javascript" src="/Scripts/jquery-1.8.2.min.js"></script>
</head>
<body>
  <h3>Rudiments de JQuery</h3>
  <div id="element1">
    Elément 1
  </div>
</body>
</html>
  • рядок 6: імпорт JQuery (підгоніть версію відповідно до вашої версії Visual Studio);
  • рядки 10–12: елемент сторінки з ідентифікатором [element1]. Ми будемо працювати з цим елементом.

Переглянемо цей файл у браузері Google Chrome: [4] та [5]:

У браузері Google Chrome натисніть [Ctrl-Maj-I], щоб відкрити інструменти розробника [6]. Вкладка [Console] [7] дозволяє виконувати код JavaScript. Нижче наведено команди JavaScript, які слід ввести, та їх пояснення.

JS
результат
$("#element1")
: повертає колекцію всіх елементів з ідентифікатором [element1], тобто, як правило, колекцію з 0 або 1 елемента, оскільки на одній сторінці не може бути двох однакових ідентифікаторів HTML.
$("#element1").text("blabla")
: застосовує текст [blabla] до всіх елементів колекції. Це призводить до зміни вмісту, що відображається на сторінці
$("#element1").hide()
приховує елементи колекції. Текст [blabla] більше не відображається.
$("#element1")
: знову відображає колекцію. Це дозволяє нам побачити, що елемент з ідентифікатором [element1] має атрибут CSS style='display: none;', завдяки якому елемент приховано.
$("#element1").show()
: відображає елементи колекції. Текст [blabla] знову з’являється. Це забезпечується атрибутом CSS style='display : block;'.
$("#element1").attr('style','color: red')
: встановлює атрибут для всіх елементів колекції. Атрибутом тут є [style], а його значення — [color: red]. Текст [blabla] стає червоним.
Tableau
Dictionnaire

Слід зауважити, що URL браузера не змінився під час усіх цих операцій. Обміну даними з веб-сервером не відбувалося. Усе відбувається всередині браузера. Тепер переглянемо вихідний код сторінки:

 

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>JQuery-01</title>
  <script type="text/javascript" src="/Scripts/jquery-1.8.2.min.js"></script>
</head>
<body>
  <h3>Rudiments de JQuery</h3>
  <div id="element1">
    Elément 1
  </div>
</body>
</html>

Це початковий текст. Він жодним чином не відображає маніпуляції, які ми виконали з елементом у рядках 10–12. Про це важливо пам’ятати під час налагодження JavaScript. Тому часто немає сенсу переглядати вихідний код відображеної сторінки. Щоб дізнатися вихідний код сторінки, яка зараз відображається, слід вчинити так:

 

Ми знаємо достатньо, щоб зрозуміти скрипти JS, які будуть наведені далі.

7.3. Оновлення сторінки за допомогою потоку HTML

7.3.1. Види

Пропонуємо розглянути такий приклад:

  • у [1] — час завантаження сторінки;
  • у [2] виконуються чотири арифметичні операції над двома дійсними числами A та B;
  • у [3] відповідь сервера записується в певну область сторінки;
  • у [4] — час обчислення. Він відрізняється від часу завантаження сторінки [5]. Останній дорівнює [1], що свідчить про те, що область [6] не була перезавантажена. Крім того, URL та [7] на сторінці не змінилися.

7.3.2. Контролер, дії, модель, представлення

Ми створюємо контролер із назвою [Premier]:

Щоб відобразити початковий вигляд, створюємо таку дію [Action01Get]:


    [HttpGet]
    public ViewResult Action01Get()
    {
      ViewModel01 modèle = new ViewModel01();
      modèle.HeureChargement = DateTime.Now.ToString("hh:mm:ss");
      return View(modèle);
}
  • рядок 4: створення екземпляра моделі подання;
  • рядок 5: ініціалізація часу завантаження подання;
  • рядок 6: відображення подання [Action10Get.cshtml] та його моделі.

Модель [ViewModel01] має такий вигляд:


using System;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;

namespace Exemple_04.Models
{
  [Bind(Exclude = "AplusB, AmoinsB, AmultipliéparB, AdiviséparB, Erreur, HeureChargement, HeureCalcul")]
  public class ViewModel01
  {
    // форма
    [Required(ErrorMessage="Donnée requise")]
    [Display(Name="Valeur de A")]
    [Range(0, Double.MaxValue, ErrorMessage = "Tapez un nombre positif ou nul")]
    public double A { get; set; }
    [Required(ErrorMessage = "Donnée requise")]
    [Display(Name = "Valeur de B")]
    [Range(0, Double.MaxValue, ErrorMessage="Tapez un nombre positif ou nul")]
    public double B { get; set; }

    // результати
    public string AplusB { get; set; }
    public string AmoinsB { get; set; }
    public string AmultipliéparB { get; set; }
    public string AdiviséparB { get; set; }
    public string Erreur { get; set; }
    public string HeureChargement { get; set; }
    public string HeureCalcul { get; set; }
  }
}
  • рядки 11–14: значення A форми;
  • рядки 15–18: значення B з форми;
  • рядки 21–24: результати чотирьох арифметичних операцій над A та B;
  • рядок 25: текст можливої помилки;
  • рядок 26: час завантаження сторінки в браузері;
  • рядок 27: час обчислення полів рядків 21–24;
  • рядок 7: цей шаблон подання також є шаблоном дії. З останнього виключаються поля, які не надсилаються браузером.

Вигляд [Action01Get.cshtml] має такий вигляд:


@model Exemple_04.Models.ViewModel01
@{
  Layout = null;
  AjaxOptions ajaxOpts = new AjaxOptions
  {
    UpdateTargetId = "résultats",
    HttpMethod = "post",
    Url = Url.Action("Action01Post"),
    LoadingElementId = "loading",
    LoadingElementDuration = 1000
  };    
}

<!DOCTYPE html>

<html lang="fr-FR">
<head>
  <meta name="viewport" content="width=device-width" />
  <title>Ajax-01</title>
  <link rel="stylesheet" href="~/Content/Site.css" />
  <script type="text/javascript" src="~/Scripts/jquery-1.8.2.min.js"></script>
  <script type="text/javascript" src="~/Scripts/jquery.validate.min.js"></script>
  <script type="text/javascript" src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>
  <script type="text/javascript" src="~/Scripts/globalize/globalize.js"></script>
  <script type="text/javascript" src="~/Scripts/globalize/cultures/globalize.culture.fr-FR.js"></script>
  <script type="text/javascript" src="~/Scripts/globalize/cultures/globalize.culture.en-US.js"></script>
  <script type="text/javascript" src="~/Scripts/jquery.unobtrusive-ajax.js"></script>
  <script type="text/javascript" src="~/Scripts/myScripts-01.js"></script>
</head>
<body>

  <h2>Ajax - 01</h2>
  <p><strong>Heure de chargement : @Model.HeureChargement</strong></p>
  <h4>Opérations arithmétiques sur deux nombres réels A et B positifs ou nuls</h4>
  @using (Ajax.BeginForm("Action01Post", null, ajaxOpts, new { id = "formulaire" }))
  {
    <table>
      <thead>
        <tr>
          <th>@Html.LabelFor(m => m.A)</th>
          <th>@Html.LabelFor(m => m.B)</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>@Html.TextBoxFor(m => m.A)</td>
          <td>@Html.TextBoxFor(m => m.B)</td>
        </tr>
        <tr>
          <td>@Html.ValidationMessageFor(m => m.A)</td>
          <td>@Html.ValidationMessageFor(m => m.B)</td>
        </tr>
      </tbody>
    </table>
    <p>
      <input type="submit" value="Calculer" />
      <img id="loading" style="display: none" src="~/Content/images/indicator.gif" />
      <a href="javascript:postForm()">Calculer</a>
    </p>
  }
  <hr />
  <div id="résultats" />
</body>
</html>
  • рядок 1: шаблон для цього подання — тип [ViewModel01];
  • рядок 21: JQuery потрібен як для перевірки, так і для Ajax;
  • рядки 22–23: бібліотеки валідації;
  • рядки 24–26: бібліотеки інтернаціоналізації;
  • рядок 27: бібліотека Ajax;
  • рядок 28: локальна бібліотека JavaScript;
  • рядок 33: відображення часу завантаження подання;
  • рядок 35: форма Ajax — до цього ми ще повернемося;
  • рядки 40–41: назви полів для введення чисел A та B;
  • рядки 46–47: поля для введення чисел A та B;
  • рядки 50–51: повідомлення про помилки при введенні чисел A та B;
  • рядок 56: кнопка, яка відправляє форму. Вона буде відправлена за допомогою запиту Ajax;
  • рядок 57: зображення, що відображається під час очікування відповіді на запит Ajax;
  • рядок 58: посилання для відправлення форми за допомогою запиту Ajax;
  • рядок 62: тег <div> з ідентифікатором [résultats]. Саме сюди ми розмістимо потік HTML, повернутий веб-сервером.

Цей вигляд відображає таку сторінку:

 

Тепер розглянемо код, який реалізує AJAX-обробку форми:


...
@{
  Layout = null;
  AjaxOptions ajaxOpts = new AjaxOptions
  {
    UpdateTargetId = "résultats",
    HttpMethod = "post",
    Url = Url.Action("Action01Post"),
    LoadingElementId = "loading",
    LoadingElementDuration = 1000
  };    
}

...
  @using (Ajax.BeginForm("Action01Post", null, ajaxOpts, new { id = "formulaire" }))
  {
....
    <p>
      <input type="submit" value="Calculer" />
      <img id="loading" style="display: none" src="~/Content/images/indicator.gif" />
      <a href="javascript:postForm()">Calculer</a>
    </p>
}
...
 <div id="résultats" />
  • рядок 15: замість [@Html.BeginForm] використовується [@Ajax.BeginForm]. Цей метод підтримує численні перевантаження. Використане перевантаження має такий сигнатур:
Ajax.BeginForm(string ActionName, RouteValueDictionary routeValues, AjaxOptions ajaxOptions, IDictionary<string,object> htmlAttributes)

Тут ми використовуємо такі фактичні параметри:

Action01Post: ім’я дії, яка оброблятиме POST з форми,

null: інформація про маршрут відсутня,

ajaxOpts: параметри виклику Ajax. Вони були визначені у рядках 6–10,

new { id = "formulaire" }: для присвоєння атрибуту [id='formulaire'] згенерованому тегу <form>;

Використовуються такі параметри Ajax:

  • рядок 8: URL — адреса призначення запиту HTTP Ajax;
  • рядок 7: метод запиту Ajax HTTP;
  • рядок 6: ідентифікатор області сторінки, яка буде оновлена у відповідь на запит Ajax;
  • рядок 9: ідентифікатор області сторінки, яка буде відображатися під час запиту Ajax — зазвичай це зображення очікування. У цьому випадку буде відображатися рядок 20. Він містить анімоване зображення, що символізує очікування. Спочатку це зображення приховане за допомогою стилю [display : none];
  • рядок 10: час очікування в мілісекундах до того, як буде відображено анімоване зображення, тут — 1 секунда.

Код HTML, згенерований Ajax-формою, має такий вигляд:


<form action="/Premier/Action01Post" data-ajax="true" data-ajax-loading="#loading" data-ajax-loading-duration="1000" data-ajax-method="post" data-ajax-mode="replace" data-ajax-update="#résultats" data-ajax-url="/Premier/Action01Post" id="formulaire" method="post">    <table>
...
    <p>
      <input type="submit" value="Calculer" />
      <img id="loading" style="display: none" src="/Content/images/indicator.gif" />
      <a href="javascript:postForm()">Calculer</a>
    </p>
</form>
<hr />
<div id="résultats" />
  • рядок 1: згенерований тег <form>. Зверніть увагу на атрибути [data-ajax-attr], які відображають значення полів об’єкта типу [AjaxOptions], пов’язаного із запитом Ajax. Ці атрибути обробляються бібліотекою Ajax. Без них тег <form> виглядає так:

<form action="/Premier/Action01Post" id="formulaire" method="post">
...
    <p>
      <input type="submit" value="Calculer" />
      <img id="loading" style="display: none" src="/Content/images/indicator.gif" />
      <a href="javascript:postForm()">Calculer</a>
    </p>
</form>

У цьому випадку ми маємо справу зі звичайною формою HTML. Саме цей код буде виконано, якщо користувач вимкне JavaScript у своєму браузері. У такому разі рядки 5–6 залишаються невикористаними.

7.3.3. Дія [Action01Post]

Дія [Action01Post], яка обробляє запит HTTP Ajax, виглядає так:


    [HttpPost]
    public PartialViewResult Action01Post(FormCollection postedData, SessionModel session)
    {
      // симуляція очікування
      Thread.Sleep(2000);
      // інстанціювання моделі дії
      ViewModel01 modèle = new ViewModel01();
      // час розрахунку
      modèle.HeureCalcul = DateTime.Now.ToString("hh:mm:ss");
      // оновлення моделі
      TryUpdateModel(modèle, postedData);
      if (!ModelState.IsValid)
      {
        // повертається помилка
        modèle.Erreur = getErrorMessagesFor(ModelState);
        return PartialView("Action01Error", modèle);
      }
      // кожен другий раз імітується помилка
      int val = session.Randomizer.Next(2);
      if (val == 0)
      {
        modèle.Erreur = "[erreur aléatoire]";
        return PartialView("Action01Error", modèle);
      }
      // обчислення
      modèle.AplusB = string.Format("{0}", modèle.A + modèle.B);
      modèle.AmoinsB = string.Format("{0}", modèle.A - modèle.B);
      modèle.AmultipliéparB = string.Format("{0}", modèle.A * modèle.B);
      modèle.AdiviséparB = string.Format("{0}", modèle.A / modèle.B);
      // перегляд
      return PartialView("Action01Success", modèle);
}
  • рядок 1: дія обробляє лише [POST];
  • рядок 2: як шаблон дії вона приймає:
    • [FormCollection postedData]: сукупність значень, надісланих у запиті Ajax POST,
    • [SessionModel session]: елементи сесії. Тут використовується техніка, описана в розділі 4.10;
  • рядок 2: дія поверне фрагмент HTML, а не повну сторінку HTML;
  • рядок 5: штучно робимо паузу на дві секунди, щоб імітувати тривалу дію Ajax;
  • рядок 7: створюється екземпляр моделі типу [ViewModel01];
  • рядок 9: ініціалізується час обчислення;
  • рядок 11: робиться спроба оновити модель типу [ViewModel01] за допомогою надісланих значень. Нагадаємо, що їх два: значення чисел A та B;
  • рядок 12: перевіряється, чи відбулося це оновлення успішно;
  • рядок 15: у разі помилки заповнюється поле [Erreur] моделі;
  • рядок 16: повертається частковий вигляд [Action01Error.cshtml], що використовує модель [ViewModel01];
  • рядки 19–24: раз на два симулюється помилка;
  • рядок 19: генерується випадкове ціле число в інтервалі [0,1]. Генератор чисел береться з сесії;
  • рядок 20: якщо згенероване значення дорівнює 0, імітується помилка;
  • рядок 22: повідомлення про помилку вноситься до шаблону;
  • рядок 23: повертається частковий вигляд [Action01Error.cshtml], що використовує модель [ViewModel01];
  • рядки 26–29: виконуються арифметичні обчислення над числами A та B, а результати записуються в модель у вигляді символьних рядків;
  • рядок 31: повертається частковий вигляд [Action01Success.cshtml], що використовує шаблон [ViewModel01];

7.3.4. Вигляд [Action01Error]

Вигляд [Action01Error.cshtml] має такий вигляд:


@model Exemple_04.Models.ViewModel01
<h4>Résultats</h4>
<p><strong>Heure de calcul : @Model.HeureCalcul</strong></p>
<p style="color: red;">Une erreur s'est produite : @Model.Erreur</p>

Нагадаємо, що цей частковий потік HTML буде надіслано у відповідь на Ajax-запит HTTP типу POST і розміщено на сторінці в області з ідентифікатором [résultats]. Уся ця інформація походить із конфігурації Ajax, що використовується на головній сторінці [Action01Get.cshtml]:


@model Exemple_04.Models.ViewModel01
@{
  Layout = null;
  AjaxOptions ajaxOpts = new AjaxOptions
  {
    UpdateTargetId = "résultats",
    HttpMethod = "post",
    Url = Url.Action("Action01Post"),
    LoadingElementId = "loading",
    LoadingElementDuration = 1000
  };    
}

Ось приклад відповіді з помилкою:

 

7.3.5. Вигляд [Action01Success]

Вигляд [Action01Success.cshtml] такий:


@model Exemple_04.Models.ViewModel01
<h4>Résultats</h4>
<p><strong>Heure de calcul : @Model.HeureCalcul</strong></p>
<p>A+B=@Model.AplusB</p>
<p>A-B=@Model.AmoinsB</p>
<p>A*B=@Model.AmultipliéparB</p>
<p>A/B=@Model.AdiviséparB</p>

Знову ж таки, цей частковий потік HTML буде надіслано у відповідь на запит HTTP Ajax типу POST і розміщено на сторінці в області з ідентифікатором [résultats]:

 

7.3.6. G управління сесією

Ми бачили, що [Action01Post] використовує сесію. Модель сесії — це наступний тип [SessionModel]:


using System;
namespace Exemple_03.Models
{
  public class SessionModel
  {
    public Random Randomizer { get; set; }
  }
}

Сесія ініціалізується у [Global.asax]:


    // Сесія
    protected void Session_Start()
    {
      SessionModel sessionModel=new SessionModel();
      sessionModel.Randomizer=new Random(DateTime.Now.Millisecond);
      Session["data"] = sessionModel;
}

Прив’язування сесії до моделі здійснюється у [Application_Start]:


    protected void Application_Start()
    {
...
      // модельні біндери
      ModelBinders.Binders.Add(typeof(SessionModel), new SessionModelBinder());
}

Описано клас [SessionModelBinder].

7.3.7. Управління зображенням очікування


@model Exemple_04.Models.ViewModel01
@{
  Layout = null;
  AjaxOptions ajaxOpts = new AjaxOptions
  {
...
    LoadingElementId = "loading",
    LoadingElementDuration = 1000
  };    
}

...
<body>

...
  @using (Ajax.BeginForm("Action01Post", null, ajaxOpts, new { id = "formulaire" }))
  {
...
    <p>
      <input type="submit" value="Calculer" />
      <img id="loading" style="display: none" src="~/Content/images/indicator.gif" />
      <a href="javascript:postForm()">Calculer</a>
    </p>
  }
...

Коли запускається запит Ajax, через секунду відображається область з ідентифікатором [loading] у рядку 7 [ligne 8]. Ця область — це зображення з рядка 21, яке спочатку було приховане. У результаті отримуємо такий інтерфейс:

7.3.8. Обробка посилання [Calculer]

Розглянемо посилання [Calculer] на головній сторінці [Action01Get.cshtml]:


<head>
  <meta name="viewport" content="width=device-width" />
  <title>Ajax-01</title>
  ...
  <script type="text/javascript" src="~/Scripts/myScripts-01.js"></script>
</head>
<body>

  <h2>Ajax - 01</h2>
  <p><strong>Heure de chargement : @Model.HeureChargement</strong></p>
  <h4>Opérations arithmétiques sur deux nombres réels A et B positifs ou nuls</h4>
  @using (Ajax.BeginForm("Action01Post", null, ajaxOpts, new { id = "formulaire" }))
  {
...
    <p>
      <input type="submit" value="Calculer" />
      <img id="loading" style="display: none" src="~/Content/images/indicator.gif" />
      <a href="javascript:postForm()">Calculer</a>
    </p>
  }
  <hr />
<div id="résultats" />
  • рядок 18: клік на посилання [Calculer] викликає виконання функції JS [postForm]. Вона визначена у файлі [myScripts-01.js] у рядку 5. Цей скрипт має такий вигляд:

function postForm() {
  // виконується ручний виклик Ajax за допомогою JQuery
  var loading = $("#loading");
  var formulaire = $("#formulaire");
  var résultats = $('#результати');
  $.ajax({
    url: '/Premier/Action01Post',
    type: 'POST',
    data: formulaire.serialize(),
    dataType: 'html',
    begin: loading.show(),
    success: function (data) {
      loading.hide()
      résultats.html(data);
    }
  })
}

// http://blog.instance-factory.com/?p=268
$.validator.methods.number = function (value, element) {
  return this.optional(element) ||
      !isNaN(Globalize.parseFloat(value));
}

$.validator.methods.date = function (value, element) {
  return this.optional(element) ||
      !isNaN(Globalize.parseDate(value));
}

jQuery.extend(jQuery.validator.methods, {
  range: function (value, element, param) {
    //Використовуйте плагін «Глобалізація» для аналізу значення        
    var val = Globalize.parseFloat(value);
    return this.optional(element) || (
        val >= param[0] && val <= param[1]);
  }
});

Функції в рядках 19–37 ми вже розглядали в розділі 6.1. Вони відповідають за інтернаціоналізацію сторінок. Ми не будемо на них зупинятися. У рядках 1–17 ми вручну виконуємо виклик Ajax, який у випадку кнопки [Calculer] здійснювався бібліотекою Ajax, пов’язаною з проектом. Для цього ми використовуємо бібліотеку JQuery, пов’язану з проектом.

  • рядок 3: посилання на компонент з ідентифікатором [loading]. [$("#loading")] повертає колекцію елементів з ідентифікатором [loading]. Таких елементів лише один;
  • рядок 4: посилання на компонент з ідентифікатором [formulaire];
  • рядок 5: посилання на компонент з ідентифікатором [résultats];
  • рядок 6: виклик Ajax із його параметрами;
  • рядок 7: ціль виклику Ajax — URL;
  • рядок 8: використовуваний метод HTTP;
  • рядок 9: дані, що надсилаються. [formulaire.serialize] створює рядок [A=val1&B=val2] з поля POST форми з ідентифікатором [formulaire];
  • рядок 10: тип даних, що очікується у відповіді. Відомо, що сервер поверне потік HTML;
  • рядок 11: метод, який потрібно виконати при запуску запиту. Тут вказано, що потрібно відобразити компонент з ідентифікатором [loading]. Це анімована картинка очікування;
  • рядок 12: метод, який потрібно виконати у разі успішного виконання запиту Ajax. Параметр [data] — це повна відповідь сервера. Відомо, що це потік HTML;
  • рядок 13: приховуємо сигнал очікування;
  • рядок 14: оновлюємо компонент з ідентифікатором [résultats], використовуючи значення HTML з параметра [data].

Читачеві пропонується перевірити посилання [Calculer]. Воно працює так само, як кнопка [Calculer], за винятком однієї а ної аномалії. Після використання цього посилання можна надсилати недійсні значення A та B:

  • у [1] та [2] було введено недійсні значення. Вони виявляються валідаторами на стороні клієнта;
  • у [3] ми натиснули на посилання [Calculer];
  • у [4] відбувся [POST], оскільки отримано відповідь [4].

Коли значення є недійсними і натискається кнопка [Calculer], запит [POST] до сервера не надсилається. У тому ж випадку, при використанні посилання [Calculer] запит [POST] на сервер надсилається. Отже, кнопка [Calculer] поводиться таким чином, що нам не вдалося відтворити цю поведінку за допомогою посилання [Calculer]. Замість того, щоб намагатися вирішити цю проблему зараз, ми залишимо її для подальшого прикладу, який також проілюструє іншу проблему клієнтської валідації.

7.4. Оновлення сторінки HTML за допомогою потоку JSON

У попередньому прикладі веб-сервер відповідав на запит Ajax HTTP потоком HTML. У цьому потоці містилися дані з форматуванням HTML. Пропонуємо повернутися до попереднього прикладу, але цього разу з відповідями JSON (JavaScript Object Notation), що містять лише дані. Перевага полягає в тому, що таким чином передається менше байтів.

7.4.1. Дія [Action02Get]

Дія [Action02Get] буде точкою входу в новий додаток. Її код такий:


@model Exemple_04.Models.ViewModel02
@{
  Layout = null;
  AjaxOptions ajaxOpts = new AjaxOptions
  {
    HttpMethod = "post",
    Url = Url.Action("Action02Post"),
    LoadingElementId = "loading",
    LoadingElementDuration = 1000,
    OnBegin = "OnBegin",
    OnFailure = "OnFailure",
    OnSuccess = "OnSuccess",
    OnComplete = "OnComplete"
  };    
}

<!DOCTYPE html>

<html lang="fr-FR">
<head>
  <meta name="viewport" content="width=device-width" />
  <title>Ajax-02</title>
....
  <script type="text/javascript" src="~/Scripts/myScripts-02.js"></script>
</head>
<body>
  <h2>Ajax - 02</h2>
  <p><strong>Heure de chargement : @Model.HeureChargement</strong></p>
  <h4>Opérations arithmétiques sur deux nombres réels A et B positifs ou nuls</h4>
  @using (Ajax.BeginForm("Action02Post", null, ajaxOpts, new { id = "formulaire" }))
  {
...
    <p>
      <input type="submit" value="Calculer" />
      <img id="loading" style="display: none" src="~/Content/images/indicator.gif" />
      <a href="javascript:postForm()">Calculer</a>
    </p>
  }
  <hr />
  <div id="entete">
    <h4>Résultats</h4>
    <p><strong>Heure de calcul : <span id="heureCalcul"/></strong></p>
  </div>
  <div id="résultats">
    <p>A+B=<span id="AplusB"/></p>
    <p>A-B=<span id="AmoinsB"/></p>
    <p>A*B=<span id="AmultipliéparB"/></p>
    <p>A/B=<span id="AdiviséparB"/></p>
  </div>
  <div id="erreur">
    <p style="color: red;">Une erreur s'est produite : <span id="msg"/></p>
  </div>
</body>
</html>
  • рядки 4–14: параметри виклику Ajax;
  • рядок 10: функція JS, яку потрібно виконати на початку запиту. Ця функція визначена у файлі JS, на який є посилання у рядку 24;
  • рядок 11: функція JS, яку слід виконати у разі невдачі запиту;
  • рядок 12: функція JS, яка має виконуватися у разі успішного виконання запиту;
  • рядок 13: функція JS, яку слід виконати після отримання результату запиту Ajax (невдача або успіх);
  • рядки 40–43: область з ідентифікатором [entete];
  • рядки 44–49: область з ідентифікатором [résultats]. Вона відображатиме результати чотирьох арифметичних операцій;
  • рядки 50–52: область з ідентифікатором [erreur]. Вона відображатиме можливе повідомлення про помилку.

7.4.2. Дія [Action02Post]

Ajax-запит обробляється наступною дією [Action02Post]:


[HttpPost]
    public JsonResult Action02Post(FormCollection postedData, SessionModel session)
    {
      // очікується симуляція
      Thread.Sleep(2000);
      // перевірка моделі
      ViewModel02 modèle = new ViewModel02();
      // час завантаження та обчислення
      string HeureChargement = DateTime.Now.ToString("hh:mm:ss");
      string HeureCalcul = DateTime.Now.ToString("hh:mm:ss");
      // оновлення моделі
      TryUpdateModel(modèle, postedData);
      if (!ModelState.IsValid)
      {
        // повертається помилка
        return Json(new { Erreur = getErrorMessagesFor(ModelState), HeureCalcul = HeureCalcul });
      }
      // кожен другий раз імітується помилка
      int val = session.Randomizer.Next(2);
      if (val == 0)
      {
        // повертається помилка
        return Json(new { Erreur = "[erreur aléatoire]", HeureCalcul = HeureCalcul });
      }
      // розрахунки
      string AplusB = string.Format("{0}", modèle.A + modèle.B);
      string AmoinsB = string.Format("{0}", modèle.A - modèle.B);
      string AmultipliéparB = string.Format("{0}", modèle.A * modèle.B);
      string AdiviséparB = string.Format("{0}", modèle.A / modèle.B);
      // повертаються результати
      return Json(new { Erreur = "", AplusB = AplusB, AmoinsB = AmoinsB, AmultipliéparB = AmultipliéparB, AdiviséparB = AdiviséparB, HeureCalcul = HeureCalcul });
    }
  • рядок 2: метод повертає тип [JsonResult], тобто текст у форматі JSON;
  • рядок 16: інформація подається у вигляді екземпляра анонімного класу, серіалізованого у JSON. Метод [getErrorMessagesFor] вже було розглянуто. Рядок JSON, що надсилається до браузера, матиме такий вигляд:
{"Erreur":"[erreur aléatoire]","HeureCalcul":"05:31:37"}
  • рядок 31: той самий підхід для арифметичних результатів. Цього разу рядок JSON, що надсилається до браузера, матиме такий вигляд:
{"Erreur":"","AplusB":"4","AmoinsB":"-2","AmultipliéparB":"3","AdiviséparB":"0,333333333333333","HeureCalcul":"05:52:17"}

7.4.3. Код JavaScript на стороні клієнта

Нагадаємо конфігурацію виклику Ajax на сторінці HTML, що надсилається до клієнтського браузера:


  AjaxOptions ajaxOpts = new AjaxOptions
  {
    HttpMethod = "post",
    Url = Url.Action("Action02Post"),
    LoadingElementId = "loading",
    LoadingElementDuration = 1000,
    OnBegin = "OnBegin",
    OnFailure = "OnFailure",
    OnSuccess = "OnSuccess",
    OnComplete = "OnComplete"
};    

Функції JS, на які є посилання в рядках 7–10 (праворуч від знака =), визначені в наступному файлі [myScripts-02.js]:


// загальні дані
var entete;
var loading;
var résultats;
var erreur;
var heureCalcul;
var msg;
var AplusB;
var AmoinsB;
var AmultipliéparB;
var AdiviséparB;
var formulaire;
...
function postForm() {
...
}

// під час завантаження документа
$(document).ready(function () {
  formulaire = $("#formulaire");
  entete = $("#entete");
  loading = $("#loading");
  erreur = $("#erreur");
  résultats = $('#результати');
  heureCalcul = $("#heureCalcul");
  msg = $("#msg");
  AplusB = $("#AplusB");
  AmoinsB = $("#AmoinsB");
  AmultipliéparB = $("#AmultipliéparB");
  AdiviséparB = $("#AdiviséparB");

  // приховування деяких елементів сторінки
  entete.hide();
  résultats.hide();
  erreur.hide();
});

// початок
function OnBegin() {
....
}

// завершення запиту
function OnComplete() {
...
}

// успіх
function OnSuccess(data) {
....
}

// помилка
function OnFailure(request, error) {
...
}
  • рядок 19: функція JS, що виконується після завершення завантаження сторінки в браузері;
  • рядки 20–30: отримуються посилання на всі компоненти сторінки, що нас цікавлять. Пошук компонента на сторінці вимагає певних ресурсів, тому краще виконувати його лише один раз;
  • рядки 33–35: компоненти [entete], [résultats] та [loading] приховуються;

Під час запуску Ajax-запиту виконується така функція:


// початок
function OnBegin() {
  // сигнал очікування увімкнено
  loading.show();
  // деякі елементи сторінки приховано
  entete.hide();
  résultats.hide();
  erreur.hide();
}
  • рядок 4: відображається компонент [loading]. Це анімоване зображення;
  • рядки 6–8: компоненти [entete], [résultats] та [erreur] приховані;

Якщо запит Ajax виконано успішно, виконується наступний код JS:


// успішно
function OnSuccess(data) {
  // відображення результатів
  heureCalcul.text(data.HeureCalcul);
  entete.show();
  if (data.Erreur != '') {
    msg.text(data.Erreur);
    erreur.show();
    return;
  }
  // помилок немає
  AplusB.text(data.AplusB);
  AmoinsB.text(data.AmoinsB);
  AmultipliéparB.text(data.AmultipliéparB);
  AdiviséparB.text(data.AdiviséparB);
  résultats.show();
}

Щоб зрозуміти цей код, слід згадати два тексти JSON, які можуть бути надіслані у відповідь браузеру:

{"Erreur":"[erreur aléatoire]","HeureCalcul":"05:31:37"}

у разі помилки, інакше — рядок:

{"Erreur":"","AplusB":"4","AmoinsB":"-2","AmultipliéparB":"3","AdiviséparB":"0,333333333333333","HeureCalcul":"05:52:17"}

Якщо цей рядок назвати [data], значення поля [Erreur] отримується за допомогою запису [data.Erreur] або [data["Erreur"]], на вибір. Те саме стосується інших полів ланцюга JSON. Крім того, щоб присвоїти неформатований текст компоненту з ідентифікатором X, записують [X.text(chaine)]. Повернемося до коду функції [OnSuccess]:

  • рядок 2: [data] — це отриманий рядок JSON;
  • рядок 4: компонент [heureCalcul] отримує своє значення;
  • рядок 5: компонент [entete] виводиться на екран;
  • рядок 6: перевірка поля [Erreur] рядка JSON;
  • рядок 7: компонент [msg] отримує своє значення;
  • рядок 8: відображається компонент [erreur];
  • рядок 9: у разі помилки процес завершується;
  • рядок 12: компонент [AplusB] отримує своє значення;
  • рядок 13: компонент [AmoinsB] отримує своє значення;
  • рядок 14: компонент [AmultipliéparB] отримує своє значення;
  • рядок 15: компонент [AdiviséparB] отримує своє значення;
  • рядок 16: компонент [résultats] відображається.

Функція [OnFailure] буде виконана у разі невдачі запиту HTTP Ajax. Ця невдача визначається за кодом HTTP, який повертає сервер. Наприклад, код 500 [Internal Server Error] вказує, що сервер не зміг виконати запит. Функція [OnFailure] має такий вигляд:


// помилка
function OnFailure(request, error) {
  alert("L'erreur suivante s'est produite :" + error);
}

Ми просто відображаємо діалогове вікно з повідомленням про помилку, що сталася. На практиці слід бути більш точним. Незабаром ми запропонуємо інше рішення.

Нарешті, функція [OnComplete] виконується після завершення запиту, незалежно від того, чи був він успішним, чи ні.


// завершення запиту
function OnComplete() {
  // сигнал очікування вимкнено
  loading.hide();
}

Нагадаємо, що саме налаштування виклику Ajax у поданнях [Action02Get.cshtml] зумовлює виклик цих різних функцій:


  AjaxOptions ajaxOpts = new AjaxOptions
  {
...
    OnBegin = "OnBegin",
    OnFailure = "OnFailure",
    OnSuccess = "OnSuccess",
    OnComplete = "OnComplete"
};

7.4.4. Посилання [Calculer]

Код HTML посилання [Calculer] у подання [Action02Get.cshtml] має такий вигляд:


      <a href="javascript:postForm()">Calculer</a>

Функція JS [postForm] знайдена в імпортованому файлі [myScripts-02.js]:


  <script type="text/javascript" src="~/Scripts/myScripts-02.js"></script>

Її код такий:


function postForm() {
  // виконується ручний виклик Ajax за допомогою JQuery
  $.ajax({
    url: '/Premier/Action02Post',
    type: 'POST',
    data: formulaire.serialize(),
    dataType: 'json',
    beforeSend: OnBegin,
    success: OnSuccess,
    error: OnFailure,
    complete: OnComplete
  })
}

Ми вже зустрічали подібний код.

  • рядок 4: URL — ціль виклику Ajax;
  • рядок 5: команда HTTP, що використовується у виклику Ajax;
  • рядок 6: відправлені значення. Вони є результатом серіалізації значень форми. Ця форма, позначена ідентифікатором [formulaire], посилається на змінну [formulaire]. [data] буде рядком у форматі [A=val1&B=val2];
  • рядок 7: тип форматування очікуваної відповіді. Це рядок JSON;
  • рядок 8: функція JS, яку потрібно виконати при запуску Ajax-виклику;
  • рядок 9: функція JS, яку слід виконати, якщо виклик Ajax завершився успішно;
  • рядок 10: функція JS, яку слід виконати, якщо виклик Ajax завершився невдало;
  • рядок 11: функція JS, яка має виконуватися після отримання відповіді від сервера, незалежно від того, чи це успіх, чи помилка.

Повернемося до функції JavaScript, яка обробляє випадок, коли виклик Ajax завершується невдало (рядок 10). Ajax-запит може завершитися невдало в різних ситуаціях, наприклад, коли сервер повертає код помилки, такий як [403 Forbidden], [404 Not Found], [500 Internal Server Error], [301 Moved Permanently], ...

У наведеному вище прикладі функція [OnFailure] виглядає так:


// помилка
function OnFailure(request, error) {
  alert("L'erreur suivante s'est produite :" + error);
}

Як правило, відображення об’єкта [error] не надає жодної цікавої інформації. Якщо використовується Ajax-запит, виконаний за допомогою JQuery, можна скористатися таким методом [OnFailure]:


// помилка
function OnFailure(jqXHR) {
  alert("Erreur : " + jqXHR.status + " " + jqXHR.statusText);
  msg.html(jqXHR.responseText);
  erreur.show();
}

Об’єкт JQuery [jqXHR] має серед своїх властивостей такі:

  • responseText: текст відповіді сервера;
  • status: код помилки, повернутий сервером;
  • statusText: текст, пов'язаний із цим кодом помилки.
  • рядок 3: відображається код помилки та відповідний текст;
  • рядок 4: відповідь сервера HTML поміщається в компонент з ідентифікатором [msg];
  • рядок 5: виводиться регіон з ідентифікатором [erreur].

Щоб перевірити цю функцію обробки помилок, ми штучно створимо виняток у дії [Action02Post]:


    [HttpPost]
    public JsonResult Action02Post(FormCollection postedData, SessionModel session)
    {
      // штучне виключення для тестування функції обробки помилок виклику Ajax
      throw new Exception();
      // імітація очікування
      Thread.Sleep(2000);
      // перевірка моделі
...

У рядку 5 генерується виняток. Тепер протестуємо додаток:

Ми отримуємо такі відповіді: [1] та [2]:

Відповідь сервера дозволяє нам побачити, на якому рядку коду сервера сталася помилка. Це часто корисна інформація. Відтепер ми будемо використовувати цю техніку для обробки помилок викликів Ajax.

7.5. Веб-додаток з однією сторінкою

Технологія Ajax дозволяє створювати односторінкові додатки:

  • перша сторінка завантажується за допомогою звичайного запиту браузера;
  • наступні сторінки отримуються за допомогою викликів Ajax. Таким чином, у підсумку браузер ніколи не змінює URL і ніколи не завантажує нову сторінку. Такий тип додатка називають односторінковим додатком (APU) або, англійською, Single Page Application (SPA).

Ось простий приклад такого додатка. Новий додаток матиме два види:

  • у [1] дія [Action03Get] дозволяє нам отримати першу сторінку, сторінку 1;
  • у [2] посилання дозволяє перейти на сторінку 2 за допомогою виклику Ajax;
  • у [3], URL не змінився. Відображається сторінка 2;
  • у [4] посилання дозволяє повернутися на сторінку 1 за допомогою виклику Ajax;
  • у [5], URL не змінився. Відображається сторінка 1.

Код дії [Action03Get] такий:


    [HttpGet]
    public ViewResult Action03Get()
    {
      return View();
}
  • рядок 4: відображається представлення [Action03Get.cshtml].

Вигляд [Action03Get.cshtml] є таким:


@{
  Layout = null;
}

<!DOCTYPE html>

<html>
<head>
  <meta name="viewport" content="width=device-width" />
  <title>Action03Get</title>
  <script type="text/javascript" src="~/Scripts/jquery-1.8.2.min.js"></script>
  <script type="text/javascript" src="~/Scripts/jquery.unobtrusive-ajax.min.js"></script>
</head>
<body>
  <h3>Ajax - 03 - Single Page Application</h3>
  <div id="content">
    @Html.Partial("Page1")
  </div>
</body>
</html>
  • рядки 16–18: елемент з ідентифікатором [content]. Саме в цьому елементі будуть відображатися різні сторінки;
  • рядок 17: за замовчуванням спочатку відображатиметься сторінка [Page1.cshtml].

Сторінка [Page1.cshtml] виглядає так:


<h4>Page 1</h4>
  <p>
    @Ajax.ActionLink("Page 2", "Action04", new { Page = 2 }, new AjaxOptions() { UpdateTargetId = "content" })
</p>
  • рядок 1: заголовок сторінки, щоб відрізнити її від сторінки 2;
  • рядок 3: посилання Ajax із такими параметрами:
    • текст посилання [Page 2];
    • цільова дія посилання [Action04];
    • параметри запиту URL. Це буде [/Premier/Action04?Page=2];
  • параметри виклику Ajax. У даному випадку використовується лише ідентифікатор області, яку потрібно оновити за допомогою відповіді сервера. Для інших параметрів використовуються значення за замовчуванням, якщо такі існують. Метод HTTP за замовчуванням — GET.

Давайте подивимося, що відбувається при натисканні на посилання. Запит URL [/Premier/Action04?Page=2] надсилається з GET. Після цього виконується дія [Action04]:


    [HttpGet]
    public PartialViewResult Action04(string page = "1")
    {
      string vue = "Page1";
      if (page == "2")
      {
        vue = "Page2";
      }
      return PartialView(vue);
}
  • рядок 2: дія повертає частковий потік HTML;
  • рядок 2: шаблон дії — рядок [page]. Однак відомо, що URL містить таку інформацію: [/Premier/Action04?Page=2]. Нагадаємо, що шаблон не чутливий до регістру;
  • рядки 4–8: [vue] отримає значення [Page2];
  • рядок 9: повертається частковий вигляд [Page2.cshtml].

Частковий вигляд [Page2.cshtml] має такий вигляд:


<h4>Page 2</h4>
  <p>
    @Ajax.ActionLink("Page 1", "Action04", new { Page = 1 }, new AjaxOptions() { UpdateTargetId = "content" })
</p>

Отже, сервер повертає наведений вище потік HTML як відповідь на Ajax-запит GET [/Premier/Action04?Page=2]. Нагадаємо, що цей Ajax-запит використовує цю відповідь для оновлення області з ідентифікатором [content] (рядок 3 нижче):


<h4>Page 1</h4>
  <p>
    @Ajax.ActionLink("Page 2", "Action04", new { Page = 2 }, new AjaxOptions() { UpdateTargetId = "content" })
</p>

Це призводить до відображення нового вмісту [1]:

Дотримуючись тієї ж логіки, бачимо, що клік на посилання [Page 1] з [1] призведе до відображення [2].

Повернемося до загальної схеми роботи додатка ASP.NET MVC:

Завдяки вбудованому в сторінки HTML коду JavaScript, що виконується в браузері, можна перенести частину коду в браузер і отримати таку архітектуру:

  • у [1] веб-шар ASP.NET MVC перетворився на веб-інтерфейс для доступу до даних, які зазвичай зберігаються в базі даних. Надані подання містять лише дані без будь-якого оформлення, наприклад, потоки XML або JSON;
  • у [2]: браузер відображає статичні (тобто не генеровані динамічно) види, що надаються веб-сервером, який може, як і не може, знаходитися на тій самій машині, що й сервер [1]. Ці статичні сторінки потім доповнюються даними, отриманими за допомогою JavaScript з веб-інтерфейсу [1];
  • код JavaScript, вбудований у сторінки HTML, може бути структурований у шари:
    • шар [présentation] відповідає за взаємодію з користувачем,
    • шар [DAO] відповідає за доступ до даних через веб-сервер [1],
    • шар [métier] відповідає шару [métier], який раніше знаходився на сервері [1] і був перенесений у браузер [2];

Перевага цієї архітектури полягає в тому, що вона задіює різні навички:

  • код веб-сервера [1] вимагає навичок роботи з .NET, але не вимагає навичок роботи з JavaScript, HTML, CSS;
  • код, вбудований у браузер [2], вимагає навичок роботи з Javascript, HTML, CSS, але не залежить від технології веб-сервера [1].

Таким чином, ця архітектура полегшує паралельну роботу команд із різними наборами навичок. Вона особливо добре підходить для односторінкових додатків.

7.6. Веб-додаток з однією сторінкою та перевірка на стороні клієнта

Раніше ми згадували про аномалію на прикладі Ajax-01. Нагадаємо контекст:

  • у [1] та [2] було введено недійсні значення. Вони були виявлені валідаторами на стороні клієнта;
  • у [3] було натиснуто на посилання [Calculer];
  • у [4] відбувся [POST], оскільки отримано відповідь [4].

Коли значення є недійсними і натискається кнопка [Calculer], запит [POST] до сервера не надсилається. У тому ж випадку, при використанні посилання [Calculer] запит [POST] на сервер надсилається. Отже, кнопка [Calculer] поводиться таким чином, що нам не вдалося відтворити за допомогою посилання [Calculer].

Ми розглянемо цей приклад у новому контексті: додаток матиме кілька вікон і буде типу [Application à Page Unique], який ми щойно описали.

7.6.1. Види прикладу

Приклад має кілька візуалізацій:

  • у [1] — вигляд [Action05Get];
  • у [2] — частковий вигляд [Formulaire05];
  • з [3] — частковий вигляд [Failure05];
  • у [4] — фрагмент [Success05].

Додаток складається з однієї сторінки: вона завантажується браузером під час першого запиту. Потім вона оновлюється за допомогою викликів Ajax.

Попередні сторінки генеруються такими поданнями [cshtml]:

Початково завантажується такий вигляд [Action05Get.cshtml]:


@model Exemple_04.Models.ViewModel05
@{
  Layout = null;
}

<!DOCTYPE html>

<html lang="fr-FR">
<head>
  <meta name="viewport" content="width=device-width" />
  <title>Ajax-05</title>
  <link rel="stylesheet" href="~/Content/Site.css" />
  <script type="text/javascript" src="~/Scripts/jquery-1.8.2.min.js"></script>
  <script type="text/javascript" src="~/Scripts/jquery.validate.min.js"></script>
  <script type="text/javascript" src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>
  <script type="text/javascript" src="~/Scripts/globalize/globalize.js"></script>
  <script type="text/javascript" src="~/Scripts/globalize/cultures/globalize.culture.fr-FR.js"></script>
  <script type="text/javascript" src="~/Scripts/globalize/cultures/globalize.culture.en-US.js"></script>
  <script type="text/javascript" src="~/Scripts/jquery.unobtrusive-ajax.js"></script>
  <script type="text/javascript" src="~/Scripts/myScripts-05.js"></script>
</head>
<body>

  <h2>Ajax - 05, Page unique - Validation formulaire côté client</h2>
  <p><strong>Heure de chargement : @Model.HeureChargement</strong></p>
  <h4>Opérations arithmétiques sur deux nombres réels A et B positifs ou nuls</h4>
  <img id="loading" style="display: none" src="~/Content/images/indicator.gif" />
  <div id="content">
    @Html.Partial("Formulaire05", Model)
  </div>
</body>
</html>

Зверніть увагу на наступні моменти:

  • рядок 1: шаблон подання — це тип [ViewModel05], який ми незабаром розглянемо;
  • рядки 13–19: тут містяться скрипти JavaScript, необхідні для роботи з Ajax та перевірки даних на стороні клієнта;
  • рядок 20: ми додамо власні функції JavaScript у [myScripts-05.js];
  • рядок 27: анімоване зображення очікування;
  • рядки 28–30: тег з ідентифікатором [content]. Саме в цей тег будуть вставлятися часткові види [Formulaire05, Success05, Failure05];
  • рядок 29: вставлення часткового виду [Formulaire05].

Частковий вигляд [Action05Get] відповідає за відображення частини [1] початкової сторінки:

Частковий вигляд [Formulaire05] згенерує наведену вище частину [2]. Його код такий:


@model Exemple_04.Models.ViewModel05

@using (Html.BeginForm("Action05Post", "Premier", FormMethod.Post, new { id = "formulaire" }))
{
  <table>
    <thead>
      <tr>
        <th>@Html.LabelFor(m => m.A)</th>
        <th>@Html.LabelFor(m => m.B)</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>@Html.TextBoxFor(m => m.A)</td>
        <td>@Html.TextBoxFor(m => m.B)</td>
      </tr>
      <tr>
        <td>@Html.ValidationMessageFor(m => m.A)</td>
        <td>@Html.ValidationMessageFor(m => m.B)</td>
      </tr>
    </tbody>
  </table>
  <p>
    <table>
      <tbody>
        <tr>
          <td><a href="javascript:calculer()">Calculer</a>
          </td>
          <td style="width: 20px" />
          <td><a href="javascript:effacer()">Effacer</a>
          </td>
        </tr>
      </tbody>
    </table>
  </p>
}
  • рядок 1: частковий вигляд приймає як шаблон тип [ViewModel05];
  • рядок 3: форма, згенерована методом [Html.BeginForm]. Оскільки ця форма буде відправлена за допомогою виклику Ajax, перші три параметри методу будуть проігноровані. За винятком випадків, коли користувач вимкнув JavaScript у своєму браузері. Тут ми не враховуємо цю можливість. Четвертий параметр є важливим. Форма матиме ідентифікатор [formulaire];
  • рядки 5–22: форма для введення чисел A та B;
  • рядок 27: посилання JavaScript, яке запускає виконання чотирьох арифметичних операцій над A та B;
  • рядок 30: посилання на JavaScript, яке очищає введені дані та можливі пов’язані з ними повідомлення про помилки.

Зауважимо, що у формі немає кнопки типу [submit]. Нам доведеться вручну обчислити [Post] із введених значень A та B.

Якщо помилок немає, результати відображаються:

Частина [4], наведена вище, генерується наступним частковим видом [Success05.cshtml]:


@model Exemple_04.Models.ViewModel05
<hr />
<p><strong>Heure de calcul : @Model.HeureCalcul</strong></p>
<p>A=@Model.A</p>
<p>B=@Model.B</p>
<h4>Résultats</h4>
<p>A+B=@Model.AplusB</p>
<p>A-B=@Model.AmoinsB</p>
<p>A*B=@Model.AmultipliéparB</p>
<p>A/B=@Model.AdiviséparB</p>
<p>
  <a href="javascript:retourSaisies()">Retour aux saisies</a>
</p>
  • рядок 1: частковий вигляд [Success05.cshtml] отримує шаблон типу [ViewModel05];
  • рядок 12: посилання JavaScript для повернення до полів введення.

У разі помилки відображається інший фрагмент [3]:

Цей фрагмент генерується за допомогою такого коду [Failure05.cshtml]:


@model Exemple_04.Models.ViewModel05
<hr />
<p><strong>Heure de calcul : @Model.HeureCalcul</strong></p>
<p>A=@Model.A</p>
<p>B=@Model.B</p>
<h2>Les erreurs suivantes se sont produites</h2>
<ul>
  @foreach (string msg in Model.Erreurs)
  {
    <li>@msg</li>
  }
</ul>
<p>
  <a href="javascript:retourSaisies()">Retour aux saisies</a>
</p>
  • рядок 1: частковий вигляд [Failure05.cshtml] отримує шаблон типу [ViewModel05];
  • рядок 14: посилання JavaScript для повернення до полів введення.

7.6.2. Шаблон переглядів

Усі попередні види мають спільний шаблон [ViewModel05]:


using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;

namespace Exemple_04.Models
{
  [Bind(Exclude = "AplusB, AmoinsB, AmultipliéparB, AdiviséparB, Erreurs, HeureChargement, HeureCalcul")]
  public class ViewModel05
  {
    // форма
    [Required(ErrorMessage="Donnée A requise")]
    [Display(Name="Valeur de A")]
    [Range(0, Double.MaxValue, ErrorMessage = "Tapez un nombre A positif ou nul")]
    public string A { get; set; }
    [Required(ErrorMessage = "Donnée B requise")]
    [Display(Name = "Valeur de B")]
    [Range(0, Double.MaxValue, ErrorMessage="Tapez un nombre B positif ou nul")]
    public string B { get; set; }

    // результати
    public string AplusB { get; set; }
    public string AmoinsB { get; set; }
    public string AmultipliéparB { get; set; }
    public string AdiviséparB { get; set; }
    public List<string> Erreurs { get; set; }
    public string HeureChargement { get; set; }
    public string HeureCalcul { get; set; }
  }
}

Це модель [ViewModel01], яка вже була представлена, за винятком деяких дрібних деталей:

  • рядки 15 та 19: поля A та B тепер мають тип [string], щоб під час початкового відображення форми введення даних показувати порожні поля для введення, а не поля зі значенням 0;
  • рядки 14 та 18: це не заважає перевіряти введене значення за допомогою валідатора [Range];
  • рядок 26: список повідомлень про помилки, що відображається поданням [Failure05].

7.6.3. Дані області дії [Session]

У розділі 7.3.6 ми бачили, що дані сеансу були інкапсульовані в таку модель [SessionModel]:


using System;
namespace Exemple_03.Models
{
  public class SessionModel
  {
    // генератор випадкових чисел
    public Random Randomizer { get; set; }
  }
}

Цей шаблон сеансу розширено для включення значень A та B:


using System;
namespace Exemple_03.Models
{
  public class SessionModel
  {
    // генератор випадкових чисел
    public Random Randomizer { get; set; }
    // значення A та B
    public string A { get; set; }
    public string B { get; set; }
  }
}

Дійсно, необхідно зберегти значення A та B у сесії, як показано в наступній послідовності:

Запит 1

Запит 2

У запиті [4] містяться дані, введені в запиті [1]. При цьому існує два окремі запити HTTP. Відомо, що пам’яттю між двома запитами HTTP є сесія. Щоб другий запит міг отримати значення, відправлені першим, ці значення мають бути збережені в сесії.

7.6.4. Серверна дія [Action05Get]

Дія [Action05Get] — це дія, яка відображає початкову єдину сторінку. Її код такий:


    [HttpGet]
    public ViewResult Action05Get()
    {
      ViewModel05 modèle = new ViewModel05();
      modèle.HeureChargement = DateTime.Now.ToString("hh:mm:ss");
      return View(modèle);
}
  • рядок 6: вже розглянутий вигляд [Action05Get.cshtml] відображається за допомогою шаблону типу [ViewModel05];

7.6.5. Клієнтська дія [Calculer]

Розглянемо взаємодію користувача з поданнями:

Посилання [1] є посиланням на JavaScript:


<a href="javascript:calculer()">Calculer</a>

Функція JavaScript [calculer] міститься у файлі [myScripts-05.js]:


  <script type="text/javascript" src="~/Scripts/myScripts-05.js"></script>

Код функції JavaScript [calculer] такий:


// загальні дані
var content;
var loading;

function calculer() {
  // спочатку посилання на DOM
  var formulaire = $("#formulaire");
  // потім перевірка форми
  if (!formulaire.validate().form()) {
    // неправильно заповнена форма — завершено
    return;
  }
  // виконується ручний виклик Ajax
  $.ajax({
    url: '/Premier/Action05FaireCalcul',
    type: 'POST',
    data: formulaire.serialize(),
    dataType: 'html',
    beforeSend: function () {
      loading.show();
    },
    success: function (data) {
      content.html(data);
    },
    complete: function () {
      loading.hide();
    },
    error: function (jqXHR) {
      // відображення відповіді сервера
      content.html(jqXHR.responseText);
    }
  })
}

function retourSaisies() {
 ...
}

function effacer() {
  ...
}

// під час завантаження документа
$(document).ready(function () {
  // отримано посилання на різні компоненти сторінки
  loading = $("#loading");
  content = $("#content");
  // закешовуємо анімоване зображення
  loading.hide();
});
  • нагадуємо, що код JavaScript завжди виконується на стороні клієнта, у браузері;
  • рядок 44: функція JS виконується після завершення початкового завантаження єдиної сторінки;
  • рядок 46: посилання на анімоване зображення з ідентифікатором [loading];
  • рядок 47: посилання на область з ідентифікатором [content]. Саме ця область отримує часткові види [Formulaire05, Success05, Failure05];
  • рядки 2–3: змінні з рядків 46–47 оголошені глобальними, щоб інші функції мали до них доступ. Пошук елементів на сторінці (рядки 46–47) пов’язаний з певними витратами. Немає сенсу повторювати цей пошук, якщо цього можна уникнути;
  • рядок 5: функція [calculer];
  • рядок 7: отримується посилання на форму. Частковий вигляд [Formulaire05] присвоїв їй ідентифікатор [formulaire];
  • рядок 9: ця інструкція виконує валідатори форми на стороні клієнта. Саме цього бракувало у виявленій на сторінці 183 аномалії. Цей метод надається бібліотекою [jquery.unobstrusive-ajax], яку використовує одиночна сторінка:

  <script type="text/javascript" src="~/Scripts/jquery.unobtrusive-ajax.js"></script>

Інструкція повертає [false], якщо форма визнана недійсною;

  • рядок 11: виклик Ajax на сервер не виконується, якщо форма є недійсною;
  • рядки 14–32: виконується Ajax-запит до сервера;
  • рядок 15: цільовим URL є серверна дія [Action05FaireCalcul];
  • рядок 16: вона запитується за допомогою [POST];
  • рядок 17: відправлені значення. Це дані, введені у форму, в даному випадку значення A та B;
  • рядки 22–24: у разі успішного виконання Ajax-виклику функція [calculer] оновлює область з ідентифікатором [content] за допомогою потоку HTML, надісланого сервером.

Цей потік HTML — це той, що надсилається дією [Action05FaireCalcul], на яку спрямований виклик Ajax. Код цієї дії на стороні сервера такий:


    [HttpPost]
    public PartialViewResult Action05FaireCalcul(FormCollection postedData, SessionModel session)
    {
      // шаблон
      ViewModel05 modèle = new ViewModel05();
      // час обчислення
      modèle.HeureCalcul = DateTime.Now.ToString("hh:mm:ss");
      // оновлення шаблону
      TryUpdateModel(modèle, postedData);
      if (!ModelState.IsValid)
      {
        // повертається помилка
        modèle.Erreurs = getListOfMessagesFor(ModelState);
        return PartialView("Failure05", modèle);
      }
...
}
  • рядок 1: дія приймає лише [post];
  • рядок 2: вона повертає частковий вигляд;
  • рядок 2: вона отримує як параметри відправлені значення (postedData) та модель сесії (session);
  • рядок 5: створюється шаблон часткового виду;
  • рядок 7: він оновлюється з часом обчислення;
  • рядок 9: робиться спроба застосувати передані значення до шаблону. Після цього будуть виконані його валідатори. Можна запитати, навіщо це робити, якщо валідатори на стороні клієнта блокують POST у разі введення недійсних даних. Насправді ми не впевнені у походженні POST. Можливо, він був створений кодом, який не належить нам. Тому завжди слід проводити перевірки на стороні сервера;
  • рядок 10: перевіряємо, чи валідатори пройшли успішно;
  • рядок 13: якщо шаблон недійсний, його оновлюють із зазначенням списку помилок. Не будемо детально описувати внутрішній метод [getListOfMessagesFor], аналогічний методу [GetErrorMessagesFor], описаному на сторінці 65;
  • рядок 14: відображається частковий вигляд [Failure05] разом із його шаблоном. Нагадаємо код цього вигляду;

@model Exemple_04.Models.ViewModel05
<hr />
<p><strong>Heure de calcul : @Model.HeureCalcul</strong></p>
<p>A=@Model.A</p>
<p>B=@Model.B</p>
<h2>Les erreurs suivantes se sont produites</h2>
<ul>
  @foreach (string msg in Model.Erreurs)
  {
    <li>@msg</li>
  }
</ul>
<p>
  <a href="javascript:retourSaisies()">Retour aux saisies</a>
</p>
  • рядки 7–12: список помилок шаблону відображається за допомогою тегу <ul>.

Нагадаємо, що функція JS [calculer], яка єяка походить від [Post] у серверній дії [Action05FaireCalcul], розмістить цей потік HTML у регіоні з ідентифікатором [content]. У результаті виходить щось на зразок цього:

Продовжимо розгляд коду дії [Action05FaireCalcul]:


    [HttpPost]
    public PartialViewResult Action05FaireCalcul(FormCollection postedData, SessionModel session)
    {
      // модель
      ViewModel05 modèle = new ViewModel05();
...
      // значення A та B заносяться до сесії
      session.A = modèle.A;
      session.B = modèle.B;
      // поки що помилок немає
      List<string> erreurs = new List<string>();
      // кожен другий раз імітується помилка
      int val = session.Randomizer.Next(2);
      if (val == 0)
      {
        erreurs.Add("[erreur aléatoire]");
      }
      if (erreurs.Count != 0)
      {
        modèle.Erreurs = erreurs;
        return PartialView("Failure05", modèle);
      }
      // розрахунки
      double A = double.Parse(modèle.A);
      double B = double.Parse(modèle.B);
      modèle.AplusB = string.Format("{0}", A + B);
      modèle.AmoinsB = string.Format("{0}", A - B);
      modèle.AmultipliéparB = string.Format("{0}", A * B);
      modèle.AdiviséparB = string.Format("{0}", A / B);
      // перегляд
      return PartialView("Success05", modèle);
}
  • рядок 7: модель визнано дійсною;
  • рядки 8–9: введені значення A та B зберігаються у сесії. Це потрібно для того, щоб мати змогу використовувати їх у наступному запиті;
  • рядки 11–22: раз на два випадки генерується випадкова помилка;
  • рядки 24–29: виконуються чотири арифметичні операції над введеними дійсними числами;
  • рядок 31: повертається частковий вигляд [Success05] разом із його моделлю. Цей частковий вигляд має такий вигляд:

@model Exemple_04.Models.ViewModel05
<hr />
<p><strong>Heure de calcul : @Model.HeureCalcul</strong></p>
<p>A=@Model.A</p>
<p>B=@Model.B</p>
<h4>Résultats</h4>
<p>A+B=@Model.AplusB</p>
<p>A-B=@Model.AmoinsB</p>
<p>A*B=@Model.AmultipliéparB</p>
<p>A/B=@Model.AdiviséparB</p>
<p>
  <a href="javascript:retourSaisies()">Retour aux saisies</a>
</p>

Нагадаємо, що функція JS [calculer], яка є[Post] під час серверної дії [Action05FaireCalcul] помістить цей потік HTML у область з ідентифікатором [content]. У результаті виходить щось на зразок цього:

7.6.6. Клієнтська дія [Effacer]

Посилання на JavaScript [Effacer] дозволяє повернути форму до початкового стану:

У формі посилання JS [Effacer] визначено наступним чином:


<a href="javascript:effacer()">Effacer</a>

Функція JS [effacer] визначена у файлі [myScripts-05.js] наступним чином:


// загальні дані
var content;
var loading;

function calculer() {
...
}

function retourSaisies() {
...
}

function effacer() {
  // спочатку посилання на DOM
  var formulaire = $("#formulaire");
  var A = $("#A");
  var B = $("#B");
  // присвоюємо допустимі значення введеним даним
  A.val("0");
  B.val("0");
  // потім підтверджуємо форму, щоб прибрати
  // можливі повідомлення про помилки
  formulaire.validate().form();
  // потім до полів введення даних присвоюються порожні рядки
  A.val("");
  B.val("");
}

// під час завантаження документа
$(document).ready(function () {
  // отримуємо посилання на різні компоненти сторінки
  loading = $("#loading");
  content = $("#content");
  // приховуємо анімоване зображення
  loading.hide();
});
  • рядки 15–17: отримуються посилання на різні елементи DOM (Document Object Model);
  • рядки 19–20: у поля введення чисел A та B вводяться допустимі значення;
  • рядок 23: виконуємо перевірки на стороні клієнта. Оскільки значення A та B є правильними, це призведе до зникнення можливих повідомлень про помилки, які могли б з’явитися;
  • рядки 25–26: у поля введення чисел A та B вводяться порожні рядки;

7.6.7. Клієнтська дія [Retour aux Saisies]

Посилання JavaScript [Retour aux Saisies] дозволяє повернутися до форми після отримання результатів:

У формі посилання JS [Retour aux Saisies] визначено наступним чином:


  <a href="javascript:retourSaisies()">Retour aux saisies</a>

Функція JS [retourSaisies] визначена у файлі [myScripts-05.js] наступним чином:


// глобальні дані
var content;
var loading;

function calculer() {
...
}

function retourSaisies() {
  // виконується ручний виклик Ajax
  $.ajax({
    url: '/Premier/Action05RetourSaisies',
    type: 'POST',
    dataType: 'html',
    beforeSend: function () {
      loading.show();
    },
    success: function (data) {
      content.html(data);
    },
    complete: function () {
      loading.hide();
      // IMPORTANT !! перевірка
      $.validator.unobtrusive.parse($("#formulaire"));
    },
    error: function (jqXHR) {
      content.html(jqXHR.responseText);
    }
  })
}

function effacer() {
...
}

// під час завантаження документа
$(document).ready(function () {
  // отримуємо посилання на різні компоненти сторінки
  loading = $("#loading");
  content = $("#content");
  // заховуємо анімоване зображення
  loading.hide();
});
  • рядки 11–29: виклик Ajax;
  • рядок 12: цільова функція URL;
  • рядок 13: вона буде запитана командою HTTP POST. Це POST без переданих параметрів. Тому тут немає рядка такого типу:
    data: formulaire.serialize(),

у виклику Ajax;

  • рядок 14: очікуваний потік від сервера — це потік HTML;
  • рядки 18–20: цей потік HTML буде використано для оновлення області з ідентифікатором [content];

Дія сервера [Action05RetourSaisies] є такою:


    [HttpPost]
    public PartialViewResult Action05RetourSaisies(SessionModel session)
    {
      // перегляд
      return PartialView("Formulaire05", new ViewModel05() { A = session.A, B = session.B });
}
  • рядок 2: дія отримує як параметр шаблон сеансу, у якому ми раніше зберегли введені значення A та B;
  • рядок 5: повертається частковий вигляд [Formulaire05] із шаблоном типу [ViewModel05], у якому поля A та B ініціалізуються значеннями A та B, взятими із сесії;

Тепер повернемося до коду функції JavaScript [retourSaisies]:


function retourSaisies() {
  // виконується ручний виклик Ajax
  $.ajax({
    url: '/Premier/Action05RetourSaisies',
    type: 'POST',
    dataType: 'html',
    beforeSend: function () {
      loading.show();
    },
    success: function (data) {
      content.html(data);
    },
    complete: function () {
      loading.hide();
      // IMPORTANT !! перевірка
      $.validator.unobtrusive.parse($("#formulaire"));
    },
    error: function (jqXHR) {
      content.html(jqXHR.responseText);
    }
  })
}
  • рядок 13: метод, що виконується після завершення виклику Ajax;
  • рядок 14: анімоване зображення очікування приховано;
  • рядок 16: інструкція, яка для мене є дещо незрозумілою, знайдена в Інтернеті для вирішення такої проблеми: у формі, що відображається за посиланням [Retour aux saisies], валідатори на стороні клієнта перестали працювати. Шукаючи інформацію про бібліотеку JS [jquery.unobtrusive-ajax], я знайшов рішення, описане в рядку 16. Воно аналізує форму, можливо, для активації валідаторів на стороні клієнта.

7.7. Як зробити додаток ASP.NET доступним в Інтернеті

Див. розділ 9.26.

7.8. Створення нативного додатка для Android на основі односторінкового додатка APU

Див. розділ 9.27.