8

Configuración y optimización de los motores de vista (View Engines) de ASP.NET MVC

Los motores de vista (View engines) de mvc son los responsables del renderizado HTML de las vistas. Por defecto el framework MVC incluye dos motores: Webforms y Razor, pero existen muchos más como Spark, NHaml o Bellevue (por citar algunos) que podemos añadir a colección de view engines de nuestro proyecto.

Cuando devolvemos un objeto ViewResult en una acción de un controlador, por ejemplo a través del método View, el framework itera sobre los motores de vista registrados y por cada uno de ellos intenta localizar la plantilla que corresponde con el nombre solicitado. El primer view engine que lo encuentre será el motor que renderizará la vista, dejando de iterar sobre los restantes.

Cada motor de vistas tiene su propia lógica para buscar las plantillas, normalmente lo hacen a traves del sistema de archivos, es decir, por ubicación física. Los motores Webforms (.aspx, .ascx) y Razor (.cshtml, .vbhtml) implementados en las clases WebFormViewEngine y RazorViewEngine respectivamente, buscan las plantillas por ubicación. Concretamente buscan el nombre de la plantilla con un determinado nombre de extensión en diferentes directorios.

Si ejecutamos una acción de un controlador haciendo referencia a una vista que no existe, recibiremos esta respuesta.

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View("TestViewEngines");
    }
}

MVCViewEngines_ViewNotFound
Podemos ver que efectivamente el framework ha buscado la plantilla primero con el motor Webforms y luego con el motor Razor en varias ubicaciones.

La convención por defecto de estos dos motores para la localización de las vistas es la siguiente:

  • ~/Views/{0}/{1}.{2}
  • ~/Views/Shared/{1}.{2}

{0} Nombre del controlador
{1} Nombre de la acción
{2} la extensión del archivo.(cshtml y vbhtml para Razor. aspx y .ascx para Webforms)


En este punto ya podemos intuir que el orden en el que están incluidos los motores de vista en nuestro proyecto altera el rendimiento.

Por defecto el framework MVC sitúa en primera posición al motor de vistas Webforms, seguido a continuación por el de razor.

Si tenemos un proyecto donde únicamente utilizamos Razor, podemos eliminar el motor Webforms de nuestro sistema para mejorar el rendimiento.

Esto debemos hacerlo al iniciarse la aplicación, por tanto el código debe incluirse en el Application_Start del Global.asax

protected void Application_Start()
{
  AreaRegistration.RegisterAllAreas();

  ViewEngines.Engines.RemoveAt(0); 

  FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
  RouteConfig.RegisterRoutes(RouteTable.Routes);
  BundleConfig.RegisterBundles(BundleTable.Bundles);
}

Si ejecutamos la misma acción anterior después de haber eliminado el motor Webforms obtendremos este resultado.


Como se puede ver, al eliminar de la colección el motor webforms, el framework ya no busca las plantillas aspx, ascx.

De la misma forma podemos alterar el orden, ya que se puede dar el caso que nuestro proyecto utilice muchas plantillas Razor y sólo unas pocas en Webforms.

var webFormViewEngine = ViewEngines.Engines[0];
ViewEngines.Engines.RemoveAt(0);
ViewEngines.Engines.Add(webFormViewEngine);

O si lo preferís, quizás por claridad, podéis limpiar la colección y añadirlos de nuevo cambiando el orden. Yo prefiero no volver a instanciar pero como se dice por ahí, contra gustos no hay nada escrito.

ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new RazorViewEngine());
ViewEngines.Engines.Add(new WebFormViewEngine());

También podemos añadir otros motores de vista

ViewEngines.Engines.Add(new SparkViewFactory());

Entrando un poco en profundidad.

En el código anterior, ViewEngines es una clase estática proporcionada por el framework MVC con el objetivo de administrar los diferentes motores de vista.

Cuando desde una acción devolvemos una vista o una vista parcial, estamos devolviendo las clases ViewResult o ViewPartialResult respectivamente, ambas heredan de la clase base ViewResultBase, que a su vez hereda de ActionResult, es por ello que en las acciones vemos que el objeto de salida del método es un ActionResult. No son los únicos tipos de ActionResult que se pueden devolver, existen otros como FileResult, ContentResult, JsonResult, etc.., de hecho hasta podemos crear nuestro propio ActionResult personalizado. Pero no es el objetivo de este post hablar de ellos ya  que en los mismos no entra en acción ningún motor de vistas.

ViewResultBase expone una propiedad llamada ViewEngineCollection. Este es su código:

public ViewEngineCollection ViewEngineCollection
{
    get {
        return (this._viewEngineCollection ?? ViewEngines.Engines);
    }
    set {
        this._viewEngineCollection = value;
    }
}

Cuando el framework recibe el objeto ActionResult, ejecuta el método ExecuteResult de la clase. Este método está sobrescrito en la clase ViewResultBase e implementa la lógica necesaria para generar el contenido y devolverlo al cliente.

