Wstęp

Dziś wpis na temat tego, co każda bardziej zaawansowana strona posiada – panel administracyjny. Jeśli nie jest to najważniejsza część naszego projektu oraz gdy ma służyć to dla nas jako forma przeglądu funkcjonowania naszej witryny, to nie chcielibyśmy poświęcać na wykonanie jej zbyt dużo cennego czasu. Ten wpis jest właśnie na temat tego, jak zrobić funkcjonalny dostęp administracyjny, gdzie możemy przejrzeć wszystko co tylko możliwe przy minimalnym nakładzie zasobów. Postaram się także porównać podejścia zarówno w PHP (w moim przypadku na Kohanie, ale będzie dość ogólnikowo :)), jak i w ASP.NET. To bez ociągania – zaczynamy!

ASP.NET

ASP.NET Dynamic Data

Zaczniemy od platformy Microsoftu, gdzie jest dostępne jakby dedykowane rozwiązanie do przedstawionego przeze mnie problemu. Nazywa się ono ASP.NET Dynamic Data i jest to template projektu webowego do ściągnięcia i instalacji w VS. Zauważalne już na tym etapie są dwa problemy z tą solucją: po pierwsze – to osobny projekt, czyli integracja z naszą aplikacją nie będzie perfekcyjna, a po drugie – ten projekt nie ma nic wspólnego z MVC. Jeśli więc tworzymy jakieś rozwiązanie w MVC, to musimy robić nowy, zupełnie niezwiązany projekt.

Do działania będziemy potrzebowali Data Modelu, na bazie którego będzie generowany nasz dynamiczny model danych. Nie może być to jakikolwiek Data Model. Dostępne mamy 2 opcje: LINQ to SQL lub Entity Framework; jak dobrze się orientuję wykorzystanie innego ORMa nie jest możliwe (a raczej nie jest możliwe bez długiej i uporczywej walki – tak przynajmniej przypuszczam). Jeśli już mamy wybierać to polecam Entity Framework, nie dlatego, że go lubię, ale jest mniej bolesny przy kolejnych krokach. Generujemy model dla naszej bazy, konfigurujemy scaffolding (w referencjach jest cały i dokładny opis) i odpalamy. Jeśli nic po drodze nie popsuliśmy, powinniśmy zobaczyć stronę z listą tabel do przeglądania. Jak widać mamy w pełni funkcjonalny CRUD dla wszystkich tabelek, które uwzględniliśmy w modelu. Ok – osiągnęliśmy całkiem dużo prawie nic nie robiąc. To co działa na dzień dobry, to: rozpoznanie kluczy (obcych i głównych), a przez to – powiązanie tabel (w tym też w relacji many-to-many, które nie jest oficjalnie wspierane w Entity Framework), prosta walidacja typu wprowadzonych danych i pól obowiązkowych, oraz sortowanie i proste grupowanie elementów na listach.

Jesteśmy jednak programistami i dla nas wyklikanie “jakiegoś” rozwiązania może stanowić co najwyżej początek pracy, a nie pełnoprawny produkt.

Pierwsza rzecz jaką na ogół będzie trzeba modyfikować lub dodać, to dodatkowe walidacje, ukrywanie niektórych pól oraz nazwy pól. Jak to zrobić? Generalnie potrzeba nam do tego prostego kodu obiektowego, który przedstawiałby diagram Data Modelu. Można albo edytować plik *.Designer.cs pod spodem naszego modelu (bardzo zły pomysł), albo stworzyć POCO Entities (generalnie zły pomysł), albo samemu od ręki napisać kod POCO dla naszego diagramu (marny pomysł, ale lepszy od użycia automatycznego POCO Entity Generatora). Pliku designera nie należy nigdy edytować samemu, bo wystarczy przesunąć cokolwiek na diagramie Entities, by utracić wszelkie zmiany, które wprowadziliśmy; do tego kod wygenerowany w ten sposób jest naprawdę marny i dość trudno się połapać (ma mnóstwo niepotrzebnych informacji – np. położenie na diagramie i wielkość).

POCO – stanowi maksymalnie uproszczoną wersję powyższego kodu, gdzie całość sprowadza się jedynie do użycia prostych klas z publicznymi polami, odpowiadającymi za konkretne pola w bazie. Generator POCO Entities jest dość mocno utożsamiany z Entity Framework v2 i tylko na tej wersji możemy tego użyć (zapominamy przy okazji o Visual Studio 2008 – Entity Framework v2 jest dostępny tylko na Visual Studio 2010). Mając model klikamy w pustym miejscu PPM i wybieramy Code Generation Tool. Z listy wybieramy ADO.NET POCO Entity Generator i zatwierdzamy. W ten sposób wyłączyliśmy generowanie standardowego pliku designera. Zamiast tego tworzone są pliki z uproszczoną strukturą danych, którą możemy odrobinę dostosować do naszych potrzeb. Tutaj, zamiast omawiać posłużę się przykładem dostosowującym:

public partial class Projects  
    {
        #region Primitive Properties
        [ScaffoldColumn(false)]
        public virtual int id_project
        {
            get;
            set;
        }

        [DisplayAttribute(Name = "Nazwa projektu")]
        public virtual string projectName
        {
            get;
            set;
        }

        [Display(Name = "Opis projektu")]
        public virtual string projectDescription
        {
            get;
            set;
        }

        [DisplayAttribute(Name = "Utworzony przez")]
        public virtual int createdBy
        {
            get;
            set;
        }

        #endregion
   }

Taki stan rzeczy ma jednak kilka wad – w dalszym ciągu bowiem każda większa zmiana (np. dodanie jednego pola w bazie) usunie nam wszystkie nasze wprowadzone zmiany w kodzie. Dlatego dobrym pomysłem jest skopiowanie gdzieś utworzonych klas oraz usunięcie generatora POCO. W dalszym ciągu jest to jednak nieco naciągane… jednak to jedyne rozsądne rozwiązanie jakie znalazłem. Jakbyście wiedzieli jak sobie poradzić z tym problemem, to serdecznie zapraszam :)

EDIT: Zgodnie z sugestią z komentarzy (dzięki, eN) powinno się robić to w jeden z poniższych sposób, wykorzystując DataAnnotations:

[MetadataType(typeof(ProjectsMetaData))]
public partial class Projects  
{
    public class ProjectsMetaData 
    {
        [ScaffoldColumn(false)]
        public int id_project {    get; set; }

        [DisplayAttribute(Name = "Nazwa projektu")]
        public string projectName {    get; set; }

        [Display(Name = "Opis projektu")]
        public string projectDescription {    get; set; }

        [DisplayAttribute(Name = "Utworzony przez")]
        public int createdBy {    get; set; }
    }
}

lub:

public partial class Projects : IProjectsMetaData  
{    }

public interface IProjectsMetaData  
{
    [ScaffoldColumn(false)]
    int id_project { get; set; }

    [DisplayAttribute(Name = "Nazwa projektu")]
    string projectName { get; set; }

    [Display(Name = "Opis projektu")]
    string projectDescription { get; set; }

    [DisplayAttribute(Name = "Utworzony przez")]
    int createdBy { get; set; }
}

Inna sprawa to kwestia wydajności. Najpierw generujemy z listy tabel i pól obiektowy Data Model, który następnie poddajemy dość częstym refleksjom w celu utworzenia Dynamicznych widoków (a jest to ciężka obliczeniowo operacja). Wydaje się to nieoptymalne i trudno mi to w pełni zweryfikować; pewne jest natomiast, że przed nieuniknioną zgubą braku wydajności chroni nas (przynajmniej częściowo) mechanizm cache’owania, który jest obecny zarówno przy tworzeniu formularzy, jak i całego modelu.

Kilka zdjęć ilustrujących jak wygląda gotowe rozwiązanie ze wszystkimi omawianymi bajerami:

image image image image

Na screenach widać przeglądarkę IE9. Powód dla której ją akurat wybrałem do prezentacji jest prozaiczny – nie używam jej na co dzień, więc i nie musiałbym zamalowywać tytułów innych otwartych kart ^^

