¿Qué es ViewEngine en ASP.NET MVC?

Uno de los principales beneficios de crear una aplicación con ASP.NET MVC es que obtenemos mayor control en cuanto al código HTML se refiere. Aún así, es posible que al ver una vista generada para este patrón, resulte complicada la lectura para aquellos acostumbrados a trabajar con WebForms.

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<%
    if (Request.IsAuthenticated) {
%>
        Welcome <b><%= Html.Encode(Page.User.Identity.Name) %></b>!
        [ <%= Html.ActionLink("Log Off", "LogOff", "Account") %> ]
<%
    }
    else {
%>
        [ <%= Html.ActionLink("Log On", "LogOn", "Account") %> ]
<%
    }
%>

Si bien este es un pequeño ejemplo del user control del LogOn que viene con la plantilla de ASP.NET MVC, la cosa se puede llegar a complicar bastante y puede dificultar su lectura y comprensión.

¿Qué es View Engine?

ASP.NET MVC utiliza un motor de vistas (View Engine) para reemplazar los métodos de renderizado de las aplicaciones por código HTML. Generalmente se está usando el proporcionado por ASP.NET MVC pero es posible utilizar otras alternativas creadas por la comunidad. Para ver un ejemplo de uno de los más conocidos, vamos a modificar una aplicación para utilizar Spark View Engine.

Configurar Spark View Engine

Nota: Para este ejemplo he utilizado una plantilla de ASP.NET MVC 1.0 ya que la versión actual de Spark no soporta ASP.NET MVC 2 RC 2.

La configuración es bastante simple. Para ello, debemos bajar la última versión del proyecto de CodePlex y adjuntar las siguientes librerías a nuestro proyecto:

Una vez que tenemos las librerías agregadas, necesitamos añadir Spark como motor de vistas a través del archivo Global.asax.

protected void Application_Start()
{
    RegisterRoutes(RouteTable.Routes);
    ViewEngines.Engines.Add(new SparkViewFactory());
}

También es necesario generar un archivo donde especificamos las librerías necesarias para el renderizado. Este debe llamarse _global.spark y contedrá los siguientes assemblies (también puede definirse en el web.config).

<use namespace="System" />
<use namespace="System.Web.Mvc.Html" />
<use namespace="System.Collections.Generic" />
<use namespace="System.Linq" />

Esto sería lo mínimo necesario para comenzar a trabajar con Spark View Engine.

Las vistas utilizando Spark View Engine

Para que el motor de Spark reconozca la vistas como suyas, es necesario que las mismas tengan extensión .spark. Una cosa importante a tener en cuenta es que la master page toma un nombre distinto: Application.spark.  Si modificamos la master page por defecto de la plantilla de ASP.NET MVC quedaría de la siguiente manera:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>
        <use content="title">Default title</use>
    </title>
    <link href="~/Content/Site.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <div>
        <div id="header">
            <div id="title">
                <h1>
                    My MVC Application</h1>
            </div>
            <div id="logindisplay">
                <LogOnUserControl/>
            </div>
            <div id="menucontainer">
                <ul id="menu">
                    <li>${Html.ActionLink("Home", "Index", "Home")}</li>
                    <li>${Html.ActionLink("About", "About", "Home")}</li>
                </ul>
            </div>
        </div>
        <div id="main">
            <use content="view" />
            <div id="footer">
            </div>
        </div>
    </div>
</body>
</html>

Como diferencias podemos encontrar:

  1. Para declarar los ContentPlaceHolder utilizamos use content.
    <title><asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title>
    <title>
        <use content="title">Default title</use>
    </title>
  2. Para incluir un control de usuario utilizamos tags con el nombre del mismo, por ejemplo <LogOnUserControl />
    <div>
        <% Html.RenderPartial("LogOnUserControl"); %>
    </div>
    <div>
        <LogOnUserControl/>
    </div>
  3.  En cuanto al uso de helpers, variables y todo aquello relacionado con el servidor, podemos utilizarlo con la misma sintaxis que hasta ahora, a excepción de <% %>. Con Spark View Engine utilizaremos ${}
    <ul id="menu">
        <li>${Html.ActionLink("Home", "Index", "Home")}</li>
        <li>${Html.ActionLink("About", "About", "Home")}</li>
    </ul>

Si queremos usar varias master pages en nuestra aplicación, basta con incluir la etiqueta <use master=””/> en las vistas que lo requiera.

