Artykuł

freeimages.com freeimages.com
maj 28 2014
0

Siła atrybutów w modelu w danych w ASP.NET MVC

Atrybuty w ASP.NET MVC są niezwykle ważne i pojawiają się w wielu miejscach tego frameworka. Dla przykładu, dwa miesiące temu pisałem o routingu opartym na atrybutach. Jest to nowość wprowadzona w wersji 5, która w wielu przypadkach może usprawnić i ułatwić skomplikowane zasady przetwarzania żądań przez aplikację. I choć wspomniane rozwiązanie jest ważne, to jednak tytułowy framework z atrybutami zaprzyjaźnił się już dużo wcześniej, głównie za sprawą Data Annotations.

Nazwa brzmi poważnie, ale tak naprawdę chodzi tu tylko o przestrzeń nazw zawierającą klasy atrybutów, które później używane są w logice aplikacji do opisu zawartej w nich danych. Jest to niezwykle istotne rozwiązanie, które w wymierny sposób wpływa na pracę całej aplikacji webowej.

Wprowadzenie

Wyobraźcie sobie taką sytuację. Tworzycie aplikację będącą katalogiem osób. W tym celu potrzebować będziecie odpowiedniej klasy logiki przechowującej dane wybranej osoby, a także kilku widoków odpowiedzialnych za wyświetlanie formatek umożliwiających zarządzanie rzeczoną bazą.

W normalnych warunkach większość kodu wygeneruje za Was Visual Studio. Niestety z automatu nie dostaniecie rozbudowanej walidacji, czy przyjaznych nazw. W praktyce więc otrzymacie formularz na którym widnieć będą pola FirstName, LastName itd., a polu wiek będziecie mogli wprowadzić nawet liczbę 5000.

Naturalnym krokiem byłaby teraz modyfikacja wszystkich formatek polegająca na dodaniu do nich przyjaznych polskich nazw oraz walidacji. Walidację trzeba by było również dodać po stronie serwera. Innymi słowy byłoby dużo tematów do ogarnięcia i to w wielu różnych miejscach aplikacji. Oczywistym jest fakt, że pewne rzeczy powtarzały się by w wielokrotnie, co jest prostym przepisem na katastrofę.

Wykorzystując Data Annotations można wszystkie powyższe problemy (i wiele innych) rozwiązać w jednym miejscu. Benefity tego podejścia zaprezentuję przy użyciu testowego projektu.

Co możemy uzyskać?

Poniżej lista punktów, które chciałbym zmienić i które są w zasięgu za sprawą atrybutów Data Annotations:

  • Ustawienie wymagalności dla konkretnych pól
  • Określenie dokładnych reguł walidacji - np. wiek musi być w określonym przedziale, numer telefonu musi mieć 9 cyfr itp.
  • Określenie przyjaznej, polskiej nazwy dla każdej z kolumn, która będzie pojawiać się na formularzu

Wygląda to wszystko bardziej niż obiecująco, czas więc przystąpić do pracy;-)

Projekt wstępny

W dziale download umieściłem dzisiejszy projekt w początkowej fazie. Znajduje się tam cała potrzebna logika oraz wszelkie niezbędne dowiązania. Brakuje tylko kontrolera oraz widoków, ale to możecie bardzo szybko zmienić korzystając z opcji Add/New Scaffolded Item (PPM na assembly w Solution Explorer). Opcja scaffoldingu została dodana w VS 2013 (również w wersji Express) i umożliwia ona wygenerowanie kontrolera oraz widoków dla operacji CRUD na bazie klasy modelu oraz kontekstu. W naszym projekcie wykorzystujemy EF, dlatego też w nowo otwartym oknie należy wybrać opcję MVC 5 Controller with Views, using Entity Framework i zatwierdzić operację.

Na ekranie pojawi się drugi i zarazem ostatni dialog. Sugeruję go ustawić w taki sposób, jak widoczne jest to na screenie poniżej (klasa logiki Person oraz klasa kontekstu PersonContext). Stosując kontroler Home, unikniemy zmiany domyślnych reguł routingu. Jeśli klasy modelu i kontekstu nie są widoczne w polach wyboru, to przerwijcie operację i przebudujcie projekt. Powinno to pomóc. Całość zatwierdzamy przyciskiem Add i dajemy Visualowi kilka sekund na wykonanie całej magii za nas;-)

