PizzaMVC - wprowadzenie
To właściwie powinien być pierwszy post dotyczący mojego projektu, ale najpierw musiałem zrobić małą refaktoryzację, a później znaleźć wenę. W tym poście opisuję podstawowe klasy, z których korzysta się w PizzaMVC, a także postaram się wyjaśnić czemu w taki sposób to zaimplementowałem oraz jakoś umotywować swoje decyzje projektowe. Zapraszam do lektury i ostrzegam, że będzie raczej długo.
Wprowadzenie
Celem PizzyMVC jest umożliwienie tworzenia aplikacji crudowych minimalnym nakładem kodu, ale przy zachowaniu dobrych zasad programowania, czyli uniknięciu kardynalnych błędów. Oczywiście dałoby się zrobić framework wymagający od programisty jeszcze mniej kodu do napisania, ale odbyłoby się to kosztem łamania SRP i spadkiem wydajności, a tego chciałem uniknąć.
Żeby była jasność czym jest CRUD - PizzaMVC pozwala na połączenie tabeli w bazie danych z zestawem ekranów umożliwiających wyświetlenie i edycję danych z tejże tabeli. Są to: Index (z gridem), Create, Edit i Delete. Oczywiście tabeli nie definiujemy za pomocą SQL tylko stosujemy code first. Jesteśmy w końcu programistami, a nie administratorami baz danych.
Korzystanie z PizzaMVC
Dodanie nowej tabeli oraz odpowiadających jej ekranów wymaga kilku kroków:
Model danych
Zaczynamy od zdefiniowania klasy opisującej strukturę danych w bazie. Klasa taka musi implementować interfejs IPersistenceModel
. Można także skorzystać z dostarczanej przez Pizzę standardowej implementacji w postaci klasy PersistenceModelBase
. Klasa ta oferuje także wersjonowanie na potrzeby optimistic concurrency oraz prosty audyt (data utworzenia, kto utworzył, data modyfikacji, kto ostatnio zmodyfikował). Jeśli zaś chcemy, aby klasa podlegała pod soft delete, wystarczy skorzystać z SoftDeletableModelBase
, Ponadto, w przestrzeni nazw Pizza.Persistence.Attributes
znajduje się kilka atrybutów pozwalających na zdefiniowanie sposobu mapowania właściwości na kolumny w bazie. Pozwalają one na:
- zezwolenie na wstawianie do kolumny wartości
null
; - definiowanie rozmiaru oraz typu kolumny tekstowej;
- unikalność wartości;
- a także obsługę komponentów - czyli mapowanie właściwości jakiegoś typu nie jako oddzielna tabela lecz zestaw kolumn w tabeli-rodzicu.
Przykład modelu danych:
Modele prezentacji
Następnym krokiem jest zdefiniowanie zestawu modeli prezentacji dla każego ekranu. Klasy te muszą implementować: IGridModelBase
, ICreateModelBase
, IEditModelBase
oraz IDetailsModelBase
. Pizza oferuje oczywiście domyślne implementacje w postaci klas: GridModelBase
, CreateModelBase
, EditModelBase
oraz DetailsModelBase
. Jeśli chodzi o manipulowanie wyświetlaniem, to obsługiwane są standardowe atrybuty z przestrzeni nazw System.ComponentModel.DataAnnotations
, takie jak Display
, Editable
, Required
, DataType
. W przypadku rich text edit potrzebny jest oczywiście System.Web.Mvc.AllowHtml
. Czyli wszystko tak samo, jak w standardowej aplikacji ASP.NET MVC. Przykłady:
Warstwa serwisów aplkacyjnych
PizzaMVC w przeciwieństwie do podejścia znanego z mainstreamowej architektury nie ma pseudorepozytoriów będących jedynie wraperami na ORM ani logiki innej niż prezentacyjna w kontrolerach. Nie ma też żadnych nadmiarowych warstw ani obiektów - CRUD jest prosty, więc PizzaMVC też jest prosta. Ponieważ w dobrze napisanej aplikacji, cała logika aplikacyjna znajduje się w serwisach aplikacyjnych (czemu - to powinno być oczywiste, ale praktyka pokazuje, że nie jest, więc będę to musiał wyjaśnić w przyszłych postach), to o PizzaMVC oparta jest właśnie o warstwę serwisów aplikacyjnych. Główną klasą jest klasa CrudServiceBase<TPersistenceModel, TGridModel, TDetailsModel, TEditModel, TCreateModel>
. Jak widać łączy ona model danych z wszystkimi modelami widoku. Klasa ta sama nie robi praktycznie nic - operacje modyfikujace dane deleguje do PersistenceModelsStore<TPersistenceModel, TEditModel, TCreateModel>
natomiast odczyt danych do ViewModelsReader<TPersistenceModel, TGridModel, TDetailsModel, TEditModel, TCreateModel>
. CrudServiceBase
przyjmuje w zależności ISession
i operuje bezpośrednio na nim.
Serwisy crudowe dla naszych własnych modeli muszą dziedziczyć z tej klasy, wygląda to na przykład tak:
Istotna uwaga - potrzebny jest także interfejs dla serwisu. W tym przypadku wygląda on tak:
Jest on niezbędny, gdyż interfejs serwisu jest znany klienciej aplikacji webowej. Jak widać interfejs zna jedynie viewmodele. Gdyby klasa serwisu miała być znana aplikacji webowej, wówczas musiała by mieć ona referencje do asembly zawierającego kontrakty modelu danych, a to nie miałoby przecież sensu.
Kontrolery
Ostatni etap to kontroler:
Jak widać jest on parametryzowany interfejsem serwisu oraz czterema typami view modeli. Ponadto znajduje się w nim definicja layoutu grida, który zostanie wyświetlony w widoku Index
oraz tytuły stron dla pozostałych widoków.
Efekt końcowy
Dzięki temu uzyskujemy w bazie tabelę, która wygląda tak:
Widok listy rekordów z funkcjonalnym gridem:
Widok tworzenia nowego rekordu:
Edycję istniejącego rekordu:
Oraz podgląd szczegółów:
A tak wygląda komunikat przy usuwaniu rekordu:
Podsumowanie
I to tyle. Jak widać nie musimy bezpośrednio korzystać z ORMa, zarządzać transakcjami, nie piszemy ani linijki HTML czy JS. A co najważniejsze nie kopiujemy plików cshtml, nie tworzymy bliżniaczych kontrolerów z niemalże identyczną implementacją akcji. Wszystko to za nas robi framework, a do tego jeszcze ładnie odczytuje viewmodele odczytując z bazy tylko te właściwości, które chcemy mieć na widoku, dba o soft delete, audyt, transakcje i optimistic concurrency. A my mamy więćej czasu na tę część aplikacji, która wymaga czegoś więcej niż prostego CRUD.
Wady
Oczywiście można powiedzieć, że niepotrzebne są te wszystkie “puste” interfejsy i klasy. Oczywiśćie dałoby się zrobić automagicznie. Myślałem nad tym, ale doszedłem do wniosku, że w praktyce do takiego CrudControllera czy CrudSerwisu zawsze trzeba będzie dodać jeszcze jakieś metody, albo nadpisać te z klasy bazowej. W tym celu i tak trzeba byłoby utworzyć te klasy. Moim zdaniem nie warto pisać jakiegoś automatycznego generowania dla mniejszości przypadków.
Nie jestem też do końca przekonany, czy konfiguracja grida powinna znajdować się w kontrolerze. Możliwe, że to się zmieni.