Už několikrát jsem dostal otázku, jak udělat v ASP.NET MVC „widgety“, tedy „komponenty“, které jsou někde na okraji každé stránky. Slyšel jsem názory, že ASP.NET WebForms jsou mnohem více nakloněny komponentovému vývoji (souhlasím) a tak je vývoj takových komponentových udělátek ve WebForms mnohem jednodušší než v MVC – s tím si ale dovolím nesouhlasit. Aby byl člověk schopen napsat kvalitní komponentu ve WebForms, tak je nutné tento framework znát velmi zevrubně. A v ASP.NET MVC je to úplně stejné – pokud znáte MVC dobře, tak dokážete i v MVC pohodlně napsat widgetový systém. Mé řešení vám ukáži v tomto článku.

Data pro widgety

Moje filozofie je taková, že ve ViewModelu (který se vrací v akční metodě pomocí return View(viewModel);) musí být všechna data potřebná pro rendering celé stránky – tedy „hlavní obsah“ i data widgetů.

Na úrovni Controlleru tak musíme zajistit, že načteme data pro všechny widgety, které budeme zobrazovat. Ano, už v Controlleru musíme vědět, co přesně budeme chtít vykreslovat – tak to má být – rozhodně bychom neměli takové rozhodnutí nechávat až do View!

Widgety ale nejsou „tím hlavním“ co chceme v akční metodě dělat – to je něco jiného – např. v případě blogového systému načtení informací o článku (text, autor, …). Přidávat ručně kód pro načítaní widgetů do každé akční metody samozřejmě nebudeme. Takový úkol lze elegantně řešit pomocí aspektů, které jsou v ASP.NET MVC dostupné formou Action Filterů. Implementujeme tedy v Action Filteru metodu OnActionExecuted, která se volá po provedení akční metody.

Jak ale zařídit, abychom mohli v této metodě uložit data pro widgety do ViewModelu? Jednoduše si vytvoříme rozhraní IViewModelWithWidgets, které musí implementovat každý ViewModel, jenž odpovídá stránce, na níž chceme vykreslovat widgety. Zde záleží, jestli máme widgety pevné nebo např. uživatelsky konfigurované.
V prvním případě můžeme mít dílčí ViewModely pro widgety přímo jako properties rozhraní IViewModelWithWidgets.
V druhém případě (který budu uvažovat dále) bude lepší mít jen jednu property typu IEnumerable<IWidget> nesoucí data pro všechny widgety. Rozhraní IWidget by mělo obsahovat minimálně property string PartialViewName – můžeme ale přidat třeba informace o pořadí widgetů apod.

Kostra Action Filteru může vypadat takto:

public void OnActionExecuted(ActionExecutedContext filterContext)
{
  if (!(filterContext.Result is ViewResultBase))
  {
     return; // we will not render a View
  }
  var vm = ((ViewResultBase)filterContext.Result).Model as IViewModelWithWidgets;
  if (vm == null)
  {
    return; // we don't want to render widgets
  }
  vm.Widgets = InjectedWidgetProvider.GetWidgetsForCurrentUser();
}

Atributem můžeme odekorovat jednotlivé metody, celé Controllery, příp. zaregistrovat Action Filter jako globální nebo ho servírovat pomocí vlastního Filter Provideru (pak ho nezapomeňte zaregistrovat). Poslední možnost doporučuji zejména tehdy, když potřebujete řídit životnost Action Filteru nebo do něj injectovat nějaké závislosti (např. připojení do databáze na přečtení nakonfigurovaných widgetů pro přihlášeného uživatele).

Vykreslení

Při použití ASPX enginu (ne-Razor) vyřešíme vykreslení widgetů elegantně pomocí Master Pages (když si ve Visual Studiu vytvoříte nový ASP.NET MVC 3 Web Application, tak ten Master Pages používá).
To znamená, že máme soubor Site.Master, který obsahuje „šablonu“, jak má vypadat celá stránka. V této „šabloně“ máme díry, jejichž obsah se mění – takže nějaký ContentPlaceHolder pro hlavní obsah, pro menu, pro nadpis stránky apod.
V konkrétním View (např. Detail.aspx) pak pomocí atributu MasterPageFile určíme, jaká Master Page se použije (často máme v projektu jen jednu – Site.Master) a naším úkolem je dodat obsah pro ContentPlaceHoldery.

Ale zpět k našim widgetům. Kód v Site.Master bude přímočarý – zjistíme, zda se mají widgety renderovat, a pokud ano, tak je na nějakém vhodném místě (třeba uvnitř nějakého divu) vykreslíme:

if (Model is IViewModelWithWidgets)
{
  foreach(IWidget widgetViewModel in ((IViewModelWithWidgets)Model).Widgets)
  {
     Html.RenderPartial(widgetViewModel.PartialViewName, widgetViewModel);
  }
}

Pro každý widget pak budeme mít Partial View (ascx), které bude mít Model odpovídajícího typu (implementující rozhraní IWidget).

Závěr

Co jsme tedy museli udělat:

  • Připravit si datový model, tj. zavést si rozhraní IWidget a IViewModelWithWidgets.
  • ViewModely pro widgety musí implementovat rozhraní IWidget.
  • ViewModely pro stránky, které obsahují widgety, musí implementovat rozhraní IViewModelWithWidgets.
  • Zajistit naplnění kolekce Widgets na úrovni Controlleru – k tomu nám skvěle poslouží Action Filter.
  • Vytvořit Partial View (ascx soubor) pro každý widget a zajistit jeho vykreslení v Site.Master.

Dle mého názoru tedy nic extrémně složitého – ViewModel a Partial View pro widgety jsou nutné minimum (proto to děláme) a infrastrukturního kódu není tolik – prakticky jen načtení dat v Action Filteru a vykreslení widgetů v Site.Master.

V praxi můžeme chtít toto řešení dále rozvinout – např. načítat widgety asynchronně pomocí AJAXu. Ani to ale není nic složitého…
[navazující článek]