public override void ExecuteResult(ControllerContext context)
{
    if (context == null)
    {
        throw new ArgumentNullException("context");
    }
    if (string.IsNullOrEmpty(this.ViewName))
    {
        this.ViewName = context.RouteData.GetRequiredString("action");
    }
    ViewEngineResult result = null;
    if (this.View == null)
    {
        result = this.FindView(context);
        this.View = result.View;
    }
    TextWriter output = context.HttpContext.Response.Output;
    ViewContext viewContext = new ViewContext(context, this.View, this.ViewData, this.TempData, output);
    this.View.Render(viewContext, output);
    if (result != null)
    {
        result.ViewEngine.ReleaseView(context, this.View);
    }
}

Esta lógica incluye una llamada a FindView (resaltada en el código) que es un método que sobrescriben las clases ViewResult y ViewPartialResult y que a su vez utilizan el método FindView de la propiedad ViewEngineCollection de ViewResultBase (clase de la que heredan) para que identificar los motores de vista registrados en el sistema y buscar las vistas.

El método FindView de ViewResult se expone a continuación:

protected override ViewEngineResult FindView(ControllerContext context)
{
    ViewEngineResult result = base.ViewEngineCollection.FindView(context, base.ViewName, this.MasterName);
    if (result.View != null)
    {
        return result;
    }
    StringBuilder builder = new StringBuilder();
    foreach (string str in result.SearchedLocations)
    {
        builder.AppendLine();
        builder.Append(str);
    }
    throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, MvcResources.Common_ViewNotFound, new object[] { base.ViewName, builder }));
}

Como vemos, se produce la llamada a FindView de ViewEngineCollection, propiedad de la que anteriormente hemos hablado y expuesto su código y que por defecto es inicializada con la colección Engines de la clase estática ViewEngines, que es la que hemos modificado al principio en el Application_Start.

También podemos observar que si la vista no existe se genera un StringBuilder con las localizaciones y se lanza una excepción. Este error será y es el que hemos visto al llamar a la acción que devolvía una vista inexistente.

Si seguimos tirando del hilo en las llamadas del framework, al final damos con una llamada al método Find de ViewEngineCollection donde se ve la iteración por los motores de vista registrados y como se añade la ruta de localización cuando no se encuentra la vista.

private ViewEngineResult Find(Func<IViewEngine, ViewEngineResult> lookup, bool trackSearchedPaths)
{
    List<string> source = null;
    if (trackSearchedPaths)
    {
        source = new List<string>();
    }
    foreach (IViewEngine engine in this.CombinedItems)
    {
        if (engine != null)
        {
            ViewEngineResult result = lookup(engine);
            if (result.View != null)
            {
                return result;
            }
            if (trackSearchedPaths)
            {
                source.AddRange(result.SearchedLocations);
            }
        }
    }
    if (trackSearchedPaths)
    {
        return new ViewEngineResult(source.Distinct<string>().ToList<string>());
    }
    return null;
}

No puedo acabar el articulo  sin comentar que podemos crear nuestros propios view engines o sencillamente crearlos a partir de los ya existentes, ya sea para modificar su comportamiento por defecto o para ampliarlos.

