Artykuł

freeimages.com freeimages.com
wrz 05 2014
0

ViewModel a ASP.NET MVC

MVC był dotychczas moim ulubionym wzorcem architektonicznym, a aplikacje wykorzystujące ten właśnie wzorzec, mają kilka niezaprzeczalnych zalet. Przede wszystkim mamy tutaj podział na trzy części:

  • Kontroler
  • Model
  • Widok

I choć wszystkie powyższe elementy są od siebie zależne, to w praktyce w tym przypadku dużo łatwiej jest napisać złożone aplikacje, w którym widokom jest bliżej do statycznego HTMLu, a konkretny kod C#/VB znajduje się wewnątrz modeli i kontrolerów. Niestety im dłużej pracuję z MVC, tym bardziej zaczynam dostrzegać pewne braki w tym rozwiązaniu. Można by powiedzieć dostałem palec, a chciałbym całą rękę... Na szczęście mój problem, jest stosunkowo łatwy do rozwiązania;-)

Problem

MVC zapewnia logiczny podział aplikacji na trzy elementy. Takie podejście daje nam sporo benefitów:

  • Łatwo rozdzielić pracę dla frontendu (widok) oraz backendu (model i kontroler)
  • Aplikacja jest bardziej modularna - dużo łatwiej jest utworzyć model domeny w innym projekcie i dołączyć go jako bibliotekę do aplikacji WWW
  • Dzięki kontrolerom możliwe jest porzucenie klasycznej nawigacji odnoszącej się do plików na rzecz np. przyjaznych linków

Zalet jest oczywiście więcej (można by tu jeszcze choćby wspomnieć o wyjątkowej rozszerzalności i skalowalności całej platformy) i na początku pracy z ASP.NET MVC można naprawdę być szczęśliwym. Po pewnym czasie zaczynają pojawiać się jednak pewne wątpliwości.

Cechą charakterystyczną MVC, jest wykorzystanie modelu jako nośnika danych pomiędzy kontrolerem i widokiem. Bardzo często jest jednak tak, że klasy modelu funkcjonują również w modelu domeny (bazie danych). Teraz jest to szczególnie popularne w momencie gdy korzystamy z rozwiązań pokroju Entity Framework, czy NHibernate. Jeśli zaczynacie dostrzegać tutaj pewien problem, to bardzo dobrze myślicie. Jak można bowiem w jednej klasie pogodzić tematy związane z bazą danych oraz z warstwą prezentacji?

W teorii możemy wykorzystać Data Annotations, jednak rozwiązanie to nie zadziała w przypadku bardziej złożonych zagadnień, a ponadto takie podejście wymusza dodanie dodatkowych referencji związanych z ASP.NET, co będzie bardzo niepożądane w przypadku składowania modelu domeny w osobnej bibliotece. Data Annotations w dużej ilości doprowadzi też do bardzo brzydkiego kodu:

public class Task
{
    [Key]
    public Guid Id { get; set; }

    [Required(ErrorMessage="To pole jest wymagane")]
    [MinLength(2)]
    [MaxLength(100)]
    public string Title { get; set; }

    [MaxLength(3000)]
    public string Memo { get; set; }

    [DataType(DataType.Date)]
    public DateTime? DueDate { get; set; }

    public Guid ContextId { get; set; }

    [ForeignKey("ContextId")]
    [Required]
    public Context Context { get; set;}
}

Oczywiście to jest tylko przykład, ale pokazuje jak łatwo jest nabałaganić w klasie modelu, popsuć czytelność i pomieszać aspekty związane z warstwą prezentacji, a samym kształtem bazy danych. Jak to naprawić? Bardzo prosto - wprowadzając ViewModel (element pośredniczący między widokiem a modelem)!

Istota elementu ViewModel

Nasz przykładowy model był kompletnie bezużyteczny i tak naprawdę nie sprawdzał się ani w kontekście budowy domeny, ani w kontekście nośnika danych dla widoku. W pierwszym przypadku mieliśmy zbyt wiele informacji. Pojawiły się zbędne atrybuty, a nasza klasa stała się po prostu nieczysta. Model domeny powinien być możliwie prosty, a konfiguracja winna odbywać się w miejscu do tego przeznaczonym.

Klasa ta również kompletnie nie sprawdziła się jako nośnik danych do widoku. Zakładając, że do naszego Taska będziemy przypisywać kontekst, to kompletnie zabrakło tutaj listy dostępnych kontekstów. Dla widoku zbędne są również informacje o powiązaniach Taska z innymi członkami modelu domeny. Czego zatem oczekiwać od modelu, a czego od elementu typu ViewModel?

