Jak na widgety v ASP.NET MVC

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]

http://alexgorbatchev.com/pub/sh/current/scripts/shCore.js
http://alexgorbatchev.com/pub/sh/current/scripts/shBrushCSharp.js

if (SyntaxHighlighter) {
SyntaxHighlighter.all();
}

28 thoughts on “Jak na widgety v ASP.NET MVC

  1. Model2 bol vymysleny na velmi jednuchu web interakciu – view = screen. Znasilnovat ho takymto sposobom sa mi zda zbytocne.

    Este mi nie je jasne, ako je vyriesene to, ked widget potrebuje nejake data. (napr. poslednych 5 komentarov)

    „…musí být všechna data potřebná pro rendering celé stránky – tedy „hlavní obsah“ i data widgetů….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!“

    Ak som to spravne pochopil, tak mame uzko previazane widgety s celou aplikaciou. Pouzitie aspektov je invecne riesenie, ale ako riesi tento problem?

    Nechavat queryovanie na widgety? To vobec nie je zla myslienka, je to ozivenie velmi starej myslienky. Pozri si povodny MVC pattern(sipka od M ku V). Naco robit pri query dotazoch toto vsetko –
    db -> orm, orm -> dto, dto -> viewModel, viewModel -> display? Ja chcem vybrat data z DB a zobrazit. Uz si sa na tym niekedy zamyslel?
    Lenze ano, ani na toto model2 frameworky nie su stavane.

    „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“

    Ved prave. Staci si uvedomit, ze potrebujes nieco ako master controller, ktory bude riesit master view a podobnu zakladnu UI infrastrukturu. A dnu mas potom vsetky tie „widgety“ a vzdy nejaky view s core aplikacnou aktivitou. Obe skupiny viewov mozu mat svoj vlastny controller(kludne embednty, zase to nie je nejaky novy objav)

    To se mi líbí

  2. Přesně tak, komponenty v ASP.NET MVC nejsou žádná věda:-)

    Proč na ukládání widgetů nepoužít rovnou ViewData? Rozhraní IViewModelWithWidgets mi moc nepomáhá, stejně musím testovat jestli ho model implementuje.

    Proč ne RenderAction? Je to daleko jednodušší a pokud není potřeba nějak agregovat dotazy ze všech widgetů (dělat coarse-grained dotazy na aplikační vrstvu), pak by to mělo stačit.

    To se mi líbí

  3. T:
    Model2 bol vymysleny na velmi jednuchu web interakciu – view = screen. Znasilnovat ho takymto sposobom sa mi zda zbytocne.
    Ale vždyť já nedělám nic jiného, než že view == screen a jeho datový model je ViewModel.

    Este mi nie je jasne, ako je vyriesene to, ked widget potrebuje nejake data. (napr. poslednych 5 komentarov)
    Widgety jsem zamýšlel jako komponenty nezávislé na aktuálním obsahu. Ale Action Filter může WidgetProvideru zpřístupnit i „velký“ ViewModel (i v ukázce už mám na něj přímo referenci).

    Ak som to spravne pochopil, tak mame uzko previazane widgety s celou aplikaciou. Pouzitie aspektov je invecne riesenie, ale ako riesi tento problem?
    S celou aplikací? Kde je ta vazba? Widgety jsou integrální součástí prezentační vrstvy, ale aplikace (která je schovaná třeba za webovou službou) se to nijak přímo nedotýká.

    Nechavat queryovanie na widgety? To vobec nie je zla myslienka, je to ozivenie velmi starej myslienky. Pozri si povodny MVC pattern(sipka od M ku V).
    Ale já nejedu podle prapůvodního MVC patternu, já jedu podle Passive View (jak jsem se snažil naznačit v článku).

    Naco robit pri query dotazoch toto vsetko –
    db -> orm, orm -> dto, dto -> viewModel, viewModel -> display? Ja chcem vybrat data z DB a zobrazit. Uz si sa na tym niekedy zamyslel?

    Ou-maj-gad! Už více než rok se intenzivně zajímám o CQ(R)S, takže nejen že jsem se nad tím zamyslel, ale v praxi to používám…
    Nicméně to je něco, co je zcela mimo záběr tohoto článku, takže nechápu, proč bych tím měl zbytečně článek zesložiťovat.

    To se mi líbí

  4. dkl:
    Proč na ukládání widgetů nepoužít rovnou ViewData? Rozhraní IViewModelWithWidgets mi moc nepomáhá, stejně musím testovat jestli ho model implementuje.
    Původně jsem měl v článku obě varianty, ale pro lepší srozumitelnost jsem variantu s ViewData odmazal.

    Proč ne RenderAction? Je to daleko jednodušší a pokud není potřeba nějak agregovat dotazy ze všech widgetů (dělat coarse-grained dotazy na aplikační vrstvu), pak by to mělo stačit.
    Ano, s RenderAction by to mohlo být jednodušší, ale IMHO ne o tolik. Hlavně mám ale k RenderAction tak trošku odpor, protože mi přijde zvrhlé vracet se „zpět“ z View do Controlleru 🙂

    To se mi líbí

  5. Súhlasim s ostatnými, že je lepšie použiť RenderAction, hlavne ak majú widgety potom nejak ajax komunikovať, tak je Model zaviazaný na RenderAction oveľa lepšie použiteľný. Tiež som najskôr bol jeho odporca, ale potom som si uvedomil, že to vlastne nie je nič ine ako iframe/ajax rendering na strane servera a teda to šetrí čas.

    To se mi líbí

  6. @augi:

    „Ale vždyť já nedělám nic jiného, než že view == screen a jeho datový model je ViewModel.“

    View je v tvojom pripade kompozitny, nie je to view s jednou zodpovednostou.

    „Widgety jsem zamýšlel jako komponenty nezávislé na aktuálním obsahu.“
    Ako sa do nich dostanu data?

    „S celou aplikací? Kde je ta vazba? Widgety jsou integrální součástí prezentační vrstvy, ale aplikace (která je schovaná třeba za webovou službou) se to nijak přímo nedotýká.“

    Nehovorim o vazbe medzi modelom a prezentaciou, ale o vazbe medzi jednotlivymi castiach prezentacie, viewoch, UI infrastrukture. Widgety mozu a mali by byt autonomne. Pozri sa na to z pohladu testovatelnosti.(hlavne v pripade scenara s manageovatelnymi widgetmi)

    „Ale já nejedu podle prapůvodního MVC patternu, já jedu podle Passive View (jak jsem se snažil naznačit v článku).“

    Je jasne ze Model2 je passive view – tvrdil si ze „rozhodně bychom neměli takové rozhodnutí nechávat až do View!“ a vtomto pripade by to bolo jedno z rieseni, ako by sa dali obist limity model2.

    „Ou-maj-gad! Už více než rok se intenzivně zajímám o CQ(R)S, takže nejen že jsem se nad tím zamyslel, ale v praxi to používám…“

    tak to uz je naozaj dlho. Ospravelnujem sa, ze si dovolil nieco v tomto smere naznacit/napisat, aj ked aplikacia by bola mozno lepsim riesienim ako prezentujes.

    „Nicméně to je něco, co je zcela mimo záběr tohoto článku, takže nechápu, proč bych tím měl zbytečně článek zesložiťovat.“

    Pretoze si zavrhol kategoricky to, aby view queryoval model. Bohuzial, ludia beru niektore veci ako dogmy(toto je jedna z nich) a potom miesto funkcneho designu robia design podla toho co sa prave nosi v komunite, co je cool. A viac ako polovica ludi sa nikdy nezmysli, ze to prehlasenie sa tykalo len nejakeho specifickeho kontextu.

    To se mi líbí

  7. Díky za článek, v oficiální dokumentaci jsem nějak o tomto tématu nemohl nic moc najít, takže se tvůj blog post vyloženě hodí.

    Asi nejkontroverznější věcí, soudě podle komentářů, je ViewModel pro celou stránku, ale třeba jak jsme se o tom bavili na MSFestu, tak s tímto patternem, pokud si vzpomínám, všichni MVC-znalí souhlasili. Nicméně, zeptám se, vychází to z nějakého oficiálního doporučení MS nebo jiného autoritativního zdroje? A co si myslíš o Html.Action, použil bys to někdy nebo je to podle tebe antipattern?

    To se mi líbí

  8. Díky Borku! 😉

    ViewModel pro celou stránku je něco, k čemu jsem si došel sám a nejen v tomhle kontextu, kdy se ignoruje AJAX, se mi jeví jako přirozený a snadno pochopitelný – proto jsem ho zvolil.

    Btw. předávat si data pro widgety přes ViewData (jak psal Dan) vnímám jako ekvivalent, protože Model a ViewData to samé jest (Model je property na ViewData). Takže i když data pro widgety nejsou přímo v objektu „hlavního“ ViewModelu ale ve ViewData (pod nějakým klíčem), tak jsou z mého hlediska pořád předávám data pro celou stránku v jednom objektu (ViewData).

    Jestli mít jedny data (ať už Model nebo ViewData) pro celou stránku záleží na tom, jakou architekturu v rámci prezentační vrstvy se člověk rozhodne používat. Neodvažuju se dávat univerzální radu, protože každý projekt má svá specifika. Rozhodoval bych se jinak v případě intranetové aplikace, v případě internetové (která třeba musí fungovat i bez JavaScriptu), v případě single-page aplikace atd.

    Použití Html.(Render)Action mi smrdí jen v případě Passive View (o kterém je celý článek), ale obecně proti němu nic nemám. Naopak si myslím, že vnáší skvělou možnost vytvářet modulární aplikace a komunita si o něj přímo řekla. Sám jsem tuhle funkčnost používal (samozřejmě ne tak dokonale) dříve než se oficiálně v ASP.NET MVC objevila.
    Jestli to porušuje nějakou teoretickou poučku je mi (jako inženýrovi ;-)) upřímně jedno – důležité pro mě je, že to zjednodušuje architekturu aplikace (~míň práce) a nemá negativní vliv na testovatelnost 😉

    To se mi líbí

  9. Jak jsem se snažil naznačit posledním odstavcem zakončeným třemi tečkami, mělo přijít pokračování článku. To mělo umět načítat widgety AJAXově i neAJAXově (když není k dispozici JavaScript), což by samozřejmě vedlo na použití Html.(Render)Action.

    Ale už se nedivím, že v CS skoro nikdo nebloguje – když člověk pojme článek trošku edukativně a nezahltí čtenáře všemi detaily, alternativami a historií přilehlého vesmíru, tak je pomalu označen za tupce, který nezná víc, než napsal do článku…

    To se mi líbí

  10. Takové komentáře jsou u nás běžné. Neber si to tak, nebo zakaž komentáře. Článek je určitě dobrej => víc takových.

    To se mi líbí

  11. Neřešte komentáře, vod toho tady sou, ne? Mně ten článek přijde dobrej, to co navrhuju jsou jen dílčí vylepšení.

    Dávat to do ViewData mi přijde lepší z toho důvodu, že když použiješ interface, musí ho všechny tvé ViewModely implementovat. Jak to udělat? Buď do všech ViewModelů musíš přidat property Widgets (takže se ti duplikuje), nebo musí všechny dědit z nějakého ViewModelWithWidgetsBase (což je zneužití dědičnosti ke kompozici).

    Dát to do ViewData navíc dobře pasuje k attributům, které můžeš přidávat a ubírat dle libosti. Kdybys použil IViewModelWithWidgets, musel bys při přidání attributu k metodě kontrolleru změnit ViewModel, který ta metoda vytváří.

    Jestli použít Html.Action nebo tohle dost závisí na konkrétním případě, podle mě se nedá říct kterej způsob je lepší. Jen mě zajímalo, jaký jsou tvá důvody použít tohle, protože se mi zdá Html.Action jednodušší.

    To se mi líbí

  12. Dane, tvoje komentáře beru, ty jsou na místě 😉

    Jak jsem psal, původně jsem měl v článku tvojí i mojí variantu, ale abych to nekomplikoval, tak jsem tu tvoji vyhodil.
    Ohledně duplikace – ano, mlčky jsem předpokládal, že by to skončilo nějakým bázovým ViewModelem.
    V praxi je předávání přes ViewData lepší, protože není tak invazivní a náročné na údržbu, uznávám.

    Pro použití Passive View (a nepoužití Html.RenderAction) jsem se rozhodl čistě z „edukativních“ důvodů. Přechod k akčním metodám, které se dají volat jak přes AJAX tak přes Html.RenderAction, jsem plánoval v pokračování…

    To se mi líbí

  13. Ja mam bazovy ViewModel, z ktereho dedi neco jako MasterViewModel a UserControlViewModel. Pred MVC3 sem pak mel vsechny data nacteny (cachoval sem na urovni dat) a muj jiz konkretni model zdedeny z master mel userModely jako parametry, ktere sem volal pres html.render().

    Po MVC3 sem ale widgety hlavne kvuli cachovani (pouzivam vlastni atribut, vubec nechapu, proc microsoft neni schopnej dodat naky poradny cachovani, hlavne ze delaj 1000 veci okolo, mobilni kravinky atd, ale zaklad furt nemaj) presunul do samostanych Action a ridim to primo ze sablony (nevidim v tom naky veklky logicky problem)

    RenderAction je necachovany a myslim, ze cachovat by se melo skoro vse.

    To se mi líbí

  14. @Borek IMHO bys měl mít nějaký dobrý důvod, proč použít widgety jak je tady Augi popisuje. Html.Action je prostě jednodušší, takže je to pro mě default.

    Dobrý důvod je třeba rozdělení na webovej a aplikační server (webovej server by pak měl requesty z widgetů agregovat do jednoho coarse-grained requestu (opak chatty interface), aby byl schopen všechny data dostat najednou; to ale musíš mít paranodní požadavky), nebo třeba dynamicky konfigurovatelnej portál, kde si může uživatel sám říct jaký widgety chce používat a kde spolu ty widgety nějak komunikujou.

    To se mi líbí

  15. Borku, vím, že jsi zkoušel Webmium. Když si vzpomeneš na ty jednotlivé pagelety, které tam máme. Tak každý má vlastní controller, který nejen renderuje data, ale i poskytuje další akce, které konzumuje editor. Vzhledem k tomu, že počet widgetů na stránce je variabilní, nebyla by cesta popisovaná Augim použitelná.

    Ve výsledku máme vlastní generátor view v Razoru, který do jednotlivých @section vkládá volání @Html.Action. Každej pagelet je autonomní, má svoje partial view, má svoji logiku získávání dat, má svůj view a input model. Jsou tam krásně oddělený zodpovědnosti a PgeController, kterej má na strarosti rendrování konkrétních stránek popravdě o žádných widgetech nic neví.

    Na Tropu jsem taky měl vševědoucí view modely implementující i deset rozhranní, ze kterých si data vysávaly server controly. Psal jsem o tom před lety u sebe. Bylo to daný tím, že v tý době ještě nebyla podpora pro RenderAction funkční, existovala implementace ve Futures, ale byla nepoužitelná. Musel jsem s tím pracovat a věř mi, že to bylo o poznání složitější.

    To se mi líbí

  16. Borek: Sorry, za pozdní odpověď… Je to přesně tak, jak píše Dan.
    Pokud webový server komunikuje přes dráty s aplikačním serverem, tak je efektivnější získat všechna data pro celou stránku na jeden request, resp. na co nejmenší počet requestů (jako se o tom mluví i v CQRS).

    Nicméně i to by se dalo učitě řešit nějakým inteligentním přednačtením všech dat pro widgety pomocí jednoho requestu, ale pak už by to nemuselo být tak jednoduché…

    To se mi líbí

  17. Aleši, díky za super příklad, přesně realizace něčeho takového mě zajímala. Co u vás dělá typický page controller?

    Augi a Dane, díky za odpovědi, mám o něco jasněji 🙂

    To se mi líbí

  18. @Aleš:
    a.d. CQRS a requesty – suhlas … mozno to augi nejako zovseobecnil na zaklade toho, ze ked mas dobry query model tak odpadaju zbytocne roundtripy voci db ako napr. na lazy loadovanie v pripade klasickej architektury a pouzitia ORM.

    To se mi líbí

Napsat komentář

Tento web používá Akismet na redukci spamu. Zjistěte více o tom, jak jsou data z komentářů zpracovávána.