Los controles de usuarios también tienen extensión .spark y el nombre del archivo debe comenzar con guión bajo para ser reconocidos. Además es case sensitive cuando se utiliza el “modo tag”:

<LogOnUserControl />

Si no se tienen en cuenta estas dos indicaciones, no lo reconocerá y tampoco mostrará ningún tipo de error. Si quisieramos “traducir” el user control a lenguaje Spark quedaría de esta forma:

<if condition="Request.IsAuthenticated">
    Welcome <b>${Context.User.Identity.Name}</b>!
    [ !{Html.ActionLink("Log Off", "LogOff", "Account")} ]
</if>
<else>
    [ !{Html.ActionLink("Log On", "LogOn", "Account")} ]
</else>

Podemos crear condiciones a través de tags e intercalar código HTML con código del servidor de una forma, quizás, más clara.

Por último, comentar que para el caso de las vistas no utilizamos un guión bajo  al inicio para el nombre del archivo y que la extensión sigue siendo .spark, al igual que el resto de archivos.

<content name="title">
Index
</content>
<h2>
    Index</h2>
<p>
    This is the Message in ViewData: ${ViewData["Message"]}</p>
<p>
    ${Html.ActionLink("About us", "About")}</p>
<p>
    The time is ${DateTime.Now}.</p>

Además de Spark View Engine, disponemos de otros motores como pueden ser Brail, NVelocity, NHaml entre otros. A gusto del consumidor ;)

Adjunto el proyecto  por si fuera de utilidad.

¡Saludos!

IDataErrorInfo y ASP.NET MVC 1

Versión actualizada para ASP.NET MVC 2 aquí

En la mayoría de las aplicaciones es probable que el usuario final necesite rellenar un formulario, modificar datos requeridos, etc. Como es normal, en muchas de estas ocasiones no se introducen los datos de forma correcta, obviamos alguno de los campos requeridos, etcétera.

La interfaz IDataErrorInfo nos ofrece la posibilidad de generar errores personalizados y poder mostrarlos en la interfaz de usuario correspondiente. Cuando creamos una vista de manera automática con MVC,  está preparada para mostrar estos errores gracias a los siguientes elementos:

  •     ValidationSummary: nos permite mostrar todos los errores producidos a modo de resumen en la vista.
  •     ValidationMessage: puede resultar útil a la hora de mostrar cada error de forma particular, enlazando el mismo con un control de nuestra interfaz.
  •     ModelState: se encargará de recopilar todos los errores producidos en nuestro objeto.

 Si generamos una vista de tipo Create o Edit, podemos ver ValidationSummary al comienzo y ValidationMessage en cada uno de los controles.


<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MovieManager.Models.Objects.Movie>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Edit
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <h2>
        Edit</h2>
    <%= Html.ValidationSummary("Edit was unsuccessful. Please correct the errors and try again.") %>
    <% using (Html.BeginForm())
       {%>
    <fieldset>
        <legend>Fields</legend>
        <p>
            <label for="Name">
                Name:</label>
            <%= Html.TextBox("Name", Model.Name) %>
            <%= Html.ValidationMessage("Name", "*") %>
        </p>
        <p>
            <label for="Genre">
                Genre:</label>
            <%= Html.TextBox("Genre", Model.Genre) %>
            <%= Html.ValidationMessage("Genre", "*") %>
        </p>
        <p>
            <label for="Synopsis">
                Synopsis:</label>
            <%= Html.TextBox("Synopsis", Model.Synopsis) %>
            <%= Html.ValidationMessage("Synopsis", "*") %>
        </p>
        <p>
            <label for="Year">
                Year:</label>
            <%= Html.TextBox("Date", String.Format("{0:g}", Model.Date)) %>
            <%= Html.ValidationMessage("Date", "*") %>
        </p>
        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
    <% } %>
    <div>
        <%=Html.ActionLink("Back to List", "Index") %>
    </div>
</asp:Content>

Para mostrar un ejemplo, voy a implementar IDataErrorInfo en la clase Movie para controlar la creación y edición de películas.


using System;
using System.ComponentModel;

namespace MovieManager.Models.Objects
{
    public class Movie : IDataErrorInfo
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Genre { get; set; }
        public string Synopsis { get; set; }
        public DateTime? Date { get; set; }

