niedziela, 22 marca 2009

Walidacja danych w ASP.NET MVC za pomocą xVal

Pierwsza zasada programisty: nigdy nie ufaj danym wejściowym. Sama walidacja tych danych może zostać przeprowadzona na wiele sposobów, przekazując odpowiedzialność od warstwy najwyższej do najniższej. Takie podejście pozwala na eliminowanie zagrożenia w wielu przypadkach jeszcze bez przesłania danych do serwera. (Jeśli ktoś nie lubi czytać przydługich wstępów, może przejść od razu do punktu: "Dlaczego xVal?").


Walidacja po stronie warstwy prezentacji
<asp:TextBox ID="TextBox1" runat="server" />
<asp:RequiredFieldValidator ID="reqVal" runat="server" 
       ErrorMessage="Please enter a value" ControlToValidate="TextBox1" />
<asp:Button ID="Button1" runat="server" Text="Button" />



Należy pamiętać, że walidacja po stronie klienta (JavaScript) ZAWSZE musi iść w parze z walidacją po stronie serwera. W przeciwnym razie, to tak jak by w ogóle tej walidacji nie było! Wywołanie metody PageIsValid:

protected void Button1_Click(object sender, EventArgs e) 
{ 
  if (Page.IsValid) 
  { 
    // TextBox1 posiada wartość.
  } 
}



Walidacja po stronie logiki biznesowej

private static void Validate (EmailAddress myEmailAddress) 
{ 
  if (myEmailAddress.ContactPersonId <= 0) 
  { 
    throw new InvalidOperationException("Błąd, brak odpowiedniego ContactPersonId!"); 
  } 
  if (string.IsNullOrEmpty(myEmailAddress.Email))
  { 
    throw new InvalidOperationException("Błąd, brak emaila!"); 
  } 
  if (!IsValidEmailAddress(myEmailAddress.Email)) 
  { 
    throw new InvalidOperationException("Błąd, wprowadzony email jest błędny!"); 
  } 
} 
public static int Save(EmailAddress myEmailAddress) 
{ 
  Validate (myEmailAddress);
  myEmailAddress.Id = EmailAddressDB.Save(myEmailAddress);
  return myEmailAddress.Id; 
} 



Walidacja po stronie serwera SQL

ALTER TABLE  dbo.Category ADD CONSTRAINT
CK_Category CHECK (SortOrder >= 0  AND SortOrder <= 100)
GO   



Wykorzystanie dostępnego frameworka do walidacji danych, np:


Przedstawione frameworki różnią się przeznaczeniem. Ja przedstawię w tym poście świeży jeszcze projekt xVal.


Dlaczego xVal?


  1. Stworzony do użycia z ASP.NET MVC

  2. Wspiera walidację client-side oraz server-side

  3. Możliwość lokalizacji językowej komunikatów

  4. Oparty o przyjęte "dobre techniki"

  5. Możliwość włączenia do projektu bez konieczności przeprowadzania "wielkich" zmian

  6. Lekki, 1 plik .dll do podłączenia

  7. Open source

  8. Mało znany, bo nowy :-)



image-thumb



Zaczynamy. xVal - validation framework for ASP.NET MVC applications.



Po stronie modelu, xVal opiera swoje działanie o atrybuty. Z tego względu atrybuty naszych klas musimy odpowiednio oznaczyć atrybutami. W moim projekcie posiadam klasę Order.cs, którą będę poddawał torturom walidacji:

public class Order
{
    [Required] [StringLength(15)]
    public string ClientName { get; set; }
 
    [Range(1, 20)]
    public int NumberOfProducts { get; set; }
 
    [Required] [DataType(DataType.Date)]
    public DateTime Date { get; set; }
}



Objaśnienie zastosowanych atrybutów:

  • [Required] : dany atrybut jest wymagany, nie może być pusty.

  • [StringLength(n)] : dany atrybut musi posiadać odpowiednią długość, dla typu String.

  • [Range(n, m)] : dany atrybut musi być z przedziału od, do.

  • [DataType(typ)] : dany atrybut musi być zgodny z podanym typem.




Należy jeszcze dodać referencję do: System.ComponentModel.DataAnnotations.dll

Nadszedł czas na kontroler. Ja wszystkie operacje dotyczące składania zamówień będę przeprowadzał w HomeController.

public class HomeController : Controller
{
    [AcceptVerbs(HttpVerbs.Get)]
    public ViewResult CreateOrder()
    {
        return View();
    }
 
    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult CreateOrder(Order order)
    {
        OrderManager.PlaceOrder(order);
        return RedirectToAction("Completed");
    }
 
    public ViewResult Completed()
    {
        return View();
    }
}



Klasa OrderManager:

public static class OrderManager
{
    public static void PlaceOrder(Order order)
    {
        var errors = DataAnnotationsValidationRunner.GetErrors(order);
        if (errors.Any())
            throw new RulesException(errors);
 
        // Business rule: Nie można składać zamówień w niedzielę
        if(order.Date.DayOfWeek == DayOfWeek.Sunday)
            throw new RulesException("ArrivalDate", "Nie można skłdać zamówień na niedzielę!", order);
 
        // Todo: zapis do bazy danych, pliku xml lub cokolwiek innego
    }
}



Czas na widok CreateOrder.aspx

