Skip to content

9. Studium przypadku

9.1. Introduction

Przedstawimy studium przypadku opublikowane już wcześniej w artykule dostępnym na stronie URL [http://tahe.developpez.com/dotnet/pam-aspnet/]. W tym artykule studium przypadku zostało przeprowadzone przy użyciu klasycznego ASP.NET oraz ORM i NHibernate. W tym miejscu zrealizujemy ją przy użyciu Entity Framework w wersjach ASP.NET, MVC oraz ORM. Podobnie jak w poprzednim artykule, studium przypadku przedstawiono jako projekt uniwersytecki. Jest ono zatem przeznaczone dla studentów. W przypadku wszelkich pytań podano odniesienia do rozdziałów, które właśnie omówiliśmy, wskazując przydatne materiały do przeczytania.

9.2. Problem do rozwiązania

Chcemy stworzyć aplikację internetową, która pozwoli użytkownikowi przeprowadzać symulacje obliczeń wynagrodzeń opiekunek dziecięcych ze stowarzyszenia „Maison de la petite enfance” w danej gminie. Skupimy się zarówno na organizacji kodu aplikacji, jak i na samym kodzie.

Aplikacja będzie typu APU [Application à Page Unique] i będzie wykorzystywać wyłącznie wywołania Ajax do komunikacji z serwerem. Będzie ona prezentować użytkownikowi następujące widoki:

  • widok [VueSaisies], który przedstawia formularz symulacji

Image

  • widok [VueSimulation] służący do wyświetlania szczegółowych wyników symulacji:

Image

  • widok [VueSimulations], zawierający listę symulacji przeprowadzonych przez klienta

Image

  • widok [VueSimulationsVides], który wskazuje, że klient nie ma żadnych symulacji lub nie ma ich już:

Image

  • widok [VueErreurs], który wskazuje na jeden lub więcej błędów (w tym przypadku SGBD i MySQL zostały zatrzymane):

Image

9.3. Architektura aplikacji

Architektura aplikacji będzie wyglądać następująco:

Warstwa [EF5] odnosi się do Entity Framework 5 (ORM). Wykorzystywanym SGBD będzie MySQL.

Najpierw zbudujemy tę aplikację z wykorzystaniem symulowanej warstwy [métier]:

Pozwoli nam to skupić się wyłącznie na warstwie [web]. Symulowana warstwa [métier] będzie zgodna z interfejsem rzeczywistej warstwy [métier]. Gdy warstwa [web] będzie gotowa do działania, utworzymy wówczas warstwy [métier], [DAO] i [EF5].

9.4. Baza danych

Dane statyczne potrzebne do sporządzenia listy płac są umieszczone w bazie danych o nazwie MySQL, nazywanej [dbpam_ef5] (pam=Paie Assistante Maternelle). Baza ta ma administratora o nazwie root bez hasła. Zawiera trzy tabele:

Image

Między kolumną EMPLOYES (INDEMNITE_ID) a kolumną INDEMNITES (ID) istnieje relacja klucza obcego. Struktura tej bazy danych wynika z jej wykorzystania wraz z tabelą EF5. Powrócimy do tego tematu podczas tworzenia niższych warstw aplikacji.

Tabela EMPLOYES: zawiera informacje o poszczególnych opiekunkach do dzieci

Struktura:

ID
klucz główny automatycznie zwiększany przez SGBD
SS
numer ubezpieczenia społecznego pracownika – unikalny
NOM
imię i nazwisko pracownika
PRENOM
jego imię
ADRESSE
jego adres
VILLE
jego miasto
CP
jego kod pocztowy
VERSIONING
liczba całkowita, która jest automatycznie zwiększana przy każdej zmianie rekordu
INDEMNITE_ID
klucz obcy w polu [ID] tabeli [INDEMNITES]

Jego zawartość mogłaby wyglądać następująco:

Image

Tabela COTISATIONS: zawiera stawki składek na ubezpieczenie społeczne pobieranych od wynagrodzenia

Struktura:

ID
klucz główny automatycznie zwiększany przez SGBD
CSGRDS
procent: ogólna składka społeczna + składka na spłatę długu społecznego
CSGD
procent: ogólna składka na ubezpieczenie społeczne podlegająca odliczeniu
SECU
procent: ubezpieczenie społeczne
RETRAITE
procent: dodatkowa emerytura + ubezpieczenie od bezrobocia
VERSIONING
liczba całkowita, która zwiększa się automatycznie przy każdej zmianie rekordu

Jego zawartość mogłaby wyglądać następująco:

Image

Stawki składek na ubezpieczenie społeczne są niezależne od pracownika. Powyższa tabela zawiera tylko jeden wiersz.

Tabela INDEMNITES: zawiera różne dodatki zależne od wskaźnika pracownika
ID
klucz główny automatycznie zwiększany przez SGBD
INDICE
indeks wynagrodzenia – unikalny
BASE_HEURE
cena netto w euro za godzinę dyżuru
ENTRETIEN_JOUR
dodatek na utrzymanie w euro za dzień opieki
REPAS_JOUR
dodatek na posiłki w euro za dzień opieki
INDEMNITES_CP
dodatek urlopowy. Jest to procent, który należy zastosować do wynagrodzenia podstawowego.
VERSIONING
liczba całkowita, która jest automatycznie zwiększana przy każdej zmianie rekordu

Jego zawartość mogłaby wyglądać następująco:

Image

9.5. Sposób obliczania wynagrodzenia opiekunki do dzieci

Poniżej przedstawiamy sposób obliczania miesięcznego wynagrodzenia opiekunki do dzieci. Jako przykład posłużymy się wynagrodzeniem pani Marie Jouveinal, która w miesiącu rozliczeniowym przepracowała 150 godzin w ciągu 20 dni.

Uwzględniono następujące elementy:
[TOTALHEURES]: łączna liczba godzin przepracowanych w miesiącu
[TOTALJOURS]: łączna liczba dni przepracowanych w miesiącu
[TOTALHEURES]=150
[TOTALJOURS] = 20
Wynagrodzenie podstawowe opiekunki do dzieci oblicza się według następującego wzoru:
[SALAIREBASE]=([TOTALHEURES]*[BASEHEURE])*(1+[INDEMNITESCP]/100)
[SALAIREBASE]=(150*[2.1])*(1+0,15)= 362,25
Od tego wynagrodzenia podstawowego należy potrącić szereg składek na ubezpieczenie społeczne:
Ogólna składka na ubezpieczenie społeczne oraz składka na spłatę długu społecznego: [SALAIREBASE]*[CSGRDS/100]
Odliczalna składka na ubezpieczenie społeczne: [SALAIREBASE]*[CSGD/100]
Ubezpieczenie społeczne, renta wdowia, emerytura: [SALAIREBASE]*[SECU/100]
Dodatkowa emerytura + AGPF + Ubezpieczenie od bezrobocia: [SALAIREBASE]*[RETRAITE/100]
CSGRDS: 12,64
CSGD: 22,28
Ubezpieczenie społeczne: 34,02
Emerytura: 28,55
Łączna kwota składek na ubezpieczenie społeczne:
[COTISATIONSSOCIALES] = [SALAIREBASE] *(CSGRDS + CSGD + SECU + RETRAITE)/100
[COTISATIONSSOCIALES]=97,48
Ponadto opiekunka ma prawo, za każdy przepracowany dzień, do dodatku na utrzymanie oraz dodatku na posiłki. W związku z tym otrzymuje następujące dodatki:
[Indemnités]=[TOTALJOURS]*(ENTRETIENJOUR+REPASJOUR)
[INDEMNITES]=104
Ostatecznie wynagrodzenie netto, które należy wypłacić opiekunce do dzieci, wynosi:
[SALAIREBASE] – [COTISATIONSSOCIALES] + [INDEMNITÉS]
[salaire NET]=368,77

9.6. Projekt Visual Studio dla warstwy [web]

Projekt Visual Web Developer dla tej aplikacji będzie wyglądał następująco:

  • w [1] ogólna struktura projektu [pam-web-01];
  • w [2] folder [Content] służy do przechowywania zasobów statycznych projektu:
    • [indicator.gif]: animowany obrazek przedstawiający oczekiwanie na zakończenie żądania Ajax,
    • [standard.jpg]: obraz tła dla różnych widoków,
    • [Site.css]: arkusz stylów aplikacji;
  • w pliku [3] znajduje się jedyny kontroler aplikacji [PamController];
  • w pliku [4] znajdują się klasy niezbędne dla aplikacji, których jednak nie można zaklasyfikować jako elementy pliku MVC:
    • [ApplicationModelBinder]: klasa umożliwiająca włączenie danych zakresu [Application] do modelu akcji,
    • [SessionModelBinder]: klasa umożliwiająca włączenie danych z zakresu [Session] do modelu akcji,
    • [Static] – klasa pomocnicza zawierająca metody statyczne;
  • w [5] znajdują się szablony aplikacji, zarówno szablony akcji, jak i widoków:
    • [ApplicationModel]: szablon zawierający dane z zakresu [Application],
    • [SessionModel]: model zawierający dane z zakresu [Session],
    • [Simulation]: klasa zawierająca elementy symulacji obliczania wynagrodzenia,
    • [IndexModel]: model pierwszego widoku [Index] wyświetlanego przez aplikację;
  • w [6] skrypty JS niezbędne do globalizacji aplikacji;
  • w pliku [7] znajdują się skrypty JS z rodziny JQuery niezbędne do internacjonalizacji, walidacji po stronie klienta oraz implementacji AJAX w aplikacji;
  • w [8], [myScripts.js] to plik zawierający nasze własne skrypty JS;
  • w pliku [9] znajdują się widoki aplikacji:
    • [Index]: strona główna,
    • [Formulaire]: formularz do wprowadzania danych pracownika oraz przepracowanych godzin i dni,
    • [Simulation]: widok przedstawiający symulację,
    • [Simulations]: widok zawierający listę przeprowadzonych symulacji,
    • [Erreurs]: widok zawierający listę ewentualnych błędów,
    • [InitFailed]: widok wyświetlający komunikaty o błędach w przypadku niepowodzenia inicjalizacji aplikacji;
  • w [10] – strona główna aplikacji [_Layout];
  • w [11] – pliki [Web.config], [Global.asax] służące do konfiguracji aplikacji.

9.7. Krok 1 – utworzenie symulowanej warstwy [métier]

Od tego momentu opisujemy kroki, które należy wykonać, aby zrealizować studium przypadku. W razie potrzeby podajemy numer rozdziału, do którego warto powrócić, aby wykonać zlecone zadanie. Niektóre elementy projektu znajdują się w folderze [aspnetmvc-support.zip], który można znaleźć na stronie internetowej tego dokumentu. Znajduje się tam folder [étudedecas-support] zawierający następujące elementy:

  

Projekt wykorzystuje ponadto elementy przedstawione w poprzednich rozdziałach. Wystarczy je skopiować i wkleić między plikiem PDF a programem Visual Studio.

9.7.1. Kompletne rozwiązanie aplikacji w Visual Studio

Najpierw utworzymy rozwiązanie Visual Studio, w którym stworzymy dwa projekty:

  • projekt dla symulowanej warstwy [métier];
  • projekt dla warstwy internetowej MVC.

Wykorzystamy dwa narzędzia:

  • Visual Studio Express 2012 dla komputerów stacjonarnych, które posłuży do zbudowania warstwy [métier];
  • Visual Studio Express 2012 dla sieci Web, które posłuży do stworzenia warstwy [web].

W programie Visual Studio Express dla komputerów stacjonarnych tworzymy rozwiązanie o nazwie [pam-td]:

  • w [1] wybieramy aplikację C#;
  • w [2] wybierz [Application console];
  • w [3] nadać nazwę rozwiązaniu;
  • w [4], utworzyć katalog dla tego rozwiązania;
  • w [5] nadać nazwę warstwie [métier];
  • w [6] – wygenerowane rozwiązanie.

9.7.2. Interfejs warstwy [métier]

W architekturze warstwowej dobrą praktyką jest, aby komunikacja między warstwami odbywała się za pośrednictwem interfejsów:

Jaki interfejs powinna udostępniać warstwa [métier] warstwie [web]? Jakie są możliwe interakcje między tymi dwiema warstwami? Przypomnijmy sobie interfejs internetowy, który zostanie wyświetlony użytkownikowi:

  1. przy pierwszym wyświetleniu formularza w warstwie [1] powinna znajdować się lista pracowników. Wystarczy uproszczona lista (nazwisko, imię, SS). Numer SS jest niezbędny, aby uzyskać dostęp do dodatkowych informacji o wybranym pracowniku (pozycje 6–11).
  2. Informacje od 12 do 15 dotyczą różnych stawek składek.
  3. Informacje od 16 do 19 dotyczą dodatków przysługujących pracownikowi
  4. Informacje od 20 do 24 to składniki wynagrodzenia obliczone na podstawie danych wprowadzonych przez użytkownika w polach od 1 do 3.

Interfejs [IPamMetier] udostępniany warstwie [web] przez warstwę [métier] musi spełniać powyższe wymagania. Istnieje wiele możliwych interfejsów. Proponujemy następujący:


using Pam.Metier.Entites;
namespace Pam.Metier.Service
{
  public interface IPamMetier
  {
    // lista wszystkich tożsamości pracowników 
    Employe[] GetAllIdentitesEmployes();

    // ------- obliczenie wynagrodzenia 
    FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés);
  }
}
  • wiersz 7: metoda umożliwiająca wypełnienie pola kombi [1]
  • wiersz 10: metoda umożliwiająca uzyskanie informacji od 6 do 24. Zostały one zebrane w obiekcie typu [FeuilleSalaire], który opiszemy wkrótce.

Umieścimy ten interfejs w folderze [metier/service]:

9.7.3. Elementy warstwy [métier]

Poprzedni interfejs wykorzystuje dwie klasy: [Employe] i [FeuilleSalaire], które musimy zdefiniować:

  • [Employe] to obraz wiersza z tabeli [employes] w bazie danych;
  • [FeuilleSalaire] to karta płacowa pracownika.

Entities zostaną umieszczone w folderze [metier / entites] projektu:

W ostatecznej architekturze warstwa [métier] będzie przetwarzać obiekty obrazów z bazy danych:

Image

Do reprezentacji wierszy z trzech tabel bazy danych wykorzystamy następujące klasy. Znaczenie poszczególnych pól opisano w punkcie 9.4.

Klasa [Employe]

Reprezentuje ona wiersz tabeli [employes]. Jej kod jest następujący:


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; }

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

Klasa [Indemnites]

Reprezentuje wiersz tabeli [indemnites]. Jej kod jest następujący:


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; }
    // podpis
    public override string ToString()
    {
      return string.Format("Indemnités[{0},{1},{2},{3},{4}]", Indice, BaseHeure, EntretienJour, RepasJour, IndemnitesCp);
    }
  }
}

Klasa [Cotisations]

Przedstawia wiersz z tabeli [cotisations]. Jej kod jest następujący:


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; }
    // podpis
    public override string ToString()
    {
      return string.Format("Cotisations[{0},{1},{2},{3}]", CsgRds, Csgd, Secu, Retraite);
    }
  }
}

Należy zauważyć, że klasy nie zawierają kolumn [ID] i [VERSIONING] z tabel. Kolumny te, przydatne w przypadku korzystania z tabel ORM i EF5, nie mają zastosowania w kontekście symulowanej warstwy [métier].

Klasa [FeuilleSalaire] zawiera informacje od 6 do 24 z przedstawionego wcześniej formularza:


namespace Pam.Metier.Entites
{
  public class FeuilleSalaire
  {

    // właściwości automatyczne 
    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);
    }
  }
}
  • wiersz 7: informacje od 6 do 11 dotyczące pracownika, dla którego obliczane jest wynagrodzenie, oraz informacje od 16 do 19 dotyczące jego dodatków. Należy tutaj pamiętać, że obiekt [Employe] zawiera obiekt [Indemnites] reprezentujący jego dodatki;
  • wiersz 8: informacje od 12 do 15;
  • wiersz 9: informacje od 20 do 24;
  • wiersze 12–14: metoda [ToString].

Klasa [ElementsSalaire] zawiera informacje od 20 do 24 z formularza:


namespace Pam.Metier.Entites
{
  public class ElementsSalaire
  {
    // właściwości automatyczne 
    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);
    }
  }
}
  • wiersze 6–10: składniki wynagrodzenia zgodnie z wyjaśnieniami zawartymi w opisanych powyżej zasadach biznesowych;
  • wiersz 6: wynagrodzenie podstawowe pracownika, zależne od liczby przepracowanych godzin;
  • wiersz 7: składki potrącane z tego wynagrodzenia podstawowego;
  • wiersze 8 i 9: dodatki do wynagrodzenia podstawowego, zależne od indeksu pracownika i liczby przepracowanych dni;
  • wiersz 10: wynagrodzenie netto do wypłaty;
  • wiersze 14–17: metoda [ToString] klasy.

9.7.4. Klasa [PamException]

Tworzymy specyficzny typ wyjątków dla naszej aplikacji. Jest to następujący typ [PamException]:


using System;

namespace Pam.Metier.Entites
{
  // klasa wyjątku
  public class PamException : Exception
  {

    // kod błędu 
    public int Code { get; set; }

    // konstruktory 
    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;
    }
  }
}
  • wiersz 6: klasa ta wywodzi się z klasy [Exception];
  • wiersz 10: posiada ona publiczną właściwość [Code], która jest kodem błędu;
  • w naszej aplikacji będziemy używać dwóch rodzajów konstruktorów:
    • ten z wierszy 23–27, którego można używać w sposób pokazany poniżej:
throw new PamException("Problème d'accès aux données",5);
  • (ciąg dalszy)
    • lub ten z wierszy 29–33, służący do zgłaszania wyjątku poprzez zamknięcie go w wyjątku typu [PamException]:
try{
....
}catch (IOException ex){
     // enkapsulujemy wyjątek ex     
    throw new PamException("Problème d'accès aux données",ex,10);
}

Zaletą tej drugiej metody jest to, że nie traci się informacji, które może zawierać pierwszy wyjątek.

9.7.5. Implementacja warstwy [métier]

Interfejs [IPamMetier] zostanie zaimplementowany przez następującą klasę [PamMetier]:


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

namespace Pam.Metier.Service
{
  public class PamMetier : IPamMetier
  {
    // lista pracowników w pamięci podręcznej
    public Employe[] Employes { get; set; }
    // pracownicy indeksowani według numerów SS
    private IDictionary<string, Employe> dicEmployes = new Dictionary<string, Employe>();

    // lista pracowników
    public Employe[] GetAllIdentitesEmployes()
    {
...
      // wyświetla listę pracowników
      return Employes;
    }

    // obliczanie wynagrodzenia
    public FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés)
    {
...
  }
}
  • wiersz 7: klasa [PamMetier] implementuje interfejs [IPamMetier];
  • wiersz 10: klasa [PamMetier] przechowuje listę pracowników w pamięci podręcznej;
  • wiersz 12: słownik, który kojarzy pracownika z jego numerem ubezpieczenia społecznego;
  • wiersze 15–20: metoda zwracająca listę pracowników;
  • wiersze 23–26: metoda obliczająca wynagrodzenie pracownika.

Metoda [GetAllIdentitesEmploye] wygląda następująco:


// lista pracowników
    public Employe[] GetAllIdentitesEmployes()
    {
      if (Employes == null)
      {
        // tworzy się tabelę zawierającą trzech pracowników
        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]);
        // fikcyjny pracownik, który nie zostanie dodany do słownika
        // w celu zasymulowania nieistniejącego pracownika
        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 }
        };
      }
      // wyświetlanie listy pracowników
      return Employes;
    }
  • wiersz 4: sprawdzamy, czy lista pracowników nie została już utworzona;
  • wiersz 7: jeśli tak nie jest, tworzymy tablicę zawierającą trzech pracowników;
  • wiersze 8–17: pierwszy pracownik;
  • wiersz 18: jest on dodawany do słownika;
  • wiersze 19–28: drugi pracownik;
  • wiersz 29: jest on dodawany do słownika;
  • wiersze 32–42: trzeci pracownik. Nie jest on dodawany do słownika z powodu, który wyjaśnimy.

Metoda [GetSalaire] będzie wyglądać następująco:


    // obliczamy wynagrodzenie
    public FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés)
    {
      // pobieramy pracownika o numerze SS
      Employe e = dicEmployes.ContainsKey(ss) ? dicEmployes[ss] : null;
      // czy istnieje?
      if (e == null)
      {
        throw new PamException(string.Format("L'employé de n° SS [{0}] n'existe pas", ss), 10);
      }
      // zwracamy fikcyjny arkusz płacowy
      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 }
      };
}
  • wiersz 2: metoda otrzymuje numer SS pracownika, dla którego chcemy obliczyć wynagrodzenie, liczbę przepracowanych godzin oraz liczbę przepracowanych dni;
  • wiersz 5: wyszukujemy pracownika w słowniku. Pamiętajmy, że jednego z nich tam nie ma;
  • wiersze 7–10: jeśli pracownik nie zostanie znaleziony, generowany jest wyjątek o numerze [PamException];
  • wiersze 12–17: zwracamy fikcyjny arkusz płacowy.

9.7.6. Test konsolowy warstwy [métier]

Projekt warstwy [métier] wygląda obecnie następująco:

Powyższa klasa [Program] będzie testować metody interfejsu [IPamMetier]. Podstawowy przykład mógłby wyglądać następująco:


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

namespace Pam.Metier.Tests
{
  class Program
  {
    public static void Main()
    {
      // instancjonowanie warstwy [métier]
      IPamMetier pamMetier = new PamMetier();
      // lista pracowników
      Employe[] employes = pamMetier.GetAllIdentitesEmployes();
      Console.WriteLine("Liste des employés--------------------");
      foreach (Employe e in employes)
      {
        Console.WriteLine(e);
      }
      // obliczenia list płac 
      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));
      }
    }
  }
}
  • wiersz 12: instancja warstwy [métier];
  • wiersze 14–19: testowanie metody [GetAllIdentitesEmploye] interfejsu [IPamMetier];
  • wiersze 21–31: test metody [GetSalaire] z interfejsu [IPamMetier].

Uruchomienie tego programu konsolowego daje następujące wyniki:

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

Zachęcamy czytelnika do powiązania tych wyników z wykonanym kodem.

Aby móc wykorzystać ten projekt w tworzonej przez nas aplikacji internetowej, przekształcamy go w bibliotekę klas:

  • w [1], w właściwościach pliku [Program.cs];
  • w pliku [2] zaznaczamy, że plik ten nie będzie częścią generowanego zestawu;
  • w pliku [3, 4], w właściwościach projektu [pam-metier-simule], w opcji [Application] [3], określono w [4], że generowanie ma dostarczyć bibliotekę klas (w postaci pliku DLL).
  • w [5] wymagany jest plik assembly typu [Release]. Drugim typem jest [Debug]. Plik assembly zawiera wówczas informacje ułatwiające debugowanie;
  • w pliku [6] generowany jest projekt [pam-metier-simule];
  • w pliku [7] wyświetlane są wszystkie pliki rozwiązania;
  • w [8], w folderze [bin / Release], plik DLL naszego projektu.

9.8. Krok 2: konfiguracja aplikacji internetowej

W poprzednim rozwiązaniu Visual Studio utworzymy projekt warstwy internetowej o nazwie MVC.

Za pomocą programu Visual Studio Express dla sieci otwieramy rozwiązanie [pam-td] utworzone wcześniej w programie Visual Studio Express dla komputerów stacjonarnych.

  • w [1] rozwiązanie [pam-td] zostało załadowane do programu Visual Studio Express dla sieci Web;
  • w [2] rozwiązanie i projekt dla symulowanej warstwy [métier], którą właśnie utworzyliśmy.

W tym kolejnym etapie utworzymy szkielet aplikacji internetowej.

  • w [1] dodajemy nowy projekt do rozwiązania [pam-td];
  • w [2] wybieramy projekt o nazwie ASP.NET MVC 4;
  • o nazwie [pam-web-01] [3];
  • w [4] wybieramy podstawowy szablon ASP.NET MVC;
  • w [5] – utworzony projekt;
  • w [6] tworzymy nowy projekt, projekt startowy rozwiązania, który zostanie uruchomiony po wykonaniu polecenia [Ctrl-F5];
  • w [7] nazwa nowego projektu jest pogrubiona, co wskazuje, że jest to projekt startowy rozwiązania.

Teraz za pomocą Eksploratora Windows zastępujemy folder projektu [Content] folderem [étudedecas-support / web / Content]. Po wykonaniu tej czynności należy dołączyć nowe pliki do projektu [pam-web-01]. Postępujemy w następujący sposób:

  • w [1] odświeżamy rozwiązanie;
  • w [2] wyświetlamy wszystkie pliki rozwiązania;
  • w [3] pojawia się folder [Images];
  • który dodajemy do projektu w pliku [4].

W folderze [Scripts] należy dodać skrypty JQuery Globalization oraz [1] niezbędne do walidacji po stronie klienta.

Strona szablonu [_Layout.cshtml] [2] będzie zawierała następującą treść:


<!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>

Uwaga: w wierszu 8 należy dostosować wersję pliku jQuery do posiadanej wersji programu Visual Studio.

  • wiersz 7: odwołanie do arkusza stylów aplikacji;
  • wiersze 8–10: odniesienia do skryptów niezbędnych do walidacji po stronie klienta;
  • wiersze 11–12: odniesienia do skryptów niezbędnych do wprowadzania liczb rzeczywistych w formacie francuskim z przecinkiem;
  • wiersz 13: odniesienie do skryptów niezbędnych do działania w trybie Ajax;
  • wiersz 14: skrypty specyficzne dla aplikacji;
  • wiersz 24: obrazek wyświetlany podczas oczekiwania na zakończenie wywołań Ajax;
  • wiersze 26–39: sześć linków JavaScript;
  • wiersz 43: sekcja, w której będą wyświetlane różne widoki aplikacji;
  • wiersz 44: treść poszczególnych widoków aplikacji.

Następnie zmodyfikujemy domyślną trasę aplikacji:

Plik [RouteConfig] będzie miał następującą treść:


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" }
      );
    }
  }
}
  • wiersz 14: pliki URL będą miały postać [{controller}/{action}];
  • wiersz 15: w przypadku braku akcji zostanie użyta akcja [Index]. W przypadku braku kontrolera zostanie użyty kontroler [Pam].

Z tej konfiguracji wynika, że URL [/] jest równoważne URL [/Pam/Index]. Ponieważ nasza aplikacja jest typu APU, URL i [/] będą jedynymi elementami typu URL w tej aplikacji.

Utwórz kontroler o nazwie [Pam]:

  

Zmodyfikuj kontroler [PamController] w następujący sposób:


using System.Web.Mvc;

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

    }
}
  • wiersz 3: umieszczamy kontroler w przestrzeni nazw [Pam.Web.Controllers];
  • wiersz 7: akcja [Index] będzie przetwarzać wyłącznie polecenie HTTP GET;
  • wiersz 8: zwracamy typ [ViewResult] zamiast typu [ActionResult].

Teraz utwórz widok [Index.cshtml] wyświetlany przez powyższą akcję [Index]:

Zmodyfikuj [Index.cshtml] w następujący sposób:


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

Uruchom aplikację za pomocą [Ctrl-F5]. Powinna pojawić się następująca strona:

 

Zadanie: Wyjaśnij, co się stało.


Aplikacja korzysta z arkusza stylów odwołującego się do strony szablonowej [_Layout.cshtml]:


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

Arkusz stylów [/Content/Site.css] definiuje obraz tła dla stron aplikacji:


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

9.9. Krok 3: wdrożenie szablonu APU

Chcemy stworzyć aplikację zgodnie z szablonem APU (aplikacja jednostronicowa) opisanym w paragrafie 7.5 oraz w paragrafie 7.6. Strona jednostronicowa to strona ładowana przez przeglądarkę po uruchomieniu aplikacji:

  • powyższa część [1] stanowi stałą część strony pojedynczej. Widzieliśmy, że jest ona dostarczana przez stronę główną [_Layout.cshtml];
  • część [2] stanowi zmienną część strony pojedynczej. Znajduje się ona w obszarze o identyfikatorze [content] strony głównej [_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>

Różne fragmenty strony aplikacji będą wyświetlane w obszarze o identyfikatorze [content] w wierszu 13. Będą one wyświetlane za pomocą wywołań Ajax. Skrypty JavaScript wykonujące te wywołania znajdują się w pliku [myScripts.js], do którego odwołuje się wiersz 6. Utwórz ten plik, który będzie nam potrzebny:

Teraz kierujemy się szablonem APU opisanym w paragrafie 7.6. Przeczytaj ten paragraf ponownie, jeśli go zapomniałeś. Teraz skonfigurujemy różne fragmenty strony wyświetlane przez aplikację.

9.9.1. Narzędzia programisty JavaScript

Przypominamy, że w przeglądarce Chrome masz do dyspozycji szereg narzędzi do debugowania kodu JavaScript (HTML, CSS) na swoich stronach. Narzędzia te zostały częściowo omówione w akapicie 7.2. W modelu APU przeglądarki przechowują w pamięci podręcznej skrypty JavaScript, do których odwołuje się pierwsza strona aplikacji. Należy więc pamiętać o wyczyszczeniu tej pamięci podręcznej podczas modyfikowania skryptów, w przeciwnym razie zmiany mogą nie zostać uwzględnione. Oto jak to zrobić w przeglądarce Chrome:

- wpisz [Ctrl-Maj-I], aby wyświetlić środowisko programistyczne

  • kliknij ikonę [1] w prawym dolnym rogu okna programistycznego;
  • następnie zaznacz opcję [2], która wyłącza pamięć podręczną w trybie programowania.

9.9.2. Wykorzystanie widoku częściowego do wyświetlenia formularza

Formularz wprowadzania danych jest jednym z fragmentów wyświetlanych przez aplikację. Obecnie formularz ten jest wyświetlany przez widok [Index.cshtml], który jest widokiem pełnym:


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

Widok ten jest wyświetlany przez akcję [Index]:


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

W wierszu 4 powyżej wyświetlany jest rzeczywiście widok [View], a nie widok częściowy [PartialView]. Potrzebujemy widoku częściowego dla formularza, który będzie fragmentem strony. Modyfikujemy widok [Index.cshtml] w następujący sposób:


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

W wierszu 4 formularz nie jest już częścią strony [Index.cshtml]. Znajduje się on teraz w widoku częściowym [Formulaire.cshtml]:

Kod [Formulaire.cshtml] wygląda po prostu następująco:


<h2>Formulaire</h2>

Wprowadź te zmiany i sprawdź, czy po uruchomieniu aplikacji nadal wyświetla się następujący widok:

 

9.9.3. Wywołanie Ajax [faireSimulation]

Interesuje nas fragment wyświetlany po kliknięciu przez użytkownika linku [Faire la simulation]:

  • w [1] użytkownik klika link [Faire la simulation];
  • w [2] symulacja pojawia się pod formularzem.

W następujący sposób modyfikujemy widok częściowy [Formulaire.cshtml], który wyświetla formularz:


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

W wierszu 3 tworzymy region o identyfikatorze [simulation], aby umieścić w nim fragment symulacji.

Tworzymy następujący widok częściowy [Simulation.cshtml]:

Zawartość widoku [Simulation.cshtml] jest następująca:


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

Teraz musimy napisać kod JavaScript, który obsługuje kliknięcie linku [Faire la simulation]. Postępujemy zgodnie z procedurą opisaną w paragrafie 7.6.5. Najpierw przyjrzyjmy się kodowi HTML linku w [_Layout.cshtml]:


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

Widać, że kliknięcie linku [Faire la simulation] spowoduje uruchomienie funkcji JS [faireSimulation]. Funkcja ta zostanie zapisana w pliku [myScripts.js] wraz z innymi funkcjami JS niezbędnymi dla aplikacji:


// zmienne globalne
var loading;
var content;

function faireSimulation() {
  // ręczne wywołanie Ajax
...
}

function effacerSimulation() {
  // kasowanie danych z formularza
...
}

function enregistrerSimulation() {
  // ręczne wywołanie Ajax
  ...
}

function voirSimulations() {
  // ręczne wywołanie Ajax
  ...
}

function retourFormulaire() {
  // ręczne wywołanie Ajax
...
}

function terminerSession() {
...
}

// podczas ładowania dokumentu
$(document).ready(function () {
  // pobieramy odwołania do różnych elementów strony
  loading = $("#loading");
  content = $("#content");
});
  • wiersze 35–39: funkcja JQuery wykonywana podczas uruchamiania aplikacji;
  • wiersze 37–38: inicjuje się zmienne globalne z wierszy 2 i 3.

Przypominamy, że elementy o identyfikatorach [loading] i [content] są zdefiniowane na stronie głównej [_Layout.cshtml] (wiersze 14 i 21 poniżej):


<!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>

Zadanie: postępując zgodnie z procedurą opisaną w paragrafie 7.6.5, napisz funkcję JS [faireSimulation]. Funkcja ta wyśle wywołanie Ajax typu POST do akcji [/Pam/FaireSimulation]. Na razie nie będą przesyłane żadne dane.Akcja [/Pam/FaireSimulation] zwróci widok częściowy [Simulation.cshtml] do funkcji JS [faireSimulation], która następnie umieści ten strumień HTML w obszarze o identyfikatorze [simulation] formularza.


Przetestuj link [Faire la simulation] w swojej aplikacji.

9.9.4. Wywołanie Ajax [enregistrerSimulation]

Link [Enregistrer la simulation] jest zdefiniowany w następujący sposób w [_Layout.cshtml]:


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

Zadanie: postępując zgodnie z powyższą procedurą, napisz funkcję JS [enregistrerSimulation]. Funkcja ta wyśle wywołanie Ajax typu POST do akcji [/Pam/EnregistrerSimulation]. Na razie nie będą przesyłane żadne dane.Akcja [/Pam/EnregistrerSimulation] zwróci widok częściowy [Simulations.cshtml] do funkcji JS [enregistrerSimulation], która następnie umieści ten strumień HTML w obszarze o identyfikatorze [content] na stronie głównej.


Widok [Simulations.cshtml] wygląda następująco:

Jego zawartość jest następująca:


<h2>Simulations</h2>

Oto przykładowe wyniki działania:

9.9.5. Wywołanie Ajax [voirSimulations]

Link [Voir les simulations] jest zdefiniowany w następujący sposób w [_Layout.cshtml]:


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

Zadanie: postępując zgodnie z powyższą procedurą, napisz funkcję JS [voirSimulations]. Funkcja ta wyśle wywołanie Ajax typu POST do akcji [/Pam/VoirSimulations]. Na razie nie będą przesyłane żadne dane.Akcja [/Pam/VoirSimulations] zwróci widok częściowy [Simulations.cshtml] do funkcji JS [voirSimulations], która następnie umieści ten strumień HTML w obszarze o identyfikatorze [content] na stronie głównej.


Widok [Simulations.cshtml] to ten sam, który został już wykorzystany w poprzednim pytaniu.

Oto przykładowe wykonanie:

9.9.6. Wywołanie Ajax [retourFormulaire]

Link [Retour au formulaire de simulation] jest zdefiniowany w następujący sposób w [_Layout.cshtml]:


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

Zadanie: postępując zgodnie z powyższą procedurą, napisz funkcję JS [retourFormulaire]. Funkcja ta wyśle wywołanie Ajax typu POST do akcji [/Pam/Formulaire]. Na razie nie będą przesyłane żadne dane.Akcja [/Pam/Formulaire] zwróci widok częściowy [Formulaire.cshtml] do funkcji JS [retourFormulaire], która następnie umieści ten strumień HTML w obszarze o identyfikatorze [content] na stronie głównej.


Widok [Formulaire .cshtml] został już zdefiniowany. Oto przykładowe wykonanie:

9.9.7. Wywołanie Ajax [terminerSession]

Link [Terminer la session] jest zdefiniowany w następujący sposób w [_Layout.cshtml]:


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

Zadanie: postępując zgodnie z poprzednią instrukcją, zapisz funkcję JS [terminerSession]. Funkcja ta wyśle wywołanie Ajax typu POST do akcji [/Pam/TerminerSession]. Na razie nie będą przesyłane żadne dane.Akcja [/Pam/TerminerSession] zwróci widok częściowy [Formulaire.cshtml] do funkcji JS [terminerSession], która następnie umieści ten strumień HTML w obszarze o identyfikatorze [content] na stronie głównej.


Oto przykład wykonania:

9.9.8. Funkcja JS [effacerSimulation]

Powiązanie [Effacer la simulation] jest zdefiniowane w następujący sposób w [_Layout.cshtml]:


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

Funkcja JS [effacerSimulation] ma na celu:

  • ukrycie fragmentu [Simulation], jeśli istnieje;
  • przywrócenie pól wprowadzania danych w formularzu do stanu, w jakim znajdowały się podczas pierwszego załadowania aplikacji (gdy pojawią się pola wprowadzania danych – na razie ich nie ma).

Zadanie: napisz funkcję JS [effacerSimulation]. Nie ma tu wywołania Ajax. Proces ten odbywa się wewnątrz przeglądarki i nie angażuje serwera.


Oto przykładowe wykonanie:

9.9.9. Zarządzanie nawigacją między ekranami

Na razie linki są nadal wyświetlane. Teraz zajmiemy się zarządzaniem ich wyświetlaniem za pomocą funkcji JavaScript. Przypomnijmy najpierw kod sześciu linków JavaScript w [_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>

Wszystkie linki posiadają atrybut [id], który pozwoli nam zarządzać nimi za pomocą JavaScriptu. Modyfikujemy metodę JS wykonywaną podczas ładowania strony w następujący sposób:


// zmienne globalne
var loading;
var content;
var lnkFaireSimulation;
var lnkEffacerSimulation
var lnkEnregistrerSimulation;
var lnkTerminerSession;
var lnkVoirSimulations;
var lnkRetourFormulaire;
var options;

...
// podczas ładowania dokumentu
$(document).ready(function () {
  // pobieramy odwołania do różnych elementów strony
  loading = $("#loading");
  content = $("#content");
  // linki w menu
  lnkFaireSimulation = $("#lnkFaireSimulation");
  lnkEffacerSimulation = $("#lnkEffacerSimulation");
  lnkEnregistrerSimulation = $("#lnkEnregistrerSimulation");
  lnkVoirSimulations = $("#lnkVoirSimulations");
  lnkTerminerSession = $("#lnkTerminerSession");
  lnkRetourFormulaire = $("#lnkRetourFormulaire");
  // umieszczamy je w tablicy
  options = [lnkFaireSimulation, lnkEffacerSimulation, lnkEnregistrerSimulation, lnkVoirSimulations, lnkTerminerSession, lnkRetourFormulaire];
  // ukrywanie niektórych elementów strony
  loading.hide();
  // ustalamy położenie menu
  setMenu([lnkFaireSimulation, lnkVoirSimulations, lnkTerminerSession]);
});

  • wiersze 19–24: pobieramy odwołania do sześciu linków. Odwołania te są definiowane jako zmienne globalne w wierszach 4–9;
  • wiersz 26: tablica [options] jest inicjowana sześcioma odwołaniami. Tablica ta jest zdefiniowana jako zmienna globalna w wierszu 10;
  • wiersz 28: ukrywa się animowany obrazek wskazujący na oczekiwanie na zakończenie wywołań Ajax;
  • wiersz 30: wyświetlane są linki [lnkFaireSimulation, lnkVoirSimulations, lnkTerminerSession]. Pozostałe zostaną ukryte.

Funkcja JS [setMenu] ma następujący wygląd:


function setMenu(show) {
  // wyświetlanie linków z tabeli [show]
...
}

Zadanie: napisz funkcję JS [setMenu].


Jeśli T jest tablicą linków:

  • T.length to liczba linków;
  • T[i] to link nr i;
  • T[i].show() wyświetla link nr i;
  • T[i].hide() ukrywa link nr i.

Dzięki tym nowym funkcjom JS strona wyświetlana po uruchomieniu wygląda następująco:

 

Dostosuj funkcje JS i [faireSimulation, effacerSimulation, enregistrerSimulation, voirSimulations, retourFormulaire, terminerSession], aby uzyskać następujące ekrany:

Teraz, gdy szablon APU i linki nawigacyjne są już gotowe, możemy przejść do pisania akcji i widoków po stronie serwera. W miarę postępów w kolejnych krokach zauważysz, że niektóre z linków Ajax, które obecnie działają, przestaną działać, ponieważ będziesz modyfikować częściowe widoki wysyłane do klienta. W miarę tworzenia poszczególnych akcji i widoków po stronie serwera linki Ajax po stronie klienta będą znów działać tak, jak to zaplanowaliście.

9.10. Krok 4: tworzenie akcji serwerowej [Index]

Obecnie po uruchomieniu aplikacji wyświetla się następujący ekran:

 

Zamiast tego ekranu chcielibyśmy uzyskać następujący:

 

To akcja [Index] powinna wygenerować tę stronę. Kilka uwag:

  • strona zawiera formularz z trzema polami do wypełnienia:
    • pracownika, dla którego obliczane jest wynagrodzenie,
    • liczba przepracowanych godzin,
    • liczba przepracowanych dni;
  • formularz jest przesyłany za pomocą linku [Faire la simulation];
  • należy sprawdzić poprawność pól wprowadzania danych [Heures travaillées] i [Jours travaillés];
  • lista pracowników pochodzi z warstwy [métier], którą utworzyliśmy wcześniej.

Przypomnijmy aktualny kod akcji [Index]:


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

a kod widoku [Index.cshtml], który wyświetla ta akcja, to:


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

oraz kod widoku częściowego [Formulaire.cshtml]:


<h2>Formulaire</h2>

W tych trzech miejscach zostaną wprowadzone zmiany.

9.10.1. Szablon formularza

Wróćmy do łańcucha przetwarzania URL [/Pam/Index]:

  • żądanie klienta o numerze HTTP przychodzi jako [1];
  • w [2] informacje zawarte w żądaniu zostaną przekształcone w szablon akcji [3], który posłuży jako dane wejściowe dla akcji [4];
  • w [4] akcja, na podstawie tego modelu, wygeneruje odpowiedź. Będzie ona składać się z dwóch elementów: widoku V o nazwie [6] oraz modelu M tego widoku o nazwie [5];
  • widok V [6] wykorzysta swój szablon M [5] do wygenerowania odpowiedzi HTTP przeznaczonej dla klienta.

Akcja, która nas interesuje, to akcja [Index], która na razie wygląda następująco:


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

Akcja [Index] nie przekazuje żadnego szablonu do widoku [Index.cshtml]. Widok ten nie będzie zatem mógł wyświetlić listy pracowników. Listę tę można uzyskać z warstwy [métier]. W tym celu projekt [pam-web-01] musi zawierać odniesienie do projektu [pam-metier-simule]. Teraz utworzymy to odniesienie:

  • w [1], kliknij prawym przyciskiem myszy na [References] w projekcie [pam-web-01], a następnie na [Ajouter une référence];
  • w [2] wybierz opcję [Solution], a następnie projekt [pam-metier-simule] w [3];
  • w [4] projekt [pam-metier-simule] został dodany do odwołań projektu [pam-web-01].

9.10.2. Model aplikacji

W punkcie 4.10 na stronie 78 omówiliśmy ważne pojęcia modelu aplikacji i modelu sesji. Teraz będziemy z nich korzystać. Przypominamy, że w modelu umieszcza się:

  • dane w trybie tylko do odczytu dla wszystkich użytkowników. Model ten stanowi pamięć współdzieloną przez wszystkie zapytania wszystkich użytkowników;
  • model sesji zawierający dane dostępne do odczytu i zapisu dla danego użytkownika. Model ten stanowi pamięć współdzieloną przez wszystkie żądania tego użytkownika.

Co umieścimy w modelu aplikacji? Wróćmy do jej architektury:

Warstwa [web] zawiera odwołanie do warstwy [métier]. Ta ostatnia może być współdzielona przez wszystkich użytkowników. Możemy więc umieścić ją w modelu aplikacji. Ponadto założymy, że lista pracowników nie ulega zmianom. Można ją zatem odczytać tylko raz, a następnie udostępnić wszystkim użytkownikom. Proponujemy zatem następujący model aplikacji:

Kod klasy [ApplicationModel] mógłby wyglądać następująco:


using Pam.Metier.Entites;
using Pam.Metier.Service;
namespace PamWeb.Models
{
  public class ApplicationModel
  {
    // --- dane o zasięgu aplikacji ---
    public Employe[] Employes { get; set; }
    public IPamMetier PamMetier { get; set; }
  }
}

Aby wyświetlić listę rozwijaną w widoku, pisze się coś takiego:


        <!-- lista rozwijana -->
        <tr>
          <td>Liste déroulante</td>
          <td>@Html.DropDownListFor(m => m.DropDownListField,
           new SelectList(@Model.DropDownListFieldItems, "Value", "Label"))
          </td>
</tr>

Metoda [DropDownListFor] oczekuje jako drugiego parametru typu SelectListItem[], który został wcześniej dostarczony przez typ [SelectList]. Musimy utworzyć taką tablicę zawierającą listę pracowników. Ponieważ lista pracowników nie ulega zmianom, tablicę tę również można umieścić w modelu aplikacji. Modyfikujemy go w następujący sposób:


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

namespace Pam.Web.Models
{
  public class ApplicationModel
  {
    // --- dane w zakresie aplikacji ---
    public Employe[] Employes { get; set; }
    public IPamMetier PamMetier { get; set; }
    public SelectListItem[] EmployesItems { get; set; }
  }
}

Kiedy należy utworzyć ten szablon? Pokazaliśmy to w punkcie 4.10. Dzieje się to podczas wykonywania metody [Application_Start] z pliku [Global.asax]:

Metoda [Application_Start] ma obecnie następującą postać:


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);
    }
  }
}

Zmieniamy ją w następujący sposób:


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()
    {
      // ----------Wygenerowane automatycznie
      AreaRegistration.RegisterAllAreas();
      WebApiConfig.Register(GlobalConfiguration.Configuration);
      FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
      RouteConfig.RegisterRoutes(RouteTable.Routes);
      BundleConfig.RegisterBundles(BundleTable.Bundles);
      // -------------------------------------------------------------------
      // ---------- konfiguracja specyficzna
      // -------------------------------------------------------------------
      // dane zakresu aplikacji
      ApplicationModel application = new ApplicationModel();
      Application["data"] = application;
        // instancja warstwy [métier]
        application.PamMetier = ...
        // tabela pracowników 
        application.Employes = ...
        // elementy listy rozwijanej pracowników
        application.EmployesItems = ...
      // model binder dla [ApplicationModel]
      ...
    }
  }
}

Zadanie: uzupełnij kod metody [Application_Start]. Wszystko, czego potrzebujesz, znajduje się w paragrafie 4.10. Poświęć chwilę na ponowne przeczytanie tego długiego, ale ważnego paragrafu.


Wiersz 33 składa się w rzeczywistości z kilku wierszy. Aby utworzyć obiekt typu [SelectListItem], można skorzystać z następującej metody:


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

Ten obiekt [SelectListItem] posłuży do wygenerowania następującego tagu HTML <option>:

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

z listy rozwijanej. Należy zadbać o to, aby:

  • unTexte stanowiło imię, a po nim nazwisko pracownika;
  • uneValeur to numer pracownika SS.

W wierszu 35 powyżej potrzebna będzie klasa [ApplicationModelBinder] opisana w punkcie 4.10 na stronie 82:

9.10.3. Kod akcji [Index]

Teraz, gdy zdefiniowaliśmy szablon dla aplikacji, możemy zmodyfikować kod akcji [Index] w następujący sposób:


    [HttpGet]
    public ViewResult Index(ApplicationModel application)
    {
      return View();
}
  • wiersz 4: szablon aplikacji jest teraz parametrem akcji [Index]. W punkcie 4.10 wyjaśniliśmy, w jaki sposób parametr ten jest inicjowany przez framework.

9.10.4. Model widoku [Index.cshtml]

Teraz akcja [Index] ma dostęp do pracowników zapisanych w modelu aplikacji. Musi teraz przekazać ich do widoku [Index.cshtml], który zostanie wyświetlony. Można by przekazać typ [ApplicationModel] jako model do widoku [Index.cshtml], ale wkrótce zobaczymy, że ten widok potrzebuje dodatkowych informacji, których nie ma w [ApplicationModel]. Wykorzystamy następujący szablon widoku [IndexModel]:


namespace Pam.Web.Models
{
  public class IndexModel
  {
    // dane dotyczące zakresu aplikacji
    public ApplicationModel Application { get; set; }
  }
}
  • wiersz 6: [IndexModel] ładuje szablon aplikacji.

Akcja [Index] przyjmuje następującą postać:


    [HttpGet]
    public ViewResult Index(ApplicationModel application)
    {
      return View(new IndexModel() { Application = application });
}
  • w wierszu 4 wyświetlany jest domyślny widok [Index.cshtml] z szablonem typu [IndexModel] zainicjowanym danymi z szablonu aplikacji.

Wiemy, że widok [Index.cshtml] powinien wyświetlać formularz:

Image

Wróćmy do łańcucha przetwarzania żądania:

W przypadku zapytania [GET /Pam/Index]:

  • akcją jest [Index];
  • szablonem tej akcji jest [ApplicationModel];
  • widok to [Index.cshtml];
  • szablonem tego widoku jest [IndexModel].

Gdy formularz zostanie wysłany, proces przetwarzania będzie przebiegał analogicznie:

  • akcja przetwarza POST;
  • jej szablon gromadzi przesłane wartości, w tym przypadku:
    • numer SS wybranego pracownika;
    • liczba przepracowanych godzin;
    • liczba przepracowanych dni;

Można by utworzyć szablon akcji łączący te trzy wartości. Często też ponownie wykorzystuje się szablon, który służył do wyświetlenia formularza. Tak właśnie postąpimy w tym przypadku. Klasa [IndexModel] zmienia się w następujący sposób:


using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace Pam.Web.Models
{
  [Bind(Exclude = "Application")]
  public class IndexModel
  {
    // dane dotyczące zakresu aplikacji
    public ApplicationModel Application { get; set; }

    // wartości zaksięgowane
    [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; }
  }
}
  • wiersze 13, 16, 18: trzy wartości przesłane. Należy zauważyć, że [joursTravaillés] został zadeklarowany jako typ [double], podczas gdy w rzeczywistości oczekiwana jest liczba całkowita. Typ [double] został wprowadzony w celu ułatwienia walidacji tego pola po stronie klienta, ponieważ walidacja typu [int] sprawiała problemy;
  • wiersze 12, 14, 17: nazwy metod [Html.LabelFor] w widoku powiązanym z modelem;
  • wiersz 15: adnotacja umożliwiająca wyświetlanie pola [HeuresTravaillées] z dwoma miejscami po przecinku;
  • wiersz 5: zaznaczono, że właściwość o nazwie [Application] nie jest częścią wartości przesyłanych.

9.10.5. Widoki [Index.cshtml] i [Formulaire.cshtml]

Widok [Index.cshtml] jest wyświetlany przez następującą akcję [Index]:


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

Co ciekawe, widok [Index.cshtml] pozostaje niezmieniony:


@{
  ViewBag.Title = "Pam";
}
@Html.Partial("Formulaire")
  • widok nie deklaruje żadnego szablonu;
  • wiersz 4: zawiera widok częściowy [Formulaire.cshtml], również bez przekazywania do niego szablonu. W trakcie testów stwierdzono, że szablon [IndexModel] przekazany do widoku [Index.cshtml] był domyślnie propagowany do widoku częściowego [Formulaire.cshtml]. Ten ostatni widok mógłby teraz mieć następującą postać:

@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" />
  • wiersz 1: widok otrzymuje szablon typu [IndexModel];
  • wiersz 3: formularz;
  • wiersze 6–10: nagłówki tabeli danych wprowadzanych;
  • wiersze 12–14: wiersz danych;
  • wiersze 15–17: ewentualne komunikaty o błędach.

Zadanie: uzupełnij kod widoku [Formulaire.cshtml]. Wykorzystaj metody [DropDownListFor, EditorFor, LabelFor, ValidationMessageFor] opisane w paragrafie 5.7.


9.10.6. Test akcji [Index]

Napisaliśmy wszystkie elementy łańcucha przetwarzania dla akcji URL i [/Pam/Index]:

Testujemy aplikację przy użyciu [Ctrl-F5]:

Należy sprawdzić, czy lista rozwijana została poprawnie wypełniona listą pracowników, którą zdefiniowaliśmy w symulowanej warstwie [métier].

9.11. Krok 5: wdrożenie walidacji danych wprowadzanych

9.11.1. Problem

Mimo że nie podjęliśmy żadnych działań w tym zakresie, walidacje po stronie klienta już działają:

 
 

Walidacja po stronie klienta działa domyślnie z powodu poniższego wiersza 3 w pliku [Web.config] aplikacji.


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

Jednakże, ponieważ w pliku [IndexModel] zadeklarowano pole [JoursTravaillés] jako typu [double]:


    public double JoursTravaillés { get; set; }

w polu tym można wprowadzić liczbę rzeczywistą:

 

Ponadto w obu polach można wprowadzić dowolne wartości:

 

Szablon formularza [IndexModel] wygląda obecnie następująco:


using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace Pam.Web.Models
{
  [Bind(Exclude = "Application")]
  public class IndexModel
  {
    // dane dotyczące zakresu aplikacji
    public ApplicationModel Application { get; set; }

    // wartości zaksięgowane
    [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; }
  }
}

Zadanie: ulepsz ten szablon, aby:

  • uzyskać spersonalizowane komunikaty o błędach;
  • akceptować wyłącznie wartości rzeczywiste z przedziału [0,400] dla pola [HeuresTravaillées];
  • akceptować wyłącznie wartości całkowite z przedziału [0,31] dla pola [JoursTravaillées];

Można skorzystać z przykładu podanego w paragrafie 7.6.2. Aby sprawdzić, czy liczba przepracowanych dni jest liczbą całkowitą, można użyć wyrażenia regularnego (por. przykłady z paragrafu 5.9.1).

Oto przykłady oczekiwanych wyników:

 
 
 

9.11.2. Wprowadzanie liczb rzeczywistych w formacie francuskim

W obecnej wersji aplikacji liczba przepracowanych godzin musi być liczbą dziesiętną w formacie anglosaskim (z kropką dziesiętną). Format francuski z przecinkiem nie jest akceptowany:

 

Problem ten został zidentyfikowany i rozwiązany w punkcie 6.1.


Zadanie: postępując zgodnie z instrukcjami zawartymi w wyżej wymienionym paragrafie, wprowadź niezbędne zmiany, aby umożliwić wprowadzanie liczb rzeczywistych w francuskim formacie dziesiętnym. Przetestuj swoją aplikację.


Teraz poprzedni ekran wygląda następująco:

 

9.11.3. Weryfikacja formularza za pomocą linku JavaScript [Faire la simulation]

Obecnie można wysyłać nieprawidłowe wartości, jak pokazuje poniższa sekwencja:

 

Obecność symulacji w [1] oraz zmiana menu w [2] wskazują, że kliknięcie linku [Faire la simulation] spowodowało wysłanie formularza, mimo że wprowadzone wartości były nieprawidłowe. Problem ten został zidentyfikowany i rozwiązany w punkcie 7.6.5.


Zadanie: postępując zgodnie z procedurą opisaną w wyżej wymienionym paragrafie, należy zapewnić, aby wywołanie POST z linku [Faire la simulation] nie mogło nastąpić w przypadku wprowadzenia nieprawidłowych wartości. Należy pamiętać o wyczyszczeniu pamięci podręcznej przeglądarki przed przetestowaniem wprowadzonych zmian.


Przypominamy, że widok częściowy [Formulaire.cshtml] generuje formularz HTML o identyfikatorze [formulaire] (wiersz 1 poniżej):


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

Można to sprawdzić, wyświetlając kod źródłowy formularza w przeglądarce:


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

9.12. Krok 6: przeprowadzenie symulacji

9.12.1. Problem

Podczas przeprowadzania symulacji chcemy uzyskać następujący wynik:

Widok częściowy [Simulation.cshtml] wyświetla teraz listę płac pracownika.

9.12.2. Tworzenie widoku [Simulation.cshtml]

Widok [Simulation.cshtml] zmienia się w następujący sposób:


@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>
  • wiersz 1: widok [Simulation.cshtml] opiera się na typie [FeuilleSalaire] zdefiniowanym w paragrafie 9.7.3;
  • widok wykorzystuje klasy [libellé, info, valeur] zdefiniowane w arkuszu stylów aplikacji [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;
}

Ponadto, również w pliku [Site.css], ustala się wysokość wierszy poszczególnych tabel HTML w obszarze o identyfikatorze [simulation], dokładnie tam, gdzie wyświetlany jest arkusz płacowy:


#symulacja tabeli tr {
  height: 30px;
}

Zadanie: uzupełnij widok [Simulation.cshtml].


Aby wyświetlić kwotę w euro, należy użyć metody [string.Format]:

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

Powyższa instrukcja wyświetla [somme] jako wartość pieniężną [C] (waluta) z dwoma miejscami po przecinku [C2].

Aby przetestować ten widok, należy dostarczyć mu kartę wynagrodzenia. Musi ona zostać dostarczona przez akcję [/Pam/FaireSimulation], która jest celem wywołania Ajax linku [Faire la simulation]. Obecnie akcja ta wygląda następująco:


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

    // przeprowadź symulację
    [HttpPost]
    public PartialViewResult FaireSimulation()
    {
      return PartialView("Simulation");
}

W powyższym przykładzie akcja [FaireSimulation] nie przekazuje żadnego szablonu do widoku [Simulation.cshtml]. Musi ona przekazać do niego kartę wynagrodzenia. Wiadomo, że to warstwa [métier] dokonuje obliczeń list płac. Dostęp do tej warstwy [métier] można uzyskać poprzez szablon aplikacji [ApplicationModel], który zdefiniowaliśmy w punkcie 9.10.2:


  public class ApplicationModel
  {
    // --- dane dotyczące zakresu zastosowania ---
    public Employe[] Employes { get; set; }
    public IPamMetier PamMetier { get; set; }
    public SelectListItem[] EmployesItems { get; set; }
}

Dostęp do warstwy [métier] można uzyskać poprzez właściwość z wiersza 5 powyżej. Aby akcja [FaireSimulation] miała dostęp do warstwy [métier], przekażemy jej szablon aplikacji, tak jak zrobiliśmy to w przypadku akcji [Index]. Kod zmienia się wówczas w następujący sposób:


    // przeprowadź symulację
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application)
    {
      return PartialView("Simulation");
}

Teraz w ramach tej akcji możemy obliczyć fikcyjną kartę wynagrodzenia. Kod wygląda następująco:


// przeprowadź symulację
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application)
    {
      FeuilleSalaire feuilleSalaire = application.PamMetier.GetSalaire("254104940426058", 150, 20);
      return PartialView("Simulation", feuilleSalaire);
    }
  • W wierszu 5 obliczamy fikcyjne wynagrodzenie. Pierwszym parametrem jest istniejący numer SS. Został on zdefiniowany w klasie [métier], symulowanej w punkcie 9.7.5. Drugim parametrem jest liczba przepracowanych godzin, a trzecim – liczba przepracowanych dni;
  • wiersz 6: ten arkusz płacowy jest przekazywany jako szablon do widoku [Simulation.cshtml].