        public string this[string columnName]
        {
            get
            {
                var result = string.Empty;
                switch (columnName)
                {
                    case "Name":
                        {
                            if (string.IsNullOrEmpty(Name))
                                result = "Movie Name is required";
                            break;
                        }
                    case "Genre":
                        {
                            if (string.IsNullOrEmpty(Genre))
                                result = "Movie Genre is required";
                            break;
                        }
                    case "Year":
                        {
                            if (Date.HasValue)
                            {
                                if (Date.Value == DateTime.MinValue)
                                {
                                    result = "Movie Date is must be real";
                                }
                            }
                            break;
                        }
                }
                return result;
            }
        }

        public string Error
        {
            get { return string.Empty; }
        }
    }
}

Debemos crear dos propiedades:

  • Item: Devuelve el mensaje de error de la propiedad solicitada entre corchetes. En este caso, se están cubriendo aquellas propiedades que se consideran requeridas o, en el caso de Date, con una fecha correcta. Un string vacío se considera como acertado.
  • Error: Devuelve el mensaje de error por el cual el objeto es incorrecto. En esta ocasión, nos interesa más el caso particular de cada propiedad. Por ello, retornamos solamente un string.empty, ya que sería su valor por defecto.

Cuando llamamos a una acción desde la vista, y bindeamos la respuesta dentro de un objeto te tipo Movie, automáticamente comprueba que cada propiedad bindeada cumpla las condiciones implementadas en la propiedad ítem de nuestra clase. Si alguna de ellas no cumple las condiciones establecidas, quedará registrada en ModelState como una propiedad inválida.

[AcceptVerbs(HttpVerbs.Post)]
 public ActionResult Create([Bind(Exclude = "Id")] Movie newMovie)
 {
     if (ModelState.IsValid)
     {
         _movieRepository.SaveMovie(newMovie);
         return RedirectToAction("Index");
     }
     return View();
 }

Si intentamos crear una nueva película y no rellenamos los campos obligatorios obtendríamos la siguiente imagen:

¡Saludos!

Session-Per-Request en NHibernate con Action Filters en ASP.NET MVC

Una de las buenas prácticas a tener en cuenta antes de trabajar con NHibernate es la siguiente: La sesión y transacción pertenecen a un mismo ciclo de vida. Con esto quiero decir que es importante abrir una transacción cada vez que trabajamos con una base de datos dentro de una aplicación, ya sea para realizar una consulta, como una actualización, eliminación, etc. 

Dado que en el post anterior, donde comencé a montar una aplicación con NHibernate y MVC, realicé los ajustes básicos para poder instanciar una sesión y dejar una clase, Connection.cs, lista para su uso, ahora voy a mostrar una forma adaptada a la estructura de MVC para manejar tanto la sesión como la transacción. 

Llegados a este punto necesitamos conocer un nuevo concepto: Action Filters.
Un action filter es un atributo que podemos asignar tanto a una acción de nuestro controlador como a un controlador entero para poder proporcionarle una funcionalidad adicional. Para crear un atributo de tipo Action Filter, debemos generar una clase y heredar de ActionFilterAttribute. Esta clase contiene los siguientes métodos, ejecutados en este orden: 

  1. OnActionExecuting, se lanza antes de que la acción sea ejecutada.
  2. OnActionExecuted, es llamada después de que la acción finalice.
  3. OnResultExecuting, comienza antes de que el resultado sea devuelto a la página.
  4. OnResultExecuted, ocurre cuando ha terminado de ejecutar el resultado que generó la acción.

En este caso, Session-Per-Request se podría traducir en Session-Per-Action  ya que, lo que realmente nos interesa, es tener una sesión disponible para una acción en concreto o para todas las acciones de un controlador. Además, me interesa que la sesión de NHibernate esté disponible ANTES de realizar cualquier operación dentro de la acción en cuestión, ya que si no es así lo más probable es que se lanzara una excepción al no tener un objeto session listo al que consultar. Por último, para que el ciclo de vida se complete correctamente, cuando la acción solicitada finalice, necesito que mi transacción sea completada y mi sesión finalice junto con el ciclo de vida de la petición. Teniendo presentes estas anotaciones, podríamos obtener lo siguiente: 

using System.Web.Mvc;
using NHibernate.Context; 

namespace MovieManager.Models
{
    public class SessionPerRequest : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext1)
        {
            var session = MvcApplication.SessionFactory.OpenSession();
            session.BeginTransaction();
            CurrentSessionContext.Bind(session);
        }
        public override void OnResultExecuted(ResultExecutedContext filterContext)
        {
            var session = CurrentSessionContext.Unbind(MvcApplication.SessionFactory);
            if (session != null)
            {
                if (session.Transaction.IsActive)
                {
                    try
                    {
                        session.Transaction.Commit();
                    }
                    catch
                    {
                        session.Transaction.Rollback();
                    }
                } 

                session.Close();
            }
        }
    }
}

