Zine.net online

Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc
w Szukaj

mgrzeg.net - Admin on Rails :)

Konkurs Enterprise Library - Validation Application Block (VAB)

Kontynuujemy nasz konkurs i po przygodach z Logging Application Block oraz Exception Handling Application Block przechodzimy do kolejnego bloku z Enterprise Library.

Opis znajdujący się poniżej nie stanowi jedynego słusznego sposobu wprowadzania VAB do aplikacji. Skupia się jedynie na scenariuszu związanym z aplikacjami WinForms, choć w bardzo prosty sposób da się poniższe rozwiązanie przenieść na grunt aplikacji opartych o WebForms. Wydaje mi się, że po zaznajomieniu się Czytelnika z poniższym rozwiązaniem, wykorzystanie VAB w aplikacjach Web nie będzie stanowiło żadnego problemu.

Nie należy traktować poniższych kroków refaktoryzacyjnych jako zestawu do automatyzacji, a raczej jako zbiór wskazówek, którymi można posługiwać się podczas wdrażania Validation Application Block we własnej aplikacji.

Poniższy tekst nie jest również tutorialem wprowadzającym w VAB i ogólniej Enterprise Library, takie teksty można znaleźć na blogach Davida Haydena i zespołu P&P Microsoftu, a także bezpośrednio na stronach związanych z projektem Enterprise Library. Szczególnie warte polecenia są znajdujące się tam webcasty poświęcone Enterprise Library 3.x oraz cykl screencastów na stronie Davida Haydena pnpguidance.net. Po więcej szczegółów oraz niezbędne linki zapraszam na stronę wprowadzającą w konkurs.

Refaktoryzacja do Validation Application Block (VAB)

Przypadek

Walidacja wykonywana jest w kodzie widoku, a także w warstwie domenowej. Brak spójnej polityki reguł poprawności.

Rozwiązanie

Zlokalizuj walidację danych w jednym miejscu, skorzystaj z możliwości deklaratywnego definiowania reguł poprawności danych.

Uzasadnienie

Weryfikacja danych jest jedną z pierwszych operacji, które wykonujemy na danych dostarczanych nam przez użytkownika. Jest kilka powodów, dla których wykonujemy walidację:

  1. Bezpieczeństwo. Sprawdzamy, czy dane przekazywane przez użytkownika nie spowodują niewłaściwego działania naszej aplikacji. Błędne dane wejściowe mogą stanowić źródło wielu różnych ataków, jak chociażby SQL Injection czy ogólniej script injection.
  2. Poprawność. Gwarantujemy, że system nie będzie przetwarzał błędnych danych. Nasze reguły biznesowe często wymuszają odpowiedni format, zakres, postać, etc. danych. Oczywiście zależy nam, żeby dane były zgodne z wewnętrzną reprezentacją danych, schematami danych, etc.
  3. Wykrycie błędnych danych zatrzymuje dalsze operacje – nierzadko bardzo kosztowne.

Co nam daje VAB?

Najlepiej odpowiedzieć sobie na to pytanie obserwując dostępne obecnie mechanizmy walidacji danych. Zazwyczaj korzystamy z różnego rodzaju walidatorów ściśle związanych z UI, co zmusza do umieszczania logiki walidacji w wielu różnych miejscach aplikacji – 'na wejściu', przy przetwarzaniu danych pochodzących z formularzy oraz przy przekazywaniu danych dalej – przetwarzając odpowiednie reguły biznesowe. W oczywisty sposób takie podejście może prowadzić do braku konsystencji i stosowania różnych reguł określających poprawność danych. To, co nam oferuje VAB to właśnie utrzymanie spójnych reguł poprawności. Teraz wszystkie te reguły możemy określać programowo (tak jak ma to miejsce w klasycznym podejściu), korzystając z atrybutów, bądź bezpośrednio w plikach konfiguracyjnych. W czasie działania naszej aplikacji mamy do dyspozycji możliwość przełączania się między różnymi zestawami reguł poprawności i stosować je zgodnie z naszymi wymaganiami. Enterprise Library dostarcza nam integrację z Windows Forms, ASP.NET oraz WCF, kontrolka do integracji z WPF dostępna jest w ramach bliźniaczego projektu EntLib Contrib.
Enterprise Library zawiera zestaw umożliwiający zarządzanie konfiguracją przy użyciu Group Policy lub WMI, o czym pisałem już w jednym z tekstów na blogu. Niestety VAB oraz PIAB nie są obsługiwane przez te mechanizmy.

