Motto: foreach je fajn na prototypování, ale většinou mu za
chvíli dojde dech a svádí k zanášení příliš logiky do kódu šablony.
A to je špatně.
Už jsem se zmiňoval o tom, že v ASP.NET MVC lze užívat serverové ovládací prvky, takže je využijeme k zapouzdření složitější zobrazovací logiky.
Motivace
Jednou z hlavních nevýhod, kterou jsem viděl na MVC, byl přístup ke skládání šablon. Jednotlivé kusy šablony jsou rozházeny v MasterPage, ViewPage (aspx) a ViewUserControlech (ascx). V Atlasu jsme razili teorii, že šablona by měla být pokud možno co nejcelistvější, aby se nemuselo nikde nic hledat a kodér rychle udělal potřebné změny. Proto existovala sada serverových ovládacích prvků, které byly plně šablonovatelné.
Když naběhlo CH s Ellou, nechápali jsme, jak to může někdo používat. Každičká část šablony byla (v té době, jak je to dnes – nevím) byla rozeseta v hierarchii složek (podle dědičnosti). A to je v podstatě hlavní rozdíl mezi ASP.NET a architekturou MVC. Tedy pokud jste omezeni view enginem, který používáte.
Již nějaký pátek pracuji na web 2.0 aplikaci, která je postavená mj.
na ASP.NET MVC. Měl jsem tedy možnost vyzkoušet hodně možných postupů: od
logiky v šabloně, přes HTML helpery, RenderPartial a
RenderAction až po vlastní serverové ovládací prvky. A ty
nakonec vítězí na plné čáře! Pojďme se podívat, jak si napsat
elegantní serverové ovládací prvky pro ASP.NET MVC.
Evoluce logiky v šabloně
Začínal jsem s nadšením s jednoduchou logikou v kódu, tak jak to vidíte v ukázkách či na prezentacích o ASP.NET MVC. Složitější věci jsem se snažil přesunout do HtmlHelperu pomocí vlastních extenzí. Když jsem pak narazil na helpery, které zanáší do view „lambda hell“, trochu mě zamrazilo. Vyberu jen dva příklady: Philův Code based Repeater for ASP.NET MVC a Jardův Simple MVC controls. Nebojte, podobných programátorských krás najdete povícero. Bohužel, je to nepoužitelné pro kodéra. Navíc jsem zastáncem myšlenky minima kódu v šabloně.
Další věcí, která mě tak trochu děsí, je jakým způsobem se předvádí generování HTML formuláře.
<% using(Html.BeginForm("Send", "Comments")) { %>
<!-- prvky formuláře -->
<% } %>
Je to krásná ukázka užití vzoru
IDisposable, ale do šablony nepatří. Helpery uživát jen
jako dobré koření – po špetkách.
<form action='<%= Url.Action("Send", "Comments")) %>' method="post">
<!-- prvky formuláře -->
</form>
Myslím, že takovýto zápis je mnohem srozumitelnější a přitom dělá
to samé. Jako bonus můžete ve vašem HTML editoru využívat scope
collapsing. Vhodnější by ještě bylo použít helper
Url.RouteUrl, který hledá routy podle klíče a tudíž je
o dost výkonnější (pokud máte definováno více rout).
Opusťme teď formulář a pokročme k vypisování dat.
Prvním způsobem, jak vypisovat data, je foreach. Je silně
typový, což považuju za obrovskou výhodu, a nepřináší overhead
v podobě instanciování tříd a parsování šablon serverových
ovládacích prvků.
<ul>
<% foreach (var user in Model.Users) { %>
<li><%= user.Name %></li>
<% } %>
</ul>
Tím však jeho možnosti končí. Pokud potřebujeme např. odlišit každou
druhou položku, nebo vypsat něco jiného, pokud nejsou žádná data, musíme
kód znepřehledňovat, nebo zvolit jiné řešení. Philův repeater už jsem
zmiňoval. Další možností je využít asp:Repeater nebo
mvc:Repeater. Ani jeden mi nevyhovuje. První se musí nějak
nalít daty a pak s nimi svázat (nutnost codebehind nebo script runat=server),
druhý zase pracuje s ViewData slovníkem a evalováním. Takže nezbývá než
si napsat vlastní.
Silně typový repeater v ASP.NET MVC
Základem je jednoduchá myšlenka. Použít MvcControl
z futures a view model opatřit kontrakty.
public interface IHaveUsers {
IEnumerable<User> Users { get; }
}
public class UsersListViewData : IHaveUsers {
public IEnumerable<User> Users { get; set; }
// další vlastnosti view modelu
}
Zavedli jsme si view model třídu, která se nejspíš bude posílat na
pohled Index řadičem UsersController.
Možná. Každopádně jsme si zavedli jednoduchou abstrakci a možnost
znovupoužití v podobě rozhranní IHaveUsers. Snad
můžeme dál.
using Microsoft.Web.Mvc;
using Rarous.Web.UI;
[ParseChildren(true)]
public partial class UsersRepeater : MvcControl, ILayoutTemplateable {
[DefaultValue(typeof(ITemplate), "")]
[PersistenceMode(PersistenceMode.InnerProperty)]
[TemplateContainer(typeof(IGenericContainer<User>))]
[TemplateInstance(TemplateInstance.Multiple)]
public ITemplate ItemTemplate { get; set; }
[DefaultValue(typeof(ITemplate), "")]
[PersistenceMode(PersistenceMode.InnerProperty)]
[TemplateContainer(typeof(IGenericContainer<User>))]
[TemplateInstance(TemplateInstance.Multiple)]
public ITemplate AlternatingItemTemplate { get; set; }
[DefaultValue(typeof(ITemplate), "")]
[PersistenceMode(PersistenceMode.InnerProperty)]
[TemplateContainer(typeof(INamingContainer))]
[TemplateInstance(TemplateInstance.Multiple)]
public ITemplate SeparatorTemplate { get; set; }
public string LayoutContainerId { get; set; }
[DefaultValue(typeof(ITemplate), "")]
[PersistenceMode(PersistenceMode.InnerProperty)]
[TemplateContainer(typeof(INamingContainer))]
[TemplateInstance(TemplateInstance.Single)]
public ITemplate LayoutTemplate { get; set; }
}
Podědili jsme si MvcControl, který mimo jiné zpřístupňuje
ViewData, a implementovali nějaké šablony.
public partial class UsersRepeater {
private class UsersViewDataFetcher {
public IEnumerable<User> GetUsers(object model) {
var result = model as IHaveUsers;
if (result != null) {
return result.Users;
}
return null;
}
}
}
Jednoduchý helper pro získávání dat z modelu. Zkouší využít
kontraktu IHaveUsers, který jsme si zavedli výše, k získání
dat z modelu. Zde je místo pro budoucí rozšíření o další možné
zdroje. Pokud nic nenajdeme, vrátíme null.
public partial class UsersRepeater {
protected override void OnPreRender(EventArgs e) {
base.OnPreRender(e);
var fetcher = new UsersViewDataFetcher(ViewData.Model);
var users = fetcher.GetUsers();
if (users == null) {
return; // nebo verenderovat NoDataTemplete
}
Controls.Clear();
Control layoutContainer = TemplatingHelper.CreateLayoutContainer(this, this) ?? this;
var iterator = new ItemsIterator<User>(users);
foreach (var user in iterator.Iterate()) {
if (iterator.IsFirst == false) {
TemplatingHelper.Instantiate(new EmptyContainer(), SeparatorTemplate, layoutContainer);
}
ITemplate template = iterator.IsAlternate ? AlternatingItemTemplate ?? ItemTemplate : ItemTemplate;
TemplatingHelper.Instantiate(new GenericContainer<User>(user), template, layoutContainer);
}
layoutContainer.DataBind();
}
}
Nakonec přepíšeme metodu OnPreRender, ve které získaná
data proměníme pomocí šablon na výstupní kód. Používám zde spoustu
věcí, které jsem použil již dříve.
Jedinou novinkou je třída ItemsIterator, která zaobaluje logiku,
pro zjišťování, zda jde o první prvek, alternativní prvek a počítá
aktuální index prvku.
public class ItemsIterator<T> {
private readonly IEnumerable<T> _items;
public ItemsIterator(IEnumerable<T> items)
: this(items, 0) {
}
public ItemsIterator(IEnumerable<T> items, int firstIndex) {
_items = items;
IsFirst = true;
IsAlternate = false;
CurrentIndex = firstIndex;
}
public bool IsFirst { get; private set; }
public bool IsAlternate { get; private set; }
public int CurrentIndex { get; private set; }
public IEnumerable<T> Iterate() {
foreach (var item in _items) {
yield return item;
IsFirst = false;
IsAlternate = !IsAlternate;
CurrentIndex++;
}
}
}
Jednoduchá věcička, která je určená k eliminaci otrocky opakovaného kódu.
Užití takového repeateru je pak jednoduché:
<rarous:UsersRepeater runat="server" LayoutContainerId="UsersPlaceHolder">
<LayoutTemplate>
<ul>
<asp:PlaceHolder ID="UsersPlaceHolder" runat="server"/>
</ul>
</LayoutTemplate>
<ItemTemplate>
<li><%# Container.DataItem.Name %></li>
</ItemTmplate>
</rarous:UsersRepeater>
Závěr
Snažil jsem se sdělit svůj názor, že v šablonách by mělo být jen tolik programového kódu, kolik je nezbytně nutné. Zároveň se držet zásady jediné zodpovědnosti tříd a znuvupoužitelnosti s využitím generického pomocníka pro iteraci a kontraktů ve view modelu. Zároveň maximálně využít kód, který jsem už psal v předchozích spotech. Je možné, že jsem nepoužil dostatečně kvalitní názvy tříd nebo metod, připomínky klidně piště i k nim.











Komentáře
Augi
08.16 - 30. března 2009 | #
Petr
10.27 - 30. března 2009 | #
Aleš Roubíček
10.55 - 30. března 2009 | #
Adam
10.55 - 7. dubna 2009 | #
Aleš Roubíček
11.13 - 7. dubna 2009 | #
Adam
11.56 - 7. dubna 2009 | #
Aleš Roubíček
15.36 - 7. dubna 2009 | #
Adam
20.19 - 7. dubna 2009 | #
Some1
16.41 - 28. dubna 2009 | #
Aleš Roubíček
17.27 - 28. dubna 2009 | #
Místo pro tvůj názor