Skip to content

4. Модель дії

Повернемося до архітектури додатка ASP.NET MVC:

У попередньому розділі ми розглянули процес, завдяки якому запит [1] надходить до контролера та дії [2a], які його оброблять — цей механізм називається маршрутизацією. Крім того, ми розглянули різні типи відповідей, які дія може надати браузеру. Наразі ми розглянули дії, які не використовували запит, що надходив до них. Запит [1] містить у собі різноманітну інформацію, яку ASP.NET та MVC передають дії у вигляді шаблону. Не слід плутати цей термін із шаблоном M для подання V [2c], яке генерується дією:

  • запит HTTP від клієнта надходить у вигляді [1];
  • у [2] інформація, що міститься у запиті, буде перетворена на модель дії [3] — часто, але не обов’язково, клас, який слугуватиме вхідними даними для дії [4];
  • у [4] дія, на основі цієї моделі, згенерує відповідь. Вона матиме дві складові: представлення V [6] та модель M цього представлення [5];
  • представлення V [6] використовуватиме свою модель M [5] для генерації відповіді HTTP, призначеної для клієнта.

У моделі MVC дія [4] є частиною C (контролера), модель подання [5] є M, а подання [6] є V.

У цьому розділі розглядаються механізми зв’язку між інформацією, що передається у запиті (яка за своєю природою є рядками символів), та моделлю дії, яка може бути класом із властивостями різних типів.

4.1. Ініціалізація параметрів дії

До існуючого рішення ми додаємо новий проект [1] на основі ASP.NET та MVC:

  • в [2] — ім’я нового проєкту;
  • у [3, 4] ми обираємо базовий проект ASP.NET MVC;
  • у [5] — новий проєкт.

Новий проєкт стане стартовим проєктом рішення.

Як це було зроблено в розділі 3.1, ми створюємо контролер із назвою [First] [1]:

  

У цьому контролері ми створюємо таку дію [Action01]:


using System.Web.Mvc;

namespace Exemple_02.Controllers
{
  public class FirstController : Controller
  {
    // Дія01
    public ContentResult Action01(string nom)
    {
      return Content(string.Format("Contrôleur=First, Action=Action01, nom={0}", nom));
    }

  }
}

Нововведення полягає у рядку 8: метод [Action01] має параметр. У цьому розділі ми розглянемо різні способи ініціалізації параметрів дії. Параметр [nom], наведений вище, ініціалізується послідовно з такими значеннями:

Request.Form["nom"]
параметр із назвою [nom], надісланий командою POST
RouteData.Values["nom"]
елемент URL з іменем [nom]
Request.QueryString["nom"]
параметр з іменем [nom], надісланий командою GET
Request.Files["nom"]
завантажений файл з назвою [nom]

Розглянемо ці різні випадки. Запросимо безпосередньо в браузері URL [/First/Action01?nom=someone]. Отримаємо таку відповідь:

 

Запит HTTP з браузера мав такий вигляд:

1
2
3
GET /First/Action01?nom=someone HTTP/1.1
Host: localhost:55483
...
  • рядок 1: запит має вигляд GET. Запитаний URL містить параметр [nom]. На стороні сервера запит надходить до дії [Action01], яка має такий підпис:

public ContentResult Action01(string nom)

Щоб присвоїти значення параметру «nom», ASP.NET MVC послідовно та в зазначеному порядку перевіряє значення Request.Form["nom"], RouteData.Values["nom"], Request.QueryString["nom"], Request.Files["nom"]. Він зупиняється, щойно знаходить значення. Параметр [nom], вбудований у URL з GET, був розміщений фреймворком у Request.QueryString["nom"]. Саме з цим значенням [someone] буде ініціалізовано параметр [nom] з [Action01]. Далі виконується код з [Action01]:


return Content(string.Format("Contrôleur=First, Action=Action01, nom={0}", nom), "text/plain", Encoding.UTF8);

Цей код містить відповідь, що надсилається клієнту:

 

Примітка: механізм зв’язування параметрів не враховує регістр. Отже, якщо наша дія визначена так:


public ContentResult Action01(string NOM)

а переданий параметр — [?NoM=zébulon], зв’язування відбудеться. Параметр [NOM] з [Action01] отримає значення [zébulon].

Тепер спробуємо отримати те саме URL за допомогою POST. Для цього використовуємо додаток [Advanced Rest Client]:

  • у [1] — запитуваний URL;
  • у [2] буде використано команду POST;
  • у [3] — параметри POST.

Надішлімо цей запит і переглянемо журнали HTTP. Запит HTTP має такий вигляд:

  • у [1] — параметри POST;
  • у [2] — параметри з POST. Технічно вони були надіслані після заголовків HTTP, після порожнього рядка, що сигналізує про кінець цих заголовків;
  • у [3] — отримана відповідь. Параметр [nom] з POST дійсно відновлюється. Серед значень, що випробовувалися для параметра «nom»: Request.Form["nom"], RouteData.Values["nom"], Request.QueryString["nom"], Request.Files["nom"], спрацювало саме перше значення.

