Czytając ostatnio o rozszerzeniach do klasy HtmlHelper, stwierdziłem że w wraz z rozwojem projektu ich różnorodność może stać się trudna do ogarnięcia i przydałoby się je pogrupować, zorganizować.
Zacząłem się więc zastanawiać jak takie rozwiązanie mogłoby wyglądać. Zaznaczam przy tym że sam tego jeszcze nie doświadczyłem, a koncepcje dalej przedstawione są raczej propozycjami niż wskazówkami.
Pierwszym narzucającym się rozwiązaniem jest konwencja nazewnicza, najlepiej przedrostkowa. Aby uniknąć zbyt długiego komentarza po prostu wstawię fragment kodu tworzonego w ten sposób.
@using(Html.BootstrapGridRow()) { @using(Html.BootstrapGridCol(4)) { } @using(Html.BootstrapGridCol(8)) { } }
Kod powyższych Helperów nie różni się niczym od kodu Helperów jakie tworzy każdy kto chociaż chwilę pracował w MVC.
Jest to rozwiązanie które znacząco ułatwia szukanie interesującego Helpera, zwłaszcza po usprawnieniach w Intellisense obecnych w Visual Studio od wersji 2010 i zwłaszcza przez osobę która kilka tygodni spędziła już w zespole. Rozwiązanie to nie chroni jednak przed literówkami lub łamaniem konwencji więc zacząłem się zastanawiać czy nie da się tego zrobić lepiej.
Drugim pomysłem jest stworzenie helperów które zamiast IDisposable i HtmlString zwracają klasę która zawiera takie metody. Użycie takiego API wyglądałoby następująco:
@using(Html.Bootstrap().GridRow()) { @using(Html.Bootstrap().GridCol(4)) { } @using(Html.Bootstrap().GridCol(8)) { } }
W tym podejściu na start w Intellisense widzimy tylko Helpery tworzące Grupy zamiast widzieć wszystkie metody. Wadą tego rozwiązania jest trochę mniej przejrzysty kod samych Helperów.
public static class BootstrapHelper { public static GroupingClass Bootstrap(this HtmlHelper self) { return new GroupingClass(self); } public class GroupingClass { private HtmlHelper helper; public GroupingClass(HtmlHelper helper) { this.helper = helper; } public IDisposable GridRow() { // TODO: zwrócić co trzeba } public IDisposable GridCol(int width) { // TODO: Zrobić co trzeba } } }
Oczywiście nazwa klasy GroupingClass została wymyślona na szybko ze względu na brak lepszego pomysłu
Na koniec stwierdziłem że ten Html na początku i te nawiasy są niepotrzebnymi ozdobnikami i najfajniej by było jakbym mógł napisać po prostu:
@using(Bootstrap.GridRow()) { @using(Bootstrap.GridCol(4)) { } @using(Bootstrap.GridCol(8)) { } }
Okazuje się że jest to możliwe wymaga jednak trochę więcej gimnastyki. I wiedzy jak działa Razor. Okazuje się że działa on bardzo podobnie do silnika wykorzystywanego w WebForms. Każdy widok jest kompilowany podczas działania aplikacji do klasy dziedziczącej po System.Web.Mvc.WebViewPage – dla nietypowanych widoków, albo System.Web.Mvc.WebViewPage<TView> dla widoków typowanych. Właściwości Html, Ajax itp. pochodzą z tych właśnie klas. Klasą bazową dla kompilowanych w locie klas może być jednak inna klasa, którą można ustawić w sekcji configuration – system.web.webPages.razor – page w atrybucie pageBaseType w Web.configu, jednak nie tym Web.configu w głównym folderze aplikacji, a tym w katalogu Views.
Klasa ta powinna występować w dwóch formach (generycznej i nie) i dziedziczyć po WebViewPage. Klasa powinna być abstrakcyjna (implementację metody Execute zostawiamy widokowi).
public abstract class CustomWebViewPage : WebViewPage { protected CustomWebViewPage() { Bootstrap = new BootstrapHelper(ViewContext); } public BootstrapHelper Bootstrap { get; private set; } } public abstract class CustomWebViewPage : WebViewPage { protected CustomWebViewPage() { Bootstrap = new BootstrapHelper(ViewContext); } public BootstrapHelper Bootstrap { get; private set; } } public class BootstrapHelper { private ViewContext viewContext; public BootstrapHelper(ViewContext viewContext) { this.viewContext = viewContext; } public IDisposable GridRow() { // TODO: zwrócić co trzeba } public IDisposable GridCol(int width) { // TODO: Zrobić co trzeba } }