Jesteśmy teraz gotowi do przetestowania widoku [Simulation.cshtml]:

 

Nie wprowadzamy żadnych danych i uruchamiamy symulację. Otrzymujemy wówczas następujący wynik:

 

9.12.3. Obliczanie rzeczywistego wynagrodzenia

Nasza obecna akcja [FaireSimulation] nadal oblicza tę samą kartę wynagrodzenia:


    // przeprowadź symulację
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application)
    {
      FeuilleSalaire feuilleSalaire = application.PamMetier.GetSalaire("254104940426058", 150, 20);
      return PartialView("Simulation", feuilleSalaire);
}

Nie uwzględnia ona wprowadzonych informacji:

  • pracownika, dla którego obliczane jest wynagrodzenie;
  • liczby przepracowanych przez niego godzin;
  • liczby przepracowanych dni.

Wprowadzone wartości trafiają do akcji [FaireSimulation] w następujący sposób:

  1. użytkownik klika link [Faire la simulation]. Powoduje to uruchomienie funkcji JS [faireSimulation], którą już napisaliśmy;
  2. Następnie funkcja JS [faireSimulation] wysyła żądanie Ajax do akcji serwerowej [/Pam/FaireSimulation], nad którą obecnie pracujemy. Na razie funkcja JS [faireSimulation] nie przekazuje żadnych informacji do akcji serwerowej. Będzie musiała przekazać jej wartości wprowadzone przez użytkownika;
  3. akcja serwerowa [/Pam/FaireSimulation] pobierze wartości wprowadzone z danych przesłanych przez funkcję JS i [faireSimulation].

Zacznijmy od punktu 2: funkcja JS [faireSimulation] musi przesłać wartości wprowadzone przez użytkownika do akcji serwerowej [/Pam/FaireSimulation].


Zadanie: uzupełnij funkcję JS [faireSimulation] tak, aby wysyłała wartości wprowadzone przez użytkownika. Można skorzystać z przykładu z paragrafu 7.6.5, w którym omówiono ten problem.


Zajmijmy się teraz punktem 3 powyżej. Akcja serwera [/Pam/FaireSimulation] musi pobrać wartości przesłane przez funkcję JS [faireSimulation].


Zadanie: uzupełnij metodę serwerową [FaireSimulation] tak, aby obliczała wynagrodzenie na podstawie wartości przesłanych przez funkcje JS i [faireSimulation]. Można ponownie skorzystać z przykładu z paragrafu 7.6.5, w którym omówiono ten problem. Na razie założymy, że model oparty na przesłanych wartościach jest nadal aktualny.


Wskazówka: akcja serwera [FaireSimulation] przebiega w następujący sposób:


// przeprowadź symulację
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application, FormCollection data)
    {
      // tworzenie modelu akcji
      ...
      // próbujemy pobrać wartości zaksięgowane w tym modelu
      ...
      // obliczamy wynagrodzenie
      FeuilleSalaire feuilleSalaire = ...
      // wyświetlanie listy płac
      return PartialView("Simulation", feuilleSalaire);
    }

Oto przykładowe wykonanie:

Wybieramy [Justine Laverti]. Otrzymujemy wówczas następujący wynik:

Rzeczywiście uzyskaliśmy fikcyjny list płac o numerze [Justine Laverti]. Wcześniej obliczono tylko jeden list płac o numerze [Marie Jouveinal]. Zatem wykorzystano wartość zaksięgowaną dla wyboru pracownika. Nie można nic powiedzieć na temat liczby godzin i dni, ponieważ nasza symulowana warstwa [métier] ich nie uwzględnia.

9.12.4. Obsługa błędów

Przyjrzyjmy się następującemu przykładowi:

  • w warstwie [1] wybieramy pracownika, który nie istnieje (patrz definicja symulowanej warstwy [métier] w paragrafie 9.7.5;
  • w [2] przeprowadzamy symulację;
  • w pliku [3] poniżej wyświetla się strona błędu.

Co się stało?

Wykonano funkcję JS [faireSimulation]. Jej kod wygląda następująco:


function faireSimulation() {
...
  // ręczne wywołanie Ajax
  $.ajax({
    url: '/Pam/FaireSimulation',
...
    beforeSend: function () {
      // włącza się sygnał oczekiwania
      loading.show();
    },
    success: function (data) {
...
    },
    error: function (jqXHR) {
      // wyświetlanie błędu
      simulation.html(jqXHR.responseText);
      simulation.show();
    },
    complete: function () {
      // wyłączony sygnał oczekiwania
      loading.hide();
    }
  });
  // menu
  setMenu([lnkEffacerSimulation, lnkEnregistrerSimulation, lnkTerminerSession, lnkVoirSimulations]);
}

Wywołanie Ajax zakończyło się niepowodzeniem i uruchomiono funkcję z wierszy 14–18. Wyświetlono stronę błędu [jqXHR.responseText] zwróconą przez serwer. Jest ona dość szczegółowa. Symulowana warstwa [métier] zgłosiła wyjątek, ponieważ podany jej numer SS nie należy do żadnego istniejącego pracownika (patrz kod symulowanej warstwy [métier] w punkcie 9.7.5). Musimy poprawnie obsłużyć ten przypadek.

Utworzymy widok częściowy [Erreurs.chtml], który będzie zwracany do klienta JS za każdym razem, gdy po stronie serwera zostanie wykryty błąd:

Kod widoku częściowego [Erreurs.chtml] jest następujący:


@model IEnumerable<string>

<hr />
<h2>Les erreurs suivantes se sont produites</h2>
<ul>
  @foreach (string msg in Model)
  {
    <li>@msg</li>
  }
</ul>
  • wiersz 1: widok otrzymuje jako szablon listę komunikatów o błędach;
  • wiersze 5–10: które są wyświetlane na liście HTML;

Teraz zmodyfikujmy kod akcji serwera [FaireSimulation] w następujący sposób:


    // przeprowadź symulację
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application, FormCollection data)
    {
    ...
      // trwa obliczanie wynagrodzenia
      FeuilleSalaire feuilleSalaire = null;
      Exception exception=null;
      try
      {
        // obliczanie wynagrodzenia
        feuilleSalaire = ...
      }
      catch (Exception ex)
      {
        exception = ex;
      }
      // błąd?
      if (exception == null)
      {
        // wyświetlanie listy płac
        return PartialView("Simulation", feuilleSalaire);
      }
      else
      {
        // wyświetlanie strony błędów
        return PartialView("Erreurs", Static.GetErreursForException(exception));
      }
}
  • wiersze 9–17: obliczanie wynagrodzenia odbywa się teraz w bloku try/catch;
  • wiersz 27: jeśli wystąpił błąd, wyświetlany jest widok częściowy [Erreurs.cshtml], którego szablonem jest lista komunikatów o błędach dostarczona przez metodę statyczną [Static.GetErreursForException(exception)].

W klasie [Static] zgromadzono dwie statyczne funkcje pomocnicze [1]:


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

namespace PamWeb.Infrastructure
{
  public class Static
  {
    // lista komunikatów o błędach związanych z wyjątkiem
    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;
    }

    // lista komunikatów o błędach związanych z nieprawidłowym szablonem
    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;
    }

    // komunikat o błędzie związany z elementem szablonu akcji
    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;
    }

  }
}
  • wiersze 10–19: funkcja statyczna [GetErreursForException] zwraca listę błędów ze stosu wyjątków;
  • wiersze 22–36: funkcja statyczna [GetErreursForModel] zwraca listę błędów związanych z nieprawidłowym wzorcem akcji. Kod tej funkcji, a także kod prywatnej metody [getErrorMessageFor] (wiersze 39–54), pojawił się już wcześniej.

Po wykonaniu tych czynności możemy ponownie przetestować przypadek błędu:

  • w funkcji [1] wybieramy nieistniejącego pracownika;
  • w [2] przeprowadzamy symulację;
  • w [3] wyświetla się nowa strona błędów.

Wróćmy do akcji serwera [FaireSimulation]:


    // przeprowadź symulację
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application, FormCollection data)
    {
      // utworzenie modelu akcji
      IndexModel modèle = new IndexModel() { Application = application};
      // próba pobrania wartości zapisanych w modelu
      TryUpdateModel(modèle, data);
      // obliczanie wynagrodzenia
...
}

W wierszu 8 aktualizujemy model z wiersza 6 o wartości przesłane przez wywołanie Ajax. Nie sprawdzamy poprawności modelu. Należy to zrobić, ponieważ nie możemy wiedzieć, skąd pochodzą przesłane wartości. Ktoś mógł sfałszować akcję serwerową POST i przesłać nam nieprawidłowe dane.


Zadanie: zgodnie z modelem, który opracowaliśmy na wypadek wyjątku, zmodyfikuj akcję serwera [FaireSimulation] tak, aby wysyłała stronę błędów w przypadku nieprawidłowych danych przesłanych metodą POST. W tym celu wykorzystamy metodę statyczną [GetErreursForModel] klasy [Static].


Jak przetestować tę modyfikację? W punkcie 9.11.3 zadbaliście o to, aby funkcja JS [faireSimulation] nie przetwarzała wprowadzonych wartości za pomocą POST, jeśli były one nieprawidłowe. Proszę skomentować wiersze, które to realizują, a następnie przeprowadzić następujący test:

  • w [1] przeprowadzamy symulację z nieprawidłowymi wartościami;
  • w funkcji [2] otrzymujemy stronę błędów, którą właśnie utworzyliśmy, co dowodzi, że walidatory po stronie serwera zadziałały prawidłowo.

W dalszej części pamiętaj, aby usunąć komentarze z linii, które właśnie skomentowałeś w funkcji JS [faireSimulation].

9.13. Krok 7: Utworzenie sesji użytkownika

Aplikacja [Simulateur de calcul de paie] umożliwia użytkownikowi przeprowadzanie różnych symulacji rozliczeń płacowych za pomocą linku [Faire la simulation], zapisywanie ich za pomocą linku [Enregistrer la simulation], wyświetlać je za pomocą linku [Voir les simulations] oraz usuwać za pomocą linku [Retirer la simulation]. Wiemy, że pomiędzy dwoma kolejnymi żądaniami użytkownika nie ma pamięci, chyba że zostanie ona utworzona za pomocą mechanizmu sesji (patrz paragraf 4.10). Jest tu dość oczywiste, że musimy zachować w sesji listę symulacji zapisanych przez użytkownika na przestrzeni czasu. Istnieją również inne dane, które należy zapamiętać: gdy użytkownik przeprowadza symulację, jest ona zapisywana na liście symulacji tylko wtedy, gdy użytkownik o to poprosi za pomocą linku [Enregistrer la simulation]. Gdy to zrobi, musimy być w stanie odnaleźć symulację obliczoną w poprzednim zapytaniu. W tym celu zostanie ona również umieszczona w sesji. Na koniec będziemy numerować symulacje, zaczynając od 1. Aby poprawnie ponumerować nową symulację, należy zachować numer poprzedniej symulacji, również w sesji.

W punkcie 4.10 wprowadziliśmy pojęcie modelu sesji jako parametru wejściowego akcji, aby akcja ta miała dostęp do sesji. Powrócimy do tej koncepcji. Zachęcamy do ponownego przeczytania tego punktu, jeśli pojęcie to jest dla Państwa niejasne.

Tworzymy następującą klasę [SessionModel]:

Jej kod wygląda następująco:


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

namespace Pam.Web.Models
{
  public class SessionModel
  {
    // lista symulacji
    public List<Simulation> Simulations { get; set; }
    // numer kolejnej symulacji
    public int NumNextSimulation { get; set; }
    // ostatnia symulacja
    public Simulation Simulation { get; set; }

    // twórca
    public SessionModel()
    {
      // pusta lista symulacji
      Simulations = new List<Simulation>();
      // numer następnej symulacji
      NumNextSimulation = 1;
    }
  }
}

Klasa [Simulation] z wierszy 9 i 13 będzie rejestrować informacje dotyczące symulacji. Co musimy zarejestrować? Łącze [Faire la simulation] oblicza listę płac typu [FeuilleSalaire]. Naturalnym wydaje się umieszczenie go w symulacji. Ponadto musimy zapamiętać informacje, które doprowadziły do powstania tego arkusza płacowego:

  • wybranego pracownika. Znajdziemy go w polu [FeuilleSalaire.Employe]. Nie ma więc potrzeby zapisywania go po raz drugi;
  • liczba przepracowanych godzin i dni. Informacje te nie znajdują się w typie [FeuilleSalaire]. Musimy je zatem zapamiętać.

Wreszcie każda symulacja jest oznaczona numerem. Możemy zatem zacząć od następującej klasy [Simulation]:


using Pam.Metier.Entites;

namespace Pam.Web.Models
{
  public class Simulation
  {
    // numer symulacji
    public int Num { get; set; }
    // liczba przepracowanych godzin
    public double HeuresTravaillées { get; set; }
    // liczba przepracowanych dni
    public int JoursTravaillés { get; set; }
    // arkusz płacowy
    public FeuilleSalaire FeuilleSalaire { get; set; }
  }
}

Akcja serwera [FaireSimulation] musi, oprócz obliczenia listy płac, utworzyć symulację i umieścić ją w sesji. W tym celu otrzyma jako parametr szablon sesji:


// przeprowadź symulację
    [HttpPost]
    public PartialViewResult FaireSimulation(ApplicationModel application, SessionModel session, FormCollection data)
    {
      // utworzenie szablonu akcji
      IndexModel modèle = new IndexModel() { Application = application };
      // próbujemy pobrać wartości zapisane w szablonie
      TryUpdateModel(modèle, data);
      // czy szablon jest poprawny?
      if (!ModelState.IsValid)
      {
        // wyświetlanie strony błędów
        return PartialView("Erreurs", Static.GetErreursForModel(ModelState));
      }
      // obliczanie wynagrodzenia
      FeuilleSalaire feuilleSalaire = null;
      Exception exception = null;
      try
      {
        // obliczanie wynagrodzenia
        feuilleSalaire = application.PamMetier.GetSalaire(modèle.SS, modèle.HeuresTravaillées, (int)modèle.JoursTravaillés);
      }
      catch (Exception ex)
      {
        exception = ex;
      }
      // błąd?
      if (exception != null)
      {
        // wyświetla się strona błędów
        return PartialView("Erreurs", Static.GetErreursForException(exception));
      }
      // tworzy się symulację i umieszcza ją w sesji
      session.Simulation = ...
      // wyświetla się arkusz płacowy
      return PartialView("Simulation", feuilleSalaire);
    }
  • wiersz 3: akcja otrzymuje jako parametr szablon sesji;

Zadanie 1: uzupełnij kod akcji, wiersz 34



Zadanie 2: postępując zgodnie z instrukcjami z paragrafu 4.10, należy wykonać czynności niezbędne do tego, aby parametr [SessionModel session] akcji został prawidłowo zainicjowany przez framework. Jeśli nie zostaną podjęte żadne działania, dla tego parametru zostanie przypisany wskaźnik null.


9.14. Krok 8: zapisanie symulacji

9.14.1. Problem

Po przeprowadzeniu symulacji możemy ją zapisać:

Image

Widok częściowy [Simulations.cshtml] wyświetla teraz listę symulacji wykonanych przez użytkownika. Przypominamy, że obliczona karta płacowa jest fikcyjna.

9.14.2. Zapis akcji serwerowej [EnregistrerSimulation]

Link Ajax [Enregistrer la simulation] wywołuje akcję serwera [EnregistrerSimulation], której kod do tej pory wyglądał następująco:


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

Zmienia się on w następujący sposób:


    // zapisywanie symulacji
    [HttpPost]
    public PartialViewResult EnregistrerSimulation(SessionModel session)
    {
      // zapisuje się ostatnią wykonaną symulację na liście symulacji sesji
      ...
      // zwiększa się w sesji numer kolejnej symulacji
      ...
      // wyświetla listę symulacji
      ...
}
  • wiersz 1: akcja [EnregistrerSimulation] wymaga dostępu do sesji. Dlatego jako parametr przyjmuje model sesji.

Zadanie: uzupełnić akcję serwerową [EnregistrerSimulation].


9.14.3. Zapis częściowego widoku [Simulations.cshtml]

Poprzednia akcja [EnregistrerSimulation] powoduje wyświetlenie widoku częściowego [Simulations.cshtml], którego szablonem jest lista symulacji wykonanych przez użytkownika. Jego kod jest następujący:


@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>
...
}

Zadanie 1: uzupełnij kod widoku częściowego [Simulations.cshtml]. Do wyświetlania symulacji wykorzystamy tabelę HTML. Można skorzystać z przykładów podanych w paragrafie 5.4.


Uwaga: link [retirer] dla każdej symulacji z tabeli HTML będzie linkiem JavaScript o następującej postaci:

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

gdzie N to numer symulacji.


Zadanie 2: przetestuj swoją aplikację, przeprowadzając symulacje. Aby je przeprowadzić, należy wielokrotnie powtarzać następującą sekwencję czynności: 1) załaduj stronę aplikacji za pomocą [F5], 2) przeprowadź symulację, 3) zapisz ją. Symulacje będą gromadzić się w sesji, co powinno być widoczne w widoku [Simulations.cshtml].



Zadanie 3: ulepsz widok częściowy [Simulations.cshtml] w taki sposób, aby kolory wierszy tabeli HTML były naprzemienne.


Image

Wierszom <tr> tabeli HTML, klasy CSS, [pair] i [impair] zdefiniowane w arkuszu stylów [/Content/Site.css]:


.impair {
  background-color: beige;
}

.pair {
  background-color: lightsteelblue;
}

9.15. Krok 9: powrót do formularza wprowadzania danych

9.15.1. Problem

Po uzyskaniu listy symulacji możemy powrócić do formularza wprowadzania danych, czego od pewnego czasu nie było już możliwe:

Image

Image

9.15.2. Zapis akcji serwera [Formulaire]

Link Ajax [Retour au formulaire de simulation] wywołuje akcję serwera [Formulaire], której kod do tej pory wyglądał następująco:


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

Wyświetlany przez nią widok częściowy [Formulaire] oczekuje modelu [IndexModel] (wiersz 1 poniżej):


@model Pam.Web.Models.IndexModel

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

Z tego powodu link [Retour au formulaire de simulation] przestał działać.


Zadanie: napisać nową wersję akcji serwerowej [Formulaire] (2 wiersze do przepisania), a następnie przeprowadzić testy.


9.15.3. Modyfikacja funkcji JavaScript [retourFormulaire]

Dzięki wprowadzonej wcześniej zmianie można teraz powrócić do formularza, ale pojawia się wówczas błąd:

  • w [1] następuje powrót do formularza wprowadzania danych;
  • w [2] przeprowadzamy symulację z błędnymi danymi. Okazuje się wówczas, że walidatory po stronie klienta nie działają. W tym przypadku serwer został wywołany i zwrócił stronę błędów dzięki pracy wykonanej w paragrafie 9.12.4.

Ta anomalia została zidentyfikowana i usunięta w punkcie 7.6.7.


Zadanie: postępując zgodnie z procedurą opisaną w punkcie 7.6.7, popraw funkcję JavaScript [retourFormulaire], a następnie przeprowadź testy, aby sprawdzić, czy walidatory po stronie klienta znów działają.


9.16. Krok 10: zapoznaj się z listą symulacji

9.16.1. Problem

Podczas pracy z formularzem symulacyjnym można wyświetlić listę przeprowadzonych symulacji:

9.16.2. Zapis akcji serwera [VoirSimulations]

Link Ajax [Voir les simulations] wywołuje akcję serwera [VoirSimulations], której kod do tej pory wyglądał następująco:


    // przeglądaj symulacje
    [HttpPost]
    public PartialViewResult VoirSimulations()
    {
      return PartialView("Simulations");
}

Wyświetlany przez nią widok częściowy [Simulations] oczekuje szablonu [IEnumerable<Simulation>] (wiersz 1 poniżej):