Sposób wykonania

  1. Przeniesienie metod walidujących z widoku do klas odpowiedzialnych za logikę.
    1. W klasie widoku, dla każdej klasy biznesowej wyodrębnij metodę realizującą walidację.
      To jest bardzo ważny krok – zlokalizowanie wszystkich konstrukcji warunkowych w których zapisane są reguły poprawności dla danej klasy z modelu danych.
    2. Wykonaj przemieszczenie metody do odpowiedniej klasy biznesowej.
      Bułka z masłem :).
    3. Wprowadź Enterprise Library, dodając do projektu odpowiednie referencje do bibliotek Enterprise Library. Zmień sygnaturę metody oraz dodaj atrybut HasSelfValidation do klasy i SelfValidation do metody walidującej. Skorzystaj z Validation Facade do przeprowadzenia walidacji.
      Od tej pory zaczyna się zabawa. Dzięki zastosowaniu SelfValidation otwieramy się na walidację opartą o atrybuty, a jednocześnie pozostawiamy sobie możliwość stosowania bardziej złożonych reguł poprawności danych bezpośrednio w kodzie.
    4. Rozłóż metodę walidującą na kilka metod, po jednej dla każdej właściwości, starając się aby w jednej metodzie obsługiwana była jedna właściwość i opatrz każdą z metod atrybutem SelfValidation. Spodziewaj się złożonego wyniku walidacji.
      W tym kroku rozdzielamy operacje walidacji dla poszczególnych właściwości. Od tej pory każdy z elementów zgłasza swój zestaw komunikatów o błędach walidacji i jesteśmy to w stanie odebrać w kodzie realizującym widok.
    5. W każdej z metod walidujących wykorzystaj odpowiednią klasę walidującą, lub ich kombinację i zastosuj je dla odpowiedniej właściwości.
      Ostatni krok przed zastosowaniem atrybutów dla właściwości. W tym momencie dobrze jest zastanowić się nad koniecznością wprowadzenia niestandardowego walidatora, którego moglibyśmy użyć do wykonania bardziej złożonej walidacji (np. PasswordComplexityValidator).
  2. Przejście z walidacji imperatywnej na deklaratywną Przeniesienie szczegółów walidacyjnych do pliku konfiguracyjnego.
    1. Każdą z metod walidujących zastąp w miarę możliwości odpowiednimi atrybutami zastosowanymi do walidowanych właściwości.
      Domyślnie atrybuty realizują And Composite Validator, więc nie musimy tego walidatora umieszczać pośród innych atrybutów. Warto również pamiętać o tym, że środowisko nie gwarantuje nam kolejności, w jakiej zostanie przeprowadzona walidacja poszczególnych właściwości (ani kolejności, w jakiej będą kolejne właściwości walidowane).
      Te metody, które nie znajdą swoich odpowiedników w atrybutach właściwości pozostawiamy na miejscu. Dzięki [SelfValidation] dalej będą podlegać walidacji opartej o atrybuty. Jeśli jednak udało nam się przenieść wszystkie metody opisane atrybutem [SelfValidation] do odpowiednich atrybutów właściwości, możemy spokojnie usunąć atrybut [HasSelfValidation] dla klasy.
      Jeśli w tym momencie mamy jeszcze dodatkowe konstruktory wieloparametrowe, lub inne metody pomocnicze, w których przeprowadzamy walidację, to czas najwyższy się ich pozbyć.Więcej szczegółów uzasadniających takie podejście można znaleźć w pozycji [CWALINA]. W przypadku stosowania walidacji opartej o atrybuty właściwości, podejście 'domyślny konstruktor-> kolejne właściwości' wydaje się być najrozsądniejsze.
    2. Przenieś każdy z atrybutów walidacyjnych do pliku konfiguracyjnego korzystając z narzędzia Enterprise Library Configuration. Dodaj plik zasobów i zdefiniuj odpowiednie komunikaty.
      Oczywiście, część atrybutów, nad którymi my jako programiści chcemy mieć kontrolę możemy zostawić i w ten sposób stosować walidację opartą zarówno na pliku konfiguracyjnym jak i atrybutach. Tu należy pamiętać, że w przypadku kolidujących ze sobą reguł, obowiązujące są te zdefiniowane w pliku konfiguracyjnym.
    3. Dodaj obiekt klasy ValidationProvider do formularza i wykorzystaj kontrolkę ErrorProvider do wyświetlenia odpowiednich komunikatów o błędach walidacji.
      (WinForms specific! Poza integracją z WinForms dostępna jest jeszcze kontrolka dla ASP.NET oraz rozwiązanie dla WCF)
      Prawdopodobnie mamy już na formularzu jakieś ErrorProvidery, jednak teraz możemy skorzystać z kontrolki integracyjnej EntLib z WinForms i wykorzystać moc drzemiącą w walidatorach. Kontrolki integracyjne możemy dodać do zakładek designera sięgając do Microsoft.Practices.EnterpriseLibrary.Validation.Integration.WinForms.dll (dla WinForms) oraz Microsoft.Practices.EnterpriseLibrary.Validation.Integration.AspNet.dll (dla ASP.NET).

