Nota: Este post ha sido importado de mi blog de geeks.ms. Es posible que algo no se vea del todo "correctamente". En cualquier caso puedes acceder a la versión original aquí
Muy buenas! En el post anterior comenté la característica de los templates de Razor y hoy vamos a ver como podríamos crear un helper que emule un poco el control Repeater que hay en webforms (salvando las distancias, claro).
Vamos a crear un helper externo, es decir que sea reutilizable en distintos proyectos: para ello nuestro helper va a residir en una clase (en mi ejemplo en el propio proyecto web, pero se podría situar en una librería de clases para ser reutilizable).
Esqueleto inicial
A nuestro helper se le pasará lo siguiente:
- Una cabecera: template Razor que se renderizará una sola vez al principio.
- Un cuerpo: template Razor que se renderizará una vez por cada elemento
- Un pie: template Razor que se renderizará una sola vez al final.
- Una colección de elementos (por cada elemento se renderizará el cuerpo).
Recordáis que ayer comentamos que los templates Razor son Func<T, HelperResult>?
Bien, pues vamos a declarar nuestro helper:
public static IHtmlString Repeat<TItem>(
IEnumerable<TItem> items,
Func<dynamic, HelperResult> header,
Func<TItem, HelperResult> body,
Func<dynamic, HelperResult> footer
)
{
var builder = new StringBuilder();
if (header != null)
{
builder.Append(header(null).ToHtmlString());
}
foreach (var item in items)
{
builder.Append(body(item).ToHtmlString());
}
if (footer != null)
{
builder.Append(footer(null).ToHtmlString());
}
return MvcHtmlString.Create(builder.ToString());
}
<p>
Los templates siempre están declarados como Func<T, HelperResult>, en este caso declaramos tres templates:
</p>
<ol>
<li>
header, cuyo parámetro @item es dynamic.
</li>
<li>
body, cuyo parámetro @item es TItem, donde TItem es el tipo del enumerable que se le pasa al helper
</li>
<li>
footer, cuyo parámetro @item es dynamic
</li>
</ol>
<p>
Para invocar los templates y obtener el html asociado simplemente invocamos el Func (pasándole el parámetro del tipo correcto) y sobre el resultado llamamos a <strong>ToHtmlString</strong>: Este método nos devuelve la cadena HTML que ha parseado Razor.
</p>
<p>
Ahora p.ej. me creo una acción tal en el controlador:
</p>
<div style="border-bottom: silver 1px solid; text-align: left; border-left: silver 1px solid; padding-bottom: 4px; line-height: 12pt; background-color: #f4f4f4; margin: 20px 0px 10px; padding-left: 4px; width: 97.5%; padding-right: 4px; font-family: 'Courier New', courier, monospace; direction: ltr; max-height: 200px; font-size: 8pt; overflow: auto; border-top: silver 1px solid; cursor: text; border-right: silver 1px solid; padding-top: 4px" id="codeSnippetWrapper">
<pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: #f4f4f4; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px" id="codeSnippet"><span style="color: #0000ff">public</span> ActionResult List()<br />{<br /> <span style="color: #0000ff">return</span> View(<span style="color: #0000ff">new</span> List<Product><br /> {<br /> <span style="color: #0000ff">new</span> Product() {Nombre=<span style="color: #006080">"PS3"</span>, Precio=300},<br /> <span style="color: #0000ff">new</span> Product() {Nombre=<span style="color: #006080">"XBox360"</span>, Precio=150},<br /> <span style="color: #0000ff">new</span> Product() {Nombre=<span style="color: #006080">"Wii"</span>, Precio=100}<br /> });<br />}</pre>
<p>
</div>
<p>
Y en la vista asociada:
</p>
<div style="border-bottom: silver 1px solid; text-align: left; border-left: silver 1px solid; padding-bottom: 4px; line-height: 12pt; background-color: #f4f4f4; margin: 20px 0px 10px; padding-left: 4px; width: 97.5%; padding-right: 4px; font-family: 'Courier New', courier, monospace; direction: ltr; max-height: 200px; font-size: 8pt; overflow: auto; border-top: silver 1px solid; cursor: text; border-right: silver 1px solid; padding-top: 4px" id="codeSnippetWrapper">
<pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: #f4f4f4; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px" id="codeSnippet">@using MvcApplication14.Helpers<br />@model IEnumerable<span style="color: #0000ff"><</span><span style="color: #800000">MvcApplication14.Models.Product</span><span style="color: #0000ff">></span><br /><br /><span style="color: #0000ff"><</span><span style="color: #800000">table</span><span style="color: #0000ff">></span><br />@Repeater.Repeat(Model, <br /> @<span style="color: #0000ff"><</span><span style="color: #800000">thead</span><span style="color: #0000ff">><</span><span style="color: #800000">tr</span><span style="color: #0000ff">><</span><span style="color: #800000">td</span><span style="color: #0000ff">></span>Nombre<span style="color: #0000ff"></</span><span style="color: #800000">td</span><span style="color: #0000ff">><</span><span style="color: #800000">td</span><span style="color: #0000ff">></span>Precio<span style="color: #0000ff"></</span><span style="color: #800000">td</span><span style="color: #0000ff">></</span><span style="color: #800000">tr</span><span style="color: #0000ff">></</span><span style="color: #800000">thead</span><span style="color: #0000ff">></span>, <br /> @<span style="color: #0000ff"><</span><span style="color: #800000">tr</span><span style="color: #0000ff">><</span><span style="color: #800000">td</span><span style="color: #0000ff">></span>@item.Nombre<span style="color: #0000ff"></</span><span style="color: #800000">td</span><span style="color: #0000ff">><</span><span style="color: #800000">td</span><span style="color: #0000ff">></span>@item.Precio<span style="color: #0000ff"></</span><span style="color: #800000">td</span><span style="color: #0000ff">></</span><span style="color: #800000">tr</span><span style="color: #0000ff">></span>, <br /> null)<br /><span style="color: #0000ff"></</span><span style="color: #800000">table</span><span style="color: #0000ff">></span></pre>
<p>
</div>
<p>
Fijaos en la llamada a Repeater.Repeat, donde le paso el modelo que recibe la vista (que es un IEnumerable de Product) y los tres templates Razor (en este caso no le paso footer y por ello pongo null).
</p>
<p>
En el segundo template (body) puedo acceder al elemento que se está renderizando mediante @item. Además, dado que en helper he declarado el template body de tipo Func<TItem, HelperResult>, tengo soporte de Intellisense:
</p>
<p>
<a href="http://geeks.ms/cfs-file.ashx/__key/CommunityServer.Blogs.Components.WeblogFiles/etomas/image_5F00_3B6BD963.png"><img style="background-image: none; border-bottom: 0px; border-left: 0px; margin: ; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="image" border="0" alt="image" src="http://geeks.ms/cfs-file.ashx/__key/CommunityServer.Blogs.Components.WeblogFiles/etomas/image_5F00_thumb_5F00_7984F414.png" width="244" height="107" /></a>
</p>
<p>
Si teclease @item en el template header o footer no tendría intellisense porque los he declarado dynamic en el helper (además ojo, que en el helper le paso null, por lo que rebentaría si usamos @item en el template header o footer).
</p>
<p>
Y listos! El código HTML generado es:
</p>
<div style="border-bottom: silver 1px solid; text-align: left; border-left: silver 1px solid; padding-bottom: 4px; line-height: 12pt; background-color: #f4f4f4; margin: 20px 0px 10px; padding-left: 4px; width: 97.5%; padding-right: 4px; font-family: 'Courier New', courier, monospace; direction: ltr; max-height: 200px; font-size: 8pt; overflow: auto; border-top: silver 1px solid; cursor: text; border-right: silver 1px solid; padding-top: 4px" id="codeSnippetWrapper">
<pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: #f4f4f4; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px" id="codeSnippet"><span style="color: #0000ff"><</span><span style="color: #800000">thead</span><span style="color: #0000ff">><</span><span style="color: #800000">tr</span><span style="color: #0000ff">><</span><span style="color: #800000">td</span><span style="color: #0000ff">></span>Nombre<span style="color: #0000ff"></</span><span style="color: #800000">td</span><span style="color: #0000ff">><</span><span style="color: #800000">td</span><span style="color: #0000ff">></span>Precio<span style="color: #0000ff"></</span><span style="color: #800000">td</span><span style="color: #0000ff">></</span><span style="color: #800000">tr</span><span style="color: #0000ff">></</span><span style="color: #800000">thead</span><span style="color: #0000ff">></span><br /><span style="color: #0000ff"><</span><span style="color: #800000">tr</span><span style="color: #0000ff">><</span><span style="color: #800000">td</span><span style="color: #0000ff">></span>PS3<span style="color: #0000ff"></</span><span style="color: #800000">td</span><span style="color: #0000ff">><</span><span style="color: #800000">td</span><span style="color: #0000ff">></span>300<span style="color: #0000ff"></</span><span style="color: #800000">td</span><span style="color: #0000ff">></</span><span style="color: #800000">tr</span><span style="color: #0000ff">></span><br /><span style="color: #0000ff"><</span><span style="color: #800000">tr</span><span style="color: #0000ff">><</span><span style="color: #800000">td</span><span style="color: #0000ff">></span>XBox360<span style="color: #0000ff"></</span><span style="color: #800000">td</span><span style="color: #0000ff">><</span><span style="color: #800000">td</span><span style="color: #0000ff">></span>150<span style="color: #0000ff"></</span><span style="color: #800000">td</span><span style="color: #0000ff">></</span><span style="color: #800000">tr</span><span style="color: #0000ff">></span><br /><span style="color: #0000ff"><</span><span style="color: #800000">tr</span><span style="color: #0000ff">><</span><span style="color: #800000">td</span><span style="color: #0000ff">></span>Wii<span style="color: #0000ff"></</span><span style="color: #800000">td</span><span style="color: #0000ff">><</span><span style="color: #800000">td</span><span style="color: #0000ff">></span>100<span style="color: #0000ff"></</span><span style="color: #800000">td</span><span style="color: #0000ff">></</span><span style="color: #800000">tr</span><span style="color: #0000ff">></span></pre>
<p>
</div>
<p>
Guay, no?
</p>
<p>
Podríamos “complicar” un poco el helper, para que desde los templates supiesemos si estamos en una fila par o impar y así aplicar clases… Para ello nos basta con declarar una clase adicional en nuestor helper:
</p>
<div style="border-bottom: silver 1px solid; text-align: left; border-left: silver 1px solid; padding-bottom: 4px; line-height: 12pt; background-color: #f4f4f4; margin: 20px 0px 10px; padding-left: 4px; width: 97.5%; padding-right: 4px; font-family: 'Courier New', courier, monospace; direction: ltr; max-height: 200px; font-size: 8pt; overflow: auto; border-top: silver 1px solid; cursor: text; border-right: silver 1px solid; padding-top: 4px" id="codeSnippetWrapper">
<pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: #f4f4f4; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px" id="codeSnippet"><span style="color: #0000ff">public</span> <span style="color: #0000ff">class</span> RepeaterItem<TItem><br />{<br /> <span style="color: #0000ff">public</span> <span style="color: #0000ff">int</span> Index { get; <span style="color: #0000ff">private</span> set; }<br /> <span style="color: #0000ff">public</span> <span style="color: #0000ff">bool</span> IsEven { get { <span style="color: #0000ff">return</span> Index % 2 == 0; } }<br /> <span style="color: #0000ff">public</span> TItem Item { get; <span style="color: #0000ff">private</span> set; }<br /><br /> <span style="color: #0000ff">public</span> RepeaterItem(TItem item, <span style="color: #0000ff">int</span> index)<br /> {<br /> Index = index;<br /> Item = item;<br /> }<br />}</pre>
<p>
</div>
<p>
Esa clase simplemente contiene el índice del elemento actual, el propio elemento y una propiedad que indica si es par o no.
</p>
<p>
Ahora modificamos el helper para pasarle al template body un objeto RepeaterItem<TItem>:
</p>
<div style="border-bottom: silver 1px solid; text-align: left; border-left: silver 1px solid; padding-bottom: 4px; line-height: 12pt; background-color: #f4f4f4; margin: 20px 0px 10px; padding-left: 4px; width: 97.5%; padding-right: 4px; font-family: 'Courier New', courier, monospace; direction: ltr; max-height: 200px; font-size: 8pt; overflow: auto; border-top: silver 1px solid; cursor: text; border-right: silver 1px solid; padding-top: 4px" id="codeSnippetWrapper">
<pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: #f4f4f4; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px" id="codeSnippet"><span style="color: #0000ff">public</span> <span style="color: #0000ff">static</span> <span style="color: #0000ff">class</span> Repeater<br />{<br /> <span style="color: #0000ff">public</span> <span style="color: #0000ff">static</span> IHtmlString Repeat<TItem>(<br /> IEnumerable<TItem> items,<br /> Func<dynamic, HelperResult> header,<br /> Func<RepeaterItem<TItem>, HelperResult> body,<br /> Func<dynamic, HelperResult> footer<br /> )<br /> {<br /> var builder = <span style="color: #0000ff">new</span> StringBuilder();<br /> <span style="color: #0000ff">if</span> (header != <span style="color: #0000ff">null</span>)<br /> {<br /> builder.Append(header(<span style="color: #0000ff">null</span>).ToHtmlString());<br /> }<br /><br /> var count = 0;<br /> <span style="color: #0000ff">foreach</span> (var item <span style="color: #0000ff">in</span> items)<br /> {<br /> var repeaterItem = <span style="color: #0000ff">new</span> RepeaterItem<TItem>(item, count++);<br /> builder.Append(body(repeaterItem).ToHtmlString());<br /> }<br /> <span style="color: #0000ff">if</span> (footer != <span style="color: #0000ff">null</span>)<br /> {<br /> builder.Append(footer(<span style="color: #0000ff">null</span>).ToHtmlString());<br /> }<br /><br /> <span style="color: #0000ff">return</span> MvcHtmlString.Create(builder.ToString());<br /> }<br />}</pre>
<p>
</div>
<p>
Los cambios básicamente son declarar el template domo Func<RepeaterItem<Titem>> y cuando invocamos el template, crear antes un objeto RepeaterItem y pasárselo como parámetro.
</p>
<p>
Finalmente ahora debemos modificar la vista, ya que el parámetro @item de nuestro template es ahora un RepeaterItem<TItem>:
</p>
<div style="border-bottom: silver 1px solid; text-align: left; border-left: silver 1px solid; padding-bottom: 4px; line-height: 12pt; background-color: #f4f4f4; margin: 20px 0px 10px; padding-left: 4px; width: 97.5%; padding-right: 4px; font-family: 'Courier New', courier, monospace; direction: ltr; max-height: 200px; font-size: 8pt; overflow: auto; border-top: silver 1px solid; cursor: text; border-right: silver 1px solid; padding-top: 4px" id="codeSnippetWrapper">
<pre style="border-bottom-style: none; text-align: left; padding-bottom: 0px; line-height: 12pt; background-color: #f4f4f4; margin: 0em; border-left-style: none; padding-left: 0px; width: 100%; padding-right: 0px; font-family: 'Courier New', courier, monospace; direction: ltr; border-top-style: none; color: black; border-right-style: none; font-size: 8pt; overflow: visible; padding-top: 0px" id="codeSnippet">@Repeater.Repeat(Model, <br /> @<span style="color: #0000ff"><</span><span style="color: #800000">thead</span><span style="color: #0000ff">><</span><span style="color: #800000">tr</span><span style="color: #0000ff">><</span><span style="color: #800000">td</span><span style="color: #0000ff">></span>Nombre<span style="color: #0000ff"></</span><span style="color: #800000">td</span><span style="color: #0000ff">><</span><span style="color: #800000">td</span><span style="color: #0000ff">></span>Precio<span style="color: #0000ff"></</span><span style="color: #800000">td</span><span style="color: #0000ff">></</span><span style="color: #800000">tr</span><span style="color: #0000ff">></</span><span style="color: #800000">thead</span><span style="color: #0000ff">></span>, <br /> @<span style="color: #0000ff"><</span><span style="color: #800000">tr</span> <span style="color: #ff0000">style</span><span style="color: #0000ff">="@(item.IsEven ? "</span><span style="color: #ff0000">background-color:</span> <span style="color: #ff0000">white</span><span style="color: #0000ff">" : "</span><span style="color: #ff0000">background-color:pink</span><span style="color: #0000ff">")"</span><span style="color: #0000ff">></span><br /> <span style="color: #0000ff"><</span><span style="color: #800000">td</span><span style="color: #0000ff">></span>@item.Item.Nombre<span style="color: #0000ff"></</span><span style="color: #800000">td</span><span style="color: #0000ff">><</span><span style="color: #800000">td</span><span style="color: #0000ff">></span>@item.Item.Precio<span style="color: #0000ff"></</span><span style="color: #800000">td</span><span style="color: #0000ff">></</span><span style="color: #800000">tr</span><span style="color: #0000ff">></span>, <br /> null)</pre>
<p>
</div>
<p>
Fijaos como dentro del template body puedo preguntar si el elemento actual es par (@item.IsEven y acceder al propio elemento @item.Item). En este caso uso @item.IsEven para cambiar el color de fondo de la fila:
</p>
<p>
<a href="http://geeks.ms/cfs-file.ashx/__key/CommunityServer.Blogs.Components.WeblogFiles/etomas/image_5F00_634ED8C2.png"><img style="background-image: none; border-bottom: 0px; border-left: 0px; margin: ; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="image" border="0" alt="image" src="http://geeks.ms/cfs-file.ashx/__key/CommunityServer.Blogs.Components.WeblogFiles/etomas/image_5F00_thumb_5F00_5099DF0B.png" width="145" height="133" /></a>
</p>
<p>
Y listos! Espero que esos dos posts sobre templates Razor os hayan parecido interesantes!
</p>
<p>
Un saludo a todos! 😉
</p>