Skip to content

9. Приклад із практики

9.1. Introduction

Ми представимо приклад з практики, який вже був опублікований у статті, доступній за посиланням URL [http://tahe.developpez.com/dotnet/pam-aspnet/]. У цій статті аналіз конкретного випадку проводиться за допомогою класичного ASP.NET та ORM NHibernate. Тут ми реалізуємо його за допомогою ASP.NET, MVC та ORM Entity Framework. Як і в попередній статті, цей приклад представлено у вигляді університетського курсу. Тому він призначений для студентів. У разі виникнення питань наводяться посилання на розділи, які ми щойно детально розглянули, щоб вказати на корисну літературу.

9.2. Задача, яку потрібно вирішити

Ми хочемо написати веб-додаток, що дозволить користувачеві проводити симуляції розрахунку заробітної плати вихователів дитячих садків асоціації «Дім раннього дитинства» в одній з громад. Ми зосередимося як на організації коду DotNet додатка, так і на самому коді.

Додаток буде типу APU [Application à Page Unique] і використовуватиме виключно виклики Ajax для зв’язку з сервером. Він надаватиме користувачеві такі екрани:

  • екран [VueSaisies], що відображає форму моделювання

Image

  • вигляд [VueSimulation], що використовується для відображення детального результату моделювання:

Image

  • погляд [VueSimulations], що містить перелік моделювань, виконаних клієнтом

Image

  • вигляд [VueSimulationsVides], який вказує, що у клієнта немає або більше немає моделювань:

Image

  • перегляд [VueErreurs], який вказує на одну або кілька помилок (у цьому випадку SGBD та MySQL було зупинено):

Image

9.3. Архітектура додатка

Архітектура додатка буде такою:

Рівень [EF5] позначає ORM Entity Framework 5. Використовуваним SGBD буде MySQL.

Спочатку ми створимо цей додаток із симульованим шаром [métier]:

Це дозволить нам зосередитися виключно на шарі [web]. Імітований шар [métier] відповідатиме інтерфейсу реального шару [métier]. Коли рівень [web] запрацює, ми створимо рівні [métier], [DAO] та [EF5].

9.4. База даних

Статичні дані, необхідні для формування розрахункового листка, розміщуються в базі даних MySQL під назвою [dbpam_ef5] (pam=Paie Assistante Maternelle). Ця база має адміністратора з іменем root без пароля. Вона містить три таблиці:

Image

Між стовпцем EMPLOYES (INDEMNITE_ID) та стовпцем INDEMNITES (ID) існує зв’язок через зовнішній ключ. Структура цієї бази даних визначається її використанням разом із таблицею EF5. Ми повернемося до цього питання під час побудови нижніх рівнів додатка.

Таблиця EMPLOYES: містить інформацію про різних нянь

Структура:

ID
первинний ключ, який автоматично інкрементується за допомогою SGBD
SS
номер соціального страхування працівника — унікальний
NOM
ім'я працівника
PRENOM
його ім'я
ADRESSE
його адреса
VILLE
його місто
CP
його поштовий індекс
VERSIONING
ціле число, яке автоматично збільшується щоразу, коли запис змінюється
INDEMNITE_ID
зовнішній ключ у полі [ID] таблиці [INDEMNITES]

Його вміст може бути таким:

Image

Таблиця COTISATIONS: містить ставки соціальних внесків, що стягуються із заробітної плати

Структура:

ID
первинний ключ, що автоматично інкрементується за допомогою SGBD
CSGRDS
відсоток: загальний соціальний внесок + внесок на погашення соціального боргу
CSGD
відсоток: загальний соціальний внесок, що підлягає вирахуванню
SECU
відсоток: соціальне страхування
RETRAITE
відсоток: додаткова пенсія + страхування на випадок безробіття
VERSIONING
ціле число, що автоматично збільшується при кожній зміні запису

Його вміст може бути таким:

Image

Ставки соціальних внесків не залежать від працівника. Попередня таблиця містить лише один рядок.

Таблиця INDEMNITES: містить різні виплати, що залежать від індексу працівника
ID
первинний ключ, що автоматично інкрементується за допомогою SGBD
INDICE
індекс заробітної плати — унікальний
BASE_HEURE
чиста вартість однієї години чергування в євро
ENTRETIEN_JOUR
добова надбавка за утримання в євро за день перебування
REPAS_JOUR
добові на харчування в євро за день догляду
INDEMNITES_CP
допомога за оплачувану відпустку. Це відсоток, який застосовується до базової заробітної плати.
VERSIONING
ціле число, яке автоматично збільшується щоразу, коли запис змінюється

Його вміст може бути таким:

Image

9.5. Спосіб розрахунку заробітної плати няні

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

Враховуються такі елементи:
[TOTALHEURES]: загальна кількість відпрацьованих годин за місяць
[TOTALJOURS]: загальна кількість відпрацьованих днів у місяці
[TOTALHEURES]=150
[TOTALJOURS] = 20
Базова заробітна плата няні обчислюється за такою формулою:
[SALAIREBASE]=([TOTALHEURES]*[BASEHEURE])*(1+[INDEMNITESCP]/100)
[SALAIREBASE]=(150*[2.1])*(1+0,15)= 362,25
З цієї базової заробітної плати слід утримати низку соціальних внесків:
Загальний соціальний внесок та внесок на погашення соціального боргу: [SALAIREBASE]*[CSGRDS/100]
Загальний соціальний внесок, що підлягає вирахуванню: [SALAIREBASE]*[CSGD/100]
Соціальне страхування, пенсія у разі втрати годувальника, пенсія за віком: [SALAIREBASE]*[SECU/100]
Додаткова пенсія + AGPF + Страхування на випадок безробіття: [SALAIREBASE]*[RETRAITE/100]
CSGRDS: 12,64
CSGD: 22,28
Соціальне страхування: 34,02
Пенсійне страхування: 28,55
Загальна сума соціальних внесків:
[COTISATIONSSOCIALES] = [SALAIREBASE] *(CSGRDS+CSGD+SECU+RETRAITE)/100
[COTISATIONSSOCIALES]=97,48
Крім того, вихователька має право на щоденну компенсацію витрат на утримання та компенсацію витрат на харчування за кожен відпрацьований день. З цієї причини вона отримує такі компенсації:
[Indemnités]=[TOTALJOURS]*(ENTRETIENJOUR+REPASJOUR)
[INDEMNITES]=104
У підсумку чиста заробітна плата, яку слід виплатити няні, становить:
[SALAIREBASE] – [COTISATIONSSOCIALES] + [INDEMNITÉS]
[salaire NET]=368,77

9.6. Проєкт Visual Studio для шару [web]

Проєкт Visual Web Developer для цього додатка матиме такий вигляд:

  • у [1] — загальна структура проєкту [pam-web-01];
  • у [2] папка [Content] — це папка, куди розміщуються статичні ресурси проєкту:
    • [indicator.gif]: анімована картинка, що відображається під час очікування завершення запиту Ajax,
    • [standard.jpg]: фонове зображення для різних вікон,
    • [Site.css] — таблиця стилів додатка;
  • у [3] — єдиний контролер додатка [PamController];
  • у [4] — класи, необхідні для додатка, але які не можуть бути класифіковані як елементи MVC:
    • [ApplicationModelBinder] — клас, що дозволяє включити дані області дії [Application] у модель дій;
    • [SessionModelBinder]: клас, що дозволяє включити дані області дії [Session] у модель дій,
    • [Static] — допоміжний клас зі статичними методами;
  • у [5] — шаблони додатка, будь то шаблони дій чи шаблони переглядів:
    • [ApplicationModel]: модель, що містить дані області дії [Application],
    • [SessionModel]: модель, що містить дані з області дії [Session],
    • [Simulation]: клас, що інкапсулює елементи симуляції розрахунку заробітної плати,
    • [IndexModel]: модель першого вікна [Index], що відображається додатком;
  • у [6] — скрипти JS, необхідні для локалізації додатка;
  • у файлі [7] — скрипти JS із сімейства JQuery, необхідні для інтернаціоналізації, перевірки на стороні клієнта та реалізації AJAX у додатку;
  • у [8] файл [myScripts.js] містить наші власні скрипти JS;
  • у [9] — представлення додатка:
    • [Index]: головна сторінка,
    • [Formulaire] — форма для введення даних про співробітника, його робочі години та дні,
    • [Simulation]: вікно з моделюванням,
    • [Simulations]: вікно зі списком виконаних симуляцій,
    • [Erreurs]: вікно зі списком можливих помилок,
    • [InitFailed]: вікно, що відображає повідомлення про помилки у разі невдалої ініціалізації програми;
  • у [10] — головна сторінка додатка [_Layout];
  • у [11] — файли [Web.config], [Global.asax], що використовуються для налаштування додатка.

9.7. Крок 1 — створення імітованого шару [métier]

Далі ми опишемо кроки, які необхідно виконати для реалізації цього прикладу. У разі потреби ми вказуємо номер розділу, до якого слід звернутися для виконання поставленого завдання. Деякі елементи проєкту надано вам у папці [aspnetmvc-support.zip], яку можна знайти на сайті цього документа. У ній міститься папка [étudedecas-support] із таким вмістом:

  

Крім того, проект містить елементи, представлені в попередніх розділах. Тому достатньо скопіювати та вставити їх між цією папкою PDF та Visual Studio.

9.7.1. Повне рішення для додатка у Visual Studio

Спочатку ми створимо рішення Visual Studio, в якому створимо два проекти:

  • проект для імітованого рівня [métier];
  • проект для веб-шару MVC.

Ми будемо використовувати два інструменти:

  • Visual Studio Express 2012 для настільних ПК, який буде використовуватися для побудови шару [métier];
  • Visual Studio Express 2012 для веб-додатків, який буде використовуватися для створення веб-шару [web].

За допомогою Visual Studio Express для настільних ПК ми створюємо рішення [pam-td]:

  • у [1] виберіть додаток C#;
  • у [2] виберіть [Application console];
  • у [3] — надайте ім’я рішенню;
  • у [4] створіть каталог для цього рішення;
  • у [5] — надайте ім’я шару [métier];
  • у [6] — згенероване рішення.

9.7.2. Інтерфейс шару [métier]

У багатошаровій архітектурі рекомендується, щоб взаємодія між шарами відбувалася через інтерфейси:

Який інтерфейс повинен надавати рівень [métier] рівню [web]? Які можливі взаємодії між цими двома рівнями? Згадаймо веб-інтерфейс, який буде представлений користувачеві:

  1. при першому відображенні форми в [1] має бути список співробітників. Достатньо спрощеного списку (Прізвище, Ім’я, SS). Номер SS необхідний для доступу до додаткової інформації про вибраного співробітника (поля 6–11).
  2. Позиції 12–15 — це різні ставки внесків.
  3. Позиції 16–19 — це виплати працівнику
  4. дані з 20 по 24 — це складові заробітної плати, розраховані на основі даних 1–3, введених користувачем.

Інтерфейс [IPamMetier], який рівень [métier] надає рівню [web], повинен відповідати наведеним вище вимогам. Існує безліч можливих варіантів інтерфейсів. Ми пропонуємо такий:


using Pam.Metier.Entites;
namespace Pam.Metier.Service
{
  public interface IPamMetier
  {
    // список усіх ідентифікаторів співробітників 
    Employe[] GetAllIdentitesEmployes();

    // ------- розрахунок заробітної плати 
    FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés);
  }
}
  • рядок 7: метод, який дозволить заповнити комбінований список [1]
  • рядок 10: метод, який дозволить отримати дані з 6 по 24. Вони були зібрані в об’єкті типу [FeuilleSalaire], який ми опишемо найближчим часом.

Ми розмістимо цей інтерфейс у папці [metier/service]:

9.7.3. Елементи шару [métier]

Попередній інтерфейс використовує два класи [Employe] та [FeuilleSalaire], які нам потрібно визначити:

  • [Employe] — це зображення рядка таблиці [employes] з бази даних;
  • [FeuilleSalaire] — це розрахунковий лист працівника.

Об’єкти будуть розміщені в папці [metier / entites] проекту:

У кінцевій архітектурі рівень [métier] буде обробляти сутності-зображення з бази даних:

Image

Ми будемо використовувати наступні класи для представлення рядків трьох таблиць бази даних. Значення різних полів наведено в розділі 9.4.

Клас [Employe]

Він представляє рядок таблиці [employes]. Його код такий:


using System;

namespace Pam.Metier.Entites
{

  public class Employe
  {
    public string SS { get; set; }
    public string Nom { get; set; }
    public string Prenom { get; set; }
    public string Adresse { get; set; }
    public string Ville { get; set; }
    public string CodePostal { get; set; }
    public Indemnites Indemnites { get; set; }

    // підпис
    public override string ToString()
    {
      return string.Format("Employé[{0},{1},{2},{3},{4},{5}]", SS, Nom, Prenom, Adresse, Ville, CodePostal);
    }
  }
}

Клас [Indemnites]

Вона представляє рядок таблиці [indemnites]. Її код такий:


using System;

namespace Pam.Metier.Entites
{
  public class Indemnites
  {
    public int Indice { get; set; }
    public double BaseHeure { get; set; }
    public double EntretienJour { get; set; }
    public double RepasJour { get; set; }
    public double IndemnitesCp { get; set; }
    // підпис
    public override string ToString()
    {
      return string.Format("Indemnités[{0},{1},{2},{3},{4}]", Indice, BaseHeure, EntretienJour, RepasJour, IndemnitesCp);
    }
  }
}

Клас [Cotisations]

Вона представляє рядок таблиці [cotisations]. Її код такий:


using System;

namespace Pam.Metier.Entites
{

  public class Cotisations
  {
    public double CsgRds { get; set; }
    public double Csgd { get; set; }
    public double Secu { get; set; }
    public double Retraite { get; set; }
    // підпис
    public override string ToString()
    {
      return string.Format("Cotisations[{0},{1},{2},{3}]", CsgRds, Csgd, Secu, Retraite);
    }
  }
}

Слід зазначити, що класи не містять стовпців [ID] та [VERSIONING] з таблиць. Ці стовпці, корисні при використанні ORM та EF5, не є такими в контексті імітованого шару [métier].

Клас [FeuilleSalaire] інкапсулює дані з 6 по 24 з уже представленої форми:


namespace Pam.Metier.Entites
{
  public class FeuilleSalaire
  {

    // автоматичні властивості 
    public Employe Employe { get; set; }
    public Cotisations Cotisations { get; set; }
    public ElementsSalaire ElementsSalaire { get; set; }

    // ToString 
    public override string ToString()
    {
      return string.Format("[{0},{1},{2}]", Employe, Cotisations, ElementsSalaire);
    }
  }
}
  • рядок 7: дані з 6 по 11 про працівника, для якого розраховується заробітна плата, та дані з 16 по 19 про його надбавки. Тут не слід забувати, що об’єкт [Employe] інкапсулює об’єкт [Indemnites], який представляє його надбавки;
  • рядок 8: дані з 12 по 15;
  • рядок 9: дані з 20 по 24;
  • рядки 12–14: метод [ToString].

Клас [ElementsSalaire] інкапсулює дані з 20 по 24 форми:


namespace Pam.Metier.Entites
{
  public class ElementsSalaire
  {
    // автоматичні властивості 
    public double SalaireBase { get; set; }
    public double CotisationsSociales { get; set; }
    public double IndemnitesEntretien { get; set; }
    public double IndemnitesRepas { get; set; }
    public double SalaireNet { get; set; }


    // ToString 
    public override string ToString()
    {
      return string.Format("[{0} : {1} : {2} : {3} : {4} ]", SalaireBase, CotisationsSociales, IndemnitesEntretien, IndemnitesRepas, SalaireNet);
    }
  }
}
  • рядки 6–10: складові заробітної плати, як пояснено в описаних вище бізнес-правилах;
  • рядок 6: базова заробітна плата працівника, що залежить від кількості відпрацьованих годин;
  • рядок 7: внески, що утримуються з цієї базової заробітної плати;
  • рядки 8 і 9: надбавки, що додаються до базової заробітної плати, залежно від індексу працівника та кількості відпрацьованих днів;
  • рядок 10: чиста заробітна плата до виплати;
  • рядки 14–17: метод [ToString] класу.

9.7.4. Клас [PamException]

Ми створюємо спеціальний тип винятків для нашого додатка. Це наступний тип [PamException]:


using System;

namespace Pam.Metier.Entites
{
  // клас винятків
  public class PamException : Exception
  {

    // код помилки 
    public int Code { get; set; }

    // конструктори 
    public PamException()
    {
    }

    public PamException(int Code)
      : base()
    {
      this.Code = Code;
    }

    public PamException(string message, int Code)
      : base(message)
    {
      this.Code = Code;
    }

    public PamException(string message, Exception ex, int Code)
      : base(message, ex)
    {
      this.Code = Code;
    }
  }
}
  • рядок 6: клас походить від класу [Exception];
  • рядок 10: він має публічну властивість [Code], яка є кодом помилки;
  • у нашому додатку ми будемо використовувати два типи конструкторів:
    • той, що на рядках 23–27, який можна використовувати, як показано нижче:
throw new PamException("Problème d'accès aux données",5);
  • (продовження)
    • або конструктор із рядків 29–33, призначений для передачі винятку, що виник, шляхом його інкапсуляції у виняток типу [PamException]:
try{
....
}catch (IOException ex){
     // інкапсулюємо виняток ex     
    throw new PamException("Problème d'accès aux données",ex,10);
}

Перевага цього другого методу полягає в тому, що він не втрачає інформацію, яка може міститися в першому винятковому випадку.

9.7.5. Реалізація шару [métier]

Інтерфейс [IPamMetier] буде реалізовано за допомогою наступного класу [PamMetier]:


using System;
using Pam.Metier.Entites;
using System.Collections.Generic;

namespace Pam.Metier.Service
{
  public class PamMetier : IPamMetier
  {
    // список співробітників у кеші
    public Employe[] Employes { get; set; }
    // працівники, індексовані за номером SS
    private IDictionary<string, Employe> dicEmployes = new Dictionary<string, Employe>();

    // список співробітників
    public Employe[] GetAllIdentitesEmployes()
    {
...
      // виводиться список співробітників
      return Employes;
    }

    // розрахунок заробітної плати
    public FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés)
    {
...
  }
}
  • рядок 7: клас [PamMetier] реалізує інтерфейс [IPamMetier];
  • рядок 10: клас [PamMetier] зберігає список співробітників у кеші;
  • рядок 12: словник, що пов’язує співробітника з його номером соціального страхування;
  • рядки 15–20: метод, що повертає список співробітників;
  • рядки 23–26: метод, що обчислює заробітну плату співробітника.

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


// список співробітників
    public Employe[] GetAllIdentitesEmployes()
    {
      if (Employes == null)
      {
        // створюється таблиця з трьома працівниками
        Employes = new Employe[3];
        Employes[0] = new Employe()
        {
          SS = "254104940426058",
          Nom = "Jouveinal",
          Prenom = "Marie",
          Adresse = "5 rue des oiseaux",
          Ville = "St Corentin",
          CodePostal = "49203",
          Indemnites = new Indemnites() { Indice = 2, BaseHeure = 2.1, EntretienJour = 2.1, RepasJour = 3.1, IndemnitesCp = 15 }
        };
        dicEmployes.Add(Employes[0].SS, Employes[0]);
        Employes[1] = new Employe()
        {
          SS = "260124402111742",
          Nom = "Laverti",
          Prenom = "Justine",
          Adresse = "La brûlerie",
          Ville = "St Marcel",
          CodePostal = "49014",
          Indemnites = new Indemnites() { Indice = 1, BaseHeure = 1.93, EntretienJour = 2, RepasJour = 3, IndemnitesCp = 12 }
        };
        dicEmployes.Add(Employes[1].SS, Employes[1]);
        // фіктивний співробітник, який не буде внесений до словника
        // для імітації неіснуючого працівника
        Employes[2] = new Employe()
        {
          SS = "XX",
          Nom = "X",
          Prenom = "X",
          Adresse = "X",
          Ville = "X",
          CodePostal = "X",
          Indemnites = new Indemnites() { Indice = 0, BaseHeure = 0, EntretienJour = 0, RepasJour = 0, IndemnitesCp = 0 }
        };
      }
      // виводимо список співробітників
      return Employes;
    }
  • рядок 4: перевіряється, чи список працівників ще не сформовано;
  • рядок 7: якщо це не так, створюється масив із трьох співробітників;
  • рядки 8–17: перший співробітник;
  • рядок 18: його додають до словника;
  • рядки 19–28: другий співробітник;
  • рядок 29: його додають до словника;
  • рядки 32–42: третій співробітник. Його не додають до словника з причини, яку ми пояснимо.

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


    // розраховуємо заробітну плату
    public FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés)
    {
      // отримуємо співробітника з номером SS
      Employe e = dicEmployes.ContainsKey(ss) ? dicEmployes[ss] : null;
      // існує?
      if (e == null)
      {
        throw new PamException(string.Format("L'employé de n° SS [{0}] n'existe pas", ss), 10);
      }
      // повертає фіктивну платіжну відомість
      return new FeuilleSalaire()
      {
        Employe = e,
        Cotisations = new Cotisations() { CsgRds = 3.49, Csgd = 6.15, Secu = 9.38, Retraite = 7.88 },
        ElementsSalaire = new ElementsSalaire() { CotisationsSociales = 100, IndemnitesEntretien = 100, IndemnitesRepas = 100, SalaireBase = 100, SalaireNet = 100 }
      };
}
  • рядок 2: метод отримує номер SS працівника, для якого потрібно розрахувати заробітну плату, кількість відпрацьованих годин та кількість відпрацьованих днів;
  • рядок 5: шукаємо працівника у словнику. Нагадаємо, що одного з них там немає;
  • рядки 7–10: якщо працівника не знайдено, генерується виняток [PamException];
  • рядки 12–17: повертається фіктивна платіжна відомість.

