sábado, 9 de mayo de 2015

ASP.NET MVC. Personalizar el Motor de Vistas de MVC (III). Creando y registrando el motor de vistas.

Este artículo es continuación de:
ASP.NET MVC. Personalizar el Motor de Vistas de MVC (I). Creando el escenario.
ASP.NET MVC. Personalizar el Motor de Vistas de MVC (II). Comprendiendo las vistas de MVC.

En los anteriores artículos creamos un escenario en el que generábamos vistas en un formato xml personalizado y tratamos de comprender la forma en que el Framework de ASP.NET MVC gestiona las vistas.

En este artículo mostraré cómo crear y registrar el motor de vistas para gestionar estas vistas en formato xml.

Como vimos en el anterior artículo el resultado generado por el motor de vistas debe ser un objeto que implemente la interfaz IView. Así que lo primero que voy a hacer es crear una clase que implemente esta interfaz y que utilizaremos para generar el resultado de nuestro motor. He creado una nueva clase ScreenView en una carpeta Infrastructure dentro del proyecto.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace CustomViewEngine.Infrastructure
{
    public class ScreenView: IView
    {

        string _screenName;

        public ScreenView(string screenName)
        {
            _screenName = screenName;
        }

        public void Render(ViewContext viewContext, TextWriter writer)
        {
            writer.Write(_screenName);
        }

    }
}
Imports System.Web.Mvc
Imports System.IO

Public Class ScreenView
    Implements IView

    Dim _screenName As String

    Public Sub New(screenName As String)
        _screenName = screenName
    End Sub

    Public Sub Render(viewContext As ViewContext, writer As TextWriter) _
    Implements IView.Render
        writer.Write(_screenName)
    End Sub

End Class

He creado la clase de forma que el constructor reciba el nombre de la vista a generar. De esta forma sabremos a partir de qué archivo xml debemos crear el formulario.

Por el momento la vista simplemente envía como salida el nombre de la vista a mostrar. Esto nos servirá para comprobar que el motor de vistas ha respondido a la petición de la vista de forma correcta. En el siguiente artículo modificaremos esta clase ScreenView para generar el código HTML que mostrará el formulario definido en el archivo xml.

Para crear el motor de vistas voy a crear una clase ScreenViewEngine en la carpeta Infrastructure. Como vimos anteriormente el motor debe implementar la interfaz IViewEngine.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace CustomViewEngine.Infrastructure
{
    public class ScreenViewEngine: IViewEngine
    {
        public ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
        {
            if (partialViewName.StartsWith("screen_"))
            {
                string screen = partialViewName.Substring(7);
                if (File.Exists(controllerContext.HttpContext.Server
                    .MapPath(string.Format("~/Screens/{0}.xml", screen))))
                    return new ViewEngineResult(new ScreenView(screen), this);
                else
                    return new ViewEngineResult(new string[] { 
                        string.Format("~/Screens/{0}.xml", screen) });
            }
            else
                return new ViewEngineResult(new string[] { 
                    string.Format("{0} no es una vista tipo Screen", partialViewName) });
        }

        public ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
        {
            return new ViewEngineResult(new string[] { 
                string.Format("{0} no es una vista tipo Screen", viewName) });
        }

        public void ReleaseView(ControllerContext controllerContext, IView view)
        {
            // No es necesario realizar ninguna acción
        }
    }
}
Imports System.Web.Mvc
Imports System.IO

Public Class ScreenViewEngine
    Implements IViewEngine

    Public Function FindPartialView(controllerContext As ControllerContext _
                                    , partialViewName As String, useCache As Boolean) As ViewEngineResult _
        Implements IViewEngine.FindPartialView
        If partialViewName.StartsWith("screen_") Then
            Dim screen As String = partialViewName.Substring(7)
            If File.Exists(controllerContext.HttpContext.Server.MapPath(String.Format("~/Screens/{0}.xml", screen))) Then
                Return New ViewEngineResult(New ScreenView(screen), Me)
            Else
                Return New ViewEngineResult(New String() {String.Format("~/Screens/{0}.xml", screen)})
            End If
        Else
            Return New ViewEngineResult(New String() {String.Format("{0} no es una vista tipo Screen", partialViewName)})
        End If
    End Function

    Public Function FindView(controllerContext As ControllerContext _
                             , viewName As String, masterName As String, useCache As Boolean) As ViewEngineResult _
        Implements IViewEngine.FindView
        Return New ViewEngineResult(New String() {String.Format("{0} no es una vista tipo Screen", viewName)})
    End Function

    Public Sub ReleaseView(controllerContext As ControllerContext, view As IView) _
        Implements IViewEngine.ReleaseView
        ' No es necesario realizar ninguna acción
    End Sub

End Class



He establecido como convención que las vistas que gestiona el motor de vistas ScreenViewEngine tendrán un nombre prefijado por screen_. De esta forma evitaremos problemas en caso de que se definan con el mismo nombre vistas para Razor y para nuestro motor de vistas. Cada motor podrá identificar qué vistas le corresponden.

