Razor Generator i area w ASP.NET MVC
Jeśli ktoś jeszcze tego nie wie, to Razor Generator jest pomocnym narzędziem, które pozwala na umieszczanie widoków MVC w bibliotekach dll. W przypadku frameworka takiego jak PizzaMVC, jest to niezbędne - wszak z założenia wszystkie podstawowe widoki CRUDowe mają być częścią biblioteki. Brzmi prosto, ale w zeszłym tygodniu trochę czasu spędziłem zmagając się z prostym problemem, który wynikał z tego, że rzeczywistość jest trudniejsza niż tutoriale. ;)
Objawy
W zeszłym tygodniu realizowałem kilka frontendowych zadań związanych głównie z przeniesieniem plików JS i CSS do frameworka (do tej pory znajdowały się w testowej aplikacji klienckiej). Następnie aktualizowałem Pizzę w aplikacji, która na niej bazuje. Tak - PizzaMVC nie powstaje sama, równolegle powstaje korzystająca z niej aplikacja, która nie jest tylko pokazowym projektem, lecz ma prawdziwe wymagania - takie jakie tylko klient potrafi wymyślić. ;) Aplikacja ta składa się z publicznej strony - powiedzmy “wizytówki” firmy oraz panelu administracyjnego. Tenże panel jest oczywiście wydzielony i siedzi sobie w Areas/Admin
.
Praca była dość żmudna - aplikacja nie korzystała wcześniej z Bootstrapa, Pizza zaś już go ma, więc musiałem przerobić wszystkie layouty, użyć odpowiednich klas CSS, itp. - same nudy.
Niestety nie wszystko było proste… Jakaś magia (naprawdę na początku tak myślałem!) sprawiała, że gdy wchodziłem na strony w panelu administracyjnym, wyświetlał mi się layout głównej strony. Zamiast menu z funkcjami obsługi produktów czy klientów, miałem publiczne menu: “o nas”, “kontakt”, itd. No nie tak miało być…
Diagnoza
Niby nie jest to trudna sprawa, ale trochę czasu minęło, zanim załapałem o co chodzi. Ale od początku…
Gdy wysyłamy żądanie pod adres /Admin/Customers/
, domyślnie skonfigurowane ASP.NET MVC wykonuje metodę Index
z kontrolera CustomersController
. Ponieważ korzystam z PizzaMVC, to ta metoda jest zdefiniowana w GridControllerBase
i wygląda następująco:
Czyli ma po prostu zwrócić widok o nazwie Index
. A zatem ASP.NET MVC zaczyna poszukiwania widoku o tej nazwie, w następujących lokalizacjach i kolejności:
~/Areas/Admin/Views/Customers/Index.cshtml
~/Areas/Admin/Views/Shared/Index.cshtml
~/Views/Customers/Index.cshtml
~/Views/Shared/Index.cshtml
Oprócz widoków z rozszerzeniem cshtml
, których używa smutna większość programistów ASP.NET MVC, poszukiwane są najpierw te z rozszerzeniami aspx
i ascx
(jak widać pełna kultura - starsi ludzie, którzy ciągle piszą w webformsowej składni widoków mają pierwszeństwo), zaś na końcu poszukiwane są widoki z rozszerzeniem vbhtml
- czyli coś dla totalnych perwersów prawdziwych hardkorów.
Wracając do tematu - żaden z tych plików fizycznie nie istnieje. Co oznacza, że jakiś mechanizm umieszcza je pod jedną z tych ścieżek w postaci wirtualnej. Fizyczny Index.cshtml
istnieje tylko w Pizza.Mvc. w katalogu Views/Shared
. Zajrzyjmy zatem do niego:
Niezbyt imponująco, bo wszystko i tak odbywa się w DisplayTemplate dla klasy GridMetaModel
, ale mniejsza z tym. Ponieważ korzystamy z Razor Generatora, to nasz widok jest kompilowany do takiej klasy:
I tutaj, w 34 linijce widzimy istotny kod. Atrybut PageVirtualPathAttribute
z ustawioną wartością "~/Views/Shared/Index.cshtml"
. Zagadka rozwiązana - wiemy już, gdzie MVC i co znajduje. No i ponieważ znajduje go pod takim adresem, to dopasowuje mu layout ze ścieżki ~Views/Shared/_Layout.cshtml
. Taki plik istnieje fizycznie w docelowej aplikacji, więc jest używany. Layout z głównej strony. Wszystko zgodnie z zasadami i logiką.
Wynika z tego, że wszystkie gridy w aplikacji, bez względu na to, w jakim są obszarze, będą korzystały z głównego layoutu. Smutne, bo nie tego przecież chcemy.
Terapia
Na początek, naprawa metodą prób i błędów. Zmieniłem wartość atrybutu na: [System.Web.WebPages.PageVirtualPathAttribute("~/Areas/Admin/Views/Customers/Index.cshtml")]
, uruchomiłem aplikację, i… zadziałało!
No dobra, oczywiście to niczego nie naprawia. To ma działać dla wszystkich widoków, zarówno wewnątrz każdego area jak i poza nimi. Jak to osiągnąć? Na początku myślałem, żeby jakoś dynamicznie zmieniać wartość atrybutu. No, ale czegoś takiego nie da się przecież zrobić. Można generować jakieś klasy proxy i nadpisywać atrybut. Do zrobienia, tylko trochę potrwa… Czy nie da się prościej?
Ogólnie, Razor Generator działa dzięki temu, że jego PrecompiledMvcEngine
jest rejestrowany jako jedna z VirtualPathFactory
. Ten kod wyglądał tak:
I jak wiadomo działał świetnie dopóki nie chciałem skorzystać z area. Na szczęście odkryłem, że PrecompiledMvcEngine
posiada przeciążony konstruktor przyjmujący drugi parametr - baseVirtualPath
. W ten sposób naokoło, ale skutecznie, rozwiązałem problem. Wystarczyło nieco zmodyfikować powyższy kod i napisać go w ten spośób:
I tyle. Teraz wszystko działa. No dobra, nie wszystko, ale to akurat tak.