@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>
...
}

Z tego powodu link [Voir les simulations] przestał działać.


Zadanie: napisać nową wersję akcji serwerowej [VoirSimulations] (2 wiersze do przepisania), a następnie przeprowadzić testy.


9.17. Krok 11: zakończenie sesji

9.17.1. Problem

W dowolnym momencie można zakończyć sesję użytkownika za pomocą linku [Ajax] [Terminer la session]. Powoduje to zamknięcie bieżącej sesji i rozpoczęcie nowej. Ponadto następuje powrót do widoku formularza:

  • w [1] przeprowadzono dwie symulacje, a następnie zakończono sesję;
  • w [2] powrócono do formularza wprowadzania danych. Chcemy wyświetlić symulacje;
  • w [3], z powodu zmiany sesji lista symulacji jest teraz pusta.

9.17.2. Zapis akcji serwera [TerminerSession]

Link Ajax [Terminer la session] wywołuje akcję serwera [TerminerSession], której kod do tej pory wyglądał następująco:


    // zakończenie sesji
    [HttpPost]
    public PartialViewResult TerminerSession()
    {
      return PartialView("Formulaire");
}

Wyświetlany przez nią widok częściowy [Formulaire] oczekuje modelu [IndexModel] (wiersz 1 poniżej):


@model Pam.Web.Models.IndexModel

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

Z tego powodu link [Terminer la session] przestał działać.


Zadanie: napisać nową wersję akcji serwerowej [TerminerSession] (2 wiersze do przepisania), a następnie przeprowadzić testy.


Uwaga: aby zakończyć sesję w akcji, należy wpisać:

Session.Abandon() ;

9.17.3. Modyfikacja funkcji JavaScript [terminerSession]

Dzięki wprowadzonej wcześniej modyfikacji można teraz powrócić do formularza, ale pojawia się wówczas błąd opisany wcześniej w paragrafie 9.15.3.


Zadanie: postępując zgodnie z procedurą opisaną w punkcie 9.15.3, popraw funkcję JavaScript [terminerSession], a następnie przeprowadź testy, aby sprawdzić, czy walidatory po stronie klienta znów działają.


9.18. Krok 12: usunięcie symulacji

9.18.1. Problem

Po przeprowadzeniu symulacji można ją usunąć za pomocą linku JavaScript [Effacer la simulation]:

9.18.2. Zapis akcji klienta [effacerSimulation]

Funkcja JavaScript [effacerSimulation] ma obecnie następujący kod:


function effacerSimulation() {
  // kasuje się dane wprowadzone w formularzu
  // ...
  // ukrywa symulację, jeśli istnieje
  $("#simulation").hide();
  // menu
  setMenu([lnkFaireSimulation, lnkTerminerSession, lnkVoirSimulations]);
}

Zadanie: uzupełnij ten kod. Można się zainspirować przykładem z paragrafu 7.6.6


9.19. Krok 13: usunięcie symulacji

9.19.1. Problem

Na stronie symulacji można usunąć niektóre z nich za pomocą linku JavaScript [retirer]:

Image

Image

9.19.2. Zapis akcji klienta [retirerSimulation]

Linki [retirer] mają następującą postać: HTML:

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

gdzie N to numer symulacji.


Zadanie: postępując zgodnie z instrukcjami zawartymi w paragrafach 9.9.3, napisz funkcję JS [retirerSimulation]. Funkcja ta wyśle wywołanie Ajax typu POST do akcji [/Pam/RetirerSimulation]. Przekaże ona wartość N w postaci num=N.


Uwaga: funkcja JS [retirerSimulation] działa analogicznie do innych funkcji JS, które zostały napisane i które wysyłają żądanie Ajax do serwera. Jedyną nowością jest tutaj wysłanie wartości POST, która nie znajduje się w formularzu. Wiadomo, że wysyłane wartości są zebrane w ciągu znaków w postaci:

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

, więc funkcja JS [retirerSimulation] będzie miała następującą postać:


function retirerSimulation(N) {
  // ręczne wywołanie Ajax
  $.ajax({
    url: '/Pam/RetirerSimulation',
...
    data:"num="+N,
...
  });
  // menu
  setMenu([lnkRetourFormulaire, lnkTerminerSession]);
}
  • wiersz 6: właściwość [data] wywołania Ajax JQuery reprezentuje ciąg znaków wysłany na serwer.

9.19.3. Zapis akcji serwerowej [RetirerSimulation]

Akcja serwera [RetirerSimulation]:

  • odbiera przesłany parametr o nazwie [num], który jest numerem symulacji;
  • musi usunąć z listy symulacji zapisanych w sesji symulację o tym numerze;
  • następnie musi wyświetlić nową listę symulacji.

Zadanie: napisz akcję serwera [RetirerSimulation]. Zapoznaj się z paragrafem 4.1, aby dowiedzieć się, jak pobrać parametr przesłany metodą POST o nazwie [num].


9.20. Krok 14: ulepszenie metody inicjalizacji aplikacji

Nasza aplikacja internetowa jest gotowa. Działa poprawnie z symulowaną klasą [métier]. Przypomnijmy sobie architekturę, którą opracowaliśmy:

Zanim przejdziemy do rzeczywistej implementacji warstwy [métier], pozostaje jeszcze kilka szczegółów do dopracowania, a dotyczy to metody inicjalizacji aplikacji: metody [Application_Start] w klasie [Global.asax]:

Metoda [Application_Start] w klasie [Global.asax] jest wykonywana tylko raz podczas uruchamiania aplikacji. To właśnie w tym miejscu można wykorzystać plik konfiguracyjny [Web.config]. Na razie nasza metoda [Application_Start] wygląda następująco:


// aplikacja
    protected void Application_Start()
    {
      // ----------Wygenerowane automatycznie
      AreaRegistration.RegisterAllAreas();
      WebApiConfig.Register(GlobalConfiguration.Configuration);
      FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
      RouteConfig.RegisterRoutes(RouteTable.Routes);
      BundleConfig.RegisterBundles(BundleTable.Bundles);
      // -------------------------------------------------------------------
      // ---------- konfiguracja specyficzna
      // -------------------------------------------------------------------
      // dane zakresu aplikacji
      ApplicationModel application = new ApplicationModel();
      Application["data"] = application;
      // instancja warstwy [métier]
      application.PamMetier = new PamMetier();
...
      // modele powiązań
...
}

W wierszu 17 warstwa biznesowa jest instancjonowana za pomocą operatora new. Ponadto model aplikacji jest zdefiniowany w następujący sposób:


  public class ApplicationModel
  {
    // --- dane zakresu aplikacji ---
    public Employe[] Employes { get; set; }
    public IPamMetier PamMetier { get; set; }
    public SelectListItem[] EmployesItems { get; set; }
}

W wierszu 5 powyżej widać, że typ właściwości [PamMetier] jest zgodny z typem interfejsu [IPamMetier]. Oznacza to, że właściwość ta może zostać zainicjowana przez dowolny obiekt implementujący ten interfejs. Jednak w wierszu 17 klasy [Application_Start] zapisaliśmy na stałe nazwę klasy implementującej interfejs [IPamMetier]. Gdyby więc warstwa [métier] miała zostać zaimplementowana za pomocą nowej klasy implementującej [IPamMetier], należałoby zmienić ten wiersz. Nie jest to zbyt istotne, ale można tego uniknąć. Definicję klasy implementującej interfejs [IPamMetier] można przenieść do pliku konfiguracyjnego. Aby zmienić implementację, wystarczy wówczas zmodyfikować zawartość tego pliku konfiguracyjnego. Kod .NET nie wymaga zmian.

W tym przypadku wykorzystamy kontener wstrzykiwania zależności [Spring.net]. Istnieją inne frameworki .NET, które pozwalają osiągnąć ten sam cel, być może w lepszy i prostszy sposób.

Architektura projektu wygląda następująco:

  • w [A] metoda inicjalizacji warstwy [ASP.NET MVC] zwróci się do [Spring.net] o odwołanie do symulowanej warstwy [métier];
  • w [B], [Spring.net] utworzy symulowaną warstwę [métier], wykorzystując swój plik konfiguracyjny w celu ustalenia, którą klasę ma instancjonować;
  • w [C], [Spring.net] zwróci odwołanie do symulowanej warstwy [métier] do warstwy [ASP.NET MVC].

Należy zauważyć, że domyślnie obiekty zarządzane przez [Spring.net] są singletonami: istnieją tylko w jednym egzemplarzu. Jeśli więc w dalszej części naszego przykładu kod ponownie zażąda od [Spring.net] odwołania do symulowanej warstwy [métier], [Spring.net] po prostu zwróci odwołanie do obiektu utworzonego początkowo.

9.20.1. Dodanie odwołań [Spring] do projektu internetowego

Wykorzystamy [Spring.net]. Ten framework jest dostarczany w postaci pliku DLL, który należy dodać do odwołań projektu. Można to zrobić w następujący sposób:

W [1] kliknij prawym przyciskiem myszy gałąź [References] projektu, a następnie wybierz opcję [Gérer les packages NuGet]. Wymagane jest połączenie z Internetem. Następnie należy postępować tak samo, jak wcześniej w przypadku biblioteki JQuery [Globalize]. Należy wyszukać słowo kluczowe [Spring.core] i zainstalować ten pakiet. Instalacja obejmuje dwa pakiety DLL: [Spring.core], [2] oraz [Common.Logging], [3]. W poniższych przykładach wykorzystano wersję 1.3.2 biblioteki Spring.

Uwaga: jeśli nie masz połączenia z Internetem, pliki te znajdziesz w folderze [lib] w materiałach do tego studium przypadku.

9.20.2. Konfiguracja [web.config]