Przykład

Rys 1. Przykładowy formularz

//class UserForm
private void Add_Click(object sender, EventArgs e)
{
  //check for rules
  //Len(Pass) > 0, < 10
  //email contains '@' & '.'
  if (!String.IsNullOrEmpty(UserFirstName.Text)
    && !String.IsNullOrEmpty(UserLastName.Text)
    && !String.IsNullOrEmpty(UserEmail.Text)
    && !String.IsNullOrEmpty(UserPassword.Text)
    && (UserPassword.Text.Length > 0 && UserPassword.Text.Length < 10)
    && (UserEmail.Text.IndexOf('@') > -1)
    && (UserEmail.Text.IndexOf('.') > -1))
  {
    User user = new User();
    user.FirstName = UserFirstName.Text;
    user.LastName = UserLastName.Text;
    user.Email = UserEmail.Text;
    user.Password = UserPassword.Text;
    UserDAO userDao = new UserDAO();
    userDao.Add(user);
  }
  else
    MessageBox.Show("Invalid data!");
}

//class UserDAO
public class UserDAO
{
  public void Add(User user)
  {
    //user should be valid in here.
  }
}

  1. W klasie widoku dla każdej klasy biznesowej reprezentowanej na formularzu wyodrębnij metodę realizującą walidację.

    //class UserForm
    private void Add_Click(object sender, EventArgs e)
    {
      User user = new User();
      user.FirstName = UserFirstName.Text;
      user.LastName = UserLastName.Text;
      user.Email = UserEmail.Text;
      user.Password = UserPassword.Text;

      if (!ValidUser(user))
        MessageBox.Show("Invalid data!");
      else
      {
        UserDAO userDao = new UserDAO();
        userDao.Add(user);
      }
    }

    private bool ValidUser(User user)
    {
      //check for rules
      //Len(Pass) > 0, < 10
      //email contains '@' & '.'
      if (!String.IsNullOrEmpty(user.FirstName)
        && !String.IsNullOrEmpty(user.LastName)
        && !String.IsNullOrEmpty(user.Email)
        && !String.IsNullOrEmpty(user.Password)
        && (user.Password.Length > 0 && user.Password.Length < 10)
        && (user.Email.IndexOf('@') > -1)
        && (user.Email.IndexOf('.') > -1))
      {
        return true;
      }
      else return false;
    }

  2. Wykonaj przemieszczenie metody do odpowiedniej klasy biznesowej.

    //class UserForm
    private void Add_Click(object sender, EventArgs e)
    {
      User user = new User();
      user.FirstName = UserFirstName.Text;
      user.LastName = UserLastName.Text;
      user.Email = UserEmail.Text;
      user.Password = UserPassword.Text;

      if (!User.ValidUser(user))
        MessageBox.Show("Invalid data!");
      else
      {
        UserDAO userDao = new UserDAO();
        userDao.Add(user);
      }
    }

    //class User
    public static bool ValidUser(User user)
    {
      //check for rules
      //Len(Pass) > 0, < 10
      //email contains '@' & '.'
      if (!String.IsNullOrEmpty(user.FirstName)
        && !String.IsNullOrEmpty(user.LastName)
        && !String.IsNullOrEmpty(user.Email)
        && !String.IsNullOrEmpty(user.Password)
        && (user.Password.Length > 0 && user.Password.Length < 10)
        && (user.Email.IndexOf('@') > -1)
        && (user.Email.IndexOf('.') > -1))
      {
        return true;
      }
      else return false;
    }

  3. Wprowadź Enterprise Library. Zmień sygnaturę metody oraz dodaj atrybut HasSelfValidation do klasy i SelfValidation do metody walidującej.
    Skorzystaj z Validation Facade do przeprowadzenia walidacji.

    //class UserForm
    private void Add_Click(object sender, EventArgs e)
    {
      User user = new User();
      user.FirstName = UserFirstName.Text;
      user.LastName = UserLastName.Text;
      user.Email = UserEmail.Text;
      user.Password = UserPassword.Text;

      //Validation Facade
      ValidationResults results = Validation.Validate(user);

      if (!results.IsValid)
      {
        MessageBox.Show("Invalid data!");
      }
      else
      {
        UserDAO userDao = new UserDAO();
        userDao.Add(user);
      }
    }

    //class User
    [HasSelfValidation]
    public class User {
    ...
    [SelfValidation]
    public void Validate(ValidationResults results)
    {
      if (String.IsNullOrEmpty(FirstName)
       || String.IsNullOrEmpty(LastName)
       || String.IsNullOrEmpty(Email)
       || (Email.IndexOf('@') == -1)
       || (Email.IndexOf('.') == -1)
       || String.IsNullOrEmpty(Password)
       || (Password.Length == 0 || Password.Length >= 10))
        results.AddResult(
          new ValidationResult("Invalid data!", this, null, null, null));
    }
    }

  4. Rozbij metodę walidującą wszystkie właściwości na kilka metod atomowych operujących na poszczególnych właściwościach, każdą opatrując atrybutem SelfValidation. Spodziewaj się złożonego wyniku walidacji.

    //class User
    [HasSelfValidation]
    public class User {
    ...
    [SelfValidation]
    public void ValidateFirstName(ValidationResults results)
    {
      if (String.IsNullOrEmpty(FirstName))
        results.AddResult(
          new ValidationResult("First name not valid!", this, null, null, null));
    }
    [SelfValidation]
    public void ValidateLastName(ValidationResults results)
    {
      if(String.IsNullOrEmpty(LastName))
        results.AddResult(
          new ValidationResult("Last name not valid!", this, null, null, null));
    }
    [SelfValidation]
    public void ValidateEmail(ValidationResults results)
    {
      if(String.IsNullOrEmpty(Email)
        || (Email.IndexOf('@') == -1)
        || (Email.IndexOf('.') == -1))
        results.AddResult(
          new ValidationResult("Email not valid!", this, null, null, null));
    }
    [SelfValidation]
    public void ValidatePassword(ValidationResults results)
    {
      if (String.IsNullOrEmpty(Password)
        || (Password.Length == 0 || Password.Length >= 10))
        results.AddResult(
          new ValidationResult("Password not valid!", this, null, null, null));
    }
    }

    //class UserForm
    private void Add_Click(object sender, EventArgs e)
    {
      User user = new User();
      user.FirstName = UserFirstName.Text;
      user.LastName = UserLastName.Text;
      user.Email = UserEmail.Text;
      user.Password = UserPassword.Text;

      //Validation Facade
      ValidationResults results = Validation.Validate(user);

      if (!results.IsValid)
      {
        string message = "";
        foreach (ValidationResult error in results)
          message += error.Message + System.Environment.NewLine;
        MessageBox.Show(message);
      }
      else
      {
        UserDAO userDao = new UserDAO();
        userDao.Add(user);
      }
    }

  5. W każdej z metod walidujących wykorzystaj odpowiednią klasę walidującą, lub ich kombinację i zastosuj je dla odpowiedniej właściwości.

    //class User
    [SelfValidation]
    public void ValidateFirstName(ValidationResults results)
    {
      Validator firstNameValidator = new StringLengthValidator(
        1, RangeBoundaryType.Inclusive, 50, RangeBoundaryType.Inclusive);
      results.AddAllResults(firstNameValidator.Validate(FirstName));
    }
    [SelfValidation]
    public void ValidateLastName(ValidationResults results)
    {
      Validator lastNameValidator = new StringLengthValidator(
        1, RangeBoundaryType.Inclusive, 50, RangeBoundaryType.Inclusive);
      results.AddAllResults(lastNameValidator.Validate(LastName));
    }
    [SelfValidation]
    public void ValidateEmail(ValidationResults results)
    {
      Validator emailValidator = new RegexValidator(
       @"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*");
      results.AddAllResults(emailValidator.Validate(Email));
    }
    [SelfValidation]
    public void ValidatePassword(ValidationResults results)
    {
      Validator passwordValidator = new StringLengthValidator(
        1, RangeBoundaryType.Inclusive, 10, RangeBoundaryType.Exclusive);
      results.AddAllResults(passwordValidator.Validate(Password));
    }

  6. Każdą z metod walidujących zastąp w miarę możliwości odpowiednimi atrybutami zastosowanymi do walidowanych właściwości.

    //class User
    [HasSelfValidation]
    public class User
    {
      string password;
      public string Password
      {
        get { return password; }
        set { password = value; }
      }

      string email;
      [RegexValidator(@"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*")]
      public string Email
      {
        get { return email; }
        set { email = value; }
      }

      string lastName;
      [StringLengthValidator(1, RangeBoundaryType.Inclusive, 50, RangeBoundaryType.Inclusive)]
      public string LastName
      {
        get { return lastName; }
        set { lastName = value; }
      }

      string firstName;
      [StringLengthValidator(1, RangeBoundaryType.Inclusive, 50, RangeBoundaryType.Inclusive)]
      public string FirstName
      {
        get { return firstName; }
        set { firstName = value; }
      }
      [SelfValidation]
      public void ValidatePassword(ValidationResults results)
      {
        Validator passwordValidator = new StringLengthValidator(
          1, RangeBoundaryType.Inclusive, 10, RangeBoundaryType.Exclusive);
        results.AddAllResults(passwordValidator.Validate(Password));
      }

    ...
    }

  7. Przenieś każdy z atrybutów walidacyjnych do pliku konfiguracyjnego korzystając z narzędzia Enterprise Library Configuration. Dodaj plik zasobów i zdefiniuj odpowiednie komunikaty.

    Rys 2. Konfiguracja VAB w app.config.

    //class User
    [HasSelfValidation]
    public class User
    {
      string password;
      public string Password
      {
        get { return password; }
        set { password = value; }
      }

      string email;
      public string Email
      {
        get { return email; }
        set { email = value; }
      }

      string lastName;
      public string LastName
      {
        get { return lastName; }
        set { lastName = value; }
      }

      string firstName;
      public string FirstName
      {
        get { return firstName; }
        set { firstName = value; }
      }
      [SelfValidation]
      public void ValidatePassword(ValidationResults results)
      {
        Validator passwordValidator = new StringLengthValidator(
          1, RangeBoundaryType.Inclusive, 10, RangeBoundaryType.Exclusive);
        results.AddAllResults(passwordValidator.Validate(Password));
      }

    ...
    }

  8. Dodaj obiekt klasy ValidationProvider do formularza i wykorzystaj odpowiedni obiekt klasy ErrorProvider do wyświetlenia odpowiednich komunikatów o błędach walidacji.

    Rys 3. VAB Error Provider w akcji.

    Rys 4. Właściwości kontrolki Validation Provider w oknie designera.

    Rys 5. Rozszerzenie TextBoxa o właściwości powiązane z VAB.
     

    1. Ustaw dla formularza właściwość AutoValidate: EnableAllowFocusChange.
    2. Dodaj do formularza ValidationProvider oraz ErrorProvider.
    3. Ustaw następujące właściwości dla dodanego ErrorProvidera:
      (Name): UserErrorProvider
    4. Ustaw następujące właściwości dla dodanego ValidationProvidera:
      (Name): UserValidationProvider
      ErrorProvider: UserErrorProvider
      RulesetName: BaseRuleSet
      SourceTypeName: ZineNet.VABRefactorIllustrated.BL.User, ZineNet.VABRefactorIllustrated
    5. Dla pola tekstowego UserFirstName ustaw:
      SourcePropertyName on UserValidationProvider: FirstName
      PerformValidation on UserValidationProvider: true
      Dla pozostałych pól tekstowych ustaw odpowiednie właściwości.