Po zakończeniu operacji przez VS, możecie skompilować i uruchomić projekt. Jeśli wszystko pójdzie pomyślnie, to na Waszym ekranie ukaże się pusta lista. Korzystając z dostępnych formatek, szybko będziecie mogli dodać nowe osoby do bazy.

Patrząc na screen, widać już problemy o których mówiłem wcześniej. Są to między innymi nieprzetłumaczone etykiety pól oraz brak konieczności podania imienia oraz nazwiska. Oczywiście różnych niedogodności jest znacznie więcej...

Magiczne atrybuty

Tak jak wspominałem wcześniej, ASP.NET MVC jest na tyle inteligentnie zorganizowany, że wystarczy zmienić klasę logiki w jednym miejscu, by mieć wpływ na to w jaki sposób działa cała aplikacja. Naszym polem manewrów będzie klasa Person znajdująca się w katalogu Models. Jej wyjściowa struktura wygląda następująco:

namespace PersonAttributeTest.Models
{
    public class Person
    {
        public int PersonID { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
        public int? Phone { get; set; }
    }
}

W kolejnych akapitach skupimy się już na konkretnych atrybutach, które możemy umieścić w klasie logiki.

Klucz główny oraz Identity

Nasza klasa jest niczym tabela, a każda tabela musi mieć klucz główny. Zapewne w tym momencie zastanawiacie się, jakim cudem aplikacja działała wcześniej, skoro w żaden sposób nie go wskazaliśmy. Otóż dzieje się tu pewna magia. Wystarczy że jedno z pól w tabeli nazywa się Id, bądź jego nazwa kończy się na Id właśnie (wielkość liter nie ma znaczenia), by EF z automatu określił że to właśnie to pole jest kluczem głównym tejże tabeli. Na dodatek jeśli rzeczone pole jest typu int, bądź GUID, to automatycznie zyskuje ono również właściwość IDENTITY.

Jeśli z naszego kodu nie wynika jakie pole powinno być kluczem, bądź też chcemy wskazać inne pole które ma pełnić taką rolę, zawsze możemy to zrobić za pomocą atrybutu Key:

[Key]
public int PersonID { get; set; }

Dodatkowo aby ustawić IDENTITY musimy użyć atrybutu DatabaseGenerated:

[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int PersonID { get; set; }

Jak pokazuje powyższy listing, atrybuty można łączyć - wystarczy w tym celu użyć przecinka;-) Taka konstrukcja również będzie prawidłowa:

[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int PersonID { get; set; }

Oczywiście w naszym przypadku atrybuty te nie są potrzebne, ponieważ zadziałał automat.

Przyjazne nazwy

W chwili obecnej w naszej aplikacji znajdują się angielskie nazwy pól, które z wiadomych przyczyn nie posiadają również spacji oraz innych znaków specjalnych. Oczywiście taki stan rzeczy jest niedopuszczalny i musimy to jakoś zmienić. Moglibyśmy ręcznie zmodyfikować każdy z formularzy, ale czy ma to jakiś sens? Lepiej będzie zastosować atrybut DisplayName:

public class Person
{
    [DisplayName("Id")]
    public int PersonID { get; set; }

    [DisplayName("Imię")]
    public string FirstName { get; set; }

    [DisplayName("Nazwisko")]
    public string LastName { get; set; }

    [DisplayName("Wiek")]
    public int Age { get; set; }

    [DisplayName("Telefon")]
    public int? Phone { get; set; }
}

Wprowadzone zmiany będą widoczne po przekompilowaniu i ponownym uruchomieniu aplikacji.

Określanie typu danych

Mimo podania typu przy nazwie każdej właściwości klasy logiki, możemy się pokusić o dokładniejsze określenie tego, co będzie przechowywane w danym polu. W tym celu wystarczy zastosować atrybut DataType wraz z odpowiednią wartością enumeracji o tej samej nazwie. Co daje nam takie rozwiązanie? Wbrew pozorom całkiem sporo:

  • Po pierwsze w niektórych przypadkach aktywowane są dodatkowe reguły walidacji (o walidacji kilka słów więcej za chwilę)
  • Po drugie - ASP.NET MVC jest w stanie odpowiednio przygotować wyświetlaną kontrolkę. Np. dla typu DateTime zaserwuje nam kalendarz, a dla MultilineText przerobi inputa na element textarea

W naszym przypadku dla właściwości Phone możemy przypisać typ danych PhoneNumber:

[DataType(DataType.PhoneNumber)]
public int? Phone { get; set; }

Walidacja

Walidacja zawsze była złożonym tematem, dlatego też twórcy frameworka robią co mogą by umilić nam pracę nad tym mechanizmem. By żyło się nam lepiej, niektóre elementy dostają automatyczną walidację.

Jak z pewnością pamiętacie wcześniejsze screeny, na jednym z nich było widać reguły wymagalności dla pola Wiek. Dzieje się tak dlatego, ponieważ engine frameworka automatycznie wymaga wartości dla typów, które nie przyjmują wartości NULL (non-nullable).

Z tego też powodu dodałem znak zapytania przy typie int dla pola Phone, ponieważ nie każda osoba może mieć numer telefonu - musimy dopuścić podanie pustej wartości.

Automatyczna walidacja nie objęła naszych pól typu string, dlatego też musimy sami o nie zadbać. W tym celu dodamy atrybut Required dla pól FirstName oraz LastName:

[Required]
public string FirstName { get; set; }

[Required]
public string LastName { get; set; }

Dzięki tym atrybutom, ASP.NET MVC zadba o wymagalność tych pól w aplikacji wyświetlając komunikat:

Pole xyz jest wymagane

W sytuacji gdy użytkownik nie wprowadzi wartości do wskazanego pola. Możesz również wyświetlić własny komunikat błędu. W tym celu wystarczy tylko dodać parametr do konstruktora atrybutu Required:

[Required(ErrorMessage = "Proszę wprowadzić imię")]
public string FirstName { get; set; }

Imię i nazwisko to dopiero połowa naszego formularza. Warto lepiej przyjrzeć się również pozostałym polom. Na ten moment możemy wprowadzić dowolną liczbę w polu wiek. Jest to oczywiście niedopuszczalne, ponieważ ciężko zakładać, że wiek może być ujemny, bądź też może być nie wiadomo jak dużą liczbą. Aby naprawić zaistniałą sytuację, wykorzystamy atrybut Range, który przyjmuje minimalną i maksymalną wartość dla pola:

[Range(1, 150)]        
public int Age { get; set; }

Opcjonalnie możemy dodać również własny komunikat błędu, stosując nazwany parametr ErrorMessage (identyczna reguła jak w przypadku atrybutu Required):

[Range(1, 150, ErrorMessage="Proszę wprowadzić liczbę z przedziału 1-150.")]
public int Age { get; set; }

Na koniec zostało pole Phone. Tak jak pisałem wcześniej, nie wymagamy od użytkownika podania tej wartości, ale przydałoby się mimo wszystko zweryfikować to, co użytkownik zdecyduje się wprowadzić. Zakładając że poprawny numer telefonu w Polsce składa się z 9 cyfr, to problem ten możemy rozwiązać na kilka sposobów:

  • Wykorzystać ponownie atrybut Range przekazując minimalną i maksymalną 9 cyfrową liczbę
  • Zastosować atrybut RegularExpression odpowiedzialny za weryfikację wartości przy użyciu wyrażenia regularnego
  • Zbudować własną regułę walidacji wykorzystując atrybut CustomValidation

Wydaje mi się, że 2 opcja będzie najbardziej optymalna. Spójrzcie na przykład poniżej:

[RegularExpression("[0-9]{9}", ErrorMessage="Numer telefonu musi składać się z 9 cyfr.")]
public int? Phone { get; set; }

Miksujemy wszystko razem

Na poniższym listingu zaprezentowałem klasę logiki, która wykorzystuje większość przedstawionych w tekście rozwiązań;-)

public class Person
{
    [DisplayName("Id")]
    public int PersonID { get; set; }

    [DisplayName("Imię"), Required(ErrorMessage="Proszę wprowadzić imię.")]
    public string FirstName { get; set; }

    [DisplayName("Nazwisko"), Required]
    public string LastName { get; set; }

    [DisplayName("Wiek")] 
    [Range(1, 150, ErrorMessage = "Proszę wprowadzić liczbę z przedziału 1-150.")]
    public int Age { get; set; }

    [DisplayName("Telefon"), DataType(DataType.PhoneNumber)]
    [RegularExpression("[0-9]{9}", ErrorMessage = "Numer telefonu musi składać się z 9 cyfr.")]
    public int? Phone { get; set; }
}

Przestrzenie nazw potrzebne do wykorzystania powyższych atrybutów, uzyskacie za pomocą mechanizmu rozwiązywania nazw w VS.

Z działu download możecie z kolei pobrać pełną, działającą wersję projektu.

Podsumowanie

Uff.. przyszła pora na podsumowanie. Zaprezentowanie idei wykorzystania atrybutów do stworzenia lepszego modelu, zajęło całkiem sporo miejsca. Choć przedstawione wyżej atrybuty są tymi, które prawdopodobnie będzie wykorzystywać najczęściej, to w praktyce możliwości są znacznie większe. Zainteresowanych odsyłam raz jeszcze na strony MSDN, gdzie możecie poczytać trochę więcej o Data Annotations.

Uwagi techniczne

Na koniec - dwie małe techniczne uwagi, związane z problemami na jakie możecie natrafić.

Atrybuty a kształt modelu

Wprowadzając nowe atrybuty możemy przez przypadek wpłynąć na kształt utworzonej wcześniej bazy danych. Atrybuty typu Key, Range itp. mogą wpłynąć na to, w jaki sposób składowane są poszczególne pola w bazie danych. Zasadniczo wyjścia z tej sytuacji są dwa:

  • Jeśli nie w tabeli nie ma żadnych istotnych danych, to można ją najzwyczajniej w świecie usunąć i pozwolić na jej ponowne wygenerowanie według nowych reguł (w przypadku bazy w pliku .mdf można usunąć cały plik i wygenerować całą bazę od nowa - plik mdf w moim przykładzie znajduje się w katalogu App_Data aplikacji. Usuńcie również pliku logu)
  • Możecie również zastosować mechanizm Code First Migrations - rozwiązanie skrajnie zalecane w nietestowych bazach danych

Osobiście podczas testów wybrałem wariant I. Tutaj jednak mała uwaga - przy ręcznym usuwaniu bazy danych, czasem pojawia się problem z jej wygenerowaniem. W tym przypadku można go szybko obejść zmieniając wartość właściwości Initial Catalog w connectionStringu w pliku Web.config.

Aktywacja walidacji

W projekcie który przygotowałem, wszystkie niezbędne mechanizmy do poprawnej pracy walidacji zostały już skonfigurowane. Jeśli tworzysz nowy projekt od zera, to musisz zadbać o kilka rzeczy:

  • Zainstalować wtyczki jQuery: jQuery Validation oraz jQuery Unobtrusive Validation - polecam to zrobić za pomocą menadżera pakietów NuGet (oczywiście wymagają one jQuery)
  • W formularzu dodać dwa helpery:
    • Html.ValidationSummary - w miejscu w którym chcemy wyświetlić podsumowanie wszystkich błędów walidacji
    • Html.ValidationMessageFor - w miejscu w którym chcemy wyświetlić błąd dla konkretnego pola
  • W kodzie widoku dodać referencje script do dwóch zainstalowanych wtyczek oraz do samego jQuery
  • Umieścić sprawdzenie czy model jest poprawny w treści metody obsługującej nadchodzące żądanie formularza w kontrolerze:
    if (ModelState.IsValid)
    {
        // ...
    }
  • Jak widać jest tego całkiem sporo, ale wszystkie te mechanizmy zapewniają nam walidację zarówno po stronie klienta jak i serwera i to w całej aplikacji.

    Data ostatniej modyfikacji: 05.09.2014, 22:52.

Podoba Ci się ten wpis? Powiedz o tym innym!

Send to Kindle

Komentarze

blog comments powered by Disqus