Тепер змінимо маршрут за замовчуванням у [App_Start/RouteConfig]. Наразі цей маршрут виглядає так:


      routes.MapRoute(
          name: "Default",
          url: "{controller}/{action}/{id}",
          defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

Змінимо її на:


      routes.MapRoute(
          name: "Default",
          url: "{controller}/{action}/{nom}",
          defaults: new { controller = "Home", action = "Index", nom = UrlParameter.Optional }
);
  • у рядку 3 ми назвали [nom] третім елементом маршруту;
  • у рядку 4 цей елемент оголошено необов’язковим.

Тепер перекомпілюємо додаток і запитуємо URL [/First/Action01/zébulon] безпосередньо в браузері. Отримуємо таку відповідь:

 

Серед значень, які ми перевірили для параметра «name»: Request.Form["nom"], RouteData.Values["nom"], Request.QueryString["nom"], Request.Files["nom"], спрацювало саме друге значення.

Зробимо той самий запит із POST та [Advanced Rest Client]:

  • у [1] ми присвоїли значення елементу {nom} маршруту;
  • у [2] до відправленого запиту додано параметр [nom];
  • отриману відповідь можна побачити в [3].

Серед значень, які ми перевіряли для параметра [nom], Request.Form["nom"], RouteData.Values["nom"], Request.QueryString["nom"], Request.Files["nom"], підійшли два — перші два. Було використано перший.

4.2. Перевірка правильності параметрів дії

Якщо дія має параметр із назвою [p], ASP.NET та MVC спробують присвоїти йому одне з таких значень: Request.Form["p"], RouteData.Values["p"], Request.QueryString["p"], Request.Files["p"]. Перші три значення є символьними рядками. Якщо параметр [p] не має типу [string], можуть виникнути проблеми.

Створимо таку нову дію:


    // Дія02
    public ContentResult Action02(int age)
    {
      string texte = string.Format("Contrôleur={0}, Action={1}, âge={2}", RouteData.Values["controller"], RouteData.Values["action"],age);
      return Content(texte, "text/plain", Encoding.UTF8);
}
  • У рядку 2 дія [Action02] приймає параметр із назвою [age] типу int. Отриманий рядок символів має бути перетворюваним у int.

Запросимо URL [http://localhost:55483/First/Action02?age=21]. Отримаємо таку сторінку:

 

Запросимо URL та [http://localhost:55483/First/Action02?age=21x]. Отримаємо таку сторінку:

 

Цього разу ми отримали сторінку з помилкою. Цікаво подивитися на заголовки HTTP, надіслані сервером у цьому випадку:

1
2
3
4
5
HTTP/1.1 500 Internal Server Error
...
Content-Type: text/html; charset=utf-8
...
Content-Length: 12438
  • рядок 1: сервер відповів кодом [500 Internal Server Error] і надіслав сторінку HTML (рядок 3) розміром 12438 байт (рядок 5), щоб пояснити можливі причини цієї помилки.

Тепер створимо таку дію [Action03]:


    // Дія03
    public ContentResult Action03(int? age)
    {
      ...
}

[Action03] ідентична [Action02], за винятком того, що тип параметра [age] було змінено на int?, що означає ціле число або null.

Запросимо URL [http://localhost:55483/First/Action03?age=21x]. Отримаємо таку сторінку:

 

ASP.NET MVC не зміг перетворити [21x] у тип int. Тоді він присвоїв параметру [age] значення null, як це дозволяє його тип int?. Однак можна дізнатися, чи отримав параметр значення із запиту чи ні.

Створюємо нову дію [Action04] наступного вигляду:


    // Дія04
    public ContentResult Action04(int? age)
    {
      bool valide = ModelState.IsValid;
      string texte = string.Format("Contrôleur={0}, Action={1}, âge={2}, valide={3}", RouteData.Values["controller"], RouteData.Values["action"], age, valide);
      return Content(texte, "text/plain", Encoding.UTF8);
}
  • рядок 2: збережено тип [int?]. Це, зокрема, дозволяє запиту не надавати параметр [age], який у такому разі отримує значення null;
  • рядок 4: перевіряється, чи є модель дії дійсною. Модель дії складається з усіх її параметрів, у даному випадку — [age]. Модель є дійсною, якщо всі параметри отримали значення із запиту або значення null, якщо це дозволяє тип параметра;
  • рядок 5: додаємо значення змінної [valide] до тексту, що надсилається клієнту.

Запитуємо URL [http://localhost:55483/First/Action04?age=21x]. Отримуємо таку сторінку:

 

ASP.NET MVC не вдалося перетворити [21x] у тип int. Тоді він присвоїв значення null параметру [age], як це дозволяє його тип int?. Однак, як показує значення [valide], під час перетворення сталися помилки.

Можливо, з’явиться повідомлення про помилку, пов’язане з невдалим перетворенням. Розглянемо наступну нову дію:


    // Дія05
    public ContentResult Action05(int? age)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("Contrôleur={0}, Action={1}, âge={2}, valide={3}, erreurs={4}", RouteData.Values["controller"], RouteData.Values["action"], age, ModelState.IsValid, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

Новим є рядок 4. У ньому викликається приватний метод [getErrorMessagesFor], якому передається стан моделі дії. Він повертає рядок, що об’єднує повідомлення про всі помилки, які сталися. Цей метод виглядає так:


private string getErrorMessagesFor(ModelStateDictionary état)
    {
      List<String> erreurs = new List<String>();
      string messages = string.Empty;
      if (!état.IsValid)
      {
        foreach (ModelState modelState in état.Values)
        {
          foreach (ModelError error in modelState.Errors)
          {
            erreurs.Add(getErrorMessageFor(error));
          }
        }
        foreach (string message in erreurs)
        {
          messages += string.Format("[{0}]", message);
        }
      }
      return messages;
    }
  • рядок 1: фактичний параметр [ModelState], що передається методу, має тип [ModelStateDictionary];
  • рядок 3: список повідомлень про помилки, спочатку порожній;
  • рядок 5: перевіряється, чи є стан, переданий як параметр, дійсним. Якщо ні, то всі повідомлення про помилки об’єднуються в один рядок;
  • рядок 7: тип [ModelStateDictionary] має властивість [Values], яка є колекцією типів [ModelState]. На кожен елемент моделі припадає один [ModelState]. Наприклад:
    • ModelState["age"]: стан моделі дії для параметра [age],
    • ModelState["age"].Errors: набір помилок для цього параметра. Помилки мають формат [ModelError],
    • ModelState["age"].Errors[i].ErrorMessage: можливе повідомлення про помилку № i для параметра [age] шаблону
    • ModelState["age"].Errors[i].Exception: виняток з помилки № i з колекції помилок для параметра [age],
    • ModelState["age"].Errors[i].Exception.InnerException: причина цього винятку,
    • ModelState["age"].Errors[i].Exception.InnerException.Message: повідомлення про причину винятку;
  • рядок 9: обходимо колекцію [Errors] для конкретного [ModelState];
  • рядок 11: отримується повідомлення про помилку з конкретного [ModelError] і додається до списку повідомлень про помилки в рядку 3;
  • рядки 14–17: об’єднуємо елементи списку повідомлень про помилки в єдиний рядок.

Метод [getErrorMessageFor] у рядку 11 виглядає наступним чином:


    private string getErrorMessageFor(ModelError error)
    {
      if (error.ErrorMessage != null && error.ErrorMessage.Trim() != string.Empty)
      {
        return error.ErrorMessage;
      }
      if (error.Exception != null && error.Exception.InnerException == null && error.Exception.Message != string.Empty)
      {
        return error.Exception.Message;
      }
      if (error.Exception != null && error.Exception.InnerException != null && error.Exception.InnerException.Message != string.Empty)
      {
        return error.Exception.InnerException.Message;
      }
      return string.Empty;
}
  • рядок 1: отримується тип [ModelError], який інкапсулює помилку в одному з елементів шаблону дії. Повідомлення про помилку шукається у трьох різних місцях:
    • у [ModelError].ErrorMessage, рядки 3–6;
    • у [ModelError].Exception.Message, рядки 7–10;
    • у файлі [ModelError].Exception.InnerException.Message, рядки 11–14;

Під час тестування помітно, що повідомлення про помилку зустрічається в цих трьох місцях залежно від типу елемента моделі. Має існувати правило, яке дозволяє безвідмовно отримати повідомлення про помилку, пов’язане з певним елементом моделі, але я його не знаю. Тому я шукаю його в різних місцях, де можу його знайти, дотримуючись певного порядку. Як тільки знайдено непорожнє повідомлення, воно повертається.

Запитаймо URL [http://localhost:55483/First/Action05?age=21x]. Отримуємо таку сторінку:

 

4.3. Дія з кількома параметрами

Розглянемо таку нову дію:


    // Дія06
    public ContentResult Action06(double? poids, int? age)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("Contrôleur={0}, Action={1}, poids={2}, âge={3}, valide={4}, erreurs={5}", RouteData.Values["controller"], RouteData.Values["action"], poids, age, ModelState.IsValid, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}
  • рядок 2: ми маємо два параметри [poids] та [age].

Описані вище правила тепер застосовуються до обох параметрів. Ось кілька прикладів виконання:

 
 

4.4. Використання класу як шаблону дії

Визначимо клас, який буде шаблоном для дії. Помістимо його в папку [Models] [1].

Його код буде таким:


namespace Exemple_02.Models
{
  public class ActionModel01
  {
    public double? Poids { get; set; }
    public int? Age { get; set; }
  }
}

Наш клас має як автоматичні властивості два параметри [Poids] та [Age], які ми розглядали раніше. Цей клас буде вхідним параметром дії [Action07]:


    // Дія07
    public ContentResult Action07(ActionModel01 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("Contrôleur={0}, Action={1}, poids={2}, âge={3}, valide={4}, erreurs={5}", RouteData.Values["controller"], RouteData.Values["action"], modèle.Poids, modèle.Age, ModelState.IsValid, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}
  • рядок 2: модель дії є екземпляром типу [ActionModel01].

Повернемося до тих самих двох прикладів, що й раніше:

 
 

Слід зауважити, що при зв’язуванні параметрів не враховується регістр. Параметри запиту були такими: [age] та [poids]. Вони заповнили властивості [Age] та [Poids] класу [ModelAction01].

Крім того, до цього моменту ми використовували запити HTTP та [GET]. Продемонструємо, що запити [POST] поводяться так само. Для цього знову скористаємося додатком [Advanced Rest Client]:

  • у [1] замість запитуваного URL;
  • у [2] це буде зроблено за допомогою команди POST;
  • у [3] — параметри POST.

Отримуємо той самий результат, що й для GET:

Image

4.5. Модель дії з обмеженнями дійсності — 1

З попередньою моделлю:


namespace Exemple_02.Models
{
  public class ActionModel01
  {
    public double? Poids { get; set; }
    public int? Age { get; set; }
  }
}

параметри [poids] та [age] можуть бути відсутніми у запиті. У цьому випадку властивості [Poids] та [Age] отримують значення [null], і жодних помилок не повідомляється. Можливо, ви захочете перетворити модель наступним чином:


namespace Exemple_02.Models
{
  public class ActionModel01
  {
    public double Poids { get; set; }
    public int Age { get; set; }
  }
}

У рядках 5 і 6 властивості [Poids] та [Age] більше не можуть мати значення [null]. Давайте подивимося, що відбувається з цією новою моделлю, коли параметри [poids] та [age] відсутні у запиті.

 

Помилок не сталося, а властивості [Poids] та [Age] зберегли свої початкові значення: 0. ASP.NET MVC:

  • створено екземпляр моделі за допомогою оператора new ActionModel01. Саме тоді властивості [Poids] та [Age] отримали значення 0;
  • не присвоїв жодного значення цим двом властивостям, оскільки не було жодного параметра з їхніми іменами.

Перша модель дозволяє перевірити відсутність параметра: у цьому випадку відповідний атрибут має значення [null]. Друга модель цього не дозволяє. Можна додати інші обмеження перевірки, крім простого типу параметрів. Тепер ми їх розглянемо.

Розглянемо таку нову модель дії:

  

using System.ComponentModel.DataAnnotations;
namespace Exemple_02.Models
{
  public class ActionModel02
  {
    [Required]
    [Range(1, 200)]
    public double? Poids { get; set; }
    [Required]
    [Range(1, 150)]
    public int? Age { get; set; }
  }
}
  • рядок 6: вказує, що поле [Poids] є обов’язковим;
  • рядок 7: вказує, що поле [Poids] має знаходитися в діапазоні [1,200];
  • рядок 9: вказує, що поле [Age] є обов’язковим;
  • рядок 7: вказує, що поле [Age] має знаходитися в діапазоні [1,150];

Дія, що використовує цей шаблон, буде такою: [Action08]:


    // Дія08
    public ContentResult Action08(ActionModel02 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("Contrôleur={0}, Action={1}, poids={2}, âge={3}, valide={4}, erreurs={5}", RouteData.Values["controller"], RouteData.Values["action"], modèle.Poids, modèle.Age, ModelState.IsValid, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}
  • рядок 2: дія отримує екземпляр шаблону [ActionModel02];

Проведемо кілька тестів:

 
 
 
 

Помилки виявлено правильно. Тепер давайте вдосконалимо модель таким чином:


using System.ComponentModel.DataAnnotations;
namespace Exemple_02.Models
{
  public class ActionModel02
  {
    [Required]
    [Range(1, 200)]
    public double Poids { get; set; }
    [Required]
    [Range(1, 150)]
    public int Age { get; set; }
  }
}

У рядках 8 і 11 властивості більше не можуть мати значення [null]. Скомпілюємо та повторимо тест без параметрів:

 

Відсутність параметрів призвела до того, що властивості [Poids] та [Age] зберегли значення, отримане під час інстанціювання моделі: 0. Потім відбувається перевірка. Атрибут [Required] у цьому випадку задовольняє вимоги. Бачимо, що наведене вище повідомлення про помилку стосується атрибута [Range]. Отже, щоб перевірити наявність параметра, пов’язана властивість має бути nullable, тобто повинна приймати значення null.

Повернемося до початкової моделі [ActionModel02] і розглянемо дію, модель якої складається з екземпляра [ActionModel02] та типу [DateTime] nullable:


    // Дія09
    public ContentResult Action09(ActionModel02 modèle, DateTime? date)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("Contrôleur={0}, Action={1}, poids={2}, âge={3}, date={4}, valide={5}, erreurs={6}", RouteData.Values["controller"], RouteData.Values["action"], modèle.Poids, modèle.Age, date, ModelState.IsValid, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

Проведемо кілька тестів:

 

Ми не передали жодних параметрів до дії. Атрибути [Required] властивостей [Poids] та [Age] виконали свою роль. Дата отримала значення null, і жодних помилок не було зафіксовано.

Тепер передамо недійсні параметри:

 

Тепер передаємо допустимі значення:

 

Розглянемо інші обмеження щодо правильності. Нова модель дії така:

  

using System.ComponentModel.DataAnnotations;
namespace Exemple_02.Models
{
  public class ActionModel03
  {
    [Required(ErrorMessage = "Le paramètre email est requis")]
    [EmailAddress(ErrorMessage = "Le paramètre email n'a pas un format valide")]
    public string Email { get; set; }

    [Required(ErrorMessage = "Le paramètre jour est requis")]
    [RegularExpression(@"^\d{1,2}$", ErrorMessage = "Le paramètre jour doit avoir 1 ou 2 chiffres")]
    public string Jour { get; set; }

    [Required(ErrorMessage = "Le paramètre info1 est requis")]
    [MaxLength(4, ErrorMessage = "Le paramètre info1 ne peut avoir plus de 4 caractères")]
    public string Info1 { get; set; }

    [Required(ErrorMessage = "Le paramètre info2 est requis")]
    [MinLength(2, ErrorMessage = "Le paramètre info2 ne peut avoir moins de 2 caractères")]
    public string Info2 { get; set; }

    [Required(ErrorMessage = "Le paramètre info3 est requis")]
    [MinLength(4, ErrorMessage = "Le paramètre info3 doit avoir 4 caractères exactement")]
    [MaxLength(4, ErrorMessage = "Le paramètre info3 doit avoir 4 caractères exactement")]
    public string Info3 { get; set; }
  }
}
  • рядок 6: атрибут [Required], цього разу з повідомленням про помилку, яке ми встановлюємо самостійно;
  • рядок 7: атрибут [EMailAddress] вимагає, щоб поле [Email] містило електронну адресу у правильному форматі;
  • рядок 11: атрибут [RegularExpression] вимагає, щоб поле [Jour] містило рядок з однієї або двох цифр. Перший параметр — це регулярний вираз, на відповідність якому має перевірятися поле;
  • рядок 15: атрибут [MaxLength] вимагає, щоб поле [Info1] містило не більше 4 символів;
  • рядок 19: атрибут [MinLength] вимагає, щоб поле [Info2] містило не менше 2 символів;
  • рядки 23–24: атрибути [MaxLength] та [MinLength] у сукупності вимагають, щоб поле [Info3] містило рівно 4 символи;

Дія [Action10] використовуватиме цей шаблон:


// Дія10
    public ContentResult Action10(ActionModel03 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("email={0}, jour={1}, info1={2}, info2={3}, info3={4}, erreurs={5}",
        modèle.Email, modèle.Jour, modèle.Info1, modèle.Info2, modèle.Info3, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
    }

Давайте проведемо кілька тестів із цією дією.

Спочатку без параметрів:

 

Потім із недійсними параметрами:

 

Потім із правильними параметрами:

 

4.6. Модель дії з обмеженнями щодо дійсності — 2

Ми представляємо інші обмеження цілісності. Новою моделлю дії буде наступний клас [ActionModel04]:


using System.ComponentModel.DataAnnotations;

namespace Exemple_02.Models
{
  public class ActionModel04
  {
    [Required(ErrorMessage="Le paramètre url est requis")]
    [Url(ErrorMessage="URL invalide")]
    public string Url { get; set; }
    [Required(ErrorMessage = "Le paramètre info1 est requis")]
    public string Info1 { get; set; }
    [Required(ErrorMessage = "Le paramètre info2 est requis")]
    [Compare("Info1",ErrorMessage="Les paramètres info1 et info2 doivent être identiques")]
    public string Info2 { get; set; }
    [Required(ErrorMessage = "Le paramètre cc est requis")]
    [CreditCard(ErrorMessage = "Le paramètre cc n'est pas un n° de carte de crédit valide")]
    public string Cc { get; set; }
  }
}
  • рядок 8: вимагає, щоб поле з анотацією було дійсним URL;
  • рядок 13: вимагає, щоб властивості [Info1] та [Info2] мали однакове значення;
  • рядок 16: вимагає, щоб поле з анотацією було дійсним номером кредитної картки.

Дія, що використовує цей шаблон, буде такою:


    // Дія11
    public ContentResult Action11(ActionModel04 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("URL={0}, Info1={1}, Info2={2}, CC={3},erreurs={4}",
        modèle.Url, modèle.Info1, modèle.Info2, modèle.Cc, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

Щоб протестувати дію [Action11], ми використовуємо додаток [Advanced Rest Client]:

  • у [1] — URL дії [Action11];
  • у [2] ця URL буде запитуватися разом із POST;
  • у [3] слід вибрати вкладку [Form];
  • у [4] — значення чотирьох очікуваних параметрів. Ця ініціалізація є функцією, яку надає [ARC]. Параметри, що фактично надсилаються, можна переглянути на вкладці [Raw] [5];
  • у [6] — параметри POST.

На цей запит отримуємо таку відповідь:

Передамо недійсні параметри:

 

Тоді ми отримуємо таку відповідь:

4.7. Модель дії з обмеженнями валідності — 3

Іноді наявних обмежень цілісності не вистачає. Тоді їх можна створити самостійно. Зокрема, можна використати модель, що реалізує інтерфейс [IValidatableObject]. У цьому випадку ми додаємо власні перевірки моделі в метод [Validate] цього інтерфейсу. Розглянемо приклад. Новою моделлю дії стане наступний клас [ActionModel05]:


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

namespace Exemple_02.Models
{
  public class ActionModel05 : IValidatableObject
  {
    [Required(ErrorMessage = "Le paramètre taux est requis")]
    public double? Taux { get; set; }
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
      List<ValidationResult> résultats = new List<ValidationResult>();
      bool ok = Taux < 4.2 || Taux > 6.7;
      if (!ok)
      {
        résultats.Add(new ValidationResult("Le paramètre taux doit être < 4.2 ou > 6.7", new string[] { "Taux" }));
      }
      return résultats;
    }
  }
}
  • рядок 6: модель реалізує інтерфейс [IValidatableObject];
  • рядок 10: метод [Validate] цього інтерфейсу. Він повертає колекцію елементів типу [ValidationResult]. Цей тип інкапсулює помилки, про які потрібно повідомити;
  • рядок 9: дійсна ставка — це ставка <4,2 або > 6,7;
  • рядок 12: створюється порожній список елементів типу [ValidationResult];
  • рядок 13: перевіряється правильність властивості [Taux];
  • рядки 14–17: якщо властивість [Taux] є недійсною, то до списку результатів додається елемент типу [ValidationResult]. Перший параметр — це повідомлення про помилку. Другий параметр, який є необов’язковим, — це набір властивостей, яких стосується ця помилка.

Дія, що використовує цей шаблон, буде такою:


    // Дія12
    public ContentResult Action12(ActionModel05 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("taux={0}, erreurs={1}", modèle.Taux, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

Ось приклад виконання:

 

4.8. Шаблон дії типу «Таблиця» або «Список»

Розглянемо таку дію [Action13]:


// Дія13
    public ContentResult Action13(string[] data)
    {
      string strData = "";
      if (data != null && data.Length != 0)
      {
        strData = string.Join(",", data);
      }
      string texte = string.Format("data=[{0}]", strData);
      return Content(texte, "text/plain", Encoding.UTF8);
    }
  • рядок 2: модель дії складається з таблиці [string]. Вона дозволяє нам отримати параметр з іменем [data], який може бути присутнім кілька разів у параметрах запиту, як-от у [?data=data1&data=data2&data=data3]. Різні параметри запиту [data] заповнюють масив [data] моделі дії. Такий випадок трапляється зі списками з множинним вибором. У цьому разі браузер надсилає різні значення, вибрані користувачем, із однаковим іменем параметра.

Ось приклад:

 

Модель також може бути списком:


    // Дія14
    public ContentResult Action14(List<int> data)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string strData = "";
      if (data != null && data.Count != 0)
      {
        strData = string.Join(",", data);
      }
      string texte = string.Format("data=[{0}], erreurs=[{1}]", strData, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

У цьому випадку шаблон — це список цілих чисел (рядок 2). Ось результат першого виконання:

 

та другий:

 

4.9. Фільтрування шаблону дії

Іноді ми маємо шаблон, але хочемо, щоб запит HTTP ініціалізував лише певні елементи шаблону. Розглянемо такий шаблон дії [ActionModel06]:


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

namespace Exemple_02.Models
{
  [Bind(Exclude = "Info2")]
  public class ActionModel06
  {
    [Required(ErrorMessage = "Le paramètre [info1] est requis")]
    public string Info1 { get; set; }

    public string Info2 { get; set; }
  }
}
  • рядки 9–10: параметр [info1] є обов’язковим;
  • рядок 6: параметр [info2] із рядка 12 виключено з зв’язку запиту HTTP з його шаблоном.

Дія буде такою: [Action15]:


    // Дія 15
    public ContentResult Action15(ActionModel06 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("valide={0}, info1={1}, info2={2}, erreurs={3}", ModelState.IsValid, modèle.Info1, modèle.Info2, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

Ось приклад виконання:

  • у [1]: параметр [info2] передається в URL;
  • у [2]: властивість [Info2] моделі дії залишилася порожньою.

4.10. Розширення моделі зв’язку даних

Повернемося до архітектури виконання дії:

Клас дії інстанціюється на початку запиту клієнта і знищується в кінці цього запиту. Тому він не може використовуватися для збереження даних між двома запитами, навіть якщо його викликають повторно. Може виникнути потреба зберегти два типи даних:

  • дані, спільні для всіх користувачів веб-додатку. Зазвичай це дані, доступні лише для читання. Для реалізації такого спільного використання даних використовуються три файли:
    • [Web.Config]: файл конфігурації додатка
    • [Global.asax, Global.asax.cs]: дозволяє визначити клас, який називається глобальним класом додатка, термін існування якого збігається з терміном існування самого додатка, а також обробники для певних подій цього ж додатка.

Глобальний клас додатка дозволяє визначити дані, які будуть доступні для всіх запитів усіх користувачів.

  • дані, спільні для запитів одного й того самого клієнта. Ці дані зберігаються в об’єкті, який називається «Сесія». У цьому контексті термін «сесія клієнта» позначає пам’ять клієнта. Усі запити одного клієнта мають доступ до цієї сесії. Вони можуть зберігати та зчитувати інформацію з неї.

Вище наведено типи пам’яті, до яких має доступ дія:

  • пам'ять додатка, яка здебільшого містить дані, доступні лише для читання, і до якої мають доступ усі користувачі;
  • пам'ять конкретного користувача, або сесія, яка містить дані для читання та запису і є доступною для послідовних запитів одного й того самого користувача;
  • не показана вище, існує пам'ять запиту, або контекст запиту. Запит користувача може оброблятися кількома послідовними діями. Контекст запиту дозволяє дії 1 передавати інформацію дії 2.

Розглянемо перший приклад, що ілюструє ці різні типи пам’яті:

Спочатку ми змінюємо файл [Web.config] у проєкті [Exemple-02] наступним чином:


  <appSettings>
    <add key="webpages:Version" value="2.0.0.0" />
    ...
    <add key="infoAppli1" value="infoAppli1"/>
</appSettings>

Ми додаємо рядок 4, який прив’язує до ключа [infoAppli1] значення [infoAppli1]. Це будуть наші дані загального доступу [Application]: вони будуть доступні для всіх запитів усіх користувачів.

Далі ми змінюємо метод [Application_Start] у файлі [Global.asax]. Цей метод виконується лише один раз під час запуску програми. Саме тут потрібно використати файл [Web.config]:


    protected void Application_Start()
    {
      AreaRegistration.RegisterAllAreas();

      WebApiConfig.Register(GlobalConfiguration.Configuration);
      FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
      RouteConfig.RegisterRoutes(RouteTable.Routes);
      BundleConfig.RegisterBundles(BundleTable.Bundles);
      // ініціалізація програми
      Application["infoAppli1"] = ConfigurationManager.AppSettings["infoAppli1"];
}

Ми додаємо рядок 10. Він виконує дві дії:

  • вона отримує значення ключа [infoAppli1] із файлу [Web.config] за допомогою класу [System.Configuration.ConfigurationManager];
  • записує її у словник [HttpApplication.Application], пов’язаний із ключем [infoAppli1]. Усі дії мають доступ до цього словника.

У тому ж файлі [Gloabal.asax] додається такий метод [Session_Start]:


    protected void Session_Start()
    {
      // ініціалізація лічильника
      Session["compteur"] = 0;
}

Метод [Session_Start] виконується для кожного нового користувача. Що таке новий користувач? Користувач «відстежується» за допомогою сесійного токена. Цей токен:

  • створюється веб-сервером і надсилається новому користувачеві в заголовках HTTP першої відповіді, що надсилається йому;
  • повертається браузером користувача при кожному новому запиті, який він надсилає. Це дозволяє серверу розпізнавати користувача та керувати для нього пам’яттю, яку називають сесією користувача.

Веб-сервер розпізнає, що має справу з новим користувачем, коли той не надсилає йому токен сеансу. Тоді сервер створює його для нього.

У рядку 4 вище ми додаємо до сесії користувача лічильник, який буде збільшуватися з кожним запитом цього користувача. Це ілюструє пам’ять, пов’язану з користувачем. Клас [Session] використовується як словник (рядок 4).

Зробивши це, ми пишемо таку дію [Action16]:


// Дія 16
    public ContentResult Action16()
    {
      // отримано контекст запиту HTTP
      HttpContextBase contexte = ControllerContext.HttpContext;
      // отримано інформацію з області дії «Додаток»
      string infoAppli1 = contexte.Application["infoAppli1"] as string;
      // та про сесію
      int? compteur = contexte.Session["compteur"] as int?;
      compteur++;
      contexte.Session["compteur"] = compteur;
      // відповідь клієнту
      string texte = string.Format("infoAppli1={0}, compteur={1}", infoAppli1, compteur);
      return Content(texte, "text/plain", Encoding.UTF8);
}
  • рядок 5: отримуємо контекст запиту HTTP, що обробляється. Цей контекст надасть нам доступ до даних областей [Application] та [Session];
  • рядок 7: отримуємо інформацію з області дії [Application];
  • рядок 9: отримуємо лічильник із сесії;
  • рядки 10–11: лічильник інкрементується, а потім повертається в сесію;
  • рядки 13–14: обидві величини надсилаються клієнту.

Ось приклади виконання:

[Action16] запитується спочатку [1], потім сторінка оновлюється [F5] двічі [2]:

У випадку з [2] клієнт загалом зробив три запити. Щоразу йому вдавалося отримати лічильник, оновлений попереднім запитом.

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

У [3] другий користувач успішно отримує ту саму інформацію про діапазон, що й у [Application], але має власний лічильник діапазону [Session].

Повернемося до коду дії [Action16]:


// Action16
    public ContentResult Action16()
    {
      // отримуємо контекст запиту HTTP
      HttpContextBase contexte = ControllerContext.HttpContext;
      // отримуємо інформацію з області дії «Додаток»
      string infoAppli1 = contexte.Application["infoAppli1"] as string;
      // та про сесію
      int? compteur = contexte.Session["compteur"] as int?;
      compteur++;
      contexte.Session["compteur"] = compteur;
      // відповідь клієнту
      string texte = string.Format("infoAppli1={0}, compteur={1}", infoAppli1, compteur);
      return Content(texte, "text/plain", Encoding.UTF8);
}

Однією з цілей фреймворку ASP.NET MVC є забезпечення можливості тестування контролерів та дій ізольовано, без веб-сервера. Однак у рядку 5 бачимо, що контекст запиту HTTP необхідний для отримання даних з областей дії [Application] та [Session]. Пропонується створити нову дію [Action17], яка отримуватиме дані з областей [Application] та [Session] як параметри:


    // Action17
    public ContentResult Action17(ApplicationModel applicationData, SessionModel sessionData)
    {
      // отримуємо інформацію з області дії «Додаток»
      string infoAppli1 = applicationData.InfoAppli1;
      // та з області дії «Сесія»
      int compteur = sessionData.Compteur++;
      // відповідь клієнту
      string texte = string.Format("infoAppli1={0}, compteur={1}", infoAppli1, compteur);
      return Content(texte, "text/plain", Encoding.UTF8);
}

Тепер код більше не залежить від запиту HTTP. Отже, його можна тестувати окремо від веб-сервера.

Давайте розглянемо, як це зробити. Насамперед нам потрібно створити класи [ApplicationModel] та [SessionModel], які відповідно інкапсулюють дані з областей дії [Application] та [Session]. Це такі класи:


namespace Exemple_02.Models
{
  public class ApplicationModel
  {
    public string InfoAppli1 { get; set; }
  }
}

namespace Exemple_02.Models
{
  public class SessionModel
  {
    public int Compteur { get; set; }
    public SessionModel()
    {
      Compteur = 0;
    }
  }
}

Далі нам потрібно змінити методи [Application_Start] та [Session_Start] у файлі [Global.asax]:


public class MvcApplication : System.Web.HttpApplication
  {
    protected void Application_Start()
    {
      AreaRegistration.RegisterAllAreas();

      WebApiConfig.Register(GlobalConfiguration.Configuration);
      FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
      RouteConfig.RegisterRoutes(RouteTable.Routes);
      BundleConfig.RegisterBundles(BundleTable.Bundles);
      // Ініціалізація додатка — випадок 1
      Application["infoAppli1"] = ConfigurationManager.AppSettings["infoAppli1"];
      // ініціалізація додатка — випадок 2
      ApplicationModel data=new ApplicationModel();
      data.InfoAppli1=ConfigurationManager.AppSettings["infoAppli1"];
      Application["data"] = data;
    }

    protected void Session_Start()
    {
      // ініціалізація лічильника — випадок 1
      Session["compteur"] = 0;
      // ініціалізація лічильника — випадок 2
      Session["data"] = new SessionModel();
    }
  }
  • рядок 14: створюється екземпляр [ApplicationModel];
  • рядок 15: вона ініціалізується;
  • рядок 16: і розміщується у словнику [Application], пов’язана з ключем [data]. [Application] є властивістю класу [HttpApplication] з рядка 1;
  • рядок 24: створюється екземпляр [SessionModel], який розміщується у словнику [Session] і пов’язується з ключем [data]. [Session] є властивістю класу [HttpApplication] з рядка 1;

Якщо виходити з того, що ми бачили до цього моменту, сигнатура


    public ContentResult Action17(ApplicationModel applicationData, SessionModel sessionData)

означає, що запит HTTP, який обробляється цією дією, повинен містити параметри з іменами [applicationData] та [sessionData]. Це не так. Нам потрібно створити нову модель зв’язку даних, щоб коли дія отримує як параметр тип:

  • [ApplicationModel], їй передавалися дані з областю дії [Application] та ключем [data];
  • [SessionModel], при цьому йому мають бути надані дані з діапазону [Session] та ключа [data].

Для цього потрібно створити класи, що реалізують інтерфейс [IModelBinder].

Почнемо зі створення папки [Infrastructure] у проєкті [Exemple-02]:

  

У ньому створюємо такий клас [ApplicationModelBinder]:


using System.Web.Mvc;

namespace Exemple_02.Infrastructure
{
  public class ApplicationModelBinder : IModelBinder
  {
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
      // повертаються дані з області [Application]
      return controllerContext.RequestContext.HttpContext.Application["data"];
    }
  }
}
  • рядок 5: клас реалізує інтерфейс [IModelBinder]. Щоб зрозуміти його код, потрібно знати, що він буде викликатися щоразу, коли дія матиме параметр типу [ApplicationModel]. Це зв’язування [ApplicationModel] --> [ApplicationModelBinder] буде встановлено під час запуску додатка, у методі [Application_Start] класу [Global.asax];
  • рядок 7: єдиний метод інтерфейсу [IModelBinder];
  • рядок 7: параметр типу [ControllerContext] надає нам доступ до запиту HTTP, що обробляється;
  • рядок 7: параметр типу [ModelBindingContext] надає нам доступ до інформації про модель, що має бути побудована, у даному випадку типу [ApplicationModel];
  • рядок 7: результатом [BindModel] є об’єкт, який буде присвоєно пов’язаному параметру, у даному випадку параметру типу [ApplicationModel];
  • рядок 10: ми просто повертаємо об’єкт з областю дії [Application] та ключем [data].

Клас [SessionModelBinder] побудований за тією ж схемою:


using System.Web.Mvc;

namespace Exemple_02.Infrastructure
{
  public class SessionModelBinder : IModelBinder
  {
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
      // повертаються дані з області [Session]
      return controllerContext.HttpContext.Session["data"];
    }
  }
}

Залишається лише пов’язати кожну з моделей [XModel] з її binder та [XModelBinder]. Це робиться в методі [Application_Start] класу [Global.asax]:


    protected void Application_Start()
    {
....
      // ініціалізація додатка — випадок 2
      ApplicationModel data=new ApplicationModel();
      data.InfoAppli1=ConfigurationManager.AppSettings["infoAppli1"];
      Application["data"] = data;
      // модельні зв’язувачі
      ModelBinders.Binders.Add(typeof(ApplicationModel), new ApplicationModelBinder());
      ModelBinders.Binders.Add(typeof(SessionModel), new SessionModelBinder());
}
  • рядок 9: коли дія матиме параметр типу [ApplicationModel], буде викликано метод [ApplicationModelBinder.Bind]. Відомо, що він повертає дані з області дії [Application], пов’язані з ключем [data];
  • рядок 10: те саме стосується типу [SessionModel].

Повернемося до нашої дії [Action17]:


    // Дія 17
    public ContentResult Action17(ApplicationModel applicationData, SessionModel sessionData)
    {
      // отримуємо інформацію з області дії «Додаток»
      string infoAppli1 = applicationData.InfoAppli1;
      // та з області дії «Сесія»
      sessionData.Compteur++;
      int compteur = sessionData.Compteur;
      // відповідь клієнту
      string texte = string.Format("infoAppli1={0}, compteur={1}", infoAppli1, compteur);
      return Content(texte, "text/plain", Encoding.UTF8);
}
  • рядок 2: коли буде викликано [Action17], воно отримає для
    • перший параметр: дані області дії [Application], пов’язані з ключем [data],
    • другий параметр: дані області дії [Session], пов’язані з ключем [data];

Ці два набори даних можуть бути настільки складними, наскільки це потрібно, і об’єднувати, наприклад, для одного — усі дані з області дії [Application], а для іншого — усі дані з області дії [Session].

Ось приклад виконання дії [Action17]:

 

4.11. Пізнє прив’язування шаблону дії

Ми написали таку дію [Action12]:


// Дія 12
    public ContentResult Action12(ActionModel05 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("taux={0}, erreurs={1}", modèle.Taux, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

Приховано, ASP.NET MVC:

  • створює екземпляр типу [ActionModel05], використовуючи його конструктор без параметрів;
  • ініціалізує його даними запиту, які мають таку саму назву (незалежно від регістру), як і одна з властивостей [ActionModel05].

Іноді така поведінка нас не влаштовує. Зокрема, це стосується випадків, коли потрібно використати конкретний конструктор моделі дії. У такому разі можна вчинити наступним чином:


    // Дія 18
    public ContentResult Action18()
    {
      ActionModel05 modèle = new ActionModel05();
      TryUpdateModel(modèle);
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("taux={0}, erreurs={1}", modèle.Taux, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}
  • рядок 2: дія більше не отримує параметрів. Отже, автоматичного зв’язування даних більше немає;
  • рядок 4: ми самостійно створюємо екземпляр моделі дії. Саме тут можна використати інший конструктор;
  • рядок 5: ініціалізуємо модель за допомогою інформації із запиту. Цю роботу виконує ASP.NET MVC. Він робить це так само, як би робив, якби модель була параметром;
  • рядок 6: тепер ми опинилися в тій самій ситуації, що й у дії [Action12].

Ось приклад виконання:

 

4.12. Conclusion

Повернемося до архітектури додатка ASP.NET MVC:

Запит [1] несе з собою різноманітну інформацію, яку ASP.NET MVC передає [2a] для виконання у вигляді шаблону, який ми назвали шаблоном дії.

  • запит HTTP від клієнта надходить у [1];
  • у [2] інформація, що міститься у запиті, перетворюється на модель дії [3];
  • у [4] дія на основі цієї моделі згенерує відповідь. Вона матиме дві складові: представлення V [6] та модель M цього представлення [5];
  • представлення V [6] використовуватиме свою модель M [5] для генерації відповіді HTTP, призначеної для клієнта.

У моделі MVC дія [4] є частиною C (контролера), модель подання [5] є M, а подання [6] є V.

У цьому розділі ми розглянули механізми зв’язку між інформацією, що передається у запиті (яка за своєю природою є рядками символів), та моделлю дії, яка може бути класом із властивостями різних типів. Ми також побачили, що можна перевірити правильність моделі, переданої дії. Нарешті, ми розглянули, як розширити цю модель на дані з діапазону [Session] та [Application].

Тепер ми розглянемо завершальну стадію обробки запиту [1]: створення подання [6] та його моделі [5]. Ці два елементи створюються дією [4].