[CWALINA] "Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries". Krzysztof Cwalina, Brad Adams, Sep 19, 2005 by Addison Wesley Professional.

Zadanie konkursowe #3 – Validation Application Block, Enterprise Library 3.1

Zgodnie z obietnicą i założeniami konkursu na koniec zadania do wykonania

Zgodnie z opisanymi powyżej krokami zrefaktoryzować kod aplikacji konkursowej tak, aby obsługa walidacji w warstwie widoku została przeniesiona do modelu danych i konfigurowana za pomocą pliku app.config. W szczególności:

  1. Walidacja danych związanych z klientem (klasa Customer) powinna obejmować pola:

    Pole Warunek
    Imię Nie dłuższe niż 25 znaków, zaczyna się od dużej litery, dozwolone są jedynie małe i duże litery alfabetu polskiego oraz może zawierać spację i myślnik
    Nazwisko j.w.
    Email Spełnia wymagania zgodnie z odpowiednim RFC :)
    Data urodzin Interesują nas osoby między 15 a 50 rokiem życia
    Adres pocztowy Ciąg znaków nie dłuższy niż 50 znaków

  2. Należy wykorzystać kontrolki ValidationProvider i ErrorProvider do informowania o błędnych danych;
  3. Walidacja imienia, emaila i adresu pocztowego powinna być sterowana plikiem konfiguracyjnym;
  4. Data urodzin podlega walidacji sterowanej atrybutami;
  5. Nazwisko podlega walidacji w kodzie.