Przy okazji warto zauważyć jedną rzecz: nie wszystko jesteśmy w stanie dostosować. Można próbować dodawać template’y do pól, które nas gnębią i przez to wpłynąć na ich wygląd i funkcjonalność. Brakuje też racjonalnej obsługi relacji One-to-One (do tego własnego template trzeba napisać). Bardzo boli także jakość produkowanego kodu HTML. Po pierwsze widoczny jest ViewState, czyli ukryta zmienna w kodzie html, która odpowiada za obecny stan strony (postbacks history i inne) oraz zwiększa wielkość strony wprost proporcjonalnie do czasu na niej spędzonym (tak mniej więcej :)). Inna sprawa to layout oparty na tabelach. Wszyscy poza Microsoftem wiedzą bowiem, że tabele służą do prezentacji danych (zbiorów danych), a nie do modyfikacji designu strony. Trudno także to zmienić, bez większego buszowania w kodzie (wynika to przez używanie kontrolek ASP.NET tj. DetailsView i innych, które same z siebie generują taki kod – żeby się tego pozbyć, trzeba byłoby stworzyć własne kontrolki rozszerzające funkcjonalność o produkowanie poprawnego i rozsądnego kodu HTML). Oczywiście wszyscy w tym momencie wzruszają ramionami, tłumacząc, że skoro nie wpływa to na wygląd, to pewnie jest bez znaczenia. Ponadto skoro robi w ten sposób firma notowana na amerykańskiej giełdzie (oraz mająca niezły przychód) to jest to droga, którą każdy web developer powinien się kierować. Nic bardziej mylnego! Nieznajomość standardów, które obecnie stanowią podstawę komunikacji w Internecie przez firmę typu Microsoft nie zwalnia nas od odpowiedzialności za produkcję rozwiązań także z tymi standardami niezgodnymi. Jest to jeden z tych momentów, kiedy warto sięgnąć do otwartych rozwiązań, które między innymi stawiają na zgodność ze standardami.

Jeśli nie znamy się na HTML, o standardach W3 nigdy nie słyszeliśmy lub po prostu nie jesteśmy programistami, to wzruszymy rękoma i zaczniemy się bawić w konfigurowanie solucji ASP.NET Dynamic Data, coś w rezultacie osiągając, ale nie zdając sobie większej sprawy z tego co robimy. Bardzo amerykańskie podejście ;] Inne podejście widać w przypadku MVC:

ASP.NET MVC

Alternatywnie, jeśli bardzo nam zależy na dużym dostosowaniu kodu warto zainteresować się scaffold generatorem dla ASP.NET MVC (tylko wersja 1 niestety). Wypróbowałem jedno bezpłatne rozwiązanie tego typu: Asp.Net MVC Scaffold Generator 1.1. Ma ono kilka zalet – między innymi tworzy kod dobrej jakości ze wszystkimi niezbędnymi rzeczami, a jednocześnie bez jakichś udziwnień. Bardzo dobrze taki kod dostosować (dla każdej tabeli generowany jest 1 kontroler, 3 widoki i 1 model). Jednak nie jest to dobre rozwiązanie, gdy chcemy mieć np. widok generyczny do tabel i jeśli coś chcemy zmienić we wszystkich tabelach, to musimy wszystkie widoki tabel modyfikować. Inną alternatywą może być MVCCrud (którego niestety nie próbowałem) albo… własny Scaffold Generator, który obsługiwałby nasze najdziksze wymagania.

Ponadto używanie rozwiązań tego typu ma też dużą zaletę wydajnościową. Nie ma potrzeby generować widoków i modeli na podstawie struktury bazy, następnie modyfikować jej strukturą rozszerzającą, następnie walidować poprawność naszej modyfikacji, itd. Po prostu działamy na “statycznych” widokach, modelach i kontrolerach. Nic nie jest generowane dynamicznie przy odświeżaniu strony ;]

Ciekawe wydaje się także użycie T4 do półautomatycznego generowania widoków, modeli i kontrolerów. Poruszone to zostało na filmiku Scotta Hanselmana (dostępnego tutaj) i przystępnie opisane.

To tyle jeśli chodzi o część pierwszą. W niedługim czasie pojawi się część druga, w której opiszę jak zrobić półautomatyczny panel administracyjny w Kohanie, oraz napiszę kilka szczegółów na temat innych narzędzi typu Scaffold Generator do ASP.NET MVC.

Pozdrawiam

Referencje

New ASP.NET Dynamic Data Support

ASP.NET Dynamic Data Layer Customization

Świetny filmik na temat różnych metod scaffoldingu