Como ya comenté, la clase ViewEngineResult dispone de dos constructores y se debe usar uno u otro en función de si el motor de vistas es capaz de gestionar la vista solicitada. Así que en el método FindPartialView compruebo si el nombre de la vista tiene el prefijo screen_ y, si no es así, utilizo el contructor de ViewEngineResult que recibe una colección de Strings para indicar que el motor no puede gestionar la vista solicitada.

Si el prefijo coincide, obtengo el nombre real de la vista (sin prefijo) y compruebo si existe un archivo xml en la carpeta Screens con ese nombre. Si no existe el archivo devuelvo un objeto ViewEngineResult indicándole la ruta en la que el motor ha intentado localizar la vista.

Si el archivo existe, significa que el motor de vistas es capaz de gestionar la solicitud y devuelve la vista como una nueva instancia de la clase ScreenView. El Framework de MVC se encargará de llamar al método Render de la vista para generar la salida a enviar al navegador.

Como comentamos, vamos a tratar siempre las vistas xml como vistas parciales por lo que el método FindView devuelve directamente un texto indicando que no puede gestionar la solicitud, independientemente de la vista solicitada.

De esta forma ya hemos creado el motor de vistas, sin embargo aún queda un último paso quedar: registrarlo o, lo que es lo mismo, crear una instancia del motor e indicarle al Framework de MVC que debe utilizar este motor para gestionar las vistas de la aplicación. Este registro lo realizaremos en el método Application_Start del archivo Global.asax de la aplicación:

using CustomViewEngine.Infrastructure;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace CustomViewEngine
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            RouteConfig.RegisterRoutes(RouteTable.Routes);

            ViewEngines.Engines.Add(new ScreenViewEngine());
        }
    }
}
Imports System.Web.Mvc
Imports System.Web.Routing

Public Class MvcApplication
    Inherits System.Web.HttpApplication

    Protected Sub Application_Start()
        AreaRegistration.RegisterAllAreas()
        RouteConfig.RegisterRoutes(RouteTable.Routes)

        ViewEngines.Engines.Add(New ScreenViewEngine())
    End Sub
End Class

La colección ViewEngines.Engines contiene todos los motores de vistas utilizados por la aplicación. Cuando se genera una solicitud para mostrar una vista (o una vista parcial), MVC llama al método FindView (o FindPartialView) de cada motor de vistas. En el momento que un motor de vistas devuelve una instancia de IView en el objeto ViewEngineResult el MVC procesa la vista sin llamar al resto de motores.

Para probar el motor nos falta realizar un último cambio: modificar la vista Index para que llame a la vista parcial.

@model CustomViewEngine.Models.Persona

@{
    ViewBag.Title = "Index";
}

@{
    string screenName = string.Format("screen_{0}"
        , (string)ViewBag.ScreenName);
    Html.RenderPartial(screenName, Model);
}
@ModelType CustomViewEngine.Persona
@Code
    ViewData("Title") = "Index"
End Code

@Code
    Dim screenName As String = String.Format("screen_{0}", ViewBag.ScreenName)
    Html.RenderPartial(screenName, Model)
End Code

Como se puede observar he añadido el prefijo screen_ al nombre de la vista para que sea gestionada por el nuevo motor de vistas. Mediante el método Html.RenderPartial invoco a la vista parcial pasándole el nombre de ésta y el modelo con la información a mostrar en el formulario.

Si ahora navegamos a las rutas /Home/Index/PersonaSimple y /Home/Index/PersonaCompleto obtendremos el mismo resultado que antes, aunque ahora el nombre de la vista lo proporciona el motor de vistas ScreenViewEngine en lugar de escribirlo directamente en la vista Index.

Resultado en el navegador
 El resultado es más interesante si tratamos de mostrar una vista que no existe. Por ejemplo si introducimos la url /Home/Index/OtroFormulario.

Error de vista no encontrada

Como era de esperar la aplicación devuelve un error y, en la información del error, aparece el listado de ubicaciones donde el motor de vistas Razor a tratado de encontrar la vista a mostrar. Sin embargo a continuación aparece algo importante: la ruta en la que el motor ScreenViewEngine a tratado de encontrar la vista a mostrar (~/Screens/OtroFormulario.xml). Ésta es la cadena que pasábamos como resultado en el objeto ViewEngineResult cuando no existía el archivo xml:

                    return new ViewEngineResult(new string[] { 
                        string.Format("~/Screens/{0}.xml", screen) });
                Return New ViewEngineResult(New String() {String.Format("~/Screens/{0}.xml", screen)})

Ya tenemos registrado y funcionando el motor de vistas personalizado. Sin embargo el objetivo inicial parece seguir estando lejos: "mostrar los formularios diseñados a través de una herramienta externa en un formato propio dentro de nuestra aplicación MVC".

Pero no es así. Realmente estamos muy cerca de acabar nuestro trabajo como veremos en el próximo artículo:

El código completo de este artículo y los otros tres de la serie, tanto en C# como en Visual Basic, está disponible en:

Crear un motor de vistas personalizado en ASP.NET MVC. - Ejemplos MSDN.

No hay comentarios:

Publicar un comentario