9.7.6. Консольний тест шару [métier]

Проект шару [métier] наразі має такий вигляд:

Вищезазначений клас [Program] буде тестувати методи інтерфейсу [IPamMetier]. Ось приклад:


using Pam.Metier.Entites;
using Pam.Metier.Service;
using System;

namespace Pam.Metier.Tests
{
  class Program
  {
    public static void Main()
    {
      // інстанціювання шару [métier]
      IPamMetier pamMetier = new PamMetier();
      // список співробітників
      Employe[] employes = pamMetier.GetAllIdentitesEmployes();
      Console.WriteLine("Liste des employés--------------------");
      foreach (Employe e in employes)
      {
        Console.WriteLine(e);
      }
      // розрахунки платіжних відомостей 
      Console.WriteLine("Calculs de feuilles de salaire-----------------");
      Console.WriteLine(pamMetier.GetSalaire(employes[0].SS, 30, 5));
      Console.WriteLine(pamMetier.GetSalaire(employes[1].SS, 150, 20));
      try
      {
        Console.WriteLine(pamMetier.GetSalaire(employes[2].SS, 150, 20));
      }
      catch (PamException ex)
      {
        Console.WriteLine(string.Format("PamException : {0}", ex.Message));
      }
    }
  }
}
  • рядок 12: створення екземпляра шару [métier];
  • рядки 14–19: тестування методу [GetAllIdentitesEmploye] інтерфейсу [IPamMetier];
  • рядки 21–31: тестування методу [GetSalaire] інтерфейсу [IPamMetier].

Виконання цієї консольної програми дає такі результати:

Liste des employés--------------------
Employé[254104940426058,Jouveinal,Marie,5 rue des oiseaux,St Corentin,49203]
Employé[260124402111742,Laverti,Justine,La brûlerie,St Marcel,49014]
Employé[XX,X,X,X,X,X]
Calculs de feuilles de salaire-----------------
[Employé[254104940426058,Jouveinal,Marie,5 rue des oiseaux,St Corentin,49203],Co
tisations[3,49,6,15,9,38,7,88],[100 : 100 : 100 : 100 : 100]]
[Employé[260124402111742,Laverti,Justine,La brûlerie,St Marcel,49014],Cotisation
s[3,49,6,15,9,38,7,88],[100 : 100 : 100 : 100 : 100]]
PamException : L'employé de n° SS [XX] n'existe pas

Читачеві пропонується встановити зв’язок між цими результатами та виконаним кодом.

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

  • у [1], у властивостях файлу [Program.cs];
  • у [2] вказуємо, що файл не буде частиною згенерованої збірки;
  • у [3, 4], у властивостях проєкту [pam-metier-simule], в опції [Application] [3], вказується [4], що під час генерації має бути створено бібліотеку класів (у вигляді DLL).
  • у [5] вимагається збірка типу [Release]. Інший тип — [Debug]. У такому разі збірка містить інформацію, що полегшує налагодження;
  • у [6] генерується проект [pam-metier-simule];
  • у [7] відображаються всі файли рішення;
  • у [8], у папці [bin / Release], файл DLL нашого проєкту.

9.8. Крок 2: налаштування веб-додатку

У попередньому рішенні Visual Studio ми створимо проект для веб-шару MVC.

За допомогою Visual Studio Express для веб-розробки ми відкриваємо рішення [pam-td], створене раніше за допомогою Visual Studio Express для настільних ПК.

  • у [1] рішення [pam-td] було завантажено у Visual Studio Express для веб;
  • у [2] — рішення та проект для імітованого шару [métier], який ми щойно створили.

На цьому новому етапі ми створимо каркас веб-додатку.

  • у [1] ми додаємо новий проєкт до рішення [pam-td];
  • у [2] вибираємо проект ASP.NET MVC 4;
  • з назвою [pam-web-01] [3];
  • у [4] вибираємо базову модель ASP.NET MVC;
  • у [5] — створений проект;
  • у [6] створюється новий проект — стартовий проект рішення, який буде виконано при запуску [Ctrl-F5];
  • у [7] назва нового проєкту виділена жирним шрифтом, що вказує на те, що це стартовий проєкт збірки.

Тепер за допомогою Провідника Windows замінюємо папку [Content] у проєкті на папку [étudedecas-support / web / Content]. Після цього потрібно додати нові файли до проєкту [pam-web-01]. Для цього виконаємо такі дії:

  • у [1] оновлюємо рішення;
  • у [2] відображаємо всі файли рішення;
  • у [3] з’являється папка [Images];
  • яку додаємо до проєкту в [4].

У папці [Scripts] додайте скрипти JQuery Globalization та [1], необхідні для перевірки на стороні клієнта.

Шаблон сторінки [_Layout.cshtml] [2] матиме такий вміст:


<!DOCTYPE html>
<html>
<head>
  <title>@ViewBag.Title</title>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width" />
  <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/jquery.unobtrusive-ajax.js"></script>
  <script type="text/javascript" src="~/Scripts/myScripts.js"></script>
</head>
<body>
  <table>
    <tbody>
      <tr>
        <td>
          <h2>Simulateur de calcul de paie</h2>
        </td>
        <td style="width: 20px">
          <img id="loading" style="display: none" src="~/Content/images/indicator.gif" />
        </td>
        <td>
          <a id="lnkFaireSimulation" href="javascript:faireSimulation()">| Faire la simulation<br />
          </a>
          <a id="lnkEffacerSimulation" href="javascript:effacerSimulation()">| Effacer la simulation<br />
          </a>
          <a id="lnkVoirSimulations" href="javascript:voirSimulations()">| Voir les simulations<br />
          </a>
          <a id="lnkRetourFormulaire" href="javascript:retourFormulaire()">| Retour au formulaire de simulation<br />
          </a>
          <a id="lnkEnregistrerSimulation" href="javascript:enregistrerSimulation()">| Enregistrer la simulation<br />
          </a>
          <a id="lnkTerminerSession" href="javascript:terminerSession()">| Terminer la session<br />
          </a>
        </td>
    </tbody>
  </table>
  <hr />
  <div id="content">
    @RenderBody()
  </div>
</body>
</html>

Примітка: у рядку 8 адаптуйте версію jQuery відповідно до вашої версії Visual Studio.

  • рядок 7: посилання на таблицю стилів додатка;
  • рядки 8–10: посилання на скрипти, необхідні для перевірки на стороні клієнта;
  • рядки 11–12: посилання на скрипти, необхідні для введення французьких дійсних чисел з комою;
  • рядок 13: посилання на скрипти, необхідні для роботи в режимі Ajax;
  • рядок 14: скрипти, власні для додатка;
  • рядок 24: зображення, що відображається під час очікування завершення Ajax-запитів;
  • рядки 26–39: шість посилань на JavaScript;
  • рядок 43: розділ, у якому будуть відображатися різні види додатка;
  • рядок 44: основна частина різних візуалізацій додатка.

Далі ми змінимо маршрут за замовчуванням додатка:

Файл [RouteConfig] матиме такий вміст:


using System.Web.Mvc;
using System.Web.Routing;

namespace pam_web_01
{
  public class RouteConfig
  {
    public static void RegisterRoutes(RouteCollection routes)
    {
      routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

      routes.MapRoute(
          name: "Default",
          url: "{controller}/{action}",
          defaults: new { controller = "Pam", action = "Index" }
      );
    }
  }
}
  • рядок 14: URL матимуть вигляд [{controller}/{action}];
  • рядок 15: за відсутності дії буде використана дія [Index]. За відсутності контролера буде використано контролер [Pam].

З цієї конфігурації випливає, що URL [/] еквівалентна URL [/Pam/Index]. Оскільки наш додаток належить до типу APU, URL та [/] будуть єдиними URL для нього.

Створіть контролер [Pam]:

  

Змініть контролер [PamController] наступним чином:


using System.Web.Mvc;

namespace Pam.Web.Controllers
{
    public class PamController : Controller
    {
        [HttpGet]
        public ViewResult Index()
        {
            return View();
        }

    }
}
  • рядок 3: розміщуємо контролер у просторі імен [Pam.Web.Controllers];
  • рядок 7: дія [Index] оброблятиме лише команду HTTP GET;
  • рядок 8: повертається тип [ViewResult] замість типу [ActionResult].

Тепер створіть представлення [Index.cshtml], яке відображається дією [Index], наведеною вище:

Змініть [Index.cshtml] наступним чином:


@{
  ViewBag.Title = "Pam";
}
<h2>Formulaire</h2>

Запустіть додаток за допомогою [Ctrl-F5]. Ви повинні отримати таку сторінку:

 

Завдання: Поясніть, що сталося.


Додаток використовує таблицю стилів, на яку є посилання в шаблоні [_Layout.cshtml]:


  <link rel="stylesheet" href="~/Content/Site.css" />

Таблиця стилів [/Content/Site.css] визначає фонове зображення для сторінок додатка:


body {
  background-image: url("/Content/Images/standard.jpg");
}

9.9. Крок 3: налаштування шаблону APU

Ми хочемо написати додаток за шаблоном APU (додаток з однією сторінкою), описаним у розділі 7.5, а також у розділі 7.6. Єдина сторінка — це та, що завантажується браузером під час запуску додатка:

  • частина [1], наведена вище, є фіксованою частиною односторінкового додатка. Ми бачили, що вона надається головною сторінкою [_Layout.cshtml];
  • частина [2] є змінною частиною єдиної сторінки. Вона розміщується в області з ідентифікатором [content] головної сторінки [_Layout.cshtml]:

<!DOCTYPE html>
<html>
<head>
  <title>@ViewBag.Title</title>
  ...
  <script type="text/javascript" src="~/Scripts/myScripts.js"></script>
</head>
<body>
  <table>
...
  </table>
  <hr />
  <div id="content">
    @RenderBody()
  </div>
</body>
</html>

Різні фрагменти сторінки додатка будуть відображатися в області з ідентифікатором [content] у рядку 13. Вони будуть відображатися за допомогою викликів Ajax. Скрипти JavaScript, що виконують ці виклики, містяться у файлі [myScripts.js], на який є посилання у рядку 6. Створіть цей файл, який нам знадобиться:

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

9.9.1. Інструменти розробника JavaScript

Нагадуємо, що в браузері Chrome у вашому розпорядженні є набір інструментів для налагодження HTML, CSS та JavaScript на ваших сторінках. Ці інструменти частково були представлені в розділі 7.2. У моделі APU браузери зберігають у кеші скрипти JavaScript, на які посилається перша сторінка додатка. Тому не забудьте очистити цей кеш, коли ви змінюєте свої скрипти, інакше зміни можуть не врахуватися. Ось як це зробити в Chrome:

- виконайте [Ctrl-Maj-I], щоб відобразити середовище розробки

  • натисніть на піктограму [1] у правому нижньому куті вікна розробки;
  • потім встановіть прапорець біля опції [2], яка вимикає кеш у режимі розробки.

9.9.2. Використання часткового виду для відображення форми

Форма введення даних є одним із фрагментів, що відображаються додатком. Наразі ця форма відображається за допомогою подання [Index.cshtml], яке є повним поданням:


@{
  ViewBag.Title = "Pam";
}
<h2>Formulaire</h2>

Цей вигляд відображається дією [Index]:


    [HttpGet]
    public ViewResult Index()
    {
      return View();
}

У рядку 4 вище відображається саме повний вигляд [View], а не частковий вигляд [PartialView]. Нам потрібен частковий вигляд для форми, яка буде фрагментом сторінки. Ми модифікуємо вигляд [Index.cshtml] наступним чином:


@{
  ViewBag.Title = "Pam";
}
@Html.Partial("Formulaire")

У рядку 4 форма більше не є частиною сторінки [Index.cshtml]. Тепер вона розміщена в частковому вигляді [Formulaire.cshtml]:

Код [Formulaire.cshtml] виглядає просто так:


<h2>Formulaire</h2>

Внесіть ці зміни та переконайтеся, що при запуску програми ви як і раніше бачите таке вікно:

 

9.9.3. Ajax-запит [faireSimulation]

Нас цікавить фрагмент, що відображається, коли користувач натискає на посилання [Faire la simulation]:

  • у [1] користувач натискає на посилання [Faire la simulation];
  • у [2] під формою з’являється симуляція.

Ми модифікуємо частковий вигляд [Formulaire.cshtml], який відображає форму, наступним чином:


<h2>Formulaire</h2>
<div id="simulation" />

У рядку 3 ми створюємо область з ідентифікатором [simulation] для розміщення фрагмента симуляції.

Створюємо наступний частковий вигляд [Simulation.cshtml]:

Вміст подання [Simulation.cshtml] такий:


<hr />
<h2>Simulation</h2>

Тепер нам потрібно написати код JavaScript, який обробляє клік на посилання [Faire la simulation]. Ми будемо дотримуватися підходу, викладеного в розділі 7.6.5. Спочатку розглянемо код HTML посилання в [_Layout.cshtml]:


<a id="lnkFaireSimulation" href="javascript:faireSimulation()">| Faire la simulation<br />
</a>

Бачимо, що клік на посилання [Faire la simulation] запустить виконання функції JS [faireSimulation]. Ця функція буде записана у файл [myScripts.js] разом з іншими функціями JS, необхідними для роботи програми:


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

function faireSimulation() {
  // виконується ручний виклик Ajax
...
}

function effacerSimulation() {
  // очищення даних, введених у форму
...
}

function enregistrerSimulation() {
  // виконується ручний виклик Ajax
  ...
}

function voirSimulations() {
  // виконується ручний виклик Ajax
  ...
}

function retourFormulaire() {
  // виконується ручний виклик Ajax
...
}

function terminerSession() {
...
}

// під час завантаження документа
$(document).ready(function () {
  // отримуємо посилання на різні компоненти сторінки
  loading = $("#loading");
  content = $("#content");
});
  • рядки 35–39: функція JQuery, що виконується під час запуску програми;
  • рядки 37–38: ініціалізуються глобальні змінні з рядків 2 та 3.

Нагадаємо, що елементи з ідентифікаторами [loading] та [content] визначені на головній сторінці [_Layout.cshtml] (рядки 14 та 21 нижче):


<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
  <table>
    <tbody>
      <tr>
        <td>
          <h2>Simulateur de calcul de paie</h2>
        </td>
        <td style="width: 20px">
          <img id="loading" style="display: none" src="~/Content/images/indicator.gif" />
        </td>
...
        </td>
    </tbody>
  </table>
  <hr />
  <div id="content">
    @RenderBody()
  </div>
</body>
</html>

Завдання: дотримуючись підходу, викладеного в параграфі 7.6.5, напишіть функцію JS [faireSimulation]. Ця функція здійснить Ajax-виклик типу POST до дії [/Pam/FaireSimulation]. Наразі дані не надсилатимуться.акція [/Pam/FaireSimulation] поверне частковий вигляд [Simulation.cshtml] до функції JS [faireSimulation], яка потім розмістить цей потік HTML у регіоні з ідентифікатором [simulation] у формі.


Перевірте посилання [Faire la simulation] у вашому додатку.

9.9.4. Ajax-запит [enregistrerSimulation]

Посилання [Enregistrer la simulation] визначено в [_Layout.cshtml] наступним чином:


<a id="lnkEnregistrerSimulation" href="javascript:enregistrerSimulation()">| Enregistrer la simulation<br />
</a>

Завдання: дотримуючись попереднього підходу, напишіть функцію JS [enregistrerSimulation]. Вона буде надсилати Ajax-запит типу POST до дії [/Pam/EnregistrerSimulation]. Наразі дані не надсилатимуться.акція [/Pam/EnregistrerSimulation] поверне частковий вигляд [Simulations.cshtml] до функції JS [enregistrerSimulation], яка потім розмістить цей потік HTML у регіоні з ідентифікатором [content] на головній сторінці.


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

Його вміст такий:


<h2>Simulations</h2>

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

9.9.5. Ajax-запит [voirSimulations]

Посилання [Voir les simulations] визначено в [_Layout.cshtml] наступним чином:


<a id="lnkVoirSimulations" href="javascript:voirSimulations()">| Voir les simulations<br />
</a>

Завдання: дотримуючись попереднього підходу, напишіть функцію JS [voirSimulations]. Вона буде надсилати Ajax-запит типу POST до дії [/Pam/VoirSimulations]. Наразі дані не надсилатимуться.акція [/Pam/VoirSimulations] поверне частковий вигляд [Simulations.cshtml] до функції JS [voirSimulations], яка потім розмістить цей потік HTML у регіоні з ідентифікатором [content] на головній сторінці.


Вигляд [Simulations.cshtml] — це той самий, що вже використовувався в попередньому питанні.

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

9.9.6. Ajax-запит [retourFormulaire]

Посилання [Retour au formulaire de simulation] визначено в [_Layout.cshtml] наступним чином:


<a id="lnkRetourFormulaire" href="javascript:retourFormulaire()">| Retour au formulaire de simulation<br />
</a>

Завдання: дотримуючись попереднього підходу, напишіть функцію JS [retourFormulaire]. Вона буде надсилати Ajax-запит типу POST до дії [/Pam/Formulaire]. Наразі дані не надсилатимуться.акція [/Pam/Formulaire] поверне частковий вигляд [Formulaire.cshtml] до функції JS [retourFormulaire], яка потім розмістить цей потік HTML у регіоні з ідентифікатором [content] на головній сторінці.


Вигляд [Formulaire .cshtml] вже визначено. Ось приклад виконання:

9.9.7. Ajax-виклик [terminerSession]

Посилання [Terminer la session] визначено в [_Layout.cshtml] наступним чином:


<a id="lnkTerminerSession" href="javascript:terminerSession()">| Terminer la session<br />
</a>

Завдання: дотримуючись наведеного вище підходу, напишіть функцію JS [terminerSession]. Ця функція буде надсилати Ajax-запит типу POST до дії [/Pam/TerminerSession]. Наразі дані не надсилатимуться.акція [/Pam/TerminerSession] поверне частковий вигляд [Formulaire.cshtml] до функції JS [terminerSession], яка потім розмістить цей потік HTML у регіоні з ідентифікатором [content] на головній сторінці.


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

9.9.8. Функція JS [effacerSimulation]

Посилання [Effacer la simulation] визначено в [_Layout.cshtml] наступним чином:


<a id="lnkEffacerSimulation" href="javascript:effacerSimulation()">| Effacer la simulation<br />
</a>

Функція JS [effacerSimulation] призначена для:

  • приховати фрагмент [Simulation], якщо він існує;
  • повернути поля введення форми до стану, в якому вони перебували під час початкового завантаження програми (якщо такі поля введення будуть — наразі їх немає).

Завдання: напишіть функцію JS [effacerSimulation]. Тут немає виклику Ajax. Все відбувається всередині браузера і не задіює сервер.


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

9.9.9. Управління навігацією між екранами

Наразі посилання завжди відображаються. Тепер ми будемо керувати їхнім відображенням за допомогою функції JavaScript. Спочатку нагадаємо код шести посилань JavaScript у [_Layout.cshtml]:


<a id="lnkFaireSimulation" href="javascript:faireSimulation()">| Faire la simulation<br />
</a>
<a id="lnkEffacerSimulation" href="javascript:effacerSimulation()">| Effacer la simulation<br />
</a>
<a id="lnkVoirSimulations" href="javascript:voirSimulations()">| Voir les simulations<br />
</a>
<a id="lnkRetourFormulaire" href="javascript:retourFormulaire()">| Retour au formulaire de simulation<br />
</a>
<a id="lnkEnregistrerSimulation" href="javascript:enregistrerSimulation()">| Enregistrer la simulation<br />
</a>
<a id="lnkTerminerSession" href="javascript:terminerSession()">| Terminer la session<br />
</a>

Усі посилання мають атрибут [id], який дозволить нам керувати ними за допомогою JavaScript. Ми змінюємо метод JS, що виконується під час завантаження сторінки, наступним чином:


// глобальні змінні
var loading;
var content;
var lnkFaireSimulation;
var lnkEffacerSimulation
var lnkEnregistrerSimulation;
var lnkTerminerSession;
var lnkVoirSimulations;
var lnkRetourFormulaire;
var options;