Od tego pierwszego powinniśmy przede wszystkim oczekiwać by dobrze opisywał naszą warstwę biznesową. ViewModel powinien zaś brać z modelu to co najlepsze i dostarczać wszystkich potrzebnych elementów dodatkowych dla widoku. Innymi słowy w idealnym elemencie ViewModel znajdziemy:

  • Kluczowe dla warstwy prezentacji dane
  • Listę elementów zależnych - np. kontekstów które możemy wykorzystać w Tasku
  • Informacje/dane dodatkowe przydatne w generowaniu widoku - np. informacje o stronicowaniu

Jak to wszystko ogarnąć?

Cały temat możemy zrealizować w trzech prostych krokach. Oto one:

1. Utworzyć prostą klasę modelu

Jestem zwolennikiem tworzenia klas POCO dla modelu danych. Takie podejście ma bardzo wiele zalet, ale przede wszystkim uniezależnienia nas od jednego konkretnego rozwiązania. Przykładowa klasa TaskModel może wyglądać więc następująco:

public class TaskModel
{
    public Guid Id { get; set; }
    public string Title { get; set; }
    public string Memo { get; set; }
    public DateTime? DueDate { get; set; }
    public Guid ContextId { get; set; }
    public Context Context { get; set;}
}

Wszelkie informacje na temat wiązań, czy szczegóły związane z typem danych wybranego pola, można załatwić w konfiguracji wybranego ORMa. W przypadku Entity Framework czynimy to w metodzie OnModelCreating klasy kontekstu bazy danych.

2. Utworzyć sensowną klasę widoku modelu

ViewModel powinien zaspokajać widok i tylko to się w tym przypadku liczy. Jest to dobre miejsce by umieścić Data annotations mające wpływ na zachowanie się formularza w widoku, a także umieszczenie kolekcji elementów zależnych od aktualnego:

public class TaskViewModel
{
    [HiddenInput]
    public Guid Id { get; set; }

    [Required(ErrorMessage = "To pole jest wymagane")]
    [Range(2, 100, ErrorMessage = 
        "Pole Tytuł musi mieścić się w określonym przedziale znaków")]
    [DisplayName("Tytuł")]
    public string Title { get; set; }

    [MaxLength(3000, ErrorMessage="Pole memo nie może przekraczać 3000 znaków!")]
    public string Memo { get; set; }

    [DataType(DataType.Date)]
    [DisplayName("Data zakończenia")]
    public DateTime? DueDate { get; set; }

    public Context Context { get; set; }

    [Required(ErrorMessage="Kontekst jest wymagany"]
    [DisplayName("Kontekst")]
    public IEnumerable<Context> Contexts { get; set; }
}

Ten ViewModel jest już dużo bardziej szczegółowy i zawiera znacznie więcej informacji przydatnych z perspektywy widoku.

Jeśli denerwuje Was inne nazewnictwo klas, to możecie ten problem bardzo łatwo rozwiązać, umieszczając klasy modelu np. w katalogu Models, a klasy widoku modelu w katalogu ViewModels. W takim przypadku VS automatycznie nada im odrębne przestrzenie nazw.

3. Mapowanie

Mając dwa zbliżone typy danych (a czasem w pewnych określonych przypadkach nawet dokładnie takie same struktury), może zajść konieczność transferu danych między nimi. W teorii można to zrobić ręcznie, ale od czego są biblioteki?

W jednym z ostatnich tekstów z działu programowanie, opisałem bibliotekę AutoMapper która idealnie nadaje się do tego zadania. Nie będę tutaj rozwodził się nad jej możliwościami i zaletami, więc po prostu odsyłam Was do tamtego wpisu. W skrócie powiem tylko tyle, że jest ona zrobić naprawdę wiele w tego typu sytuacjach;-)

Podsumowanie

Można by powiedzieć, że ilu programistów tyle teorii, ale wydaje mi się, że podejście wykorzystujące ViewModel w MVC, ma coraz większą liczbę fanów. Jest to swego rodzaju kropka nad i. Element którego zawsze tutaj brakowało i element który porządkuje pewne sprawy. Oczywiście takie rozwiązanie może mieć również wady.

Można by powiedzieć, że jest tu pewnego rodzaju dublowanie danych i powtarzalność, z drugiej jednak strony cały kod staje się bardziej skalowalny i łatwiej go użyć również w innych miejscach, co jest szczególnie ważne w dobie szybko rozrastającego się sektora mobilnego;-)

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

Send to Kindle

Komentarze

blog comments powered by Disqus