Para este ejemplo, he sobrescrito los métodos OnActionExecuting y OnResultExecuted . En el primero de ellos, inicializo la sesión de NHibernate y bindeo la misma en el contexto de la aplicación. Por el contrario, en el segundo método recupero la sesión, compruebo si tiene alguna transacción en curso, realizo el commit (Si ocurriera algún error durante el commit de la transacción actual, se realizaría un rollback para evitar problemas) y cierro la sesión. 

En el archivo web.config, necesitamos declarar una propiedad para indicar el contexto en el que se enmarca la aplicación. Esta propiedad es current_session_context_class y , en este caso, podríamos declararlo como web, tal y como indico en el siguiente código: 

<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
    <session-factory>
        <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider, NHibernate</property>
        <property name="connection.connection_string">
    Server=localhostsqlexpress;initial catalog=MovieManager;Integrated Security=true
  </property>
        <property name="dialect">NHibernate.Dialect.MsSql2000Dialect</property>
        <property name="current_session_context_class">web</property>
        <property name="proxyfactory.factory_class">NHibernate.ByteCode.LinFu.ProxyFactoryFactory, NHibernate.ByteCode.LinFu</property>
        <mapping assembly="MovieManager"/>
    </session-factory>
</hibernate-configuration>

Por otro lado, podemos comprobar que hacemos referencia a MvcApplication.SessionFactory,  lo cual significa que, para tener un mayor acceso a nuestra “Factoría de sesiones” he posicionado su declaración en el archivo Global.asax para poder acceder desde cualquier parte de mi aplicación. 

using System.Web.Mvc;
using System.Web.Routing;
using NHibernate;
using NHibernate.Cfg;

namespace MovieManager
{
    // Note: For instructions on enabling IIS6 or IIS7 classic mode,
    // visit <a href="http://go.microsoft.com/?LinkId=9394801">http://go.microsoft.com/?LinkId=9394801</a>

    public class MvcApplication : System.Web.HttpApplication
    {
        public static ISessionFactory SessionFactory = new Configuration().Configure().BuildSessionFactory();

        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                "Default",                                              // Route name
                "{controller}/{action}/{id}",                           // URL with parameters
                new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
            );

        }

        protected void Application_Start()
        {
            RegisterRoutes(RouteTable.Routes);
        }
    }
}

Para ir finalizando, podemos ver la forma de utilizar el atributo SessionPerRequest en un controlador donde necesitaremos persistencia. Colocamos el mismo entre corchetes bien en la declaración del controlador o en cualquiera de las acciones.

using System.Web.Mvc;
using MovieManager.Models;

namespace MovieManager.Controllers
{
    [SessionPerRequest]
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            var movieRepository = new MovieRepository(MvcApplication.SessionFactory);
            return View(movieRepository.ListMovies());
        }

        public ActionResult About()
        {
            return View();
        }
    }
}

En la acción Index, creamos un objeto MovieRepository, donde le pasamos como parámetro el objeto SessionFactory declarado en el fichero Global.asax. Llegados a este punto debemos tener en cuenta de que la sesión de NHibernate, necesaria para llamar a la base de datos y recuperar el listado de películas, ya ha sido inicializada justo antes de entrar en la acción gracias al atributo SessionPerRequest. Si echamos un vistazo al repositorio, vemos lo siguiente:

using System.Collections.Generic;
using MovieManager.Models.Objects;
using NHibernate;

namespace MovieManager.Models
{
    public class MovieRepository
    {
        private readonly ISessionFactory _session;

        public MovieRepository(ISessionFactory sessionFactory)
        {
            _session = sessionFactory;
        }

        public IList<Movie> ListMovies()
        {
            return _session.GetCurrentSession().CreateCriteria(typeof(Movie)).List<Movie>();
        }
    }
}

A través del constructor de la clase MovieRepository, estamos inyectando el objeto SessionFactory. De tal manera que podemos utilizar la misma factoría de sesiones en cualquiera de los métodos implementados en esta clase, que necesiten hacer una consulta, actualización, etc. Cuando todo haya terminado, todo quedará perfectamente finalizado gracias a Action Filters y Session-Per-Request/Action.

¡Saludos!