<h1>Zamówienie pluszowego misia</h1>
<% using(Html.BeginForm()) { %>
    <div>
        Nazwa: <%= Html.TextBox("order.ClientName") %>
        <%= Html.ValidationMessage("order.ClientName") %>
    </div>
    <div>
        Ilość misiów: <%= Html.TextBox("order.NumberOfProducts")%>
        <%= Html.ValidationMessage("order.NumberOfProducts")%>
    </div>
    <div>
        Data dostawy: <%= Html.TextBox("order.Date")%>
        <%= Html.ValidationMessage("order.Date")%>
    </div>                
 
    <input type="submit" />
<% } %>



Potrzebujemy jeszcze prostą i niezmienną klasę ValidationRunnera, gdyż  DataAnnotations takiej nie posiada:

internal static class DataAnnotationsValidationRunner
{
    public static IEnumerable<ErrorInfo> GetErrors(object instance)
    {
        return from prop in TypeDescriptor.GetProperties(instance).Cast<PropertyDescriptor>()
               from attribute in prop.Attributes.OfType<ValidationAttribute>()
               where !attribute.IsValid(prop.GetValue(instance))
               select new ErrorInfo(prop.Name, attribute.FormatErrorMessage(string.Empty), instance);
    }
}



Musimy jeszcze zmodyfikować napisany wcześniej szkielet metody CreateOrder w kontrolerze HomeController, tak aby błędy były przechowywane w stanie modelu.

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult CreateOrder(Order order)
{
    try {
        OrderManager.PlaceOrder(order);                
    }
    catch(RulesException ex) {
        ex.AddModelStateErrors(ModelState, "order");
    }
 
    return ModelState.IsValid ? RedirectToAction("Completed")
                              : (ActionResult) View();
}



Tym oto sposobem zakończyliśmy walidację po stronie serwera. Czeka nas jeszcze implementacja sprawdzanie po stronie klienta.

screen_02 2009.03.22 14.18




Walidacja po stronie klienta (Client-side validation).
Wszystko czego będziemy potrzebować do wykonania walidacji za pomocą javascriptu, to biblioteka jquery.validate.js oraz xVal.jquery.validate.js (ten znajduje się w folderze z projektem xVal). Należy je skopiować do folderu z naszymi skryptami (/Scripts) oraz podlinkować je do naszego MasterPage.

<head>
    <script type="text/javascript" src="<%= ResolveUrl("~/Scripts/jquery-1.2.6.js")%>"></script>
    <script type="text/javascript" src="<%= ResolveUrl("~/Scripts/jquery.validate.js")%>"></script>
    <script type="text/javascript" src="<%= ResolveUrl("~/Scripts/xVal.jquery.validate.js")%>"></script>
</head>


Czas teraz na import helperów xVal, najlepiej zrobić to raz w pliku Web.config:

<system.web>
  <pages>
     <namespaces>
        <!-- leave rest as-is -->
        <add namespace="xVal.Html"/>
    </namespaces>
  </pages>
</system.web>



Wracamy do widoku oraz dodajemy linijkę służącą do wymuszenia walidacji po stronie klienta. Będzie ona zastosowana dla kontrolek prefixowanych przez nazwę order:

<%= Html.ClientSideValidation<Order>("order") %>



Od tej pory walidacja po stronie klienta powinna być uruchamiana, "potajemnie" korzystając oczwyiście z atrybutów, które definiowaliśmy wcześniej. Super!


W załączniku przesyłam zlokalizowany przeze nnie na nasz język plik z komunikatami.

3 komentarze:

jenrom pisze...

Całkiem ciekawie to wygląda, chyba będę musiał się zapoznać z xVal.

Ja korzystałem z Validation Application Block i wymagało to zdecydowanie więcej zabawy, dlatego zdecydowanie nie polecam. No i przede wszystkim nie ma wsparcia do walidacji po stronie klienta.

BTW wydaje mi się, że większy sens ma implementowanie interfejsu IDataErrorInfo w klasach, które są przekazywane do akcji kontrolera, w celu automatycznej walidacji niż przechwytywanie wyjątku w akcjach kontrolera. Rozumiem, że te rozwiązanie może być spowodowane wymuszeniem izolacji obiektów domenowych od wszelkiego rodzaju kodu infrastrukturalnego.

Tomek pisze...

Zamierzam użyć xVal w nowym projekcie i testuję właśnie.
Przygotowuję własne atrybuty sprawdzające poprawność, natomiast wykorzystują one algorytmy po stronie serwera (słynne np., sprawdzanie czy login już nie istnieje).
Najlepiej gdyby działało to tak, że moje atrybuty dziedziczą po ICustomRule i xVal produkuje kod JS wywołujący poprzez Ajax te same algorytmy których używam po stronie serwera. Czyli logika znajduje się w jednym miejscu na serwerze, a zarówno serwer (poprzez DataAnnotations) jak i klient (poprzez Ajax) korzystają z tego samego kodu walidującego.

Nie za bardzo wiem jednak jak zmusić xVal do współpracy. Tzn. mogę przygotować funkcję w JS która wywoła ajaxowe zapytanie o poprawność, ale muszę to zrobić ręcznie. A lepiej gdyby to zrobił xVal produkują odpowiednią konfigurację dla jQuery Validatora.

Może próbował ktoś to zrealizować? Może wspólnie wymyślimy i dodamy do xVal?

Dariusz Tarczyński pisze...

@Tomek
Ciekawy problem, a zarazem chyba popularny, bo np. oprocz sprawdzania zajętości loginu, można jeszcze pomyśleć o sprawdzaniu zajętości maila itp. Przyjrzę się temu przez weekend jak wygospodaruje chwilkę i coś może razem wymyślimy :-)