...
// під час завантаження документа
$(document).ready(function () {
  // отримуємо посилання на різні компоненти сторінки
  loading = $("#loading");
  content = $("#content");
  // посилання меню
  lnkFaireSimulation = $("#lnkFaireSimulation");
  lnkEffacerSimulation = $("#lnkEffacerSimulation");
  lnkEnregistrerSimulation = $("#lnkEnregistrerSimulation");
  lnkVoirSimulations = $("#lnkVoirSimulations");
  lnkTerminerSession = $("#lnkTerminerSession");
  lnkRetourFormulaire = $("#lnkRetourFormulaire");
  // їх поміщають у масив
  options = [lnkFaireSimulation, lnkEffacerSimulation, lnkEnregistrerSimulation, lnkVoirSimulations, lnkTerminerSession, lnkRetourFormulaire];
  // приховуємо деякі елементи сторінки
  loading.hide();
  // фіксуємо меню
  setMenu([lnkFaireSimulation, lnkVoirSimulations, lnkTerminerSession]);
});

  • рядки 19–24: отримуємо посилання на шість посилань. Ці посилання зберігаються у глобальних змінних у рядках 4–9;
  • рядок 26: масив [options] ініціалізується цими шістьма посиланнями. Цей масив задефіновано як глобальну змінну в рядку 10;
  • рядок 28: приховується анімований зображення, що відображає очікування завершення Ajax-запитів;
  • рядок 30: відображаються посилання [lnkFaireSimulation, lnkVoirSimulations, lnkTerminerSession]. Решта будуть приховані.

Функція JS [setMenu] має такий вигляд:


function setMenu(show) {
  // відображаємо посилання з таблиці [show]
...
}

Завдання: написати функцію JS [setMenu].


Якщо T — масив посилань:

  • T.length — кількість посилань;
  • T[i] — це посилання № i;
  • T[i].show() відображає посилання № i;
  • T[i].hide() приховує посилання № i.

Завдяки цим новим функціям JS сторінка, що відображається при запуску, виглядає наступним чином:

 

Налаштуйте функції JS та [faireSimulation, effacerSimulation, enregistrerSimulation, voirSimulations, retourFormulaire, terminerSession], щоб отримати такі екрани:

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

9.10. Етап 4: написання серверної дії [Index]

Наразі під час запуску додатка ми бачимо такий екран:

 

Замість цього екрана ми хотіли б бачити такий:

 

Саме дія [Index] повинна генерувати цю сторінку. Звернемо увагу на декілька моментів:

  • на сторінці є форма з трьома полями для введення даних:
    • працівника, для якого розраховується заробітна плата,
    • кількість відпрацьованих ним годин,
    • кількість відпрацьованих днів;
  • форма надсилається за посиланням [Faire la simulation];
  • необхідно перевірити правильність заповнення полів [Heures travaillées] та [Jours travaillés];
  • список працівників береться з шару [métier], який ми створили раніше.

Нагадаємо поточний код дії [Index]:


    [HttpGet]
    public ViewResult Index()
    {
      return View();
}

а ось код подання [Index.cshtml], яке відображає ця дія:


@{
  ViewBag.Title = "Pam";
}
@Html.Partial("Formulaire")

а також код часткового виду [Formulaire.cshtml]:


<h2>Formulaire</h2>

У цих трьох місцях відбудуться зміни.

9.10.1. Шаблон форми

Повернемося до ланцюжка обробки URL [/Pam/Index]:

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

Дія, яка нас цікавить, — це дія [Index], яка на даний момент виглядає так:


    [HttpGet]
    public ViewResult Index()
    {
      return View();
}

Дія [Index] не передає жодного шаблону до подання [Index.cshtml]. Отже, це подання не зможе відобразити список співробітників. Цей список можна отримати на рівні [métier]. Для цього проект [pam-web-01] повинен мати посилання на проект [pam-metier-simule]. Створимо це посилання зараз:

  • у [1]: клацніть правою кнопкою миші на [References] у проєкті [pam-web-01], а потім на [Ajouter une référence];
  • у [2] виберіть опцію [Solution], а потім проект [pam-metier-simule] у [3];
  • у [4] проект [pam-metier-simule] було додано до посилань проекту [pam-web-01].

9.10.2. Модель додатка

У розділі 4.10 на сторінці 78 ми ознайомилися з важливими поняттями «модель додатка» та «модель сеансу». Тепер ми будемо їх використовувати. Нагадаємо, що в модель вносяться:

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

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

Рівень [web] містить посилання на рівень [métier]. Цей рівень може бути спільним для всіх користувачів. Отже, її можна включити до моделі додатка. Крім того, припустимо, що список співробітників не змінюється. Тому його можна прочитати лише один раз, а потім надати доступ усім користувачам. Тож ми пропонуємо таку модель додатка:

Код класу [ApplicationModel] може виглядати так:


using Pam.Metier.Entites;
using Pam.Metier.Service;
namespace PamWeb.Models
{
  public class ApplicationModel
  {
    // --- дані на рівні додатка ---
    public Employe[] Employes { get; set; }
    public IPamMetier PamMetier { get; set; }
  }
}

Щоб відобразити випадаючий список у поданні, потрібно написати щось на зразок:


        <!-- випадаючий список -->
        <tr>
          <td>Liste déroulante</td>
          <td>@Html.DropDownListFor(m => m.DropDownListField,
           new SelectList(@Model.DropDownListFieldItems, "Value", "Label"))
          </td>
</tr>

Метод [DropDownListFor] очікує як другий параметр тип SelectListItem[], який вище був наданий типом [SelectList]. Нам потрібно створити такий масив зі списком співробітників. Оскільки список співробітників не змінюється, цей масив також можна розмістити в моделі додатка. Ми модифікуємо її наступним чином:


using Pam.Metier.Entites;
using Pam.Metier.Service;
using System.Web.Mvc;

namespace Pam.Web.Models
{
  public class ApplicationModel
  {
    // --- дані на рівні додатка ---
    public Employe[] Employes { get; set; }
    public IPamMetier PamMetier { get; set; }
    public SelectListItem[] EmployesItems { get; set; }
  }
}

Коли саме слід створити цю модель? Ми показали це в розділі 4.10. Це відбувається під час виконання методу [Application_Start] з файлу [Global.asax]:

Наразі метод [Application_Start] має такий вигляд:


using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;

namespace pam_web_01
{
  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);
    }
  }
}

Ми змінюємо його наступним чином:


using Pam.Metier.Entites;
using Pam.Metier.Service;
using PamWeb.Infrastructure;
using PamWeb.Models;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;

namespace pam_web_01
{
  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);
      // -------------------------------------------------------------------
      // ---------- специфічна конфігурація
      // -------------------------------------------------------------------
      // дані на рівні додатка
      ApplicationModel application = new ApplicationModel();
      Application["data"] = application;
        // інстанціювання шару [métier]
        application.PamMetier = ...
        // таблиця співробітників 
        application.Employes = ...
        // елементи комбінованого списку співробітників
        application.EmployesItems = ...
      // модельний біндер для [ApplicationModel]
      ...
    }
  }
}

Завдання: доповнити код методу [Application_Start]. Усе необхідне ви знайдете в розділі 4.10. Приділіть час, щоб ще раз прочитати цей довгий, але важливий розділ.


Рядок 33 насправді складається з декількох рядків. Щоб створити об’єкт типу [SelectListItem], ви можете використати такий метод:


new SelectListItem() { Text = unTexte, Value = uneValeur };

Цей [SelectListItem] слугуватиме для генерації наступного тегу HTML <option>:

<option value='uneValeur'>unTexte</option>

у випадаючому списку. Потрібно забезпечити, щоб:

  • unTexte — це ім’я, за яким йде прізвище працівника;
  • uneValeur — це номер SS працівника.

У рядку 35, наведеному вище, вам знадобиться клас [ApplicationModelBinder], описаний у розділі 4.10 на сторінці 82:

9.10.3. Код дії [Index]

Тепер, коли ми визначили шаблон для додатка, ми можемо змінити код дії [Index] наступним чином:


    [HttpGet]
    public ViewResult Index(ApplicationModel application)
    {
      return View();
}
  • рядок 4: шаблон додатка тепер є параметром дії [Index]. У розділі 4.10 ми пояснили, як цей параметр ініціалізується фреймворком.

9.10.4. Модель подання [Index.cshtml]

Тепер дія [Index] має доступ до співробітників, зареєстрованих у моделі додатка. Тепер вона повинна передати їх до подання [Index.cshtml], яке вона буде відображати. Можна було б передати тип [ApplicationModel] як модель для подання [Index.cshtml], але ми швидко побачимо, що цьому поданню потрібна додаткова інформація, якої немає в [ApplicationModel]. Ми будемо використовувати такий шаблон подання [IndexModel]:


namespace Pam.Web.Models
{
  public class IndexModel
  {
    // дані про сферу застосування
    public ApplicationModel Application { get; set; }
  }
}
  • рядок 6: [IndexModel] завантажує модель додатка.

Дія [Index] виглядає наступним чином:


    [HttpGet]
    public ViewResult Index(ApplicationModel application)
    {
      return View(new IndexModel() { Application = application });
}
  • у рядку 4 відображається стандартний вигляд [Index.cshtml] із шаблоном типу [IndexModel], ініціалізованим даними шаблону додатка.

Ми знаємо, що представлення [Index.cshtml] має відображати форму:

Image

Повернемося до ланцюжка обробки запиту:

Для запиту [GET /Pam/Index]:

  • дія — [Index];
  • шаблон цієї дії — [ApplicationModel];
  • представлення — [Index.cshtml];
  • модель цього подання — [IndexModel].

Коли форма буде відправлена, ми отримаємо аналогічний ланцюжок обробки:

  • акція обробляє POST;
  • її шаблон збирає відправлені значення, у даному випадку:
    • номер SS обраного співробітника;
    • кількість відпрацьованих годин;
    • кількість відпрацьованих днів;

Можна створити шаблон дії, що об’єднує ці три значення. Також часто використовують той самий шаблон, що й для відображення форми. Саме це ми й зробимо тут. Клас [IndexModel] змінюється наступним чином:


using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace Pam.Web.Models
{
  [Bind(Exclude = "Application")]
  public class IndexModel
  {
    // дані про сферу застосування
    public ApplicationModel Application { get; set; }

    // опубліковані значення
    [Display(Name = "Employé")]
    public string SS { get; set; }
    [Display(Name = "Heures travaillées")]
    [UIHint("Decimal")]
    public double HeuresTravaillées { get; set; }
    [Display(Name = "Jours travaillés")]
    public double JoursTravaillés { get; set; }
  }
}
  • рядки 13, 16, 18: три значення, що передаються. Зазначимо, що [joursTravaillés] було оголошено типом [double], хоча насправді очікується ціле число. Тип [double] було введено для полегшення перевірки цього поля на стороні клієнта, оскільки перевірка типу [int] викликала проблеми;
  • рядки 12, 14, 17: назви для методів [Html.LabelFor] у поданнях, пов’язаних із моделлю;
  • рядок 15: примітка щодо відображення поля [HeuresTravaillées] з двома десятковими знаками;
  • рядок 5: вказано, що властивість з назвою [Application] не входить до переліку значень, що передаються.

9.10.5. Види [Index.cshtml] та [Formulaire.cshtml]

Вигляд [Index.cshtml] відображається за допомогою такої дії [Index]:


    [HttpGet]
    public ViewResult Index(ApplicationModel application)
    {
      return View(new IndexModel() { Application = application });
}

Цікаво, що представлення [Index.cshtml] залишається незмінним:


@{
  ViewBag.Title = "Pam";
}
@Html.Partial("Formulaire")
  • погляд не заявляє жодного шаблону;
  • рядок 4: вона включає частковий вигляд [Formulaire.cshtml], знову ж таки без передачі йому шаблону. Під час тестування було виявлено, що модель [IndexModel], передана до подання [Index.cshtml], неявним чином поширювалася на часткове подання [Formulaire.cshtml]. Останнє подання тепер може мати такий вигляд:

@model Pam.Web.Models.IndexModel

@using (Html.BeginForm("FaireSimulation", "Pam", FormMethod.Post, new { id = "formulaire" }))
{
  <table>
    <thead>
      <tr>
...
      </tr>
    </thead>
    <tbody>
      <tr>
...
      </tr>
      <tr>
...
      </tr>
    </tbody>
  </table>
}
<div id="simulation" />
  • рядок 1: представлення отримує шаблон типу [IndexModel];
  • рядок 3: форма;
  • рядки 6–10: заголовки таблиці введення даних;
  • рядки 12–14: рядок для введення даних;
  • рядки 15–17: можливі повідомлення про помилки.

Завдання: доповніть код подання [Formulaire.cshtml]. Використовуйте методи [DropDownListFor, EditorFor, LabelFor, ValidationMessageFor], описані в розділі 5.7.


9.10.6. Тестування дії [Index]

Ми написали всі елементи ланцюжка обробки URL [/Pam/Index]:

Ми тестуємо додаток із [Ctrl-F5]:

Ви повинні перевірити, чи справді ваш випадаючий список заповнено списком співробітників, який ми визначили в імітованому шарі [métier].

9.11. Крок 5: налаштування перевірки введених даних

9.11.1. Проблема

Хоча ми нічого для цього не робили, перевірки на стороні клієнта вже працюють:

 
 

Валідація на стороні клієнта працює за замовчуванням завдяки рядку 3 у файлі [Web.config] додатка, наведеному нижче.


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

Однак, оскільки у файлі [IndexModel] поле [JoursTravaillés] було оголошено як тип [double]:


    public double JoursTravaillés { get; set; }

у це поле можна ввести дійсне число:

 

Крім того, у обидва поля можна ввести довільні значення:

 

Наразі шаблон форми [IndexModel] виглядає так:


using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace Pam.Web.Models
{
  [Bind(Exclude = "Application")]
  public class IndexModel
  {
    // дані про сферу застосування
    public ApplicationModel Application { get; set; }

    // опубліковані значення
    [Display(Name = "Employé")]
    public string SS { get; set; }
    [Display(Name = "Heures travaillées")]
    [UIHint("Decimal")]
    public double HeuresTravaillées { get; set; }
    [Display(Name = "Jours travaillés")]
    public double JoursTravaillés { get; set; }
  }
}

Завдання: вдосконалити цей шаблон, щоб:

  • отримати персоналізовані повідомлення про помилки;
  • допускати лише дійсні значення в діапазоні [0,400] для поля [HeuresTravaillées];
  • приймати лише цілочисельні значення в діапазоні [0,31] для поля [JoursTravaillées];

Можна скористатися прикладом із розділу 7.6.2. Щоб перевірити, чи кількість відпрацьованих днів є цілим числом, можна використати регулярний вираз (див. приклади в розділі 5.9.1).

Ось приклади очікуваних результатів:

 
 
 

9.11.2. Введення дійсних чисел у французькому форматі

У поточній версії програми кількість відпрацьованих годин має бути десятковим числом у англосаксонському форматі (з крапкою). Французький формат із комою не приймається:

 

Ця проблема була виявлена та вирішена в розділі 6.1.


Завдання: дотримуючись підходу, описаного у зазначеному вище розділі, внесіть необхідні зміни, щоб можна було вводити дійсні числа у французькому десятковому форматі. Протестуйте свій додаток.


Тепер попередній екран виглядає так:

 

9.11.3. Підтвердження форми за допомогою посилання Javascript [Faire la simulation]

Наразі можна надсилати недійсні значення, як показано в наступній послідовності:

 

Наявність симуляції в [1] та зміна меню в [2] свідчать про те, що натискання на посилання [Faire la simulation] призвело до відправлення форми, незважаючи на те, що введені значення були недійсними. Ця проблема була виявлена та вирішена в розділі 7.6.5.


Завдання: дотримуючись підходу, викладеного у вищезазначеному параграфі, забезпечте, щоб перехід за посиланням [Faire la simulation] до сторінки POST не відбувався, якщо введені значення є недійсними. Не забудьте очистити кеш браузера перед тестуванням ваших змін.


Нагадаємо, що частковий вигляд [Formulaire.cshtml] генерує форму HTML з ідентифікатором [formulaire] (рядок 1 нижче):


@using (Html.BeginForm("FaireSimulation", "Pam", FormMethod.Post, new { id = "formulaire" }))
{
...
}

Це можна перевірити, переглянувши вихідний код форми в браузері:


<div id="content">
    
    <form action="/Pam/FaireSimulation" id="formulaire" method="post">
    ...
    </form>
    <div id="simulation" />
</div>

9.12. Крок 6: провести симуляцію

9.12.1. Проблема

Коли ми проводимо симуляцію, ми хочемо отримати такий результат:

Частковий вигляд [Simulation.cshtml] тепер відображає розрахунковий лист працівника.

9.12.2. Створення подання [Simulation.cshtml]

Вигляд [Simulation.cshtml] змінюється наступним чином:


@model Pam.Metier.Entites.FeuilleSalaire
<hr />
<p><span class="info">Informations Employé</span></p>
<table>
  <tbody>
    <tr>
      <td><span class="libellé">Nom</span>
      </td>
      <td><span class="libellé">Prénom</span>
      </td>
      <td><span class="libellé">Adresse</span>
      </td>
    </tr>
    <tr>
      <td>
        <span class="valeur">@Model.Employe.Nom</span>
      </td>
...
    </tr>
    <tr>
      <td><span class="libellé">Ville</span>
      </td>
      <td><span class="libellé">Code Postal</span>
      </td>
      <td><span class="libellé">Indice</span>
      </td>
    </tr>
    <tr>
...
    </tr>
  </tbody>
</table>
<br />
<p><span class="info">Informations Cotisations</span></p>
<table>
...
  </tbody>
</table>
<br />
<p><span class="info">Informations Indemnités</span></p>
<table>
...
</table>
<br />
<p><span class="info">Informations Salaire</span></p>
<table>
...
</table>
<br />
<table>
...
</table>
  • рядок 1: шаблон для подання [Simulation.cshtml] — тип [FeuilleSalaire], визначений у розділі 9.7.3;
  • вид використовує класи [libellé, info, valeur], визначені у таблиці стилів додатка [Content / Site.css]:

.libellé {
  background-color: azure;
  margin: 5px;
  padding: 5px;
}

.info {
  background-color: antiquewhite;
  margin: 5px;
  padding: 5px;
}

.valeur {
  background-color: beige;
  padding: 5px;
  margin: 5px;
}

Крім того, також у [Site.css] встановлюється висота рядків різних таблиць HTML у області з ідентифікатором [simulation], саме там, де відображається розрахунковий лист:


#моделювання таблиці tr {
  height: 30px;
}

Завдання: заповніть форму [Simulation.cshtml].


Щоб відобразити суму в євро, використовується метод [string.Format]:

string.Format("{0:C2}",somme)

Наведена вище інструкція відображає [somme] як грошове значення [C] (Currency) з двома десятковими знаками [C2].

Щоб протестувати цей вигляд, потрібно надати йому відомості про заробітну плату. Вони повинні бути надані за допомогою дії [/Pam/FaireSimulation], яка є ціллю виклику Ajax за посиланням [Faire la simulation]. Наразі ця дія виглядає наступним чином:


    [HttpGet]
    public ViewResult Index(ApplicationModel application)
    {
      return View(new IndexModel() { Application = application });
    }

    // виконати симуляцію
    [HttpPost]
    public PartialViewResult FaireSimulation()
    {
      return PartialView("Simulation");
}

У наведеному вище прикладі дія [FaireSimulation] не передає жодного шаблону до подання [Simulation.cshtml]. Вона повинна передати йому розрахунок заробітної плати. Відомо, що розрахунок відомостей про заробітну плату виконує шар [métier]. Доступ до цього шару [métier] здійснюється через шаблон додатка [ApplicationModel], який ми визначили в розділі 9.10.2:


  public class ApplicationModel
  {
    // --- дані про сферу застосування ---
    public Employe[] Employes { get; set; }
    public IPamMetier PamMetier { get; set; }
    public SelectListItem[] EmployesItems { get; set; }
}

До шару [métier] можна отримати доступ через властивість у рядку 5 вище. Щоб дія [FaireSimulation] мала доступ до шару [métier], ми передамо їй шаблон додатка, як це було зроблено для дії [Index]. Тоді код виглядатиме так:


    // провести симуляцію
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application)
    {
      return PartialView("Simulation");
}

Тепер ми можемо всередині дії розрахувати фіктивну платіжну відомість. Код виглядає так:


// провести симуляцію
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application)
    {
      FeuilleSalaire feuilleSalaire = application.PamMetier.GetSalaire("254104940426058", 150, 20);
      return PartialView("Simulation", feuilleSalaire);
    }
  • у рядку 5 обчислюється фіктивна заробітна плата. Перший параметр — це існуючий номер SS. Він був визначений у класі [métier], змодельованому в розділі 9.7.5. Другий параметр — це кількість відпрацьованих годин, а третій — кількість відпрацьованих днів;
  • рядок 6: цей аркуш заробітної плати передається як шаблон до подання [Simulation.cshtml].

Тепер ми готові протестувати подання [Simulation.cshtml]:

 

Не вводимо жодних даних і запускаємо симуляцію. Отримуємо такий результат:

 

9.12.3. Розрахунок фактичної заробітної плати

Наша поточна операція [FaireSimulation] завжди розраховує ту саму платіжну відомість:


    // провести симуляцію
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application)
    {
      FeuilleSalaire feuilleSalaire = application.PamMetier.GetSalaire("254104940426058", 150, 20);
      return PartialView("Simulation", feuilleSalaire);
}

Вона не враховує введену інформацію:

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

Введені значення надходять до дії [FaireSimulation] наступним чином:

  1. користувач натискає на посилання [Faire la simulation]. Це запускає виконання функції JS [faireSimulation], яку ми вже написали;
  2. Потім функція JS [faireSimulation] виконує Ajax-запит до серверної дії [/Pam/FaireSimulation], над якою ми зараз працюємо. Наразі функції JS та [faireSimulation] не передають жодної інформації до серверної дії. Їм потрібно буде передати значення, введені користувачем;
  3. серверна дія [/Pam/FaireSimulation] отримає ці значення з даних, надісланих функціями JS та [faireSimulation].

Почнемо з пункту 2: функція JS [faireSimulation] повинна відправити значення, введені користувачем, до серверної дії [/Pam/FaireSimulation].


Завдання: доповніть функцію JS [faireSimulation] так, щоб вона відправляла значення, введені користувачем. Можна скористатися прикладом із розділу 7.6.5, де розглядається ця проблема.


Тепер розглянемо пункт 3 вище. Серверна дія [/Pam/FaireSimulation] повинна отримувати значення, відправлені функціями JS та [faireSimulation].


Завдання: доповніть метод сервера [FaireSimulation] так, щоб він обчислював заробітну плату на основі значень, переданих функціями JS та [faireSimulation]. Можна знову скористатися прикладом із розділу 7.6.5, де розглядалася ця проблема. Наразі припустимо, що модель, побудована на основі переданих значень, залишається дійсною.


Підказка: серверна операція [FaireSimulation] виконується наступним чином:


// провести симуляцію
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application, FormCollection data)
    {
      // створення моделі дії
      ...
      // спроба отримати значення, занесені в цю модель
      ...
      // розраховується заробітна плата
      FeuilleSalaire feuilleSalaire = ...
      // відображаємо розрахунковий лист
      return PartialView("Simulation", feuilleSalaire);
    }

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

Вибираємо [Justine Laverti]. Отримуємо такий результат:

Ми дійсно отримали фіктивну платіжну відомість за номером [Justine Laverti]. Раніше розраховувалася лише одна платіжна відомість — за номером [Marie Jouveinal]. Отже, було використано значення, занесене для вибору працівника. Щодо кількості годин та днів нічого сказати не можна, оскільки наш імітований шар [métier] їх не враховує.

9.12.4. Обробка помилок

Розглянемо наступний приклад:

  • у [1] вибирається співробітник, якого не існує (див. визначення модельованого шару [métier] у розділі 9.7.5;
  • у [2] проводиться симуляція;
  • у [3] нижче відображається сторінка з помилкою.

Що сталося?

Була виконана функція JS [faireSimulation]. Її код виглядає так:


function faireSimulation() {
...
  // виконується ручний виклик Ajax
  $.ajax({
    url: '/Pam/FaireSimulation',
...
    beforeSend: function () {
      // горить індикатор очікування
      loading.show();
    },
    success: function (data) {
...
    },
    error: function (jqXHR) {
      // відображення помилки
      simulation.html(jqXHR.responseText);
      simulation.show();
    },
    complete: function () {
      // сигнал очікування вимкнено
      loading.hide();
    }
  });
  // меню
  setMenu([lnkEffacerSimulation, lnkEnregistrerSimulation, lnkTerminerSession, lnkVoirSimulations]);
}

Ajax-запит завершився невдало, і була виконана функція з рядків 14–18. Була відображена сторінка помилки [jqXHR.responseText], повернута сервером. Вона є досить конкретною. Симульований рівень [métier] викликав виняток, оскільки наданий йому номер SS не належить існуючому співробітнику (див. код симульованого рівня [métier] у розділі 9.7.5). Нам потрібно правильно обробити цей випадок.

Ми створимо частковий вигляд [Erreurs.chtml], який буде повертатися клієнту JS щоразу, коли на стороні сервера буде виявлено помилку:

Код часткового виду [Erreurs.chtml] такий:


@model IEnumerable<string>

<hr />
<h2>Les erreurs suivantes se sont produites</h2>
<ul>
  @foreach (string msg in Model)
  {
    <li>@msg</li>
  }
</ul>
  • рядок 1: представлена отримує як шаблон список повідомлень про помилки;
  • рядки 5–10: які відображаються у списку HTML;

Тепер змінимо код серверної дії [FaireSimulation] наступним чином:


    // провести симуляцію
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application, FormCollection data)
    {
    ...
      // розраховується заробітна плата
      FeuilleSalaire feuilleSalaire = null;
      Exception exception=null;
      try
      {
        // розрахунок заробітної плати
        feuilleSalaire = ...
      }
      catch (Exception ex)
      {
        exception = ex;
      }
      // помилка?
      if (exception == null)
      {
        // відобразити розрахунковий лист
        return PartialView("Simulation", feuilleSalaire);
      }
      else
      {
        // відображення сторінки помилок
        return PartialView("Erreurs", Static.GetErreursForException(exception));
      }
}
  • рядки 9–17: розрахунок заробітної плати тепер виконується в блоці try/catch;
  • рядок 27: якщо сталася помилка, відображається частковий вигляд [Erreurs.cshtml] на основі списку повідомлень про помилки, наданого статичним методом [Static.GetErreursForException(exception)].

У класі [Static] об’єднано дві статичні допоміжні функції [1]:


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

namespace PamWeb.Infrastructure
{
  public class Static
  {
    // список повідомлень про помилки, пов'язаних з винятком
    public static List<string> GetErreursForException(Exception ex)
    {
      List<string> erreurs = new List<string>();
      while (ex != null)
      {
        erreurs.Add(ex.Message);
        ex = ex.InnerException;
      }
      return erreurs;
    }

    // список повідомлень про помилки, пов’язаних із недійсним шаблоном
    public static List<string> GetErreursForModel(ModelStateDictionary état)
    {
      List<string> erreurs = new List<string>();
      if (!état.IsValid)
      {
        foreach (ModelState modelState in état.Values)
        {
          foreach (ModelError error in modelState.Errors)
          {
            erreurs.Add(getErrorMessageFor(error));
          }
        }
      }
      return erreurs;
    }

    // повідомлення про помилку, пов’язане з елементом шаблону дії
    static 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;
    }

  }
}
  • рядки 10–19: статична функція [GetErreursForException] повертає список помилок зі стека винятків;
  • рядки 22–36: статична функція [GetErreursForModel] повертає список помилок, пов’язаних із недійсним шаблоном дії. Код цієї функції, а також код приватного методу [getErrorMessageFor] (рядки 39–54) ми вже розглядали раніше.

Тепер ми можемо знову перевірити випадок помилки:

  • у [1] вибираємо співробітника, якого не існує;
  • у [2] проводимо симуляцію;
  • у [3] отримуємо нову сторінку помилок.

Повернемося до серверної дії [FaireSimulation]:


    // провести симуляцію
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application, FormCollection data)
    {
      // створення моделі дії
      IndexModel modèle = new IndexModel() { Application = application};
      // спроба отримати значення, відправлені в модель
      TryUpdateModel(modèle, data);
      // розраховується заробітна плата
...
}

У рядку 8 оновлюємо модель з рядка 6 значеннями, надісланими через виклик Ajax. Ми не перевіряємо правильність моделі. Це потрібно робити, оскільки неможливо знати, звідки походять надіслані значення. Хтось міг підробити POST і надіслати нам недійсні дані.


Завдання: дотримуючись шаблону, який ми розробили для випадку винятку, змініть серверну дію [FaireSimulation] так, щоб вона відправляла сторінку помилок у разі недійсних даних. Для цього ми використаємо статичний метод [GetErreursForModel] класу [Static].


Як перевірити цю зміну? У розділі 9.11.3 ви забезпечили, щоб функція JS [faireSimulation] не виконувала POST для введених значень, якщо вони були недійсними. Позначте коментарями рядки, які це реалізують, а потім виконайте таке тестування:

  • у [1] проводимо симуляцію з недійсними значеннями;
  • у [2] ми успішно отримуємо сторінку помилок, яку щойно створили, що є доказом того, що серверні валідатори спрацювали належним чином.

Далі не забудьте розкоментувати рядки, які ви щойно закомментували у функції JS [faireSimulation].

9.13. Крок 7: створення сеансу користувача

Додаток [Simulateur de calcul de paie] дозволяє користувачеві виконувати різні симуляції розрахунку заробітної плати за посиланням [Faire la simulation], зберігати їх за посиланням [Enregistrer la simulation], переглядати їх за посиланням [Voir les simulations] та видаляти їх за посиланням [Retirer la simulation]. Ми знаємо, що між двома послідовними запитами користувача пам’ять відсутня, якщо її не створити за допомогою механізму сеансу (див. параграф 4.10). Тут цілком очевидно, що ми повинні зберігати в сесії список симуляцій, записаних користувачем з плином часу. Є й інші дані, які потрібно запам’ятати: коли користувач виконує симуляцію, вона записується до списку симуляцій лише в тому випадку, якщо користувач запитує це за посиланням [Enregistrer la simulation]. Коли він це робить, ми повинні мати можливість знайти симуляцію, обчислений у попередньому запиті. Для цього її також буде збережено в сесії. Нарешті, ми будемо нумерувати симуляції, починаючи з 1. Щоб правильно пронумерувати нову симуляцію, потрібно зберегти номер попередньої симуляції, знову ж таки в сесії.

У розділі 4.10 ми ввели поняття моделі сесії як вхідного параметра дії, щоб ця дія мала доступ до сесії. Ми повернемося до цього поняття. Пропонуємо перечитати відповідний розділ, якщо це поняття вам не зрозуміле.

Ми створюємо такий клас [SessionModel]:

Його код такий:


using Pam.Web.Models;
using System.Collections.Generic;

namespace Pam.Web.Models
{
  public class SessionModel
  {
    // список симуляцій
    public List<Simulation> Simulations { get; set; }
    // номер наступного моделювання
    public int NumNextSimulation { get; set; }
    // останнє моделювання
    public Simulation Simulation { get; set; }

    // виробник
    public SessionModel()
    {
      // порожній список моделювань
      Simulations = new List<Simulation>();
      // номер наступного моделювання
      NumNextSimulation = 1;
    }
  }
}

Клас [Simulation] у рядках 9 та 13 буде зберігати інформацію про симуляцію. Що нам потрібно зберегти? Посилання [Faire la simulation] розраховує розрахунковий лист типу [FeuilleSalaire]. Звісно, її слід включити до симуляції. Крім того, нам потрібно зберегти інформацію, на основі якої було сформовано цю платіжну відомість:

  • вибраного працівника. Його можна знайти в полі [FeuilleSalaire.Employe]. Тому немає потреби зберігати його ще раз;
  • кількість відпрацьованих годин і днів. Ця інформація відсутня у типі [FeuilleSalaire]. Тому її потрібно запам’ятати.

Нарешті, кожне моделювання позначається номером. Отже, можна вийти з наступного класу [Simulation]:


using Pam.Metier.Entites;

namespace Pam.Web.Models
{
  public class Simulation
  {
    // номер симуляції
    public int Num { get; set; }
    // кількість відпрацьованих годин
    public double HeuresTravaillées { get; set; }
    // кількість відпрацьованих днів
    public int JoursTravaillés { get; set; }
    // розрахунковий лист
    public FeuilleSalaire FeuilleSalaire { get; set; }
  }
}

Серверна операція [FaireSimulation], окрім розрахунку відомості про заробітну плату, повинна створити симуляцію та додати її до сесії. Для цього вона отримає як параметр шаблон сесії:


// провести симуляцію
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application, SessionModel session, FormCollection data)
    {
      // створення моделі дії
      IndexModel modèle = new IndexModel() { Application = application };
      // спроба отримати значення, занесені в шаблон
      TryUpdateModel(modèle, data);
      // модель дійсна?
      if (!ModelState.IsValid)
      {
        // відображається сторінка помилок
        return PartialView("Erreurs", Static.GetErreursForModel(ModelState));
      }
      // розраховується заробітна плата
      FeuilleSalaire feuilleSalaire = null;
      Exception exception = null;
      try
      {
        // розрахунок заробітної плати
        feuilleSalaire = application.PamMetier.GetSalaire(modèle.SS, modèle.HeuresTravaillées, (int)modèle.JoursTravaillés);
      }
      catch (Exception ex)
      {
        exception = ex;
      }
      // помилка?
      if (exception != null)
      {
        // відображається сторінка помилок
        return PartialView("Erreurs", Static.GetErreursForException(exception));
      }
      // створюється симуляція та додається до сесії
      session.Simulation = ...
      // відображення відомості про заробітну плату
      return PartialView("Simulation", feuilleSalaire);
    }
  • рядок 3: дія отримує як параметр шаблон сесії;

Завдання 1: доповніть код дії, рядок 34



Завдання 2: дотримуючись інструкцій, наведених у параграфі 4.10, зробіть все необхідне, щоб параметр [SessionModel session] дії був правильно ініціалізований фреймворком. Якщо нічого не зробити, для цього параметра буде використано покажчик null.


9.14. Крок 8: збереження симуляції

9.14.1. Проблема

Після виконання симуляції її можна зберегти:

Image

У частковому перегляді [Simulations.cshtml] тепер відображається список симуляцій, виконаних користувачем. Нагадуємо, що розрахована платіжна відомість є фіктивною.

9.14.2. Запис серверної дії [EnregistrerSimulation]

Посилання Ajax [Enregistrer la simulation] викликає серверну дію [EnregistrerSimulation], код якої до цього часу був таким:


    [HttpPost]
    public PartialViewResult EnregistrerSimulation()
    {
      return PartialView("Simulations");
}

Він змінюється наступним чином:


    // зберегти симуляцію
    [HttpPost]
    public PartialViewResult EnregistrerSimulation(SessionModel session)
    {
      // останню виконану симуляцію зберігають у списку симуляцій сесії
      ...
      // збільшується номер наступного моделювання в сесії
      ...
      // відображається список симуляцій
      ...
}
  • рядок 1: дія [EnregistrerSimulation] потребує доступу до сесії. Тому її параметром є модель сесії.

Завдання: доповнити серверну дію [EnregistrerSimulation].


9.14.3. Запис часткового виду [Simulations.cshtml]

Попередня дія [EnregistrerSimulation] відображає частковий вигляд [Simulations.cshtml], шаблоном для якого є список симуляцій, виконаних користувачем. Її код такий:


@model IEnumerable<Simulation>

@using Pam.Web.Models

@if (Model.Count() == 0)
{
  <h2>Votre liste de simulations est vide</h2>
}
@if (Model.Count() != 0)
{
  <h2>Liste des simulations</h2>
...
}

Завдання 1: доповнити код часткового виду [Simulations.cshtml]. Для відображення симуляцій будемо використовувати таблицю HTML. Можна скористатися прикладами з розділу 5.4.


Примітка: посилання [retirer] для кожної симуляції з таблиці HTML буде посиланням JavaScript такого вигляду:

<a href="javascript:retirerSimulation(N)">retirer</a>

де N — номер симуляції.


Завдання 2: протестуйте свій додаток, виконуючи симуляції. Для цього повторюйте таку послідовність дій: 1) завантажте сторінку додатка за посиланням [F5], 2) виконайте симуляцію, 3) збережіть її. Симуляції будуть накопичуватися в сесії, що має відображатися у вікні [Simulations.cshtml].



Завдання 3: вдоскональте частковий вигляд [Simulations.cshtml] таким чином, щоб кольори рядків таблиці HTML чергувалися.


Image

Рядкам <tr> таблиці HTML, класи CSS, [pair] та [impair], визначені у таблиці стилів [/Content/Site.css]:


.impair {
  background-color: beige;
}

.pair {
  background-color: lightsteelblue;
}

9.15. Крок 9: повернутися до форми введення даних

9.15.1. Проблема

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

Image

Image

9.15.2. Запис серверної дії [Formulaire]

Ajax-посилання [Retour au formulaire de simulation] викликає серверну дію [Formulaire], код якої до цього часу був таким:


    [HttpPost]
    public PartialViewResult Formulaire()
    {
      return PartialView("Formulaire");
}

Частковий вигляд [Formulaire], який вона відображає, очікує на модель [IndexModel] (рядок 1 нижче):


@model Pam.Web.Models.IndexModel

@using (Html.BeginForm("FaireSimulation", "Pam", FormMethod.Post, new { id = "formulaire" }))
{
...
}
<div id="simulation" />

Саме через це посилання [Retour au formulaire de simulation] перестало працювати.


Завдання: написати нову версію серверної дії [Formulaire] (переписати 2 рядки), а потім провести тестування.


9.15.3. Зміна функції JavaScript [retourFormulaire]

Після внесених раніше змін тепер можна повернутися до форми, але при цьому виникає така аномалія:

  • у [1] відбувається повернення до форми введення даних;
  • у [2] проводимо симуляцію з помилковими даними. Тоді виявляється, що валідатори на стороні клієнта більше не працюють. У цьому випадку сервер був викликаний і повернув сторінку з помилками завдяки роботі, виконаній у розділі 9.12.4.

Цю аномалію було виявлено та усунуто в розділі 7.6.7.


Завдання: дотримуючись підходу, описаного в розділі 7.6.7, виправте функцію JavaScript [retourFormulaire], а потім проведіть тестування, щоб переконатися, що валідатори на стороні клієнта знову працюють.


9.16. Крок 10: перегляньте список симуляцій

9.16.1. Проблема

Під час роботи з формою симуляції можна переглянути список виконаних симуляцій:

9.16.2. Запис серверної дії [VoirSimulations]

Посилання Ajax [Voir les simulations] викликає серверну дію [VoirSimulations], код якої до цього часу був таким:


    // перегляд симуляцій
    [HttpPost]
    public PartialViewResult VoirSimulations()
    {
      return PartialView("Simulations");
}

Частковий вигляд [Simulations], який вона відображає, очікує на шаблон [IEnumerable<Simulation>] (рядок 1 нижче):


@model IEnumerable<Simulation>

@using Pam.Web.Models

@if (Model.Count() == 0)
{
  <h2>Votre liste de simulations est vide</h2>
}
@if (Model.Count() != 0)
{
  <h2>Liste des simulations</h2>
...
}

Саме через це посилання [Voir les simulations] перестало працювати.


Завдання: написати нову версію серверної дії [VoirSimulations] (переписати 2 рядки), а потім провести тестування.


9.17. Крок 11: завершити сесію

9.17.1. Проблема

У будь-який момент можна завершити сеанс користувача за допомогою посилання [Ajax] [Terminer la session]. Це призводить до закриття поточного сеансу та початку нового. Крім того, відбувається повернення до екрану форми:

  • у [1] було виконано два моделювання, після чого сесія завершена;
  • у [2] ми повернулися до форми введення даних. Ми хочемо переглянути симуляції;
  • у [3] через зміну сесії список симуляцій тепер порожній.

9.17.2. Запис серверної дії [TerminerSession]

Посилання Ajax [Terminer la session] викликає серверну дію [TerminerSession], код якої до цього моменту був таким:


    // завершити сесію
    [HttpPost]
    public PartialViewResult TerminerSession()
    {
      return PartialView("Formulaire");
}

Частковий вигляд [Formulaire], який вона відображає, очікує на модель [IndexModel] (рядок 1 нижче):


@model Pam.Web.Models.IndexModel

@using (Html.BeginForm("FaireSimulation", "Pam", FormMethod.Post, new { id = "formulaire" }))
{
...
}
<div id="simulation" />

Саме через це посилання [Terminer la session] перестало працювати.


Завдання: написати нову версію серверної дії [TerminerSession] (переписати 2 рядки), а потім провести тестування.


Примітка: щоб завершити сесію в дії, потрібно написати:

Session.Abandon() ;

9.17.3. Зміна функції JavaScript [terminerSession]

Після внесеної раніше зміни тепер можна повернутися до форми, але при цьому виникає аномалія, описана раніше в параграфі 9.15.3.


Завдання: дотримуючись підходу, який ви застосовували в розділі 9.15.3, виправте функцію JavaScript [terminerSession], а потім проведіть тестування, щоб переконатися, що валідатори на стороні клієнта знову працюють.


9.18. Крок 12: видалення симуляції

9.18.1. Проблема

Після створення симуляції її можна видалити за допомогою посилання JavaScript [Effacer la simulation]:

9.18.2. Запис дії клієнта [effacerSimulation]

Функція JavaScript [effacerSimulation] наразі має такий код:


function effacerSimulation() {
  // очищення даних у формі
  // ...
  // приховати симуляцію, якщо вона існує
  $("#simulation").hide();
  // меню
  setMenu([lnkFaireSimulation, lnkTerminerSession, lnkVoirSimulations]);
}

Завдання: доповніть цей код. Можна скористатися прикладом із параграфа 7.6.6


9.19. Крок 13: видалити симуляцію

9.19.1. Проблема

На сторінці симуляцій можна видалити деякі з них за допомогою посилання Javascript [retirer]:

Image

Image

9.19.2. Запис дії клієнта [retirerSimulation]

Посилання [retirer] мають такий вигляд: HTML

<a href="javascript:retirerSimulation(N)">retirer</a>