Por ejemplo, la última acción que ejecutamos nos devolvió un error informando de que la vista no existía y nos detallaba los  archivos que había buscado y donde lo había hecho. Si hacéis memoria recordareis que buscaba las extensión cshtml y vbhtml (c# y visual basic respectivamente). Vamos a crear un view engine a partir del de Razor para que sólo busque los archivos cshtml. Esto es útil si nuestro proyecto está realizado en c#.

Antes de ver el código vamos a ver la jerarquía de herencia de la clase RazorViewEngine

System.Web.Mvc.IViewEngine
   System.Web.Mvc.VirtualPathProviderViewEngine
      System.Web.Mvc.BuildManagerViewEngine
         System.Web.Mvc.RazorViewEngine

Cualquier motor de vistas debe heredar de la interfaz IViewEngine. La clase VirtualPathProviderViewEngine provee métodos y propiedades para la búsqueda y acceso a los archivos/plantillas de las vistas.

Esta clase tiene una serie de propiedades que definen la ubicación de las vistas y su nombre de extensión:

public string[] AreaMasterLocationFormats { get; set; }
public string[] AreaPartialViewLocationFormats { get; set; }
public string[] AreaViewLocationFormats { get; set; }

public string[] MasterLocationFormats { get; set; }
public string[] PartialViewLocationFormats { get; set; }
public string[] ViewLocationFormats { get; set; }

public string[] FileExtensions { get; set; }

En el constructor de la clase RazorViewEngine se asignan los valores a estas propiedades, definiendo de esta manera que extensión tendrán los archivos y donde buscará las plantillas el motor Razor.

A continuación el código del constructor de RazorViewEngine:

public RazorViewEngine(IViewPageActivator viewPageActivator): base(viewPageActivator)
{
    base.AreaViewLocationFormats = new string[] {
        "~/Areas/{2}/Views/{1}/{0}.cshtml",
        "~/Areas/{2}/Views/{1}/{0}.vbhtml",
        "~/Areas/{2}/Views/Shared/{0}.cshtml",
        "~/Areas/{2}/Views/Shared/{0}.vbhtml" };

    base.AreaMasterLocationFormats = new string[] {
        "~/Areas/{2}/Views/{1}/{0}.cshtml",
        "~/Areas/{2}/Views/{1}/{0}.vbhtml",
        "~/Areas/{2}/Views/Shared/{0}.cshtml",
        "~/Areas/{2}/Views/Shared/{0}.vbhtml" };

    base.AreaPartialViewLocationFormats = new string[] {
        "~/Areas/{2}/Views/{1}/{0}.cshtml",
        "~/Areas/{2}/Views/{1}/{0}.vbhtml",
        "~/Areas/{2}/Views/Shared/{0}.cshtml",
        "~/Areas/{2}/Views/Shared/{0}.vbhtml" };

    base.ViewLocationFormats = new string[] {
        "~/Views/{1}/{0}.cshtml",
        "~/Views/{1}/{0}.vbhtml",
        "~/Views/Shared/{0}.cshtml",
        "~/Views/Shared/{0}.vbhtml" };

    base.MasterLocationFormats = new string[] {
        "~/Views/{1}/{0}.cshtml",
        "~/Views/{1}/{0}.vbhtml",
        "~/Views/Shared/{0}.cshtml",
        "~/Views/Shared/{0}.vbhtml" };

    base.PartialViewLocationFormats = new string[] {
        "~/Views/{1}/{0}.cshtml",
        "~/Views/{1}/{0}.vbhtml",
        "~/Views/Shared/{0}.cshtml",
        "~/Views/Shared/{0}.vbhtml" };

    base.FileExtensions = new string[] { "cshtml", "vbhtml" };
}

Una vez visto esto nos damos cuenta que a través de estas propiedades podemos indicar donde buscar nuestras vistas. De hecho podemos usar la clase VirtualPathProviderViewEngine como punto de partida para generar un motor de vistas que acceda a los archivos de vista por ubicación física.

En nuestro caso particular queremos toda la potencia de Razor pero sólo para c#, por lo que nos basta con heredar de la clase RazorViewEngine y modificar estas propiedades para personalizarlo.

public class CustomRazorViewEngine : RazorViewEngine
{
    public CustomRazorViewEngine() :base()
    {
        base.AreaViewLocationFormats = new string[] {
            "~/Areas/{2}/Views/{1}/{0}.cshtml",
            "~/Areas/{2}/Views/Shared/{0}.cshtml"
        };

        base.AreaMasterLocationFormats = new string[] {
            "~/Areas/{2}/Views/{1}/{0}.cshtml",
            "~/Areas/{2}/Views/Shared/{0}.cshtml"
        };

        base.AreaPartialViewLocationFormats = new string[] {
            "~/Areas/{2}/Views/{1}/{0}.cshtml",
            "~/Areas/{2}/Views/Shared/{0}.cshtml"
        };

        base.ViewLocationFormats = new string[] {
            "~/Views/{1}/{0}.cshtml",
            "~/Views/Shared/{0}.cshtml"
        };

        base.PartialViewLocationFormats = new string[] {
            "~/Views/{1}/{0}.cshtml",
            "~/Views/Shared/{0}.cshtml"
        };

        base.MasterLocationFormats = new string[] {
            "~/Views/{1}/{0}.cshtml",
            "~/Views/Shared/{0}.cshtml"
        };

        base.FileExtensions = new string[] {
            "cshtml"
        };
    }
}

Ahora sólo tenemos que modificar el Application_Start del global.asax para registrar nuestro view engine personalizado.

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    ViewEngines.Engines.Clear();
    ViewEngines.Engines.Add(new CustomRazorViewEngine());

    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
}

Si ejecutamos la acción del principio del articulo, que recordamos, indica una vista que no existe, recibiremos la siguiente respuesta:

Ahora nuestro sistema sólo busca vistas en Razor con extensión cshtml (c#). Si es únicamente el motor y lenguaje que vamos a usar, no hay razón para tener otros motores registrados.

 

8 Comments

    • Una pequeña mejora… puedes crear este método en tu Global.asax y llamarlo al inicio de la aplicación. Usa reflection para asegurarse de que elimina el motor correcto!

      private void RemoveNotUsedEngines()
      {
      var webFormsEngine = ViewEngines.Engines.FirstOrDefault(e => e.GetType() == typeof (WebFormViewEngine));
      if (null != webFormsEngine)
      ViewEngines.Engines.Remove(webFormsEngine);
      }

      • Gracias por el aporte Joan. El motor Webforms siempre es el primero por defecto, pero esta bien asegurarse sobretodo para futuras versiones del framework.

  1. Yo no tengo blog para añadirte pero si una lista de blogs favoritos…. :-)
    Felicidades por la iniciativa.

Responder a Jose Martínez Cancelar respuesta

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *