Gdy HtmlHelper to za mało

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
	} 
}