де N — номер симуляції.


Завдання: дотримуючись інструкцій, наведених у параграфах 9.9.3, напишіть функцію JS [retirerSimulation]. Ця функція надсилатиме Ajax-запит типу POST до дії [/Pam/RetirerSimulation]. Вона передаватиме значення N у формі num=N.


Примітка: функція JS [retirerSimulation] аналогічна іншим функціям JS, які ви вже написали і які виконують Ajax-запит до сервера. Єдина відмінність полягає в тому, що POST передає значення, яке не міститься у формі. Відомо, що відправлені значення об’єднуються в рядок у такому форматі:

param1=val1&param2=val2&....

отже, функція JS [retirerSimulation] матиме такий вигляд:


function retirerSimulation(N) {
  // виконати Ajax-запит вручну
  $.ajax({
    url: '/Pam/RetirerSimulation',
...
    data:"num="+N,
...
  });
  // меню
  setMenu([lnkRetourFormulaire, lnkTerminerSession]);
}
  • рядок 6: властивість [data] виклику Ajax JQuery представляє рядок, відправлений на сервер.

9.19.3. Запис серверної дії [RetirerSimulation]

Серверна дія [RetirerSimulation]:

  • отримує надісланий параметр із назвою [num], який є номером симуляції;
  • повинна видалити зі списку симуляцій, збережених у сесії, симуляцію з цим номером;
  • потім має відобразити новий список симуляцій.

Завдання: написати серверну дію [RetirerSimulation]. Перегляньте розділ 4.1, щоб дізнатися, як отримати параметр, надісланий методом POST, з іменем [num].


9.20. Крок 14: вдосконалення методу ініціалізації додатка

Наш веб-додаток готовий. Він працює з імітованим класом [métier]. Нагадаємо архітектуру, яку ми розробили:

Перед тим, як перейти до фактичної реалізації шару [métier], потрібно доопрацювати кілька деталей, і це відбувається в методі ініціалізації додатка: методі [Application_Start] у [Global.asax]:

Метод [Application_Start] у [Global.asax] виконується лише один раз під час запуску додатка. Саме тут можна використовувати файл конфігурації [Web.config]. Наразі наш метод [Application_Start] виглядає так:


// додаток
    protected void Application_Start()
    {
      // ----------Автоматично згенеровано
      AreaRegistration.RegisterAllAreas();
      WebApiConfig.Register(GlobalConfiguration.Configuration);
      FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
      RouteConfig.RegisterRoutes(RouteTable.Routes);
      BundleConfig.RegisterBundles(BundleTable.Bundles);
      // -------------------------------------------------------------------
      // ---------- конкретна конфігурація
      // -------------------------------------------------------------------
      // дані області дії додатка
      ApplicationModel application = new ApplicationModel();
      Application["data"] = application;
      // інстанціювання шару [métier]
      application.PamMetier = new PamMetier();
...
      // зв’язувачі моделей
...
}

У рядку 17 бізнес-шар інстанціюється за допомогою оператора new. Крім того, модель додатка визначається наступним чином:


  public class ApplicationModel
  {
    // --- дані області дії додатка ---
    public Employe[] Employes { get; set; }
    public IPamMetier PamMetier { get; set; }
    public SelectListItem[] EmployesItems { get; set; }
}

У рядку 5 вище видно, що тип властивості [PamMetier] відповідає типу інтерфейсу [IPamMetier]. Це означає, що цю властивість можна ініціалізувати будь-яким об’єктом, що реалізує цей інтерфейс. Однак у рядку 17 [Application_Start] ми жорстко вказали ім’я класу, що реалізує [IPamMetier]. Отже, якщо рівень [métier] буде реалізовано за допомогою нового класу, що реалізує [IPamMetier], цей рядок доведеться змінити. Це не надто важливо, але цього можна уникнути. Визначення класу реалізації інтерфейсу [IPamMetier] можна перенести у файл конфігурації. Щоб змінити реалізацію, достатньо змінити вміст цього файлу конфігурації. Код .NET змінювати не потрібно.

Тут ми будемо використовувати контейнер для ін’єкції залежностей [Spring.net]. Існують й інші фреймворки .NET, які виконують те саме, можливо, краще та простіше.

Архітектура проєкту розвивається наступним чином:

  • до [A]: метод ініціалізації шару [ASP.NET MVC] запитає у [Spring.net] посилання на імітований шар [métier];
  • у [B] [Spring.net] створить імітований шар [métier], використовуючи свій файл конфігурації, щоб визначити, який клас потрібно інстанціювати;
  • у [C] [Spring.net] передасть посилання на симульований шар [métier] шару [ASP.NET MVC].

Слід зауважити, що за замовчуванням об’єкти, якими керує [Spring.net], є синглтонами: вони існують лише в одному екземплярі. Отже, якщо пізніше в нашому прикладі код знову запитає у [Spring.net] посилання на симульований шар [métier], [Spring.net] просто поверне посилання на об’єкт, створений спочатку.

9.20.1. Додавання посилань [Spring] до веб-проєкту

Ми будемо використовувати [Spring.net]. Цей фреймворк постачається у вигляді DLL, який потрібно додати до посилань проекту. Це можна зробити таким чином:

У [1] клацніть правою кнопкою миші на гілці [References] проекту, а потім виберіть опцію [Gérer les packages NuGet]. Потрібен доступ до Інтернету. Далі слід діяти так само, як це було зроблено раніше для бібліотек JQuery та [Globalize]. Знайдіть ключове слово [Spring.core] та встановіть цей пакет. В результаті встановлення з’являться два пакети DLL: [Spring.core] [2] та [Common.Logging] [3]. У наведених нижче прикладах використовувалася версія Spring 1.3.2.

Примітка: якщо у вас немає підключення до Інтернету, ви знайдете ці файли DLL у папці [lib], що міститься в матеріалах цього прикладу.

9.20.2. Налаштування [web.config]

Визначення класу реалізації інтерфейсу [IPamMetier] міститься у файлі [web.config].


<configuration>
  <configSections>
...
    <sectionGroup name="spring">
      <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
      <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
    </sectionGroup>
  </configSections>
  <!-- конфігурація Spring -->
  <spring>
    <context>
      <resource uri="config://spring/objects" />
    </context>
    <objects xmlns="http://www.springframework.net">
      <object id="pammetier" type="Pam.Metier.Service.PamMetier, pam-metier-simule"/>
    </objects>
  </spring>
...
  • рядки 2–8: знайдіть тег <configSections> у файлі та вставте в нього рядки 4–7;
  • рядок 4: атрибут [name="spring"] містить інформацію щодо розділу [spring] у рядках 10–17;
  • рядок 5: визначає клас [Spring.Context.Support.DefaultSectionHandler], розташований у DLL [Spring.Core], як такий, що здатний обробляти розділ [objects] рядків 14–16;
  • рядок 6: визначає клас [Spring.Context.Support.ContextHandler], розташований у DLL [Spring.Core], як такий, що здатний обробляти розділ [context] у рядках 11–13;
  • рядки 11–13: цей розділ містить інформацію [<resource uri="config://spring/objects" />], яка вказує, що об’єкти Spring знаходяться у файлі конфігурації в розділі [/spring/objects], тобто у рядках 14–16;
  • рядки 14–16: тег [objects] вводить об’єкти Spring;
  • рядок 15: визначає об’єкт, ідентифікований як [id="pammetier"], який є екземпляром класу [Pam.Metier.Service.PamMetier], розташованого в DLL [pam-metier-simule]. Тут не можна помилитися. Для атрибута [id] ви можете вказати будь-яке значення. Цей ідентифікатор ви будете використовувати в [Global.asax]. Клас [Pam.Metier.Service.PamMetier] — це клас нашого імітованого шару [métier]. Потрібно повернутися до його визначення, щоб дізнатися повну назву:

namespace Pam.Metier.Service
{
  public class PamMetier : IPamMetier
  {
    ...

Для DLL [pam-metier-simule] потрібно переглянути властивості проекту C# [pam-metier-simule]:

Необхідно використовувати ім’я, вказане в [1].

9.20.3. Зміна [Application_Start]

Метод [Application_Start] змінюється наступним чином:


using Spring.Context.Support;

// додаток
    protected void Application_Start()
    {
      // ----------Автоматично згенеровано
      AreaRegistration.RegisterAllAreas();
      WebApiConfig.Register(GlobalConfiguration.Configuration);
      FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
      RouteConfig.RegisterRoutes(RouteTable.Routes);
      BundleConfig.RegisterBundles(BundleTable.Bundles);
      // -------------------------------------------------------------------
      // ---------- специфічна конфігурація
      // -------------------------------------------------------------------
      // дані області дії додатка
      ApplicationModel application = new ApplicationModel();
      Application["data"] = application;
      // інстанціювання шару [métier]
      application.PamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
...
      // зв’язувачі моделей
...
}
  • рядок 19: використовується клас Spring [ContextRegistry], який здатний обробляти файл [web.config]. Для цього потрібно імпортувати простір імен із рядка 1. Статичний метод [GetContext] дозволяє отримати вміст тегів [context], які вказують, де знаходяться об’єкти Spring. Статичний метод [GetObject] дозволяє потім отримати конкретний об’єкт, ідентифікований за його атрибутом id. Зверніть увагу, що тепер ім’я класу, який реалізує інтерфейс [IPamMetier], більше не вказано безпосередньо в коді. Воно тепер міститься у файлі [web.config].

Після внесення всіх цих змін протестуйте свій додаток. Він має працювати.

9.20.4. Обробка помилки ініціалізації додатка

У методі [Application_Start] ми написали:


application.PamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;

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

  • найбільш очевидна — помилка в імені об’єкта, який потрібно інстанціювати;
  • інша — те, що створення екземпляра шару [métier] проходить з помилками. Це не може статися для нашого імітованого шару [métier], але може статися для нашого реального шару [métier], який буде підключений до бази даних. SGBD може не запускатися, інформація про базу даних, яку потрібно обробляти, може бути неправильною тощо...

Ми будемо обробляти можливе виключення за допомогою блоку try/catch. Код виглядає наступним чином:


// додаток
    protected void Application_Start()
    {
      // ----------Автоматично згенеровано
...
      // -------------------------------------------------------------------
      // ---------- специфічна конфігурація
      // -------------------------------------------------------------------
      // дані області дії додатка
      ApplicationModel application = new ApplicationModel();
      Application["data"] = application;
      application.InitException = null;
      try
      {
        // інстанціювання шару [métier]
        application.PamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
      }
      catch (Exception ex)
      {
        application.InitException = ex;
      }
      //якщо немає помилок
      if (application.InitException == null)
      {
....
      }
      // модельні зв’язувачі
...
    }
  • у рядку 12 ми додаємо нову властивість із назвою [InitException] до моделі додатка:

  public class ApplicationModel
  {
    // --- дані області дії додатка ---
    public Employe[] Employes { get; set; }
    public IPamMetier PamMetier { get; set; }
    public SelectListItem[] EmployesItems { get; set; }
    public Exception InitException { get; set; }
}
  • у рядку 7 вище — виняток, який може виникнути під час ініціалізації додатка;
  • рядки 13–21 у [Application_Start]: інстанціювання шару [métier] тепер відбувається в блоці try/catch;
  • рядок 20: виняток зберігається;
  • рядки 23–26: якщо помилки не сталося, виконується код, який був раніше;
  • рядок 28: об’єкти [ModelBinders] створюються незалежно від того, чи сталася помилка. Це важливо. Ми хочемо переконатися, що модель додатка [ApplicationModel] буде належним чином пов’язана фреймворком.

Ми знаємо, що під час запуску додатка виконується серверна дія [Index]. Наразі вона виглядає так:


    [HttpGet]
    public ViewResult Index(ApplicationModel application)
    {
      return View(new IndexModel() { Application = application });
}

У рядку 2 дія [Index] отримує модель додатка. Таким чином, вона може визначити, чи пройшла ініціалізація успішно, і відобразити сторінку з повідомленням про помилку, якщо ініціалізація з якихось причин завершилася невдало. Ми модифікуємо код наступним чином:


    [HttpGet]
    public ViewResult Index(ApplicationModel application)
    {
      // помилка ініціалізації?
      if (application.InitException != null)
      {
        // сторінка помилок без меню
        return View("InitFailed",Static.GetErreursForException(application.InitException));
      }
      // помилки немає
      return View(new IndexModel() { Application = application });
}

У рядку 8, у разі помилки ініціалізації, ми відображаємо представлення [InitFailed.cshtml], використовуючи як шаблон список повідомлень про помилки винятку, що виник під час ініціалізації. Метод [Static.GetErreursForException] було представлено та пояснено в розділі 9.12.4. Вигляд [InitFailed.cshtml] буде таким:

Його код такий:


@model IEnumerable<string>
@{
  Layout = null;
}
<!DOCTYPE html>
<html>
<head>
  <title>@ViewBag.Title</title>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width" />
  <link rel="stylesheet" href="~/Content/Site.css" />
</head>
<body>
  <table>
    <tbody>
      <tr>
        <td>
          <h2>Simulateur de calcul de paie</h2>
        </td>
    </tbody>
  </table>
  <hr />
  <h2>Les erreurs suivantes se sont produites à l'initialisation de l'application : </h2>
  <ul>
    @foreach (string msg in Model)
    {
      <li>@msg</li>
    }
  </ul>
</body>
</html>
  • рядок 1: шаблон подання — це список повідомлень про помилки. Вони відображаються у списку HTML у рядках 24–29;
  • рядок 3: цей вигляд не використовує головну сторінку [_Layout.cshtml]. Адже нам не потрібне меню, яке надає цей документ. Тому ми створюємо повну сторінку HTML (рядки 5–23).

Для тестування достатньо змінити в [Application_Start] екземпляр шару [métier] наступним чином:


      try
      {
        // інстанціювання шару [métier]
        application.PamMetier = ContextRegistry.GetContext().GetObject("xx") as IPamMetier;
      }
      catch (Exception ex)
      {
        application.InitException = ex;
}

У рядку 4 шукається об’єкт, якого немає серед об’єктів Spring.

Після збереження цих змін і запуску додатка з’являється така сторінка:

 

З’являється сторінка з помилками без меню. Користувач не може нічого зробити, окрім як констатувати помилку. Саме цього й хотілося досягти.

9.21. На якому етапі ми перебуваємо?

Тепер ми маємо робочий веб-додаток, який працює з імітованим бізнес-шаром. Його архітектура така:

Рівень [ASP.NET MVC] взаємодіє з імітованим бізнес-рівнем через інтерфейс [IPamMetier]. Якщо ми замінимо цей імітований бізнес-рівень на реальний бізнес-рівень, який відповідає цьому інтерфейсу, нам не доведеться змінювати код веб-рівня. Завдяки [Spring.net] нам достатньо буде лише змінити в [web.config] клас реалізації інтерфейсу [IPamMetier]. Ми рухаємося в цьому напрямку.

Нова архітектура буде такою:

Ми послідовно опишемо:

  • рівень [EF5], пов’язаний із SGBD. Він буде реалізований за допомогою Entity Framework 5 (EF5);
  • шар [DAO], який керує доступом до даних через шар [EF5]. Це дозволяє йому ігнорувати існування SGBD. Цей рівень обмежується обробкою сутностей додатка [Employe, Cotisations, Indemnites];
  • шар [métier], який реалізує розрахунок заробітної плати.

Нова архітектура — це та, що була представлена на самому початку цього документа в розділі 1.1, і яку ми зараз нагадаємо:

  • шар [Web] — це шар, що взаємодіє з користувачем веб-додатка. Користувач взаємодіє з веб-додатком через веб-сторінки, що відображаються у браузері. Саме в цьому рівні розташовані ASP.NET та MVC, і лише в цьому рівні;
  • шар [métier] реалізує правила управління додатком, такі як розрахунок заробітної плати або рахунку-фактури. Цей рівень використовує дані, що надходять від користувача через рівень [Web], а також дані з SGBD через рівень [DAO];
  • шар [DAO] (об’єкти доступу до даних), шар [ORM] (об’єктно-реляційний мапер) та коннектор ADO.NET керують доступом до даних шару SGBD. Рівень [ORM] забезпечує зв’язок між об’єктами, що обробляються рівнем [DAO], та рядками й стовпцями даних реляційної бази даних. У світі найчастіше використовуються два ORM: NET, NHibernate (http://sourceforge.net/projects/nhibernate/) та Entity Framework (http://msdn.microsoft.com/en-us/data/ef.aspx);
  • інтеграцію шарів можна здійснити за допомогою контейнера введення залежностей (Dependency Injection Container), такого як Spring (http://www.springframework.net/ );

Шари [métier], [DAO], [EF5] будуть реалізовані за допомогою проектів на C#. Відтепер ми працюємо з Visual Studio Express 2012 для настільних комп’ютерів.

9.22. Крок 15: налаштування шару Entity Framework 5

Створення шару [EF5] — це не стільки питання кодування, скільки налаштування. Щоб зрозуміти, як писати цей шар, слід ознайомитися з документом [Introduction à Entity Framework 5 Code First], доступним на URL [http://tahe.developpez.com/dotnet/ef5cf-02/]. Це досить об’ємний документ. Основні відомості містяться у перших чотирьох розділах. Буде вказано, на які саме параграфи слід звернути особливу увагу. Коли ми будемо посилатися на цей документ, ми використовуватимемо позначення [refEF5].

Крім того, іноді нам знадобляться поняття з мови C#. У таких випадках ми будемо посилатися на курс [Introduction au langage C#], доступний за посиланням URL [http://tahe.developpez.com/dotnet/csharp/], використовуючи позначення [refC#].

9.22.1. База даних

База даних додатка була представлена в розділі 9.4. Це база даних MySQL під назвою [dbpam_ef5] (pam=Paie Assistante Maternelle). Ця база має адміністратора з іменем root без пароля.

Нагадаємо схему бази даних. Вона містить три таблиці:

Image

Між стовпцем EMPLOYES (INDEMNITE_ID) та стовпцем INDEMNITES (ID) існує зв’язок через зовнішній ключ. Частина структури цієї бази даних визначається її використанням разом із EF5.

Скрипт SQL для створення бази даних має такий вигляд:


-- phpMyAdmin SQL Злив
-- версія 3.5.1
-- http://www.phpmyadmin.net
--
-- Клієнт: localhost
-- Створено: понеділок, 4 листопада 2013 року о 09:34
-- Версія сервера: 5.5.24-log
-- Версія PHP: 5.4.3

SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
SET time_zone = "+00:00";


/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;

--
-- База даних: `dbpam_ef5`
--

-- --------------------------------------------------------

--
-- Структура таблиці `cotisations`
--

CREATE TABLE IF NOT EXISTS `cotisations` (
  `ID` bigint(20) NOT NULL AUTO_INCREMENT,
  `SECU` double NOT NULL,
  `RETRAITE` double NOT NULL,
  `CSGD` double NOT NULL,
  `CSGRDS` double NOT NULL,
  `VERSIONING` int(11) NOT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=12 ;

--
-- Зміст таблиці `cotisations`
--

INSERT INTO `cotisations` (`ID`, `SECU`, `RETRAITE`, `CSGD`, `CSGRDS`, `VERSIONING`) VALUES
(11, 9.39, 7.88, 6.15, 3.49, 1);

--
-- Тригери `cotisations`
--
DROP TRIGGER IF EXISTS `INCR_VERSIONING_COTISATIONS`;
DELIMITER //
CREATE TRIGGER `INCR_VERSIONING_COTISATIONS` BEFORE UPDATE ON `cotisations`
 FOR EACH ROW BEGIN
  SET NEW.VERSIONING:=OLD.VERSIONING+1;
END
//
DELIMITER ;
DROP TRIGGER IF EXISTS `START_VERSIONING_COTISATIONS`;
DELIMITER //
CREATE TRIGGER `START_VERSIONING_COTISATIONS` BEFORE INSERT ON `cotisations`
 FOR EACH ROW BEGIN
  SET NEW.VERSIONING:=1;
END
//
DELIMITER ;

-- --------------------------------------------------------

--
-- Структура таблиці `employes`
--

CREATE TABLE IF NOT EXISTS `employes` (
  `ID` bigint(20) NOT NULL AUTO_INCREMENT,
  `PRENOM` varchar(20) CHARACTER SET latin1 NOT NULL,
  `SS` varchar(15) CHARACTER SET latin1 NOT NULL,
  `ADRESSE` varchar(50) CHARACTER SET latin1 NOT NULL,
  `CP` varchar(5) CHARACTER SET latin1 NOT NULL,
  `VILLE` varchar(30) CHARACTER SET latin1 NOT NULL,
  `NOM` varchar(30) CHARACTER SET latin1 NOT NULL,
  `VERSIONING` int(11) NOT NULL,
  `INDEMNITE_ID` bigint(20) NOT NULL,
  PRIMARY KEY (`ID`),
  UNIQUE KEY `SS` (`SS`),
  KEY `FK_EMPLOYES_INDEMNITE_ID` (`INDEMNITE_ID`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=26 ;

--
-- Вміст таблиці `employes`
--

INSERT INTO `employes` (`ID`, `PRENOM`, `SS`, `ADRESSE`, `CP`, `VILLE`, `NOM`, `VERSIONING`, `INDEMNITE_ID`) VALUES
(24, 'Marie', '254104940426058', '5 rue des oiseaux', '49203', 'St Corentin', 'Jouveinal', 1, 93),
(25, 'Justine', '260124402111742', 'La Brûlerie', '49014', 'St Marcel', 'Laverti', 1, 94);

--
-- Тригери `employes`
--
DROP TRIGGER IF EXISTS `INCR_VERSIONING_EMPLOYES`;
DELIMITER //
CREATE TRIGGER `INCR_VERSIONING_EMPLOYES` BEFORE UPDATE ON `employes`
 FOR EACH ROW BEGIN
  SET NEW.VERSIONING:=OLD.VERSIONING+1;
END
//
DELIMITER ;
DROP TRIGGER IF EXISTS `START_VERSIONING_EMPLOYES`;
DELIMITER //
CREATE TRIGGER `START_VERSIONING_EMPLOYES` BEFORE INSERT ON `employes`
 FOR EACH ROW BEGIN
  SET NEW.VERSIONING:=1;
END
//
DELIMITER ;

-- --------------------------------------------------------

--
-- Структура таблиці `indemnites`
--

CREATE TABLE IF NOT EXISTS `indemnites` (
  `ID` bigint(20) NOT NULL AUTO_INCREMENT,
  `ENTRETIEN_JOUR` double NOT NULL,
  `REPAS_JOUR` double NOT NULL,
  `INDICE` int(11) NOT NULL,
  `INDEMNITES_CP` double NOT NULL,
  `BASE_HEURE` double NOT NULL,
  `VERSIONING` int(11) NOT NULL,
  PRIMARY KEY (`ID`),
  UNIQUE KEY `INDICE` (`INDICE`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=95 ;

--
-- Зміст таблиці `indemnites`
--

INSERT INTO `indemnites` (`ID`, `ENTRETIEN_JOUR`, `REPAS_JOUR`, `INDICE`, `INDEMNITES_CP`, `BASE_HEURE`, `VERSIONING`) VALUES
(93, 2.1, 3.1, 2, 15, 2.1, 1),
(94, 2, 3, 1, 12, 1.93, 1);

--
-- Тригери `indemnites`
--
DROP TRIGGER IF EXISTS `INCR_VERSIONING_INDEMNITES`;
DELIMITER //
CREATE TRIGGER `INCR_VERSIONING_INDEMNITES` BEFORE UPDATE ON `indemnites`
 FOR EACH ROW BEGIN
  SET NEW.VERSIONING:=OLD.VERSIONING+1;
END
//
DELIMITER ;
DROP TRIGGER IF EXISTS `START_VERSIONING_INDEMNITES`;
DELIMITER //
CREATE TRIGGER `START_VERSIONING_INDEMNITES` BEFORE INSERT ON `indemnites`
 FOR EACH ROW BEGIN
  SET NEW.VERSIONING:=1;
END
//
DELIMITER ;

--
-- Обмеження для експортованих таблиць
--

--
-- Обмеження для таблиці `employes`
--
ALTER TABLE `employes`
  ADD CONSTRAINT `FK_EMPLOYES_INDEMNITE_ID` FOREIGN KEY (`INDEMNITE_ID`) REFERENCES `indemnites` (`ID`);

/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

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

  • рядки 30, 73, 122: первинні ключі таблиць знаходяться в режимі [AUTO_INCREMENT]. Їх управляє MySQL, а не EF5;
  • рядок 83: номер SS має обмеження унікальності;
  • рядок 130: індекс співробітника має обмеження унікальності;
  • рядки 168–169: зовнішній ключ таблиці [employes] до таблиці [indemnites];
  • рядок 49: тригер [Trigger] — це скрипт SQL, вбудований у SGBD, який виконується в певні моменти;
  • рядки 51–54: тригер [INCR_VERSIONING_COTISATIONS] спрацьовує перед будь-якою зміною рядка в таблиці [cotisations]. Після цього він збільшує значення стовпця [VERSIONING] на одиницю;
  • рядки 59–62: тригер [START_VERSIONING_COTISATIONS] спрацьовує перед кожним вставленням нового рядка в таблицю [cotisations]. Після цього він ініціалізує стовпець [VERSIONING] до значення 1;
  • у підсумку значення стовпця [VERSIONING] дорівнює 1 під час створення рядка в таблиці [cotisations], а потім збільшується на 1 при кожній зміні, що вноситься до цього рядка. Цей механізм дозволяє EF5 керувати конкуренцією доступу до рядка таблиці [cotisations] наступним чином:
    • процес P1 зчитує рядок L з таблиці [cotisations] у момент часу T1. Цей рядок має стовпець [VERSIONING] V1;
    • процес P2 зчитує той самий рядок L з таблиці [cotisations] у момент часу T2. Рядок має стовпці [VERSIONING] та V1, оскільки процес P1 ще не підтвердив свою зміну;
    • процес P1 змінює рядок L і підтверджує цю зміну. Після цього значення стовпця [VERSIONING] у рядку L змінюється на V1+1 внаслідок спрацьовування тригера [INCR_VERSIONING_COTISATIONS];
    • потім процес P2 робить те саме. EF5 генерує виняток, оскільки процес P2 має рядок, у якому стовпець [VERSIONING] містить значення V1, яке відрізняється від знайденого в базі даних значення V1+1. Змінити рядок можна лише в тому випадку, якщо значення [VERSIONING] збігається з тим, що міститься в базі даних.

Це називається оптимістичним управлінням паралельних звернень. У випадку з EF5 поле, яке виконує цю роль, повинно мати анотацію [ConcurrencyCheck].

  • Аналогічний механізм створюється для таблиці [employes] (рядки 98–113) та таблиці [indemnites] (рядки 144–159).

Завдання: створіть базу даних MySQL та [dbpam_ef5] за допомогою попереднього скрипта SQL. Базу даних [dbpam_ef5] потрібно створити заздалегідь, оскільки скрипт її не створює. Потім на цій базі буде виконано скрипт SQL.


9.22.2. Проєкт Visual Studio

У Visual Studio Express 2012 для настільних ПК завантажуємо рішення [pam-td], яке використовувалося під час побудови шару [web]:

  • у [1], VS 2012 Express для настільних ПК не може завантажити веб-проект [pam-web-01]. Це нормально і не створює проблем;
  • у [2] додається новий проєкт до рішення [pam-td];
  • у [3] проект має тип [console] і називається [4] [pam-ef5];
  • у [5] — створений проект. Його назва не виділена жирним шрифтом, отже, це не стартовий проект рішення;
  • у [6] та [7] новий проєкт визначено як стартовий.

9.22.3. Додавання необхідних посилань до проекту

Розглянемо проект у цілому:

Нашому проєкту потрібні певні DLL:

  • DLL для Entity Framework 5;
  • DLL для коннектора ADO.NET з SGBD MySQL.

У розділі 4.2 [refEF5] пояснюється, як встановити ці DLL за допомогою інструменту [NuGet]. Наразі (листопад 2013 р.) доступною версією Entity Framework є версія 6 (EF6). На жаль, схоже, що коннектор ADO.NET з SGBD MySQL, доступний (листопад 2013 р.) через [NuGet], несумісний з EF6. Тому ми помістили в папку [lib] [1] файл DLL зEF5, а також інші файли DLL, необхідні для проекту [pam-ef5]

Ми розмістили інші DLL у папці [lib]. Ми використаємо їх пізніше. У [2] ми додаємо ці нові DLL до проєкту.

  • у файлі [3] переходимо по файловій системі до папки [lib];
  • у [4] вибираємо три DLL, а потім двічі підтверджуємо;
  • у [5] три файли DLL додано до посилань проєкту.

Нам потрібен ще один DLL. Його можна знайти серед файлів фреймворку .NET на комп’ютері.

  • у [1] додайте нове посилання на проект;
  • у [2] виберіть [Assemblys];
  • у [3] введіть [system.component];
  • у [4] виберіть збірку [System.ComponentModel.DataAnnotations];
  • у [5] посилання додано.

Тепер ми готові до написання коду та налаштування.

9.22.4. Ентітети Entity Framework

Ентітети Entity Framework — це класи, в яких інкапсульовані записи різних таблиць бази даних. Нагадаємо їх:

Image

У шарі [web] ми використовували сутності [Employe, Cotisations, Indemnités] (див. розділ 9.7.3, сторінка 219). Вони не були точним відображенням таблиць. Тому стовпці [ID, VERSIONING] були проігноровані. Тут такого не відбудеться, оскільки вони використовуються об’єктами ORM та EF5. Тому ми додамо до них відсутні властивості. Ми створюємо ці об’єкти в папці [Models] проекту:

Тепер їхній новий код виглядає так:

Клас [Cotisations]


using System;

namespace Pam.EF5.Entites
{
  public class Cotisations
  {
    public int Id { get; set; }
    public double CsgRds { get; set; }
    public double Csgd { get; set; }
    public double Secu { get; set; }
    public double Retraite { get; set; }
    public int Versioning { get; set; }

    // підпис
    public override string ToString()
    {
      return string.Format("Cotisations[{0},{1},{2},{3}, {4}, {5}]", Id, Versioning, CsgRds, Csgd, Secu, Retraite);
    }
  }
}
  • рядок 3: простір імен було адаптовано до нового проєкту;
  • були додані властивості рядків 7 і 12, щоб відобразити структуру таблиці [cotisations];
  • рядок 17: метод [ToString] тепер відображає обидва нові поля.

Клас [Indemnites]


using System;

namespace Pam.EF5.Entites
{
  public class Indemnites
  {
    public int Id { get; set; }
    public int Indice { get; set; }
    public double BaseHeure { get; set; }
    public double EntretienJour { get; set; }
    public double RepasJour { get; set; }
    public double IndemnitesCp { get; set; }
    public int Versioning { get; set; }

    // підпис
    public override string ToString()
    {
      return string.Format("Indemnités[{0},{1},{2},{3},{4}, {5}, {6}]", Id, Versioning, Indice, BaseHeure, EntretienJour, RepasJour, IndemnitesCp);
    }
  }
}
  • рядок 3: простір імен було адаптовано до нового проєкту;
  • були додані властивості рядків 7 та 13, щоб відобразити структуру таблиці [indemnites];
  • рядок 18: метод [ToString] тепер відображає обидва нові поля.

Клас [Employe]


using System;

namespace Pam.EF5.Entites
{

  public class Employe
  {
    public int Id { get; set; }
    public string SS { get; set; }
    public string Nom { get; set; }
    public string Prenom { get; set; }
    public string Adresse { get; set; }
    public string Ville { get; set; }
    public string CodePostal { get; set; }
    public Indemnites Indemnites { get; set; }
    public int Versioning { get; set; }

    // підпис
    public override string ToString()
    {
      return string.Format("Employé[{0},{1},{2},{3},{4},{5}, {6}, {7}]", Id, Versioning, SS, Nom, Prenom, Adresse, Ville, CodePostal);
    }
  }
}
  • рядок 3: простір імен було адаптовано до нового проєкту;
  • були додані властивості рядків 8 і 16, щоб відобразити структуру таблиці [employes];
  • рядок 21: метод [ToString] тепер відображає обидва нові поля.

Щоб ці класи могли використовуватися в ORM та EF5, їхні властивості повинні бути позначені анотаціями.


Завдання: скориставшись параграфом 3.4 [Création de la base à partir des entités] з [refEF5], додайте до об’єктів [Employe, Cotisations, Indemnites] анотації, необхідні для EF5.


Поради:

  • потрібно лише створити анотації. Не дотримуйтесь частини [création de base] з зазначеного параграфа;
  • для анотації [Table] слід дотримуватися прикладу MySQL з пункту 4.2 документа [refEF5];
  • для анотації [ConcurrencyCheck] щодо властивості [Versioning] слід керуватися прикладом Oracle з пункту 5.2 документа [refEF5];
  • щодо зовнішнього ключа, який таблиця [employes] має до таблиці [indemnités], дотримуйтесь прикладу 3.4.2 з документа [refEF5]. Таким чином ви додасте нову властивість до сутності [Employe]:

    public int IndemniteId { get; set; }

значення якого буде відповідати значенню стовпця [INDEMNITES_ID] у таблиці [employes]. Ви додасте анотації зовнішнього ключа до властивостей [IndemniteId] та [Indemnites] сутності [Employe]. Для цього дотримуйтесь прикладу 3.4.2 для [refEF5];

  • ви не будете обробляти зворотні зв’язки зовнішніх ключів;
  • ця робота вимагає ознайомлення з [refEF5].

9.22.5. Налаштування ORM та EF5

Розглянемо проект у цілому:

Шар [EF5] отримуватиме доступ до бази даних через коннектор [ADO.NET] з SGBD MySQL. Для доступу до цієї бази їй потрібна певна інформація. Вона розміщена в різних місцях проекту.

Спочатку нам потрібно створити контекст бази даних. Цей контекст — це клас, похідний від системного класу [System.Data.Entity.DbContext]. Він слугує для визначення об’єктних зображень таблиць бази даних. Ми розмістимо цей клас у папці [Models] проекту разом з сутностями EF5:

Клас [DbPamContext] матиме такий вигляд:


using Pam.EF5.Entites;
using System.Data.Entity;

namespace Pam.Models
{
  public class DbPamContext : DbContext
  {
    public DbSet<Employe> Employes { get; set; }
    public DbSet<Cotisations> Cotisations { get; set; }
    public DbSet<Indemnites> Indemnites { get; set; }
  }
}
  • рядок 6: клас [DbPamContext] походить від системного класу [DbContext];
  • рядки 8–10: об’єкти-зображення трьох таблиць бази даних. Їхній тип — [DbSet<Entity>], де [Entity] — це одна з сутностей Entity Framework, яку ми щойно визначили. Тип [DbSet] можна розглядати як колекцію сутностей. Запити до неї можна виконувати за допомогою LINQ (Language INtegrated Query). Читачам, які не знайомі з LINQ, рекомендується ознайомитися з параграфом 3.5.4 [Apprentissage de LINQ avec LINQPad] у [refEF5].

Надалі ми будемо називати клас [DbPamContext] контекстом персистентності бази даних [dbpam_ef5]. Це загальноприйнята термінологія в ORM (об’єктно-реляційний мапер). Цей контекст збереження є об’єктним відображенням бази даних. Також говорять про синхронізацію контексту збереження з базою даних: зміни, додавання та видалення, здійснені в контексті збереження, відображаються у базі даних. Ця синхронізація відбувається у певні моменти: під час закриття контексту збереження, після завершення транзакції або перед виконанням запиту SQL SELECT до бази даних.

Інформація про SGBD та базу даних розміщується в [App.config].

Необхідні налаштування у файлі [app.config] пояснюються у наступних параграфах файлу [refEF5]:

  • 3.4 для сервера SGBD SQL. Саме там викладено основні принципи налаштування EF5;
  • 4.2 для SGBD та MySQL.

Дотримуючись цього останнього абзацу, ми налаштовуємо файл [app.config] наступним чином:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
    <!-- конфігурація EF5 -->
    <!-- рядок підключення до бази даних [dbam_ef5] -->
    <connectionStrings>
        <add name="DbPamContext"
         connectionString="Server=localhost;Database=dbpam_ef5;Uid=root;Pwd=;"
         providerName="MySql.Data.MySqlClient" />
    </connectionStrings>
    <!--— заводський постачальник MySQL -->
    <system.data>
        <DbProviderFactories>
            <remove invariant="MySql.Data.MySqlClient"/>
            <add name="MySQL Data Provider" invariant="MySql.Data.MySqlClient" description=".Net Framework Data Provider for MySQL"
          type="MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version=6.5.4.0, Culture=neutral, PublicKeyToken=C5687FC88969C44D"
        />
        </DbProviderFactories>
    </system.data>
</configuration>
  • додано рядки 6–21. Їх слід вставити в тег <configuration> у рядках 2 та 22;
  • рядки 8–12: визначають рядки підключення до баз даних, концепція ADO.NET (див. параграф 7.3.5 у [refC#]);
  • рядки 9–11: визначають рядок підключення до бази даних MySQL [dbpam_ef5];
  • рядок 9: ім’я рядка підключення. Тут не можна вказати що завгодно. За замовчуванням слід вказати ім’я класу, що реалізує контекст бази даних:

  public class DbPamContext : DbContext
  {
    public DbSet<Employe> Employes { get; set; }
    public DbSet<Cotisations> Cotisations { get; set; }
    public DbSet<Indemnites> Indemnites { get; set; }
}

Клас називається [DbPamContext]. У рядку 9 файлу [app.config] слід вказати [name="DbPamContext"];

  • у рядку 10 — рядок підключення, характерний для SGBD MySQL:
    • [Server=localhost]: адреса IP комп’ютера, на якому розміщено SGBD. У даному випадку це локальний комп’ютер [localhost];
    • [Database=dbpam_ef5;]: назва бази даних,
    • [Uid=root;]: логін, за допомогою якого будемо підключатися до бази даних,
    • [Pwd=;]: пароль для цього логіна. Тут пароль відсутній;
  • рядок 10: [providerName="MySql.Data.MySqlClient"] — це ім’я коннектора ADO.NET, який слід використовувати. Ця назва відповідає атрибуту [invariant] у рядку 17. Можна вказати будь-що, якщо дотримуватися попереднього правила і якщо провайдер з таким самим інваріантом ще не зареєстрований;
  • рядки 15–20: визначають фабрику (factory) коннекторів (provider) ADO.NET. [DbProviderFactory] — це поняття, яке для мене є дещо незрозумілим. Якщо судити з назви, це, мабуть, клас, здатний генерувати провайдер ADO.NET, який надає доступ до SGBD, а тут — до MySQL5. Зазвичай ці рядки просто копіюють і вставляють. Вони є необхідними. Зверніть увагу на атрибут [Version=6.5.4.0] у рядку 16. Цей номер версії повинен відповідати номеру версії DLL [MySql.Data], який ви додали до посилань проекту:
  • 16-й рядок є важливим. Оскільки не можна встановити два провайдерів з однаковими іменами, спочатку потрібно видалити будь-якого вже встановленого провайдера, який мав би ім’я того, що ми встановлюємо в 17-му рядку;

Ось і все. Це складно і незрозуміло, коли робиш це вперше, але з часом стає просто, бо повторюється завжди одне й те саме.

9.22.6. Тестування шару [EF5]

Ми готові протестувати наш шар [EF5]. Це робимо за допомогою програми [Program.cs], яка вже встановлена:

Ми відобразимо вміст бази даних. Якщо це вдасться, це буде першою ознакою того, що наша конфігурація правильна. Приклад коду наведено в розділі 3.5.3 [refEF5]. Код [Program.cs] буде таким:


using Pam.EF5.Entites;
using Pam.Models;
using System;

namespace Pam
{
  class Program
  {
    static void Main(string[] args)
    {
      try
      {
        using (var context = new DbPamContext())
        {
          // відображається вміст таблиць
          Console.WriteLine("Liste des employés ----------------------------------------");
          foreach (Employe employe in context.Employes)
          {
            Console.WriteLine(employe);
          }
          Console.WriteLine("Liste des indemnités --------------------------------------");
          foreach (Indemnites indemnite in context.Indemnites)
          {
            Console.WriteLine(indemnite);
          }
          Console.WriteLine("Liste des cotisations -------------------------------------");
          foreach (Cotisations cotisations in context.Cotisations)
          {
            Console.WriteLine(cotisations);
          }
        }
      }
      catch (Exception e)
      {
        Console.WriteLine(e);
        return;
      }
    }
  }
}
  • рядок 13: будь-яка операція над BD виконується через контекст цієї бази даних. Ми реалізували цей контекст за допомогою класу [DbPamContext]. Ми також назвали його контекстом збереження бази даних;
  • рядки 13, 31: операції з контекстом збереження виконуються у клаузулі [using]. Контекст збереження відкривається на початку клаузули [using] і автоматично закривається після виходу з цієї клаузули. Це означає, що будь-які зміни, внесені до контексту збереження в клаузулі [using], будуть відображені в базі даних після виходу з клаузули. Потім у межах транзакції до BD надсилається серія команд SQL. Це означає, що якщо команда SQL завершиться з помилкою, усі раніше відправлені команди SQL будуть скасовані. У цьому випадку EF5 генерує виняток;
  • рядок 17: вираз [context.Employes] позначає об’єкт-образ таблиці [employes]. Нагадаємо, що [Employes] є властивістю контексту персистентності [DbPamContext]:

  public class DbPamContext : DbContext
  {
    public DbSet<Employe> Employes { get; set; }
    public DbSet<Cotisations> Cotisations { get; set; }
    public DbSet<Indemnites> Indemnites { get; set; }
}
  • рядок 17: те, що [foreach] обходить колекцію [context.Employes], призведе до повернення всіх співробітників з бази даних у контекст збереження. Отже, EF5 видасть запит SQL SELECT;
  • рядки 17–20: відбувається обхід колекції співробітників, а в рядку 19 використовується метод [ToString] класу [Employe] для виведення співробітників на консоль;
  • рядки 21–25: те саме для колекції виплат;
  • рядки 27–30: те саме для колекції внесків.

Повернемося до визначення сутності [Employe]:


using System;

namespace Pam.EF5.Entites
{

  public class Employe
  {
    public int Id { get; set; }
    public string SS { get; set; }
    public string Nom { get; set; }
    public string Prenom { get; set; }
    public string Adresse { get; set; }
    public string Ville { get; set; }
    public string CodePostal { get; set; }
    public Indemnites Indemnites { get; set; }
    public int Versioning { get; set; }

    // підпис
    public override string ToString()
    {
      return string.Format("Employé[{0},{1},{2},{3},{4},{5}, {6}, {7}]", Id, Versioning, SS, Nom, Prenom, Adresse, Ville, CodePostal);
    }
  }
}
  • рядок 15: працівник має посилання на виплату.

Коли ми повертаємо працівника в контекст збереження, чи повертаємо разом з ним і його виплату? За замовчуванням відповідь — ні. Це поняття, яке відображає суть [Lazy Loading]. Об’єкти, на які є посилання в іншому об’єкті, не переносяться в контекст персистентності разом із цим іншим об’єктом. Вони переносяться лише тоді, коли їх запитує код у відкритому контексті персистентності. Якщо контекст персистентності закритий, генерується виняток.

Отже, якби метод [ToString] посилався на властивість [Indemnites], як показано нижче:


    // підпис
    public override string ToString()
    {
      return string.Format("Employé[{0},{1},{2},{3},{4},{5},{6},{7},{8}]", Id, Versioning, SS, Nom, Prenom, Adresse, Ville, CodePostal, Indemnites);
}

то наступна операція в [Program.cs]:


          foreach (Employe employe in context.Employes)
          {
            Console.WriteLine(employe);
}

повернула б у контекст збереження не тільки співробітників, але й їхні виплати, оскільки у рядку 3 викликається метод [Employe.ToString], який посилається на суть [Indemnites].

Виконання [Program.cs] дає такі результати:

1
2
3
4
5
6
7
8
Liste des employés -----------------------------------------
Employé[24,1,254104940426058,Jouveinal,Marie,5 rue des oiseaux,St Corentin,49203]
Employé[25,1,260124402111742,Laverti,Justine,La Brûlerie,St Marcel,49014]
Liste des indemnités -----------------------------------------
Indemnités[93,1,2,2,1,2,1,3,1,15]
Indemnités[94,1,1,1,93,2,3,12]
Liste des cotisations -----------------------------------------
Cotisations[11,1,3,49,6,15,9,39,7,88]

Що робити, якщо це не працює? У вас щось не так... Існує багато можливих джерел помилок:

  • перевірте конфігурацію EF5 (параграф 9.22.5);
  • перевірте свої сутності Entity Framework (параграф 9.22.4).

9.22.7. DLL з рівня [EF5]

Ми перетворюємо наш проєкт на бібліотеку класів, щоб під час генерації створювався збірник .dll, а не файл .exe. Це робиться у властивостях проєкту, як було показано в розділі 9.7.6 для імітованого бізнес-шару.


Завдання: перетворіть тип проєкту [pam-ef5] на бібліотеку класів, а потім перекомпілюйте проєкт.


9.23. Крок 16: налаштування шару [DAO]

9.23.1. Інтерфейс шару [DAO]

Як і у випадку з імітованим шаром [métier], доступ до шару [DAO] здійснюватиметься через інтерфейс. Яким він буде?

Давайте розглянемо інтерфейс [IPamMetier] симульованого шару [métier], який ми створили:


    public interface IPamMetier {
        // перелік усіх ідентифікаційних даних співробітників 
        Employe[] GetAllIdentitesEmployes();

        // ------- розрахунок заробітної плати 
        FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés);
}

У рядку 3 метод [GetAllIdentitesEmployes] використовується для заповнення випадаючого списку на головній сторінці:

Цих співробітників потрібно знайти в базі даних.

У рядку 6 метод [GetSalaire] дозволяє розрахувати розрахунковий лист працівника, номер якого SS. Нагадаємо визначення типу [FeuilleSalaire]:


  public class FeuilleSalaire
  {

    // автоматичні властивості 
    public Employe Employe { get; set; }
    public Cotisations Cotisations { get; set; }
    public ElementsSalaire ElementsSalaire { get; set; }
}

Інформація з рядків 5 та 6 буде надходити з бази даних. Нагадаємо, що працівник має властивість [Indemnites]. Цю інформацію також потрібно буде отримати.

Отже, ми можемо вийти з таким інтерфейсом для шару [DAO]:


    public interface IPamDao {
        // перелік усіх ідентифікаторів співробітників 
        Employe[] GetAllIdentitesEmployes();
        // конкретний працівник із його надбавками 
        Employe GetEmploye(string ss);
        // перелік усіх внесків 
        Cotisations GetCotisations();
}

9.23.2. Проєкт Visual Studio


Завдання: додати до рішення [pam-td] новий проєкт типу [console] під назвою [pam-dao]. Встановити його як стартовий проєкт рішення.


 

9.23.3. Додавання необхідних посилань до проєкту

Розглянемо проект у цілому:

Для проекту [pam-dao] потрібна певна кількість DLL:

  • усі, що зазначені в проекті [pam-ef5];
  • а також той, що належить самому проєкту [pam-ef5].

Крім того, ми будемо використовувати [Spring.net] для створення екземпляра шару [DAO]. Для цього нам потрібні DLL, [Spring.core] та [Common.Logging]. Ці DLL знаходяться у папці [lib] у матеріалах до цього прикладу.


Завдання: додайте ці різні посилання до проєкту [pam-dao].


9.23.4. Реалізація шару [DAO]

Вище наведено клас [PamException], який було визначено в розділі 9.7.4. Просто змінюємо його простір імен (рядок 1 нижче):


namespace Pam.Dao.Entites
{
  // клас винятків
  public class PamException : Exception
  {
....
  }
}

Інтерфейс [IPamDao] — це той, який ми щойно визначили в розділі 9.23.1:


using Pam.EF5.Entites;

namespace Pam.Dao.Service
{
  public interface IPamDao
  {
    // перелік усіх ідентифікаційних даних працівників 
    Employe[] GetAllIdentitesEmployes();
    // конкретний працівник із його виплатами 
    Employe GetEmploye(string ss);
    // перелік усіх внесків 
    Cotisations GetCotisations();
  }
}

Клас [PamDaoEF5] реалізує цей інтерфейс за допомогою ORM та EF5. Його код такий:


using Pam.Dao.Entites;
using Pam.EF5.Entites;
using Pam.Models;
using System;
using System.Linq;

namespace Pam.Dao.Service
{

  public class PamDaoEF5 : IPamDao
  {
    // приватні поля 
    private Cotisations cotisations;
    private Employe[] employes;

    // Виробник
    public PamDaoEF5()
    {
      // внесок
      try
      {
....
      }
      catch (Exception e)
      {
        throw new PamException("Erreur système lors de la construction de la couche [DAO]", e, 1);
      }
    }

    // GetCotisations
    public Cotisations GetCotisations()
    {
      return cotisations;
    }

    // GetAllIdentitesEmploye
    public Employe[] GetAllIdentitesEmployes()
    {
      return employes;
    }

    // GetEmploye
    public Employe GetEmploye(string SS)
    {
      try
      {
....
      catch (Exception e)
      {
        throw new PamException(string.Format("Erreur système lors de la recherche de l'employé [{0}]", SS), e, 2);
      }
    }
  }
}

Зверніть увагу:

  • рядок 10: клас [PamDaoEF5] реалізує інтерфейс [IPamDao];
  • таблиці [cotisations] та [employes] кешуються у властивостях рядків 13–14. Співробітники не мають своїх надбавок;
  • рядки 17–28: саме конструктор ініціалізує рядки 13–14;
  • рядки 43–52: метод [GetEmploye] повертає співробітника разом із його надбавками. Він отримує як параметр номер соціального страхування цього співробітника. Якщо співробітника немає в базі даних, метод поверне покажчик null.

Завдання: доповнити код класу [PamDaoEF5].


Для конструктора слід орієнтуватися на тестовий код шару [EF5], наведений у розділі 9.22.6. Для методу [GetEmploye] слід орієнтуватися на приклад з розділу 3.5.7 [Eager and Lazy loading] класу [refEF5].

9.23.5. Налаштування шару [DAO]

Як і в розділі 9.22.5, нам потрібно налаштувати EF5 у файлі [App.config] проекту:


Завдання 1: налаштуйте EF5 у [App.config]. Достатньо повторити те, що було зроблено у файлі [App.config] шару [EF5].


Наша тестова програма використовуватиме [Spring.net] для отримання посилання на шар [DAO].


Завдання 2: скориставшись тим, що було зроблено в розділі 9.20.2, змініть файл конфігурації [app.config] проекту [pam-dao] так, щоб він визначав об’єкт Spring під назвою [pamdao], пов’язаний із класом [PamDaoEF5], який ми щойно створили. Файли [app.config] та [web.config] мають однакову структуру. Слід звернути увагу на те, щоб тег <configSections> був першим тегом, що зустрічається після кореневого тегу <configuration>.


9.23.6. Тестування шару [DAO]

Ми готові протестувати наш шар [DAO]. Це робимо за допомогою програми [Program.cs], яка вже є в наявності:

Ми перевіримо різні функції інтерфейсу шару [DAO]. Код [Program.cs] буде таким:


using Pam.Dao.Service;
using Pam.EF5.Entites;
using Spring.Context.Support;
using System;

namespace Pam.Dao.Tests
{
  public class Program
  {
    public static void Main()
    {
      try
      {
        // інстанціювання шару [dao]
        IPamDao pamDao = (IPamDao)ContextRegistry.GetContext().GetObject("pamdao");
        // список ідентифікаційних даних співробітників 
        foreach (Employe Employe in pamDao.GetAllIdentitesEmployes())
        {
          Console.WriteLine(Employe.ToString());
        }
            // працівник із його надбавками 
            Console.WriteLine("------------------------------------");
            Employe e = pamDao.GetEmploye("254104940426058");
            Console.WriteLine("employé= {0}, indemnités={1}", e, e.Indemnites);
            Console.WriteLine("------------------------------------");
        // працівник, якого не існує 
        Employe employe = pamDao.GetEmploye("xx");
        Console.WriteLine("Employé n° xx");
        Console.WriteLine((employe == null ? "null" : employe.ToString()));
        Console.WriteLine("------------------------------------");
        // список внесків 
        Cotisations cotisations = pamDao.GetCotisations();
        Console.WriteLine(cotisations.ToString());
      }
      catch (Exception ex)
      {
        // відображення винятку 
        Console.WriteLine(ex.ToString());
      }
      //пауза 
      Console.ReadLine();
    }
  }
}
  • рядок 15: отримуємо посилання на шар [DAO] завдяки [Spring.net].

Результати виконання цієї програми такі:

1
2
3
4
5
6
7
8
9
Employé[22,1,254104940426058,Jouveinal,Marie,5 rue des oiseaux,St Corentin,49203]
Employé[23,1,260124402111742,Laverti,Justine,La Brûlerie,St Marcel,49014]
------------------------------------
employé= Employé[22,1,254104940426058,Jouveinal,Marie,5 rue des oiseaux,St Corentin,49203], indemnités=Indemnités[91,1,2,2,1,2,1,3,1,15]
------------------------------------
Employé n° xx
null
------------------------------------
Cotisations[10,1,3,49,6,15,9,39,7,88]

9.23.7. DLL з шару [DAO]


Завдання: перетворіть тип проєкту [pam-dao] на бібліотеку класів, а потім перегенеруйте проєкт (повторіть дії, описані в параграфі 9.22.7).


9.24. Крок 17: налаштування шару [métier]

9.24.1. Інтерфейс шару [métier]

Інтерфейс шару [métier] буде інтерфейсом [IPamMetier] імітованого шару [métier], який ми побудували в розділі 9.7.2.


    public interface IPamMetier {
        // перелік усіх ідентифікаційних даних працівників 
        Employe[] GetAllIdentitesEmployes();

        // ------- розрахунок заробітної плати 
        FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés);
}

9.24.2. Проєкт Visual Studio


Завдання: додайте до рішення [pam-td] новий проєкт типу [console] під назвою [pam-metier]. Встановіть його як стартовий проєкт рішення.


 

9.24.3. Додавання необхідних посилань до проєкту

Розглянемо проект у цілому:

Проєкту [pam-metier] потрібна певна кількість DLL:

  • усі, на які посилаються проекти [pam-dao] та [pam-ef5];
  • а також ті, що належать до самих проектів [pam-dao] та [pam-ef5].

Завдання: додайте ці різні посилання до проекту [pam-metier].


 

9.24.4. Реалізація шару [métier]

Вище наведено чотири елементи, які вже використовувалися в імітованому шарі [métier] (див. розділ 9.7). Можливі зміни в просторах імен, імпортованих цими різними класами. Внесіть відповідні зміни. Клас [PamMetier] реалізує інтерфейс [IPamMetier] наступним чином:


using Pam.Dao.Service;
using Pam.EF5.Entites;
using Pam.Metier.Entites;
using System;

namespace Pam.Metier.Service
{

  public class PamMetier : IPamMetier
  {

    // посилання на шар [DAO], ініціалізований Spring
    public IPamDao PamDao { get; set; }

    // список усіх ідентифікаторів співробітників 
    public Employe[] GetAllIdentitesEmployes()
    {
      ...
    }

    // конкретний працівник із його надбавками 
    public Employe GetEmploye(string ss)
    {
      ...
    }

    // внески 
    public Cotisations GetCotisations()
    {
      ...
    }

    // розрахунок заробітної плати 
    public FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés)
    {
      // SS: номер SS працівника 
      // HeuresTravaillées: кількість відпрацьованих годин 
      // Відпрацьовані дні: кількість відпрацьованих днів 
...
  }
}
  • рядок 13: є посилання на шар [DAO]. Він буде ініціалізований Spring під час інстанціювання класу [PamMetier]. Отже, коли виконуються різні методи, рядок 13 вже ініціалізовано.

Завдання: доповнити код класу [PamMetier]. Якщо в [GetSalaire] виявиться, що працівник із номером ss не існує, буде запущено [PamException]. Спосіб розрахунку заробітної плати описано в розділі 9.5. Слід звернути увагу на те, щоб усі проміжні розрахунки округлювати до двох знаків після коми.


9.24.5. Налаштування шару [métier]

Як це було зроблено в параграфі 9.22.5, нам потрібно налаштувати EF5 у файлі [app.config] проекту:


Завдання 1: налаштуйте EF5 у [app.config]. Достатньо повторити те, що було зроблено у файлі [app.config] шару [EF5].


Наша тестова програма використовуватиме [Spring.net] для отримання посилання на шар [métier].


Завдання 2: спираючись на те, що ви зробили раніше в розділі 9.23.5, змініть файл конфігурації [app.config] проекту [pam-metier] так, щоб він визначав об’єкт Spring під назвою [pammetier], пов’язаний із класом [PamMetier], який ми щойно створили. Найпростіше — скопіювати файл [app.config] з проєкту [pam-dao] і додати те, чого бракує.


Тут виникає певна складність. Необхідно не тільки створити екземпляр шару [métier] за допомогою класу [PamMetier], але й ініціалізувати його властивість [PamDao]:


    // посилання на шар [DAO], ініціалізований Spring
    public IPamDao PamDao { get; set; }

Конфігурація Spring у [app.config] виглядає наступним чином:


  <spring>
    <context>
      <resource uri="config://spring/objects" />
    </context>
    <objects xmlns="http://www.springframework.net">
      <object id="pamdao" type=" Pam.Dao.Service.PamDaoEF5, pam-dao"/>
      <object id="pammetier" type="Pam.Metier.Service.PamMetier, pam-metier">
        <property name="PamDao" ref="pamdao" />
      </object>
    </objects>
</spring>
  • рядок 6: визначає об’єкт [pamdao], пов’язаний із класом [PamDaoEF5];
  • рядок 7: визначає об’єкт [pammetier], пов’язаний із класом [PamMetier];
  • рядок 8: тег [property] слугує для ініціалізації публічної властивості класу [PamMetier]. Атрибут [name="PamDao"] відповідає імені властивості, яку потрібно ініціалізувати в класі [PamMetier]. Атрибут [ref="pamdao"] вказує, що властивість ініціалізується за допомогою посилання — саме на об’єкт [pamdao] із рядка 6, тобто за допомогою посилання на шар [DAO]. Це саме те, що нам було потрібно.

9.24.6. Тестування шару [métier]

Ми готові протестувати наш шар [métier]. Це робимо за допомогою програми [Program.cs], яка вже є в наявності:

Ми перевіримо різні функції інтерфейсу шару [métier]. Код [Program.cs] буде таким:


using System;
using Pam.Dao.Entites;
using Pam.Metier.Service;
using Spring.Context.Support;
using Pam.EF5.Entites;

namespace Pam.Metier.Tests
{
  public class Program
  {
    public static void Main()
    {
      try
      {
        // інстанціювання шару [métier]
        IPamMetier pamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
        // список ідентифікаційних даних співробітників
        Console.WriteLine("Employés -----------------------------");
        foreach (Employe Employe in pamMetier.GetAllIdentitesEmployes())
        {
          Console.WriteLine(Employe);
        }

        // розрахунки заробітної плати 
        Console.WriteLine("salaires -----------------------------");
        Console.WriteLine(pamMetier.GetSalaire("260124402111742", 30, 5));
        Console.WriteLine(pamMetier.GetSalaire("254104940426058", 150, 20));
        try
        {
          Console.WriteLine(pamMetier.GetSalaire("xx", 150, 20));
        }
        catch (PamException ex)
        {
          Console.WriteLine(string.Format("PamException : {0}", ex.Message));
        }
      }
      catch (Exception ex)
      {
        Console.WriteLine(string.Format("Exception : {0}, Exception interne : {1}", ex.Message, ex.InnerException == null ? "" : ex.InnerException.Message));
      }
      // пауза 
      Console.ReadLine();
    }
  }
}
  • рядок 16: отримуємо посилання на шар [métier] завдяки [Spring.net].

Результати виконання цієї програми такі:

1
2
3
4
5
6
7
Employés -----------------------------
Employé[24,1,254104940426058,Jouveinal,Marie,5 rue des oiseaux,St Corentin,49203]
Employé[25,1,260124402111742,Laverti,Justine,La Brûlerie,St Marcel,49014]
salaires -----------------------------
[Employé[25,1,260124402111742,Laverti,Justine,La Brûlerie,St Marcel,49014],Cotisations[11,1,3,49,6,15,9,39,7,88],[64,85 : 17,45 : 10 : 15 : 72,4]]
[Employé[24,1,254104940426058,Jouveinal,Marie,5 rue des oiseaux,St Corentin,49203],Cotisations[11,1,3,49,6,15,9,39,7,88],[362,25 : 97,48 : 42 : 62 : 368,77]]
PamException : L'employé de n° [xx] n'existe pas

9.24.7. DLL з шару [métier]


Завдання: перетворіть тип проєкту [pam-metier] на бібліотеку класів, а потім перегенеруйте проєкт (повторіть дії, описані в параграфі 9.22.7).


9.25. Крок 18: налаштування шару [web]

Ми підійшли до останнього шару нашої архітектури — шару [web]:

Ми будемо повторно використовувати шар [web], який ми розробили за допомогою імітованого шару [métier].

9.25.1. Проєкт Visual Studio

Повертаємося до Visual Studio Express 2012 для веб, щоб підключити наш веб-шар до шарів [métier, DAO, EF5], які ми щойно розробили. В основному потрібно виконати налаштування та внести кілька змін у простори імен.

У Visual Studio Express 2012 для веб завантажте рішення [pam-td]:

  • у [1], а рішення [pam-td] — у VS Studio для веб-розробки. Веб-проект [pam-web-01] знову стає видимим. Ми втратили його в VS Studio для настільних ПК.
  • Конфігурацію веб-проєкту [pam-web-01] доведеться змінити. Замість того, щоб змінювати проєкт, який працює, ми внесемо зміни в його копію. Спочатку в [2] видаляємо проєкт із рішення (це не видаляє нічого у файловій системі).
  • у [3] за допомогою Провідника Windows дублюємо папку [pam-web-01] у [pam-web-02];
  • у [4] завантажуємо проект [pam-web-02] у збірку [pam-td]. Він з’являється під назвою [pam-web-01];
  • у [5] змініть цю назву на [pam-web-02] і встановіть цей проект як проект запуску;
  • на [6], завантажте старий проект [pam-web-01]. Тепер у вас є всі ваші проекти. Будьте уважні, працюючи з [pam-web-02].

9.25.2. Додавання необхідних посилань до проекту

Розглянемо проект у цілому:

Проект [pam-web-02] потребує певної кількості DLL:

  • усі, на які посилаються проекти [pam-metier], [pam-dao] та [pam-ef5];
  • а також ті, що належать самим проектам [pam-metier], [pam-dao] та [pam-ef5].

Завдання: додайте ці різні посилання до проекту [pam-web-02]. Посилання на проект [pam-metier-simule] слід видалити. Змінюємо шар [métier]. Деякі DLL вже присутні у посиланнях. Видаліть їх, а потім додайте нові.


 

9.25.3. Реалізація шару [web]

Створіть проект [pam-web-02]. З’являться такі помилки, як-от:

Клас [ApplicationModel] використовує тип [Employe]. Зі симульованим шаром [métier] цей тип був визначений у просторі імен [Pam.Metier.Entites]. Тепер він знаходиться у просторі імен [Pam.EF5.Entites]. Виправте ці помилки, як показано вище.

9.25.4. Налаштування шару [web]

Як це було зроблено в розділі 9.24.5, нам потрібно налаштувати EF5 у файлі [web.config] проекту:


Завдання 1: замініть весь поточний вміст [web.config] на вміст файлу [app.config] з проєкту [pam-metier].


Файл [Global.asax] нашого веб-додатку використовує [Spring.net] для отримання посилання на шар [métier]:


      try
      {
        // інстанціювання шару [métier]
        application.PamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
      }
      catch (Exception ex)
      {
        application.InitException = ex;
}

У рядку 4 запитується посилання на об’єкт Spring із назвою [pammetier]. Саме таку назву отримала верства [métier] (перевірте це у вашому файлі [web.config]).

9.25.5. Тестування шару [web]

Ми готові протестувати наш шар [web]. Спочатку змінимо його робочий порт. За замовчуванням [pam-web-02] має конфігурацію [pam-web-01] і, отже, працює на тому самому порту. Досвід показує, що це створює проблеми: IIS продовжує використовувати коди з проєкту [pam-web-01]. Виконайте наступні дії:

У [4] змініть номер порту, наприклад, змінивши цифру в одиницях.

Виконуємо проект [pam-web-02] замість [Ctrl-F5]. У результаті отримаємо таку стартову сторінку:

У [1] отримуємо співробітників із бази даних [dbpam_ef5]. Зверніть увагу, що співробітника [X X], який був у нас із симульованим шаром [métier], більше немає. Проведемо симуляцію:

У [2] ми отримуємо саме реальну заробітну плату, а не фіктивну. Тепер зупинімо SGBD та MySQL5 і проведемо ще одне моделювання:

У [3] ми отримали зрозумілу сторінку з помилками, хоча деякі повідомлення й англійською мовою. Тепер знову зупинимо MySQL і запустимо додаток знову в режимі VS через [Ctrl-F5]:

 

Ми отримуємо вікно [initFailed.cshtml], створене в розділі 9.20.4. Воно відображає повідомлення про помилки зі стека винятків. Читачеві пропонується провести інші тести.

9.26. Крок 19: оприлюднення в Інтернеті додатка ASP.NET

Під час розробки додатка ASP.NET у Visual Studio конфігурація, що використовується за замовчуванням, призводить до того, що розроблений додаток доступний лише за адресою [localhost]. Будь-яка інша адреса відхиляється вбудованим сервером Visual Studio, який у відповідь повертає помилку [400 Bad Request].

Це можна побачити наступним чином:

  • у вікні DOS зверніть увагу на адресу IP вашої машини для розробки:

Microsoft Windows [version 6.3.9600]
(c) 2013 Microsoft Corporation. Tous droits réservés.

dos>ipconfig

Configuration IP de Windows



Carte Ethernet Connexion au réseau local :

   Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr
   Adresse IPv6 de liaison locale. . . . .: fe80::698b:455a:925:6b13%4
   Adresse IPv4. . . . . . . . . . . . . .: 172.19.81.34
   Masque de sous-réseau. . . . . . . . . : 255.255.0.0
   Passerelle par défaut. . . . . . . . . : 172.19.0.254

Carte réseau sans fil Wi-Fi :

   Statut du média. . . . . . . . . . . . : Média déconnecté
   Suffixe DNS propre à la connexion. . . :

Адреса IP вказана тут у рядку 14. Якщо у вас є Wi-Fi-з’єднання, Wi-Fi-адреса пристрою з’явиться у рядках 20 і наступних.

  • Перевірте властивості проєкту [clic droit sur projet / propriétés / onglet web]:
 

Додаток запуститься на порту [65010] машини [localhost].

  • Запустіть свій проект за допомогою [Ctrl-F5]

Image

  • замініть [localhost] на адресу IP машини:

Image

Сервер надіслав відповідь [400 Bad Request]. Сервер IIS Express, який використовує Visual Studio, приймає лише ім’я [localhost].

Щоб зробити розроблену програму доступною для користувачів типу URL, потрібно використовувати інший сервер, а не IIS Express, наприклад, сервер IIS (не Express). Щоб перевірити наявність цього сервера (зазвичай він є у версіях Windows Pro), потрібно перейти до панелі налаштувань [Panneau de configuration\Système et sécurité\Outils d’administration]:

Image

Ця опція не завжди доступна. У такому разі потрібно перейти до [ Panneau de configuration \ Programmes] та встановити «Веб-інструменти адміністрування».

Як тільки з’явиться опція [Gestionnaire des services internet (IIS)], її потрібно активувати:

Запускаємо веб-сайт за замовчуванням. Для цього попередньо потрібно запустити службу [Service de publication World Wide Web]:

Після цього відкрийте URL [http://localhost] у веб-браузері. Спочатку переконайтеся, що порт 80 не зайнятий іншим веб-сервером. Якщо так, зупиніть його.

Сервер IIS надіслав нам відповідь. Тепер замініть [localhost] на адресу IP вашого комп’ютера:

Все працює. Тепер повернемося до Visual Studio:

  • спочатку потрібно запустити Visual Studio в режимі [administrateur]

Після цього потрібно змінити конфігурацію веб-проєкту, який ми хочемо розгорнути [clic droit sur projet / propriétés / onglet web]:

Необхідно вибрати локальний сервер IIS як сервер розгортання. Visual Studio автоматично встановлює URL для додатка. Його можна змінити. Запустіть проект за допомогою [Ctrl-F5]:

Тепер замініть [localhost] на адресу IP вашого комп’ютера:

Якщо сервер IIS недоступний, можна скористатися безкоштовним сервером ASP.NET, таким як [Ultidev Web Server Pro], доступним на URL або [http://ultidev.com/Download/ ]. Після встановлення існує два способи запуску веб-додатку за допомогою цього сервера:

Швидкий спосіб

Відкрийте провідник Windows і виберіть папку з додатком ASP.NET, який потрібно розгорнути:

Після цього веб-сервер запуститься, а веб-додаток відобразиться у браузері:

  • У [3] можна зупинити/запустити веб-сервер;
  • у [4] можна змінити порт служби веб-додатка;

Перед запуском сервера необхідно запустити наведену нижче службу [UWS HiPriv Services]:

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

При натисканні на посилання [6] відображається перша сторінка додатка:

Тоді замість [localhost] можна ввести адресу IP комп’ютера:

Отже, і в цьому випадку приймається лише ім’я [localhost].

Довгий спосіб

Запустіть програму Ultidev Web Explorer

, а потім виконайте такі кроки:

  • у полі [8] вкажіть папку веб-додатка, який потрібно розгорнути;
  • через [10-11] веб-додаток потрібно буде завантажити за допомогою URL та [http://localhost:81/];
  • запустіть веб-сервер із параметром [14];
  • запитайте URL [19] ;
  • у [20] ми отримали потрібну сторінку, використовуючи локальну адресу IP на комп’ютері замість імені [localhost]. Це саме те, що нам було потрібно;

Сервер Ultidev встановлено у вигляді служби Windows, яка запускається автоматично. Ви можете вимкнути автоматичний запуск сервера Ultidev наступним чином:

  • вибрати опцію [Panneau de configuration\Système et sécurité\Outils d’administration];
  • [1, 2]: виберіть властивості служби [Ultidev Web Server Pro];
  • [3]: встановіть для нього ручний запуск.

Щоб запустити сервер вручну, скористайтеся, наприклад, програмою [Ultidev Web Explorer]:

9.27. Крок 20: створення нативного додатка для Android

Якщо у вас є веб-додаток типу APU (односторінковий додаток), можна створити виконуваний файл для мобільних пристроїв (Android, IoS, Windows 8, ...) за допомогою інструменту [Phonegap] [http://phonegap.com/]. Існують й інші способи, зокрема з використанням продукту з відкритим кодом Apache Cordova [https://cordova.apache.org/]. Інструмент, доступний онлайн на сайті Phonegap [http://build.phonegap.com/apps], «завантажує» zip-файл сайту, який потрібно перетворити. Головна сторінка повинна називатися [index.html] і бути статичною, тобто не генеруватися веб-фреймворком (ASP.NET, JEE, PHP, ...). Почнемо з її створення.

9.27.1. Архітектура додатка

Тут слід пам’ятати, що ми хочемо створити додаток для Android. Такий додаток часто має таку архітектуру:

  • у [1] користувач використовує планшет на базі Android, який взаємодіє з одним або кількома веб-сервісами [2];

Повернемося до моделі APU:

  • у браузері завантажується початкова сторінка (на схемі вище не вказано, звідки вона походить);
  • наступні види отримуються за допомогою викликів Ajax [1-4]. Браузер не завантажуватиме жодної нової сторінки;

Початковий вигляд може надаватися тим самим сервером, що й інші вигляди, отримані за допомогою викликів Ajax, а може й ні. Якщо він не надається тим самим сервером, JavaScript початкової сторінки повинен знати URL веб-сервера, який буде надавати інші вигляди. Саме так буде в додатку для Android, який ми збираємося створити:

  • статична сторінка [index.html] буде інкапсульована в нативний додаток для Android [1], який має можливості браузера, а отже, здатний виконувати JavaScript, вбудований у сторінку [index.html];
  • ця сторінка отримуватиме інші види за допомогою Ajax-запитів до сервера [2]. Для цього їй потрібно знати URL веб-сервера;

Ми рефакторуємо додаток [pam-web-02], щоб він працював у цьому режимі. Отже, перша сторінка матиме такий вигляд:

  • в [1], URL початкової сторінки додатка. Він буде наданий нам сервером Ultidev, розглянутим у розділі 9.26;
  • в [2] користувач повинен буде ввести URL симулятора розрахунку заробітної плати. Можна було б ввести його безпосередньо в код JavaScript початкової сторінки, але це ускладнило б тестування: щойно ми змінимо симулятор адреси IP (або порт), доведеться змінити його й у коді JavaScript;
  • на [3], посилання [Connexion], яке завантажить наступний вигляд:
  • зауважимо, що при значенні [4] значення URL у браузері не змінилося. Воно залишається таким самим, як на початковій сторінці, і не змінюватиметься протягом усього терміну роботи додатка.

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

9.27.2. Рефакторизація проєкту [pam-web-02]

У папці [Content] проекту [pam-web-02] створюємо таку папку [bootstrap] (назва не має значення):

Ми включили туди статичну сторінку [index.html] та всі необхідні їй ресурси (файли CSS та JS). Сторінка [index.html] містить код з шаблону [_Layout.cshtml] проекту Visual Studio, з якого видалено все, що не є статичним. У результаті отримуємо такий код:


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Simulateur de paie</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <link rel="stylesheet" href="Site.css" />
    <script type="text/javascript" src="jquery-1.8.2.min.js"></script>
    <script type="text/javascript" src="jquery.validate.min.js"></script>
    <script type="text/javascript" src="jquery.validate.unobtrusive.min.js"></script>
    <script type="text/javascript" src="globalize.js"></script>
    <script type="text/javascript" src="globalize.culture.fr-FR.js"></script>
    <script type="text/javascript" src="jquery.unobtrusive-ajax.min.js"></script>
    <script type="text/javascript" src="myScripts.js"></script>
</head>
<body>
    <table>
        <tbody>
            <tr>
                <td>
                    <h2>Simulateur de calcul de paie</h2>
                </td>
                <td style="width: 20px">
                    <img id="loading" style="display: none" src="indicator.gif" />
                </td>
                <td>
                    <a id="lnkConnexion" href="javascript:connexion()">
                        | Connexion<br />
                    </a>
                    <a id="lnkFaireSimulation" href="javascript:faireSimulation()">
                        | Faire la simulation<br />
                    </a>
                    <a id="lnkEffacerSimulation" href="javascript:effacerSimulation()">
                        | Effacer la simulation<br />
                    </a>
                    <a id="lnkVoirSimulations" href="javascript:voirSimulations()">
                        | Voir les simulations<br />
                    </a>
                    <a id="lnkRetourFormulaire" href="javascript:retourFormulaire()">
                        | Retour au formulaire de simulation<br />
                    </a>
                    <a id="lnkEnregistrerSimulation" href="javascript:enregistrerSimulation()">
                        | Enregistrer la simulation<br />
                    </a>
                    <a id="lnkTerminerSession" href="javascript:terminerSession()">
                        | Terminer la session<br />
                    </a>
                </td>
        </tbody>
    </table>
    <hr />
    <div id="content">
        <table>
            <tr>
                <td>URL du simulateur</td>
                <td><input type="text" id="urlServiceWeb" name="urlServiceWeb" size="80"></td>
            </tr>
        </table>
        <div id="erreur">
            <h3>Réponse du serveur :</h3>
            <div id="erreur1"></div>
            <div id="erreur2"></div>
        </div>
    </div>
</body>
</html>

Ми додали наступні елементи:

  • рядки 27–29: додано пункт меню [Connexion] для підключення до служби симуляції;
  • рядки 55–56: введення URL симулятора;
  • рядки 59–63: поле для повідомлення про помилку у разі невдалого підключення;

Рефакторизація коду здійснюється виключно в коді [myScripts.js] у рядку 14 вище. Нічого іншого не змінюється. Код змінюється наступним чином:


// під час завантаження документа
$(document).ready(function () {
    // отримання посилань на різні компоненти сторінки
    loading = $("#loading");
    content = $("#content");
    erreur = $("#erreur");
    erreur1 = $("#erreur1");
    erreur2 = $("#erreur2");
    // посилання меню
    lnkConnexion = $("#lnkConnexion");
    lnkFaireSimulation = $("#lnkFaireSimulation");
    lnkEffacerSimulation = $("#lnkEffacerSimulation");
    lnkEnregistrerSimulation = $("#lnkEnregistrerSimulation");
    lnkVoirSimulations = $("#lnkVoirSimulations");
    lnkTerminerSession = $("#lnkTerminerSession");
    lnkRetourFormulaire = $("#lnkRetourFormulaire");
    // вони поміщаються в масив
    options = [lnkConnexion, lnkFaireSimulation, lnkEffacerSimulation, lnkEnregistrerSimulation, lnkVoirSimulations, lnkTerminerSession, lnkRetourFormulaire];
    // приховування деяких елементів сторінки
    loading.hide();
    erreur.hide();
    // фіксуємо меню
    setMenu([lnkConnexion]);
});
  • рядки 6–8: ідентифікатори області, що відображає помилки підключення на сторінці [index.html];
  • рядок 10: нове посилання для підключення до симулятора;
  • рядок 21: область помилок спочатку прихована;
  • рядок 23: відображається лише посилання для підключення;

На сторінці [index.html] посилання для підключення визначено таким чином:


<a id="lnkConnexion" href="javascript:connexion()">
| Connexion<br />
</a>

Функція JS [connexion] (рядок 1) має такий вигляд:


var urlServiceWeb;
var erreur, erreur1, erreur2;


function connexion() {
    // отримуємо urlServiceWeb із веб-сервісу
    urlServiceWeb = $("#urlServiceWeb").val();
    // отримуємо форму введення даних
    $.ajax({
        url: urlServiceWeb + '/Pam/Formulaire',
        type: 'POST',
        dataType: 'html',
        beforeSend: function () {
            // індикатор очікування увімкнено
            loading.show();
        },
        success: function (data) {
            // відображення результатів
            content.html(data);
            // меню
            setMenu([lnkFaireSimulation]);
        },
        error: function (jqXHR) {
            erreur2.html(jqXHR.responseText);
            erreur1.html(jqXHR.getAllResponseHeaders().replace(/\r\n/g, "<br/>").replace(/\r/g, "<br/>").replace(/\n/g, "<br/>"));
            erreur.show();
        },
        complete: function () {
            // сигнал очікування вимкнено
            loading.hide();
        }
    });
}
  • рядок 7: отримується значення URL, введене користувачем. Воно зберігається у глобальній змінній з рядка 1. Таким чином, воно буде доступне в інших функціях файлу;
  • рядок 10: виконується Ajax-запит до URL [/Pam/Formulaire] симулятора. Цей URL відображає фрагмент екрану з введеними даними симуляції (працівники, відпрацьовані години, відпрацьовані дні). У початковій версії [pam-web-02] цього URL було достатньо. До нього автоматично додавався префікс URL, який завантажував початкову сторінку. Зараз ми виходимо з припущення, що початкова сторінка може надаватися сервером, відмінним від того, що підтримує симулятор. Тоді до URL та [/Pam/Formulaire] потрібно додати префікс у вигляді змінної [urlServiceWeb] з рядка 1, яка є URL симулятора (наприклад, http://172.19.81.34/pam-web-02). Це потрібно зробити для всіх викликів Ajax у файлі;
  • рядки 17–22: у разі успішного встановлення з’єднання відображається фрагмент сторінки [Formulaire.cshtml] та меню з єдиним посиланням [Faire la simulation] (рядок 21);
  • рядки 23–27: у разі невдалого з’єднання:
    • у рядку 24 відображається відповідь HTML, надіслана веб-сервером (якщо така є);
    • у рядку 25 виводяться заголовки HTTP, надіслані веб-сервером (якщо він відповів);

Ось і все. У разі успіху ми отримуємо таку сторінку:

Тепер ми опинилися в тій самій ситуації, що й раніше, але тепер перегляди здійснюються за допомогою викликів Ajax. Отже, як показано вище, клік на посилання [Faire la simulation] буде виконано за допомогою наступного коду з файлу [myScripts.js]:


function faireSimulation() {
    // отримуємо посилання
    var simulation = $("#simulation");
    var formulaire = $("#formulaire");
    // форма дійсна?
    var formValid = formulaire.validate().form();
    if (!formValid) return;
    // виконується ручний виклик Ajax
    $.ajax({
        url: urlServiceWeb + '/Pam/FaireSimulation',
        type: 'POST',
        data: formulaire.serialize(),
        dataType: 'html',
        ...
    });
    // меню
    setMenu([lnkEffacerSimulation, lnkEnregistrerSimulation, lnkTerminerSession, lnkVoirSimulations]);
}
  • було внесено лише одну зміну — у рядку 10, де попередній код URL тепер має префікс симулятора;

9.27.3. Тестування рефакторованого проєкту

У розділі 9.26 ми показали, як встановити додаток [pam-web-02] на сервер Ultidev. Ми почнемо з цього:

  • у [6] ми запитуємо відображення сторінки [bootstrap/index.html]. Отримуємо такий вигляд:

Введемо помилкове URL:

  • у [10] — заголовки HTTP з відповіді сервера;
  • на [11] — документ HTML із відповіді сервера;

Якщо ввести правильний URL:

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

9.27.4. Створення бінарного файлу для Android

Ми створимо бінарний файл для Android на основі щойно створеного статичного веб-сайту та протестуємо [1]:

У [2] ми додаємо файл [config.xml], який слугуватиме для налаштування плагіна [Phonegap], що генеруватиме бінарний файл для Android. Його код такий:


<?xml version='1.0' encoding='utf-8'?>
<widget id="android.exemples.pam" version="0.0.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
    <name>Pam</name>
    <description>
        IstiA - Université d'Angers
    </description>
    <author email="serge.tahe@univ-angers.fr">
      Serge Tahé
    </author>
    <content src="index.html" />
    <access origin="*" />
    <allow-navigation href="*" />
    <allow-intent href="*" />
    <plugin name="cordova-plugin-whitelist" />
</widget>
  • рядки 7–9: вкажіть тут свої контактні дані;
  • рядки 11–13: ці рядки дозволяють вбудованому в веб-додаток JavaScript, який виконуватиметься на пристрої Android, надсилати запити до файлів URL, що знаходяться поза цим пристроєм;

Ми архівуємо вміст папки [Content/bootstrap]:

 

Потім переходимо на сайт Phonegap [http://build.phonegap.com/apps]:

  • перед [1], можливо, вам доведеться створити обліковий запис;
  • у [1] починаємо;
  • у [2] обираємо безкоштовний тариф, який дозволяє використовувати лише один додаток Phonegap;
  • у [3] завантажуємо архів із додатком [4];
  • у [5] вкажіть назву додатка;
  • натисніть на посилання [6], щоб створити бінарні файли OS та IoS для Android і Windows. Це може зайняти кілька секунд;
  • у [7-9] завантажте бінарний файл для Android;

Запустіть емулятор [GenyMotion] для планшета на базі Android (див. розділ 11.1):

 

Вище показано запуск емулятора планшета з версією Android API 21. Після запуску емулятора

  • розблокуйте його, потягнувши за засувку (якщо вона є) убік, а потім відпустивши її;
  • за допомогою миші перетягніть завантажений файл [Pam-debug.apk] і відпустіть його на емуляторі. Після цього програма буде встановлена та запущена;

Встановіть у [1] файл URL з емулятора, як описано в розділі 9.27.3. Після цього підключіться до емулятора за посиланням [2]:

Перевірте роботу програми на емуляторі. Вона має працювати.