Definicja klasy implementującej interfejs [IPamMetier] znajduje się w pliku [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>
  <!-- konfiguracja 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>
...
  • wiersze 2–8: należy zlokalizować tag <configSections> w pliku i wstawić do niego wiersze 4–7;
  • wiersz 4: atrybut [name="spring"] zawiera informacje dotyczące sekcji [spring] z wierszy 10–17;
  • wiersz 5: definiuje klasę [Spring.Context.Support.DefaultSectionHandler] znajdującą się w klasie DLL [Spring.Core] jako klasę zdolną do przetwarzania sekcji [objects] z wierszy 14–16;
  • wiersz 6: definiuje klasę [Spring.Context.Support.ContextHandler] znajdującą się w sekcji DLL [Spring.Core] jako klasę zdolną do przetwarzania sekcji [context] w wierszach 11–13;
  • wiersze 11–13: ta sekcja zawiera informację [<resource uri="config://spring/objects" />], która wskazuje, że obiekty Spring znajdują się w pliku konfiguracyjnym w sekcji [/spring/objects], tj. w wierszach 14–16;
  • wiersze 14–16: znacznik [objects] wprowadza obiekty Spring;
  • wiersz 15: definiuje obiekt oznaczony identyfikatorem [id="pammetier"], który jest instancją klasy [Pam.Metier.Service.PamMetier] znajdującej się w DLL [pam-metier-simule]. W tym miejscu nie można popełnić błędu. W przypadku atrybutu [id] można wpisać dowolną wartość. Identyfikator ten zostanie wykorzystany w [Global.asax]. Klasa [Pam.Metier.Service.PamMetier] odpowiada naszej symulowanej warstwie [métier]. Należy powrócić do jej definicji, aby poznać jej pełną nazwę:

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

W przypadku plików DLL i [pam-metier-simule] należy sprawdzić właściwości projektu C# [pam-metier-simule]:

Należy użyć nazwy podanej w pliku [1].

9.20.3. Modyfikacja pliku [Application_Start]

Metoda [Application_Start] zmienia się w następujący sposób:


using Spring.Context.Support;

// aplikacja
    protected void Application_Start()
    {
      // ---------- Wygenerowane automatycznie
      AreaRegistration.RegisterAllAreas();
      WebApiConfig.Register(GlobalConfiguration.Configuration);
      FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
      RouteConfig.RegisterRoutes(RouteTable.Routes);
      BundleConfig.RegisterBundles(BundleTable.Bundles);
      // -------------------------------------------------------------------
      // ---------- konfiguracja specyficzna
      // -------------------------------------------------------------------
      // dane zakresu aplikacji
      ApplicationModel application = new ApplicationModel();
      Application["data"] = application;
      // instancjonowanie warstwy [métier]
      application.PamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
...
      // modele wiązania
...
}
  • wiersz 19: wykorzystuje się klasę Spring [ContextRegistry], która umożliwia przetwarzanie pliku [web.config]. W tym celu należy zaimportować przestrzeń nazw z wiersza 1. Metoda statyczna [GetContext] pozwala uzyskać zawartość znaczników [context], które wskazują lokalizację obiektów Spring. Metoda statyczna [GetObject] pozwala następnie uzyskać konkretny obiekt identyfikowany na podstawie jego atrybutu id. Należy zauważyć, że obecnie nazwa klasy implementującej interfejs [IPamMetier] nie jest już zapisana na stałe w kodzie. Znajduje się ona teraz w pliku [web.config].

Po wprowadzeniu wszystkich tych zmian przetestuj swoją aplikację. Powinna działać.

9.20.4. Obsługa błędu inicjalizacji aplikacji

W metodzie [Application_Start] napisaliśmy:


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

Instrukcja po prawej stronie znaku = może zakończyć się niepowodzeniem. Może to wynikać z różnych przyczyn:

  • najbardziej oczywistą jest błąd w nazwie obiektu, który ma zostać zainicjowany;
  • innym jest nieprawidłowe utworzenie instancji warstwy [métier]. Nie może to mieć miejsca w przypadku naszej symulowanej warstwy [métier], ale może się zdarzyć w przypadku naszej rzeczywistej warstwy [métier], która będzie połączona z bazą danych. Warstwa SGBD może nie zostać uruchomiona, informacje o bazie danych, którą ma obsługiwać, mogą być nieprawidłowe itp.

Ewentualny wyjątek obsłużymy w bloku try/catch. Kod wygląda następująco:


// aplikacja
    protected void Application_Start()
    {
      // ----------Wygenerowane automatycznie
...
      // -------------------------------------------------------------------
      // ---------- konfiguracja specyficzna
      // -------------------------------------------------------------------
      // dane zakresu aplikacji
      ApplicationModel application = new ApplicationModel();
      Application["data"] = application;
      application.InitException = null;
      try
      {
        // instancja warstwy [métier]
        application.PamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
      }
      catch (Exception ex)
      {
        application.InitException = ex;
      }
      //, jeśli nie ma błędu
      if (application.InitException == null)
      {
....
      }
      // modele wiązania
...
    }
  • w linii 12 wprowadzamy nową właściwość o nazwie [InitException] do modelu aplikacji:

  public class ApplicationModel
  {
    // --- dane zakresu aplikacji ---
    public Employe[] Employes { get; set; }
    public IPamMetier PamMetier { get; set; }
    public SelectListItem[] EmployesItems { get; set; }
    public Exception InitException { get; set; }
}
  • w wierszu 7 powyżej – wyjątek, który może wystąpić podczas inicjalizacji aplikacji;
  • wiersze 13–21 w pliku [Application_Start]: instancja warstwy [métier] jest teraz tworzona w bloku try/catch;
  • wiersz 20: zapisujemy wyjątek;
  • wiersze 23–26: jeśli nie wystąpił błąd, wykonywany jest kod, który był wcześniej;
  • wiersz 28: obiekty [ModelBinders] są tworzone niezależnie od tego, czy wystąpił błąd, czy nie. To ważne. Chcemy mieć pewność, że model aplikacji [ApplicationModel] zostanie prawidłowo powiązany przez framework.

Wiemy, że podczas uruchamiania aplikacji wykonywana jest akcja serwerowa [Index]. Na razie wygląda ona następująco:


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

W wierszu 2 akcja [Index] odbiera model aplikacji. Dzięki temu może sprawdzić, czy inicjalizacja przebiegła pomyślnie, czy nie, i wyświetlić stronę z komunikatami o błędach, jeśli inicjalizacja z jakiegoś powodu się nie powiodła. Modyfikujemy kod w następujący sposób:


    [HttpGet]
    public ViewResult Index(ApplicationModel application)
    {
      // błąd inicjalizacji?
      if (application.InitException != null)
      {
        // strona błędów bez menu
        return View("InitFailed",Static.GetErreursForException(application.InitException));
      }
      // brak błędu
      return View(new IndexModel() { Application = application });
}

W wierszu 8, w przypadku błędu inicjalizacji, wyświetlamy widok [InitFailed.cshtml], którego szablonem jest lista komunikatów o błędach związanych z wyjątkiem, który wystąpił podczas inicjalizacji. Metoda [Static.GetErreursForException] została przedstawiona i wyjaśniona w paragrafie 9.12.4. Widok [InitFailed.cshtml] będzie wyglądał następująco:

Jej kod wygląda następująco:


@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>
  • wiersz 1: szablonem widoku jest lista komunikatów o błędach. Są one wyświetlane na liście HTML w wierszach 24–29;
  • wiersz 3: ten widok nie korzysta ze strony głównej [_Layout.cshtml]. Nie chcemy bowiem menu dostarczanego przez ten dokument. Tworzymy zatem kompletną stronę HTML (wiersze 5–23).

Aby to przetestować, wystarczy zmodyfikować w pliku [Application_Start] instancję warstwy [métier] w następujący sposób:


      try
      {
        // instancjonowanie warstwy [métier]
        application.PamMetier = ContextRegistry.GetContext().GetObject("xx") as IPamMetier;
      }
      catch (Exception ex)
      {
        application.InitException = ex;
}

W wierszu 4 szukany jest obiekt, który nie istnieje wśród obiektów Spring.

Po zatwierdzeniu tych zmian i uruchomieniu aplikacji wyświetla się następująca strona:

 

Wyświetla się strona błędów bez menu. Użytkownik nie może nic zrobić poza stwierdzeniem błędu. Taki był zamierzony efekt.

9.21. Na jakim etapie jesteśmy?

Mamy teraz działającą aplikację internetową, która współpracuje z symulowaną warstwą biznesową. Jej architektura wygląda następująco:

Warstwa [ASP.NET MVC] współpracuje z symulowaną warstwą biznesową za pośrednictwem interfejsu [IPamMetier]. Jeśli zastąpimy tę symulowaną warstwę biznesową rzeczywistą warstwą biznesową zgodną z tym interfejsem, nie będziemy musieli modyfikować kodu warstwy internetowej. Dzięki [Spring.net] wystarczy nam jedynie zmienić w [web.config] klasę implementującą interfejs [IPamMetier]. Obieramy tę ścieżkę.

Nowa architektura będzie wyglądać następująco:

Opiszemy kolejno:

  • warstwę [EF5] połączoną z SGBD. Zostanie ona zaimplementowana przy użyciu Entity Framework 5 (EF5);
  • warstwę [DAO], która zarządza dostępem do danych za pośrednictwem warstwy [EF5]. Dzięki temu może ona ignorować istnienie warstwy SGBD. Warstwa ta ogranicza się do obsługi encji aplikacji [Employe, Cotisations, Indemnites];
  • warstwa [métier], która realizuje obliczenia wynagrodzenia.

Nowa architektura to ta przedstawiona na samym początku niniejszego dokumentu w punkcie 1.1, którą teraz przypominamy:

  • warstwa [Web] jest warstwą mającą kontakt z użytkownikiem aplikacji internetowej. Użytkownik ten wchodzi w interakcję z aplikacją internetową za pośrednictwem stron internetowych wyświetlanych w przeglądarce. To właśnie w tej warstwie znajdują się ASP.NET i MVC, i wyłącznie w tej warstwie;
  • warstwa [métier] realizuje reguły biznesowe aplikacji, takie jak obliczanie wynagrodzenia lub faktury. Warstwa ta wykorzystuje dane pochodzące od użytkownika za pośrednictwem warstwy [Web] oraz dane z warstw SGBD za pośrednictwem warstwy [DAO];
  • warstwa [DAO] (obiekty dostępu do danych), warstwa [ORM] (mapowanie obiektowo-relacyjne) oraz łącznik ADO.NET zarządzają dostępem do danych z warstwy SGBD. Warstwa [ORM] stanowi pomost między obiektami obsługiwanymi przez warstwę [DAO] a wierszami i kolumnami danych w relacyjnej bazie danych. Na całym świecie powszechnie stosowane są dwa moduły: ORM, NET, NHibernate (http://sourceforge.net/projects/nhibernate/) oraz Entity Framework (http://msdn.microsoft.com/en-us/data/ef.aspx);
  • integrację warstw można zrealizować za pomocą kontenera wstrzykiwania zależności (Dependency Injection Container), takiego jak Spring (http://www.springframework.net/);

Warstwy [métier], [DAO], [EF5] zostaną zaimplementowane przy użyciu projektów w języku C#. Od tej chwili będziemy pracować w programie Visual Studio Express 2012 dla komputerów stacjonarnych.

9.22. Krok 15: wdrożenie warstwy Entity Framework 5

Utworzenie warstwy [EF5] to w mniejszym stopniu kwestia kodowania, a w większym – konfiguracji. Aby zapoznać się z procesem tworzenia tej warstwy, należy przeczytać dokument [Introduction à Entity Framework 5 Code First] dostępny na stronie URL [http://tahe.developpez.com/dotnet/ef5cf-02/]. Jest to dość obszerny dokument. Podstawowe informacje znajdują się w pierwszych czterech rozdziałach. Wskazane zostaną konkretne akapity, na które należy zwrócić szczególną uwagę. Odwołując się do tego dokumentu, będziemy używać oznaczenia [refEF5].

Ponadto czasami będziemy potrzebować pojęć z języka C#. Wówczas będziemy odwoływać się do kursu [Introduction au langage C#], dostępnego pod adresem URL [http://tahe.developpez.com/dotnet/csharp/], używając oznaczenia [refC#].

9.22.1. Baza danych

Baza danych aplikacji została przedstawiona w punkcie 9.4. Jest to baza danych o nazwie MySQL, nazwana [dbpam_ef5] (pam = Paie Assistante Maternelle). Baza ta ma administratora o nazwie root bez hasła.

Przypomnijmy schemat bazy danych. Zawiera ona trzy tabele:

Image

Między kolumną EMPLOYES (INDEMNITE_ID) a kolumną INDEMNITES (ID) istnieje relacja klucza obcego. Część struktury tej bazy danych wynika z jej wykorzystania wraz z tabelą EF5.

Skrypt SQL służący do utworzenia bazy danych wygląda następująco:


-- phpMyAdmin SQL Zrzut
-- wersja 3.5.1
-- http://www.phpmyadmin.net
--
-- Klient: localhost
-- Wygenerowano: poniedziałek, 4 listopada 2013 r. o godz. 09:34
-- Wersja serwera: 5.5.24-log
-- Wersja 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 */;

--
-- Baza danych: `dbpam_ef5`
--

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

--
-- Struktura tabeli `składki`
--

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 ;

--
-- Zawartość tabeli `cotisations`
--

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

--
-- Wyzwalacze `składki`
--
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 ;

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

--
-- Struktura tabeli `pracownicy`
--

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 ;

--
-- Zawartość tabeli `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);

--
-- Wyzwalacze `pracownicy`
--
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 ;

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

--
-- Struktura tabeli `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 ;

--
-- Zawartość tabeli `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);

--
-- Wyzwalacze `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 ;

--
-- Ograniczenia dla eksportowanych tabel
--

--
-- Ograniczenia dla tabeli `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 */;

Należy zwrócić uwagę na następujące kwestie:

  • wiersze 30, 73, 122: klucze główne tabel znajdują się w trybie [AUTO_INCREMENT]. Zarządza nimi MySQL, a nie EF5;
  • wiersz 83: numer SS podlega ograniczeniu unikalności;
  • wiersz 130: indeks pracownika ma ograniczenie unikalności;
  • wiersze 168–169: klucz obcy z tabeli [employes] do tabeli [indemnites];
  • wiersz 49: wyzwalacz o nazwie [Trigger] jest skryptem SQL osadzonym w SGBD i wykonywanym w określonych momentach;
  • wiersze 51–54: wyzwalacz [INCR_VERSIONING_COTISATIONS] uruchamia się przed każdą modyfikacją wiersza w tabeli [cotisations]. Następnie zwiększa o jeden wartość w kolumnie [VERSIONING];
  • wiersze 59–62: wyzwalacz [START_VERSIONING_COTISATIONS] uruchamia się przed każdym wstawieniem nowego wiersza do tabeli [cotisations]. Następnie ustawia on wartość kolumny [VERSIONING] na 1;
  • w rezultacie kolumna [VERSIONING] przyjmuje wartość 1 po utworzeniu wiersza w tabeli [cotisations], a następnie jest zwiększana o 1 przy każdej modyfikacji tego wiersza. Mechanizm ten pozwala procesowi EF5 zarządzać współbieżnością dostępu do wiersza w tabeli [cotisations] w następujący sposób:
    • proces P1 odczytuje wiersz L z tabeli [cotisations] w momencie T1. Wiersz ten ma kolumnę [VERSIONING] o wartości V1;
    • proces P2 odczytuje ten sam wiersz L z tabeli [cotisations] w momencie T2. Wiersz ma kolumny o wartościach [VERSIONING] i V1, ponieważ proces P1 nie zatwierdził jeszcze swojej zmiany;
    • Proces P1 modyfikuje wiersz L i zatwierdza tę zmianę. Kolumna [VERSIONING] w wierszu L zmienia się wówczas na V1+1 z powodu wyzwalacza [INCR_VERSIONING_COTISATIONS];
    • następnie proces P2 postępuje analogicznie. EF5 generuje wówczas wyjątek, ponieważ proces P2 zawiera wiersz, w którym kolumna [VERSIONING] ma wartość V1, różną od wartości znalezionej w bazie, która wynosi V1+1. Wiersz można zmodyfikować tylko wtedy, gdy wartość [VERSIONING] jest taka sama jak w bazie danych.

Nazywa się to optymistycznym zarządzaniem dostępem współbieżnym. W przypadku EF5 pole pełniące tę rolę musi posiadać adnotację [ConcurrencyCheck].

  • Analogiczny mechanizm jest tworzony dla tabeli [employes] (wiersze 98–113) oraz tabeli [indemnites] (wiersze 144–159).

Zadanie: Utwórz bazę danych MySQL i [dbpam_ef5] za pomocą poprzedniego skryptu SQL. Baza danych [dbpam_ef5] musi zostać utworzona wcześniej, ponieważ skrypt jej nie tworzy. Następnie na tej bazie uruchomimy skrypt SQL.


9.22.2. Projekt Visual Studio

W programie Visual Studio Express 2012 dla komputerów stacjonarnych wczytujemy rozwiązanie [pam-td] użyte podczas tworzenia warstwy [web]:

  • w [1], VS – Visual Studio Express 2012 dla komputerów stacjonarnych nie jest w stanie załadować projektu internetowego [pam-web-01]. Jest to normalne i nie stanowi przeszkody;
  • w pliku [2] dodajemy nowy projekt do rozwiązania [pam-td];
  • w [3] projekt jest typu [console] i nosi nazwę [4] [pam-ef5];
  • w [5] – utworzony projekt. Jego nazwa nie jest pogrubiona, więc nie jest to projekt startowy rozwiązania;
  • w [6] i [7] definiujemy nowy projekt jako projekt startowy.

9.22.3. Dodanie niezbędnych odniesień do projektu

Przyjrzyjmy się projektowi jako całości:

Nasz projekt wymaga kilku plików DLL:

  • DLL z Entity Framework 5;
  • DLL łącznika ADO.NET dla SGBD MySQL.

W punkcie 4.2 dokumentu [refEF5] wyjaśniono, jak zainstalować te pliki DLL za pomocą narzędzia [NuGet]. Obecnie (listopad 2013 r.) dostępna jest wersja Entity Framework 6 (EF6). Niestety wydaje się, że łącznik ADO.NET z pakietu SGBD MySQL dostępnego (listopad 2013 r.) poprzez [NuGet] nie jest kompatybilny z EF6. W związku z tym umieszczono w folderze [lib] [1] plik DLL zEF5, a także inne pliki DLL niezbędne do realizacji projektu [pam-ef5]

Umieściliśmy kolejne pliki DLL w folderze [lib]. Wykorzystamy je później. W pliku [2] dodajemy te nowe pliki DLL do projektu.

  • w pliku [3] przechodzimy przez system plików do folderu [lib];
  • w pliku [4] wybieramy trzy pliki DLL, a następnie dwukrotnie potwierdzamy;
  • w pliku [5] trzy pliki DLL zostały dodane do odwołań projektu.

Potrzebujemy jeszcze jednego pliku DLL. Znajdziemy go wśród plików frameworku .NET na tym komputerze.

  • w pliku [1] dodaj nowe odniesienie do projektu;
  • w [2] wybierz [Assemblys];
  • w [3] wpisz [system.component];
  • z [4] wybierz zestaw [System.ComponentModel.DataAnnotations];
  • w [5] dodano odniesienie.

Jesteśmy teraz gotowi do pisania kodu i konfiguracji.

9.22.4. Entities w Entity Framework

Entities w Entity Framework to klasy, w których hermetyzuje się wiersze z różnych tabel bazy danych. Przypomnijmy sobie te tabele:

Image

W warstwie [web] korzystaliśmy z encji [Employe, Cotisations, Indemnités] (patrz punkt 9.7.3, strona 219). Nie były one wiernym odzwierciedleniem tabel. W związku z tym kolumny [ID, VERSIONING] zostały pominięte. W tym przypadku tak nie będzie, ponieważ są one wykorzystywane przez jednostki ORM i EF5. Dodamy więc do nich brakujące właściwości. Tworzymy te jednostki w folderze [Models] projektu:

Ich nowy kod wygląda teraz następująco:

Klasa [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; }

    // podpis
    public override string ToString()
    {
      return string.Format("Cotisations[{0},{1},{2},{3}, {4}, {5}]", Id, Versioning, CsgRds, Csgd, Secu, Retraite);
    }
  }
}
  • wiersz 3: przestrzeń nazw została dostosowana do nowego projektu;
  • dodano właściwości w wierszach 7 i 12, aby odzwierciedlić strukturę tabeli [cotisations];
  • wiersz 17: metoda [ToString] wyświetla teraz oba nowe pola.

Klasa [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; }

    // podpis
    public override string ToString()
    {
      return string.Format("Indemnités[{0},{1},{2},{3},{4}, {5}, {6}]", Id, Versioning, Indice, BaseHeure, EntretienJour, RepasJour, IndemnitesCp);
    }
  }
}
  • wiersz 3: przestrzeń nazw została dostosowana do nowego projektu;
  • dodano właściwości w wierszach 7 i 13, aby odzwierciedlić strukturę tabeli [indemnites];
  • wiersz 18: metoda [ToString] wyświetla teraz oba nowe pola.

Klasa [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; }

    // podpis
    public override string ToString()
    {
      return string.Format("Employé[{0},{1},{2},{3},{4},{5}, {6}, {7}]", Id, Versioning, SS, Nom, Prenom, Adresse, Ville, CodePostal);
    }
  }
}
  • wiersz 3: przestrzeń nazw została dostosowana do nowego projektu;
  • dodano właściwości w wierszach 8 i 16, aby odzwierciedlić strukturę tabeli [employes];
  • wiersz 21: metoda [ToString] wyświetla teraz oba nowe pola.

Aby klasy te mogły być wykorzystywane przez metody ORM i EF5, ich właściwości muszą być opatrzone adnotacjami.


Zadanie: korzystając z paragrafu 3.4 [Création de la base à partir des entités] dokumentu [refEF5], dodaj do encji [Employe, Cotisations, Indemnites] adnotacje niezbędne dla EF5.


Wskazówki:

  • chodzi wyłącznie o utworzenie adnotacji. Nie należy kierować się częścią [création de base] z cytowanego akapitu;
  • w przypadku adnotacji [Table] należy postępować zgodnie z przykładem MySQL z paragrafu 4.2 dokumentu [refEF5];
  • w przypadku adnotacji [ConcurrencyCheck] dotyczącej właściwości [Versioning] należy postępować zgodnie z przykładem Oracle podanym w paragrafie 5.2 dokumentu [refEF5];
  • w przypadku klucza obcego, który tabela [employes] posiada w tabeli [indemnités], należy postępować zgodnie z przykładem 3.4.2 z dokumentu [refEF5]. W ten sposób dodasz nową właściwość do encji [Employe]:

    public int IndemniteId { get; set; }

której wartością będzie wartość z kolumny [INDEMNITES_ID] w tabeli [employes]. Należy przypisać adnotacje klucza obcego do właściwości [IndemniteId] i [Indemnites] encji [Employe]. W tym celu należy postępować zgodnie z przykładem 3.4.2 dotyczącym [refEF5];

  • nie należy zajmować się odwrotnymi relacjami kluczy obcych;
  • praca ta wymaga zapoznania się z dokumentacją [refEF5].

9.22.5. Konfiguracja ORM i EF5

Przyjrzyjmy się projektowi jako całości:

Warstwa [EF5] będzie uzyskiwać dostęp do bazy danych za pośrednictwem łącznika [ADO.NET] z SGBD MySQL. Aby uzyskać dostęp do tej bazy, warstwa ta potrzebuje pewnych informacji. Znajdują się one w różnych miejscach projektu.

Najpierw musimy utworzyć kontekst bazy danych. Kontekst ten jest klasą wywodzącą się z klasy systemowej [System.Data.Entity.DbContext]. Służy on do definiowania obrazów obiektów tabel bazy danych. Umieścimy tę klasę w folderze [Models] projektu wraz z encjami EF5:

Klasa [DbPamContext] będzie wyglądać następująco:


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; }
  }
}
  • wiersz 6: klasa [DbPamContext] wywodzi się z klasy systemowej [DbContext];
  • wiersze 8–10: obiekty obrazów z trzech tabel bazy danych. Ich typem jest [DbSet<Entity>], gdzie [Entity] jest jedną z encji Entity Framework, którą właśnie zdefiniowaliśmy. Typ [DbSet] można postrzegać jako zbiór encji. Można go wywołać za pomocą LINQ (Language INtegrated Query). Czytelnik, który nie zna LINQ, powinien zapoznać się z paragrafem 3.5.4 [Apprentissage de LINQ avec LINQPad] w dokumencie [refEF5].

W dalszej części będziemy nazywać klasę [DbPamContext] kontekstem trwałości bazy danych [dbpam_ef5]. Jest to terminologia powszechnie stosowana w ORM (Object Relational Mapper). Ten kontekst trwałości stanowi obiektowy obraz bazy danych. Mówi się również o synchronizacji kontekstu trwałości z bazą danych: zmiany, dodania i usunięcia dokonane w kontekście trwałości są odzwierciedlane w bazie danych. Synchronizacja ta odbywa się w określonych momentach: przy zamknięciu kontekstu trwałości, na końcu transakcji lub przed zapytaniem SQL SELECT skierowanym do bazy danych.

Informacje dotyczące SGBD i bazy danych są umieszczane w [App.config].

Konfiguracja wymagana w pliku [app.config] została wyjaśniona w poniższych akapitach pliku [refEF5]:

  • 3.4 dla serwera SGBD SQL. To właśnie tam określono główne zasady konfiguracji EF5;
  • 4.2 dla serwera SGBD MySQL.

Postępujemy zgodnie z tym ostatnim akapitem i konfigurujemy plik [app.config] w następujący sposób:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
    <!-- konfiguracja EF5 -->
    <!-- łańcuch połączenia z bazą danych [dbam_ef5] -->
    <connectionStrings>
        <add name="DbPamContext"
         connectionString="Server=localhost;Database=dbpam_ef5;Uid=root;Pwd=;"
         providerName="MySql.Data.MySqlClient" />
    </connectionStrings>
    <!-- dostawca fabryczny dla 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>
  • dodano wiersze 6–21. Należy je umieścić w tagu <configuration> w wierszach 2 i 22;
  • wiersze 8–12: definiują ciągi połączeń z bazami danych, zgodnie z koncepcją opisaną w pliku ADO.NET (patrz akapit 7.3.5 w pliku [refC#]);
  • wiersze 9–11: definiują ciąg połączenia z bazą danych MySQL [dbpam_ef5];
  • wiersz 9: nazwa ciągu połączenia. Nie można tu wpisać byle czego. Domyślnie należy podać nazwę klasy implementującej kontekst bazy danych:

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

Klasa nosi nazwę [DbPamContext]. W wierszu 9 pliku [app.config] należy zatem wpisać [name="DbPamContext"];

  • w wierszu 10: ciąg połączenia właściwy dla klasy SGBD MySQL:
    • [Server=localhost]: adres IP serwera, na którym znajduje się SGBD. W tym przypadku jest to komputer lokalny [localhost];
    • [Database=dbpam_ef5;]: nazwa bazy danych,
    • [Uid=root;]: nazwa użytkownika, za pomocą której będziemy logować się do bazy,
    • [Pwd=;]: hasło do tej nazwy użytkownika. W tym przypadku brak hasła;
  • wiersz 10: [providerName="MySql.Data.MySqlClient"] to nazwa łącznika ADO.NET, którego należy użyć. Nazwa ta odpowiada atrybutowi [invariant] z wiersza 17. Można wpisać dowolną nazwę, o ile przestrzega się powyższej zasady i nie zarejestrowano już dostawcy o tej samej niezmiennej;
  • wiersze 15–20: definiują fabrykę (factory) dostawców (provider) ADO.NET. [DbProviderFactory] to dla mnie nieco niejasna koncepcja. Jeśli wierzyć jego nazwie, byłaby to klasa zdolna do wygenerowania łącznika ADO.NET, który zapewnia dostęp do SGBD, a w tym przypadku do MySQL5. Zazwyczaj te linie są kopiowane i wklejane. Są one niezbędne. Należy zwrócić uwagę na atrybut [Version=6.5.4.0] w wierszu 16. Ten numer wersji musi odpowiadać numerowi wersji DLL [MySql.Data], który dodaliście do referencji projektu:
  • Wiersz 16 jest ważny. Ponieważ nie można zainstalować dwóch dostawców o tej samej nazwie, najpierw usuwamy ewentualnie zainstalowanego dostawcę o nazwie takiej samej jak ten, którego instalujemy w wierszu 17;

To wszystko. Przy pierwszym razie wydaje się to skomplikowane i niejasne, ale z czasem staje się proste, ponieważ zawsze powtarza się tę samą procedurę.

9.22.6. Testowanie warstwy [EF5]

Jesteśmy gotowi do przetestowania naszej warstwy [EF5]. Robimy to za pomocą już zainstalowanego programu [Program.cs]:

Wyświetlimy zawartość bazy danych. Jeśli nam się to uda, będzie to wstępna wskazówka, że nasza konfiguracja jest poprawna. Przykładowy kod jest dostępny w paragrafie 3.5.3 pliku [refEF5]. Kod pliku [Program.cs] będzie wyglądał następująco:


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

namespace Pam
{
  class Program
  {
    static void Main(string[] args)
    {
      try
      {
        using (var context = new DbPamContext())
        {
          // wyświetla zawartość tabel
          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;
      }
    }
  }
}
  • wiersz 13: wszelkie operacje na BD są wykonywane w kontekście tej bazy. Zaimplementowaliśmy ten kontekst za pomocą klasy [DbPamContext]. Nazwaliśmy go również kontekstem trwałości bazy;
  • wiersze 13, 31: operacje na kontekście trwałości są wykonywane w klauzuli [using]. Kontekst trwałości jest otwierany na początku klauzuli [using] i automatycznie zamykany po zakończeniu tej klauzuli. Oznacza to, że wszelkie zmiany wprowadzone w kontekście trwałości w klauzuli [using] zostaną odzwierciedlone w bazie danych po zakończeniu klauzuli. Następnie w ramach transakcji wysyłana jest seria poleceń SQL do BD. Oznacza to, że jeśli polecenie SQL zakończy się niepowodzeniem, wszystkie wcześniej wysłane polecenia SQL zostaną anulowane. Wówczas przez EF5 generowany jest wyjątek;
  • wiersz 17: wyrażenie [context.Employes] odnosi się do obiektu obrazu z tabeli [employes]. Przypominamy, że [Employes] jest właściwością kontekstu trwałości [DbPamContext]:

  public class DbPamContext : DbContext
  {
    public DbSet<Employe> Employes { get; set; }
    public DbSet<Cotisations> Cotisations { get; set; }
    public DbSet<Indemnites> Indemnites { get; set; }
}
  • wiersz 17: fakt, że [foreach] przegląda kolekcję [context.Employes], spowoduje pobranie wszystkich pracowników z bazy danych do kontekstu trwałości. W związku z tym EF5 wygeneruje polecenie SQL SELECT;
  • wiersze 17–20: przeglądamy kolekcję pracowników, a w wierszu 19 używamy metody [ToString] klasy [Employe], aby wyświetlić pracowników na konsoli;
  • wiersze 21–25: to samo dotyczy zbioru dodatków;
  • wiersze 27–30: to samo dotyczy zbioru składek.

Wróćmy do definicji encji [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; }

    // podpis
    public override string ToString()
    {
      return string.Format("Employé[{0},{1},{2},{3},{4},{5}, {6}, {7}]", Id, Versioning, SS, Nom, Prenom, Adresse, Ville, CodePostal);
    }
  }
}
  • wiersz 15: pracownik ma odniesienie do świadczenia.

Czy przywracając pracownika do kontekstu trwałości, przywracamy również jego dodatek? Domyślnie odpowiedź brzmi: nie. Na tym polega koncepcja jednostki [Lazy Loading]. Entities, do których odwołuje się inna entyteta, nie są przenoszone do kontekstu trwałości wraz z tą inną entytetą. Są one przenoszone tylko wtedy, gdy są żądane przez kod w ramach otwartego kontekstu trwałości. Jeśli kontekst trwałości jest zamknięty, generowany jest wyjątek.

Tak więc, gdyby metoda [ToString] odwoływała się do właściwości [Indemnites] w następujący sposób:


    // podpis
    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);
}

następująca operacja w metodzie [Program.cs]:


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

spowodowałaby przywrócenie do kontekstu trwałości nie tylko pracowników, ale również ich dodatków, ponieważ w wierszu 3 wywoływana jest metoda [Employe.ToString], która odwołuje się do encji [Indemnites].

Wykonanie procedury [Program.cs] daje następujące wyniki:

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]

Co zrobić, jeśli to nie działa? Masz pecha... Istnieje wiele możliwych źródeł błędów:

  • sprawdź konfigurację EF5 (punkt 9.22.5);
  • sprawdź swoje encje Entity Framework (punkt 9.22.4).

9.22.7. DLL z warstwy [EF5]

Przekształcamy nasz projekt w bibliotekę klas, aby podczas generowania tworzono plik .dll zamiast pliku .exe. Odbywa się to w właściwościach projektu, tak jak pokazano w punkcie 9.7.6 dla symulowanej warstwy biznesowej.


Zadanie: zmień typ projektu [pam-ef5] na bibliotekę klas, a następnie ponownie wygeneruj projekt.


9.23. Krok 16: Konfiguracja warstwy [DAO]

9.23.1. Interfejs warstwy [DAO]

Podobnie jak w przypadku symulowanej warstwy [métier], warstwa [DAO] będzie dostępna za pośrednictwem interfejsu. Jaki to będzie interfejs?

Przyjrzyjmy się interfejsowi [IPamMetier] symulowanej warstwy [métier], którą stworzyliśmy:


    public interface IPamMetier {
        // lista wszystkich tożsamości pracowników 
        Employe[] GetAllIdentitesEmployes();

        // ------- obliczenie wynagrodzenia 
        FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés);
}

W wierszu 3 metoda [GetAllIdentitesEmployes] służy do zasilania listy rozwijanej na stronie głównej:

Pracowników tych należy wyszukać w bazie danych.

Wiersz 6: metoda [GetSalaire] pozwala obliczyć listę płac pracownika, którego numer to SS. Przypomnijmy definicję typu [FeuilleSalaire]:


  public class FeuilleSalaire
  {

    // właściwości automatyczne 
    public Employe Employe { get; set; }
    public Cotisations Cotisations { get; set; }
    public ElementsSalaire ElementsSalaire { get; set; }
}

Informacje z wierszy 5 i 6 będą pochodzić z bazy danych. Przypomnijmy, że pracownik posiada właściwość o numerze [Indemnites]. Informacja ta również musi zostać pobrana.

Można by zatem zacząć od następującego interfejsu dla warstwy [DAO]:


    public interface IPamDao {
        // lista wszystkich identyfikatorów pracowników 
        Employe[] GetAllIdentitesEmployes();
        // konkretny pracownik wraz z jego dodatkami 
        Employe GetEmploye(string ss);
        // lista wszystkich składek 
        Cotisations GetCotisations();
}

9.23.2. Projekt Visual Studio


Zadanie: dodaj do rozwiązania [pam-td] nowy projekt typu [console] o nazwie [pam-dao]. Ustaw go jako projekt startowy rozwiązania.


 

9.23.3. Dodanie niezbędnych odwołań do projektu

Przyjrzyjmy się całemu projektowi:

Projekt [pam-dao] wymaga określonej liczby elementów DLL:

  • wszystkie elementy wymienione w projekcie [pam-ef5];
  • ten z samego projektu [pam-ef5].

Ponadto wykorzystamy [Spring.net] do instancjonowania warstwy [DAO]. W tym celu potrzebujemy plików DLL, [Spring.core] oraz [Common.Logging]. Te pliki DLL znajdują się w folderze [lib] w materiałach do studium przypadku.


Zadanie: dodaj te różne referencje do projektu [pam-dao].


9.23.4. Wdrożenie warstwy [DAO]

Powyżej klasa [PamException] to ta, która została zdefiniowana w punkcie 9.7.4. Wystarczy zmienić jej przestrzeń nazw (wiersz 1 poniżej):


namespace Pam.Dao.Entites
{
  // klasa wyjątków
  public class PamException : Exception
  {
....
  }
}

Interfejs [IPamDao] to ten, który właśnie zdefiniowaliśmy w paragrafie 9.23.1:


using Pam.EF5.Entites;

namespace Pam.Dao.Service
{
  public interface IPamDao
  {
    // lista wszystkich danych identyfikacyjnych pracowników 
    Employe[] GetAllIdentitesEmployes();
    // konkretny pracownik wraz z jego dodatkami 
    Employe GetEmploye(string ss);
    // lista wszystkich składek 
    Cotisations GetCotisations();
  }
}

Klasa [PamDaoEF5] implementuje ten interfejs za pomocą ORM i EF5. Jej kod wygląda następująco:


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

namespace Pam.Dao.Service
{

  public class PamDaoEF5 : IPamDao
  {
    // pola prywatne 
    private Cotisations cotisations;
    private Employe[] employes;

    // Producent
    public PamDaoEF5()
    {
      // składka
      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);
      }
    }
  }
}

Warto wiedzieć:

  • wiersz 10: klasa [PamDaoEF5] implementuje interfejs [IPamDao];
  • tabele [cotisations] i [employes] są buforowane we właściwościach wierszy 13–14. Pracownicy nie mają swoich dodatków;
  • wiersze 17–28: to konstruktor inicjuje wiersze 13–14;
  • wiersze 43–52: metoda [GetEmploye] zwraca pracownika wraz z jego dodatkami. Jako parametr otrzymuje numer ubezpieczenia społecznego tego pracownika. Jeśli pracownik nie istnieje w bazie, metoda zwróci wskaźnik null.

Zadanie: uzupełnij kod klasy [PamDaoEF5].


W przypadku konstruktora należy wzorować się na kodzie testowym warstwy [EF5] przedstawionym w paragrafie 9.22.6. W przypadku metody [GetEmploye] należy wzorować się na przykładzie z paragrafu 3.5.7 dotyczącym klasy [Eager and Lazy loading] z klasy [refEF5].

9.23.5. Konfiguracja warstwy [DAO]

Podobnie jak w punkcie 9.22.5, należy skonfigurować EF5 w pliku [App.config] projektu:


Zadanie 1: skonfiguruj warstwę EF5 w pliku [App.config]. Wystarczy powtórzyć czynności wykonane w pliku [App.config] dla warstwy [EF5].


Nasz program testowy wykorzysta plik [Spring.net] w celu uzyskania odniesienia do warstwy [DAO].


Zadanie 2: korzystając z rozwiązań przedstawionych w punkcie 9.20.2, zmodyfikuj plik konfiguracyjny [app.config] projektu [pam-dao] tak, aby definiował obiekt Spring o nazwie [pamdao] powiązany z klasą [PamDaoEF5], którą właśnie utworzyliśmy. Pliki [app.config] i [web.config] mają tę samą strukturę. Należy zwrócić uwagę, aby tag <configSections> był pierwszym tagiem występującym po tagu głównym <configuration>.


9.23.6. Test warstwy [DAO]

Jesteśmy gotowi do przetestowania naszej warstwy [DAO]. Robimy to za pomocą już zainstalowanego programu [Program.cs]:

Przetestujemy różne funkcje interfejsu warstwy [DAO]. Kod warstwy [Program.cs] będzie wyglądał następująco:


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
      {
        // instancja warstwy [dao]
        IPamDao pamDao = (IPamDao)ContextRegistry.GetContext().GetObject("pamdao");
        // lista tożsamości pracowników 
        foreach (Employe Employe in pamDao.GetAllIdentitesEmployes())
        {
          Console.WriteLine(Employe.ToString());
        }
            // pracownik wraz z jego dodatkami 
            Console.WriteLine("------------------------------------");
            Employe e = pamDao.GetEmploye("254104940426058");
            Console.WriteLine("employé= {0}, indemnités={1}", e, e.Indemnites);
            Console.WriteLine("------------------------------------");
        // pracownik, który nie istnieje 
        Employe employe = pamDao.GetEmploye("xx");
        Console.WriteLine("Employé n° xx");
        Console.WriteLine((employe == null ? "null" : employe.ToString()));
        Console.WriteLine("------------------------------------");
        // lista składek 
        Cotisations cotisations = pamDao.GetCotisations();
        Console.WriteLine(cotisations.ToString());
      }
      catch (Exception ex)
      {
        // wyświetlenie wyjątku 
        Console.WriteLine(ex.ToString());
      }
      //przerwa 
      Console.ReadLine();
    }
  }
}
  • wiersz 15: uzyskujemy odwołanie do warstwy [DAO] za pomocą [Spring.net].

Wyniki wykonania tego programu są następujące:

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 z warstwy [DAO]


Zadanie: zmień typ projektu [pam-dao] na bibliotekę klas, a następnie wygeneruj projekt ponownie (powtórz czynności opisane w punkcie 9.22.7).


9.24. Krok 17: utworzenie warstwy [métier]

9.24.1. Interfejs warstwy [métier]

Interfejs warstwy [métier] będzie interfejsem [IPamMetier] z symulowanej warstwy [métier], którą zbudowaliśmy w punkcie 9.7.2.


    public interface IPamMetier {
        // lista wszystkich tożsamości pracowników 
        Employe[] GetAllIdentitesEmployes();

        // ------- obliczenie wynagrodzenia 
        FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés);
}

9.24.2. Projekt Visual Studio


Zadanie: dodaj do rozwiązania [pam-td] nowy projekt typu [console] o nazwie [pam-metier]. Ustaw go jako projekt startowy rozwiązania.


 

9.24.3. Dodanie niezbędnych odwołań do projektu

Przyjrzyjmy się projektowi jako całości:

Projekt [pam-metier] wymaga pewnej liczby elementów DLL:

  • wszystkie elementy, do których odwołują się projekty [pam-dao] i [pam-ef5];
  • oraz te z samych projektów [pam-dao] i [pam-ef5].

Zadanie: dodaj te różne odniesienia do projektu [pam-metier].


 

9.24.4. Wdrożenie warstwy [métier]

Powyżej znajdują się cztery elementy już wykorzystane w symulowanej warstwie [métier] (patrz punkt 9.7). Mogą wystąpić zmiany w przestrzeniach nazw importowanych przez te różne klasy. Należy je odpowiednio dostosować. Klasa [PamMetier] implementuje interfejs [IPamMetier] w następujący sposób:


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

namespace Pam.Metier.Service
{

  public class PamMetier : IPamMetier
  {

    // odwołanie do warstwy [DAO] zainicjowanej przez Spring
    public IPamDao PamDao { get; set; }

    // lista wszystkich identyfikatorów pracowników 
    public Employe[] GetAllIdentitesEmployes()
    {
      ...
    }

    // konkretny pracownik wraz z jego dodatkami 
    public Employe GetEmploye(string ss)
    {
      ...
    }

    // składki 
    public Cotisations GetCotisations()
    {
      ...
    }

    // obliczenie wynagrodzenia 
    public FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés)
    {
      // SS: numer identyfikacyjny pracownika SS 
      // HeuresTravaillées: liczba przepracowanych godzin 
      // Dni przepracowane: liczba przepracowanych dni 
...
  }
}
  • wiersz 13: występuje odwołanie do warstwy [DAO]. Zostanie ona zainicjowana przez Spring podczas instancjonowania klasy [PamMetier]. Zatem w momencie wykonywania poszczególnych metod wiersz 13 jest już zainicjowany.

Zadanie: uzupełnij kod klasy [PamMetier]. Jeśli w klasie [GetSalaire] okaże się, że pracownik o numerze ss nie istnieje, uruchomimy klasę [PamException]. Sposób obliczania wynagrodzenia wyjaśniono w paragrafie 9.5. Należy pamiętać o zaokrągleniu wszystkich obliczeń pośrednich do dwóch cyfr po przecinku.


9.24.5. Konfiguracja warstwy [métier]

Podobnie jak w paragrafie 9.22.5, należy skonfigurować warstwę EF5 w pliku [app.config] projektu:


Zadanie 1: skonfiguruj warstwę EF5 w pliku [app.config]. Wystarczy powtórzyć czynności wykonane w pliku [app.config] dla warstwy [EF5].


Nasz program testowy wykorzysta plik [Spring.net] w celu uzyskania odniesienia do warstwy [métier].


Zadanie 2: korzystając z tego, co zrobiliście wcześniej w punkcie 9.23.5, zmodyfikuj plik konfiguracyjny [app.config] projektu [pam-metier] tak, aby definiował obiekt Spring o nazwie [pammetier] powiązany z klasą [PamMetier], którą właśnie utworzyliśmy. Najprościej jest skopiować plik [app.config] z projektu [pam-dao] i uzupełnić brakujące elementy.


Pojawia się tu jednak pewna trudność. Należy nie tylko utworzyć instancję warstwy [métier] przy użyciu klasy [PamMetier], ale także zainicjować jej właściwość [PamDao]:


    // odwołanie do warstwy [DAO] zainicjowanej przez Spring
    public IPamDao PamDao { get; set; }

Konfiguracja Springa w [app.config] wygląda zatem następująco:


  <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>
  • wiersz 6: definiuje obiekt [pamdao] powiązany z klasą [PamDaoEF5];
  • wiersz 7: definiuje obiekt [pammetier] powiązany z klasą [PamMetier];
  • wiersz 8: znacznik [property] służy do zainicjowania właściwości publicznej klasy [PamMetier]. Atrybut [name="PamDao"] odpowiada nazwie właściwości, która ma zostać zainicjowana w klasie [PamMetier]. Atrybut [ref="pamdao"] wskazuje, że właściwość jest inicjowana za pomocą odwołania do obiektu [pamdao] z wiersza 6, a więc za pomocą odwołania do warstwy [DAO]. Właśnie tego chcieliśmy.

9.24.6. Test warstwy [métier]

Jesteśmy gotowi do przetestowania naszej warstwy [métier]. Robimy to za pomocą już istniejącego programu [Program.cs]:

Przetestujemy różne funkcje interfejsu warstwy [métier]. Kod warstwy [Program.cs] będzie wyglądał następująco:


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
      {
        // instancja warstwy [métier]
        IPamMetier pamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
        // lista tożsamości pracowników
        Console.WriteLine("Employés -----------------------------");
        foreach (Employe Employe in pamMetier.GetAllIdentitesEmployes())
        {
          Console.WriteLine(Employe);
        }

        // obliczenia list płac 
        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));
      }
      // przerwa 
      Console.ReadLine();
    }
  }
}
  • wiersz 16: uzyskujemy odwołanie do warstwy [métier] za pomocą [Spring.net].

Wyniki wykonania tego programu są następujące:

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 z warstwy [métier]


Zadanie: zmień typ projektu [pam-metier] na bibliotekę klas, a następnie wygeneruj projekt ponownie (powtórz czynności opisane w punkcie 9.22.7).


9.25. Krok 18: wdrożenie warstwy [web]

Dochodzimy do ostatniej warstwy naszej architektury, warstwy [web]:

Wykorzystamy ponownie warstwę [web], którą opracowaliśmy przy pomocy symulowanej warstwy [métier].

9.25.1. Projekt w Visual Studio

Wracamy do programu Visual Studio Express 2012 dla sieci Web, aby połączyć naszą warstwę internetową z warstwami [métier, DAO, EF5], które właśnie opracowaliśmy. Trzeba przede wszystkim dokonać konfiguracji i wprowadzić kilka zmian w przestrzeniach nazw.

W programie Visual Studio Express 2012 dla sieci Web należy załadować rozwiązanie [pam-td]:

  • do [1], a rozwiązanie [pam-td] w VS Studio dla sieci. Projekt internetowy [pam-web-01] znów staje się widoczny. Zaginął on wcześniej w programie VS Studio na komputery stacjonarne.
  • Konfiguracja projektu internetowego [pam-web-01] będzie wymagała modyfikacji. Zamiast zmieniać działający projekt, wprowadzimy zmiany na jego kopii. Najpierw w [2] usuwamy projekt z rozwiązania (nie powoduje to usunięcia niczego z systemu plików).
  • w projekcie [3], za pomocą Eksploratora Windows, skopiujemy folder [pam-web-01] do folderu [pam-web-02];
  • w [4] wczytujemy projekt [pam-web-02] do rozwiązania [pam-td]. Pojawia się on pod nazwą [pam-web-01];
  • w [5] zmień tę nazwę na [pam-web-02] i ustaw ten projekt jako projekt startowy;
  • w [6], załaduj poprzedni projekt [pam-web-01]. Teraz masz już wszystkie swoje projekty. Pamiętaj, aby pracować z projektem [pam-web-02].

9.25.2. Dodawanie niezbędnych odniesień do projektu

Przyjrzyjmy się projektowi jako całości:

Projekt [pam-web-02] wymaga pewnej liczby projektów DLL:

  • wszystkie elementy, do których odwołują się projekty [pam-metier], [pam-dao] i [pam-ef5];
  • oraz te z samych projektów [pam-metier], [pam-dao] i [pam-ef5].

Zadanie: dodaj te różne odniesienia do projektu [pam-web-02]. Odniesienie do projektu [pam-metier-simule] należy usunąć. Zmieniamy warstwę na [métier]. Niektóre elementy z warstwy DLL są już obecne w odnośnikach. Należy je usunąć, a następnie dokonać dodawania.


 

9.25.3. Wdrożenie warstwy [web]

Wygeneruj projekt [pam-web-02]. Pojawią się błędy, takie jak poniższy:

Klasa [ApplicationModel] wykorzystuje typ [Employe]. W symulowanej warstwie [métier] typ ten był zdefiniowany w przestrzeni nazw [Pam.Metier.Entites]. Obecnie znajduje się on w przestrzeni nazw [Pam.EF5.Entites]. Należy skorygować te błędy zgodnie z powyższymi wskazówkami.

9.25.4. Konfiguracja warstwy [web]

Tak jak w punkcie 9.24.5, musimy skonfigurować EF5 w pliku [web.config] projektu:


Zadanie 1: zastąp całą obecną zawartość pliku [web.config] zawartością pliku [app.config] z projektu [pam-metier].


Plik [Global.asax] naszej aplikacji internetowej wykorzystuje plik [Spring.net] do pobrania odwołania z warstwy [métier]:


      try
      {
        // instancja warstwy [métier]
        application.PamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
      }
      catch (Exception ex)
      {
        application.InitException = ex;
}

W wierszu 4 żądamy odwołania do obiektu Spring o nazwie [pammetier]. Jest to właśnie nazwa nadana warstwie [métier] (proszę to sprawdzić w pliku [web.config]).

9.25.5. Test warstwy [web]

Jesteśmy gotowi do przetestowania naszej warstwy [web]. Najpierw zmienimy jej port roboczy. Domyślnie warstwa [pam-web-02] ma konfigurację warstwy [pam-web-01] i w związku z tym działa na tym samym porcie. Doświadczenie pokazuje, że powoduje to problemy: warstwa IIS nadal korzysta wówczas z kodów projektu [pam-web-01]. Należy postępować w następujący sposób:

W projekcie [4] należy zmienić numer portu, na przykład zmieniając cyfrę jedności.

Projekt [pam-web-02] uruchamia się za pomocą [Ctrl-F5]. Otrzymujemy wówczas następującą stronę główną:

W projekcie [1] uzyskujemy listę pracowników z bazy danych [dbpam_ef5]. Warto zauważyć, że nie ma już pracownika [X X], którego mieliśmy w symulowanej warstwie [métier]. Przeprowadźmy symulację:

W warstwie [2] otrzymujemy rzeczywiste wynagrodzenie, a nie już fikcyjne. Teraz zatrzymajmy warstwy SGBD i MySQL5 i przeprowadźmy kolejną symulację:

W [3] otrzymaliśmy czytelny ekran błędów, mimo że niektóre komunikaty są w języku angielskim. Teraz ponownie zatrzymajmy MySQL i uruchommy ponownie aplikację w VS poprzez [Ctrl-F5]:

 

Otrzymujemy widok [initFailed.cshtml] utworzony w paragrafie 9.20.4. Wyświetla on komunikaty o błędach ze stosu wyjątków. Zachęcamy czytelnika do przeprowadzenia dalszych testów.

9.26. Krok 19: Udostępnienie aplikacji ASP.NET w Internecie

Podczas tworzenia aplikacji ASP.NET w programie Visual Studio domyślna konfiguracja powoduje, że opracowana aplikacja jest dostępna wyłącznie pod adresem [localhost]. Wszelkie inne adresy są odrzucane przez wbudowany serwer programu Visual Studio, który zwraca wówczas błąd [400 Bad Request].

Można to zaobserwować w następujący sposób:

  • w oknie o numerze DOS należy zwrócić uwagę na adres IP komputera, na którym prowadzone jest programowanie:

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. . . :

Adres IP jest tutaj podany w wierszu 14. Jeśli masz połączenie Wi-Fi, adres Wi-Fi komputera pojawi się w wierszach 20 i kolejnych.

  • Sprawdź właściwości projektu [clic droit sur projet / propriétés / onglet web]:
 

Aplikacja zostanie uruchomiona na porcie [65010] na komputerze [localhost].

  • Uruchom projekt za pomocą [Ctrl-F5]

Image

  • zastąp [localhost] adresem IP stanowiska:

Image

Serwer zwrócił odpowiedź [400 Bad Request]. Serwer IIS Express używany przez Visual Studio akceptuje wyłącznie nazwę [localhost].

Aby udostępnić opracowaną aplikację typu URL ([http://adresseIP/contexte/...]), należy użyć innego serwera niż IIS Express, na przykład serwera IIS (nie Express). Aby sprawdzić, czy serwer ten jest dostępny (zwykle w wersjach Pro systemu Windows), należy przejść do Panelu sterowania [Panneau de configuration\Système et sécurité\Outils d’administration]:

Image

Ta opcja nie zawsze jest dostępna. W takim przypadku należy przejść do [ Panneau de configuration \ Programmes] i zainstalować Narzędzia administracji internetowej.

Gdy opcja [Gestionnaire des services internet (IIS)] będzie już dostępna, należy ją aktywować:

Uruchamiamy domyślną stronę internetową. W tym celu należy wcześniej uruchomić usługę [Service de publication World Wide Web]:

Po wykonaniu tych czynności należy wywołać serwisy URL i [http://localhost] za pomocą przeglądarki. Należy wcześniej sprawdzić, czy port 80 nie jest już zajęty przez inny serwer WWW. Jeśli tak, należy go zatrzymać.

Serwer IIS udzielił odpowiedzi. Teraz należy zastąpić adres [localhost] adresem IP swojego komputera:

Działa. Wróćmy teraz do programu Visual Studio:

  • najpierw należy uruchomić Visual Studio w trybie [administrateur]

Następnie należy zmienić konfigurację projektu internetowego, który chcemy wdrożyć [clic droit sur projet / propriétés / onglet web]:

Należy wybrać lokalny serwer IIS jako serwer wdrożeniowy. Visual Studio ustawia ścieżkę URL do aplikacji. Można ją zmienić. Uruchom projekt za pomocą [Ctrl-F5]:

Teraz należy zastąpić [localhost] adresem IP swojego komputera:

Jeśli nie dysponujemy serwerem IIS, możemy skorzystać z bezpłatnego serwera ASP.NET, takiego jak [Ultidev Web Server Pro], dostępnego na URL lub [http://ultidev.com/Download/ ]. Po zainstalowaniu istnieją dwie metody uruchomienia aplikacji internetowej za pomocą tego serwera:

Szybka metoda

Otwórz przeglądarkę plików systemu Windows i wybierz folder aplikacji ASP.NET, którą chcesz uruchomić:

Serwer internetowy zostanie wówczas uruchomiony, a aplikacja internetowa wyświetlona w przeglądarce:

  • w [3] można zatrzymać lub uruchomić serwer WWW;
  • w [4] można zmienić port usługi aplikacji internetowej;

Przed uruchomieniem serwera należy uruchomić poniższą usługę [UWS HiPriv Services]:

Po uruchomieniu serwera interfejs wygląda następująco:

Kliknięcie linku [6] powoduje wyświetlenie pierwszej strony aplikacji:

W tym momencie można zastąpić adres [localhost] adresem IP urządzenia:

W tym przypadku również akceptowana jest wyłącznie nazwa [localhost].

Metoda rozbudowana

Uruchom aplikację Ultidev Web Explorer

, a następnie wykonaj następujące kroki:

  • w polu [8] należy wskazać folder aplikacji internetowej, która ma zostać wdrożona;
  • z powodu [10-11] aplikacja internetowa będzie musiała być wywołana za pomocą URL [http://localhost:81/];
  • uruchom serwer internetowy z [14];
  • wywołaj URL [19] ;
  • w przypadku [20] uzyskano pożądaną stronę, korzystając z lokalnego adresu IP na komputerze zamiast nazwy [localhost]. To właśnie było naszym celem;

Serwer Ultidev został zainstalowany jako usługa systemu Windows, która uruchamia się automatycznie. Można wyłączyć automatyczne uruchamianie serwera Ultidev w następujący sposób:

  • wybrać opcję [Panneau de configuration\Système et sécurité\Outils d’administration];
  • [1, 2]: wybierz właściwości usługi [Ultidev Web Server Pro];
  • [3]: ustawić uruchamianie ręczne.

Aby ręcznie uruchomić serwer, należy skorzystać np. z aplikacji [Ultidev Web Explorer]:

9.27. Krok 20: generowanie aplikacji natywnej dla systemu Android

W przypadku aplikacji internetowej typu APU (aplikacja jednostronicowa) możliwe jest wygenerowanie pliku wykonywalnego dla urządzeń mobilnych (Android, IoS, Windows 8, ...) za pomocą narzędzia [Phonegap] [http://phonegap.com/]. Istnieją również inne sposoby, w szczególności z wykorzystaniem oprogramowania open source Apache Cordova [https://cordova.apache.org/]. Narzędzie dostępne online na stronie Phonegap [http://build.phonegap.com/apps] „przesyła” plik ZIP ze strony, którą chcesz przekonwertować. Strona główna musi nosić nazwę [index.html] i musi być stroną statyczną, tzn. nie może być generowana przez framework internetowy (ASP.NET, JEE, PHP, ...). Zaczniemy od stworzenia właśnie tej strony.

9.27.1. Architektura aplikacji

Należy pamiętać, że chcemy stworzyć aplikację na Androida. Taka aplikacja często ma następującą architekturę:

  • w przypadku [1] użytkownik korzysta z tabletu z systemem Android, który komunikuje się z jedną lub kilkoma usługami internetowymi [2];

Wróćmy do modelu APU:

  • w przeglądarce ładowana jest strona początkowa (powyższy schemat nie wskazuje, skąd pochodzi);
  • kolejne widoki są pobierane za pomocą wywołań Ajax [1-4]. Przeglądarka nie załaduje żadnej nowej strony;

Widok początkowy może, ale nie musi być dostarczany przez ten sam serwer, co pozostałe widoki uzyskane poprzez wywołania Ajax. Jeśli nie jest dostarczany przez ten sam serwer, kod JavaScript strony początkowej musi znać identyfikator URL serwera WWW, który będzie dostarczał pozostałe widoki. Tak będzie w przypadku aplikacji na Androida, którą zamierzamy stworzyć:

  • statyczna strona [index.html] zostanie wbudowana w natywną aplikację na Androida o identyfikatorze [1], która posiada funkcje przeglądarki, a zatem jest w stanie wykonać kod JavaScript osadzony na stronie [index.html];
  • strona ta będzie pobierać pozostałe widoki za pomocą wywołań Ajax do serwera [2]. W tym celu musi znać adres URL serwera WWW;

Przeprojektujemy aplikację [pam-web-02], aby działała w tym trybie. W ten sposób pierwsza strona będzie wyglądała następująco:

  • w [1], URL ze strony startowej aplikacji. Zostanie nam on dostarczony przez serwer Ultidev omówiony w paragrafie 9.26;
  • w [2] użytkownik będzie musiał wprowadzić URL symulatora listy płac. Można by go wpisać na stałe w kodzie JavaScript strony startowej, ale skomplikowałoby to testy: gdy tylko zmienilibyśmy symulator adresu IP (lub portu), należałoby go wówczas zmienić w kodzie JavaScript;
  • na [3], link [Connexion], który wyświetli następujący widok:
  • należy zauważyć, że w przypadku [4] adres URL przeglądarki nie uległ zmianie. Jest to nadal adres strony początkowej i pozostanie taki przez cały czas działania aplikacji.

Po uzyskaniu tego widoku wszystko działa tak jak poprzednio: poszczególne widoki są pobierane za pomocą wywołań Ajax. Zobaczymy, że konieczna jest zmiana tylko niewielkiej ilości kodu.

9.27.2. Refaktoryzacja projektu [pam-web-02]

W folderze [Content] projektu [pam-web-02] tworzymy folder [bootstrap] (nazwa nie ma znaczenia) o następującej strukturze:

Umieściliśmy w nim statyczną stronę [index.html] oraz wszystkie potrzebne jej zasoby (pliki CSS i JS). Strona [index.html] przejmuje kod ze strony głównej [_Layout.cshtml] projektu Visual Studio, usuwając wszystko, co nie jest statyczne. Daje to następujący kod:


<!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>

Dodaliśmy następujące elementy:

  • wiersze 27–29: dodano opcję menu [Connexion], aby umożliwić połączenie z usługą symulacyjną;
  • wiersze 55–56: wprowadzono identyfikator URL symulatora;
  • wiersze 59–63: pole błędu w przypadku niepowodzenia połączenia;

Refaktoryzacja kodu dotyczy wyłącznie kodu [myScripts.js] w wierszu 14 powyżej. Nic więcej się nie zmienia. Kod zmienia się w następujący sposób:


// podczas ładowania dokumentu
$(document).ready(function () {
    // pobieranie odniesień do różnych elementów strony
    loading = $("#loading");
    content = $("#content");
    erreur = $("#erreur");
    erreur1 = $("#erreur1");
    erreur2 = $("#erreur2");
    // linki w menu
    lnkConnexion = $("#lnkConnexion");
    lnkFaireSimulation = $("#lnkFaireSimulation");
    lnkEffacerSimulation = $("#lnkEffacerSimulation");
    lnkEnregistrerSimulation = $("#lnkEnregistrerSimulation");
    lnkVoirSimulations = $("#lnkVoirSimulations");
    lnkTerminerSession = $("#lnkTerminerSession");
    lnkRetourFormulaire = $("#lnkRetourFormulaire");
    // umieszczamy je w tablicy
    options = [lnkConnexion, lnkFaireSimulation, lnkEffacerSimulation, lnkEnregistrerSimulation, lnkVoirSimulations, lnkTerminerSession, lnkRetourFormulaire];
    // ukrywanie niektórych elementów strony
    loading.hide();
    erreur.hide();
    // ustalamy położenie menu
    setMenu([lnkConnexion]);
});
  • wiersze 6–8: identyfikatory obszaru wyświetlającego błędy połączenia na stronie [index.html];
  • wiersz 10: nowy link do połączenia z symulatorem;
  • wiersz 21: obszar błędów jest początkowo ukryty;
  • wiersz 23: wyświetlany jest wyłącznie link do połączenia;

Na stronie [index.html] link do połączenia jest zdefiniowany w następujący sposób:


<a id="lnkConnexion" href="javascript:connexion()">
| Connexion<br />
</a>

Funkcja JS [connexion] (wiersz 1) wygląda następująco:


var urlServiceWeb;
var erreur, erreur1, erreur2;


function connexion() {
    // pobieramy urlServiceWeb z serwisu internetowego
    urlServiceWeb = $("#urlServiceWeb").val();
    // pobieramy formularz do wprowadzania danych
    $.ajax({
        url: urlServiceWeb + '/Pam/Formulaire',
        type: 'POST',
        dataType: 'html',
        beforeSend: function () {
            // włącza się sygnał oczekiwania
            loading.show();
        },
        success: function (data) {
            // wyświetlanie wyników
            content.html(data);
            // menu
            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 () {
            // sygnał oczekiwania wyłączony
            loading.hide();
        }
    });
}
  • wiersz 7: pobieramy wartość URL wprowadzoną przez użytkownika. Jest ona umieszczana w zmiennej globalnej z wiersza 1. Dzięki temu będzie ona znana w pozostałych funkcjach pliku;
  • wiersz 10: wysyłane jest wywołanie Ajax do URL [/Pam/Formulaire] symulatora. Ten URL zwraca częściowy widok danych wprowadzonych w symulacji (pracownicy, przepracowane godziny, przepracowane dni). W pierwotnej wersji [pam-web-02] ta funkcja URL była wystarczająca. Była ona automatycznie poprzedzana funkcją URL, która wyświetlała stronę początkową. Obecnie zakłada się, że strona początkowa może być dostarczana przez serwer inny niż ten, na którym działa symulator. Należy zatem poprzedzić URL i [/Pam/Formulaire] zmienną [urlServiceWeb] z linii 1, która jest URL symulatora (na przykład http://172.19.81.34/pam-web-02). Należy to zrobić dla wszystkich wywołań Ajax w pliku;
  • wiersze 17–22: w przypadku pomyślnego nawiązania połączenia wyświetlany jest widok częściowy [Formulaire.cshtml] oraz menu zawierające wyłącznie link [Faire la simulation] (wiersz 21);
  • wiersze 23–27: w przypadku niepowodzenia połączenia:
    • w wierszu 24 wyświetlana jest odpowiedź HTML wysłana przez serwer WWW (jeśli taka istnieje);
    • w wierszu 25 wyświetlane są nagłówki HTTP wysłane przez serwer WWW (jeśli serwer udzielił odpowiedzi);

To wszystko. W przypadku powodzenia otrzymujemy następującą stronę:

Znajdujemy się zatem w sytuacji opisanej powyżej, z tą różnicą, że teraz widoki są pobierane za pomocą wywołań Ajax. Tak więc, jak pokazano powyżej, kliknięcie linku [Faire la simulation] zostanie zrealizowane przez następujący kod z pliku [myScripts.js]:


function faireSimulation() {
    // pobieranie danych referencyjnych
    var simulation = $("#simulation");
    var formulaire = $("#formulaire");
    // czy formularz jest poprawny?
    var formValid = formulaire.validate().form();
    if (!formValid) return;
    // ręczne wywołanie Ajax
    $.ajax({
        url: urlServiceWeb + '/Pam/FaireSimulation',
        type: 'POST',
        data: formulaire.serialize(),
        dataType: 'html',
        ...
    });
    // menu
    setMenu([lnkEffacerSimulation, lnkEnregistrerSimulation, lnkTerminerSession, lnkVoirSimulations]);
}
  • wprowadzono tylko jedną zmianę – w wierszu 10, gdzie poprzedni identyfikator URL jest teraz poprzedzony identyfikatorem symulatora;

9.27.3. Test projektu po refaktoryzacji

W punkcie 9.26 pokazaliśmy, jak zainstalować aplikację [pam-web-02] na serwerze Ultidev. Zaczniemy od tego miejsca:

  • w [6] żądamy wyświetlenia strony [bootstrap/index.html]. Otrzymujemy następujący widok:

Wpiszmy błędny adres URL:

  • w [10] nagłówki HTTP z odpowiedzi serwera;
  • na [11], dokument HTML z odpowiedzi serwera;

Jeśli wpiszemy prawidłowy kod URL:

otrzymujemy następującą odpowiedź:

9.27.4. Tworzenie pliku binarnego dla systemu Android

Stworzymy plik binarny dla systemu Android na podstawie właśnie utworzonej i przetestowanej strony statycznej [1]:

Dodajemy w [2] plik [config.xml], który posłuży do skonfigurowania wtyczki [Phonegap], która wygeneruje plik binarny dla systemu Android. Jego kod wygląda następująco:


<?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>
  • wiersze 7–9: wpisz tutaj swoje dane kontaktowe;
  • wiersze 11–13: te wiersze umożliwiają kodowi JavaScript wbudowanemu w aplikację internetową, który będzie wykonywany na urządzeniu z systemem Android, wysyłanie zapytań do plików URL znajdujących się poza tym urządzeniem;

Kompresujemy zawartość folderu [Content/bootstrap]:

 

Następnie przechodzimy na stronę Phonegap [http://build.phonegap.com/apps]:

  • przed [1] może być konieczne utworzenie konta;
  • w [1] rozpoczynamy;
  • w [2] wybieramy bezpłatny plan, który pozwala na tylko jedną aplikację Phonegap;
  • w [3] pobieramy spakowaną aplikację [4];
  • w [5] należy podać nazwę aplikacji;
  • kliknij link [6], aby skompilować pliki binarne OS i IoS dla systemów Android i Windows. Może to potrwać kilka sekund;
  • w [7-9], pobierz plik binarny dla systemu Android;

Uruchom emulator [GenyMotion] dla tabletu z systemem Android (patrz punkt 11.1):

 

Powyżej uruchamiamy emulator tabletu z systemem Android API 21. Po uruchomieniu emulatora

  • odblokuj go, przesuwając suwak (jeśli jest) w bok, a następnie zwalniając go;
  • za pomocą myszki przeciągnij pobrany plik [Pam-debug.apk] i upuść go na emulatorze. Zostanie on wówczas zainstalowany i uruchomiony;

Wprowadź [1] oraz URL do symulatora zgodnie z opisem w paragrafie 9.27.3. Następnie połącz się z symulatorem za pomocą linku [2]:

Przetestuj aplikację na emulatorze. Powinna działać.