Uwagi

  1. Zalecane wykorzystanie pliku zasobów do przechowywania komunikatów o błędach aplikacji;
  2. W przykładowym kodzie wykonany jest dodatkowy krok związany z wprowadzeniem PIAB, nie należy tego robić w rozwiązaniu;

Rozwiązane zadania należy przesyłać na adres mgrzeg at gmail kropka com. Reguły wiadomości:

  1. Temat wiadomości: [ENTLIB VAB] Rozwiązanie zadania konkursowego;
  2. Treść (może być w załączonym pliku .txt, .doc, .rtf) powinna zawierać krótki opis co zostało wykonane;
  3. Plik .zip zawierający kod aplikacji konkursowej po refaktoryzacji. Można dołączyć również kod wykonywalny (bez bibliotek EntLib, etc. - tylko to, co niezbędne);
  4. Dane adresowe do wysyłki nagrody - NIE! Skontaktujemy się ze "szczęśliwcem" via email i ustalimy szczegóły.

Adresy

W razie wątpliwości etc. proszę zadawać pytania.

Powodzenia!

Opublikowane 8 października 2007 14:42 przez mgrzeg

Powiadamianie o komentarzach

Jeżeli chciałbyś otrzymywać email gdy ta wypowiedź zostanie zaktualizowana, to zarejestruj się tutaj

Subskrybuj komentarze za pomocą RSS

Komentarze:

 

Ww.CodeSculptor said:

Oto ostatnia odsłona konkursu. Tym razem wprowadzenie do wykorzystania PIAB przy scalaniu poszczególnych

października 15, 2007 16:44

Co o tym myślisz?

(wymagane) 
(opcjonalne)
(wymagane) 

  
Wprowadź kod: (wymagane)
Wyślij
W oparciu o Community Server (Personal Edition), Telligent Systems