lunes, 11 de mayo de 2015

ASP.NET MVC. Personalizar el Motor de Vistas de MVC (y IV). Generando las salida HTML de vistas personalizadas.

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.
ASP.NET MVC. Personalizar el Motor de Vistas de MVC (III). Creando y registrando el motor de vistas.

En los artículos precedentes hemos visto cómo crear y registrar un motor de vistas personalizado para gestionar vistas en un formato propio. Sin embargo aún nos quedaba un último paso: la generación del código HTML para generar el formulario a partir de la vista de forma que se integre con MVC y sus características de validación y enlace de datos.

Tenemos nuestro motor de vistas funcionando pero la vista únicamente nos muestra el nombre del formulario:

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

Así que lo único que nos queda por hacer es que el método Render de ScreenView lea el archivo xml correspondiente y genere la salida HTML para mostrar el formulario.

Para mostrar los controles del formulario me crearé una instancia de la clase System.Web.Mvc.HtmlHelper, la cual proporciona múltiples métodos de ayuda para generar el código HTML de las páginas.

La clase HtmlHelper proporciona dos constructores:

public HtmlHelper(ViewContext viewContext, IViewDataContainer viewDataContainer);

public HtmlHelper(ViewContext viewContext, IViewDataContainer viewDataContainer, RouteCollection routeCollection);



El método Render ya recibe como parámetro un objeto ViewContext, pero necesitamos una instancia de un objeto que implemente la interfaz IViewDataContainer:

namespace System.Web.Mvc
{
    // Resumen:
    //     Define los métodos necesarios para un diccionario de datos de vista.
    public interface IViewDataContainer
    {
        // Resumen:
        //     Obtiene o establece el diccionario de datos de vista.
        //
        // Devuelve:
        //     Diccionario de datos de vista.
        ViewDataDictionary ViewData { get; set; }
    }
}

Como podemos ver esta interfaz tiene una única propiedad ViewData que proporciona un diccionario de datos de vista. Voy a hacer que ScreenView implemente esta interfaz de forma que sea capaz de mantener los datos de vista.

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, IViewDataContainer
    {

        string _screenName;
        ViewDataDictionary _viewData;

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

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

        public ViewDataDictionary ViewData
        {
            get
            {
                return _viewData;
            }
            set
            {
                _viewData = value;
            }
        }

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

Public Class ScreenView
    Implements IView, IViewDataContainer

    Dim _screenName As String
    Dim _viewData As ViewDataDictionary

    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

    Public Property ViewData() As ViewDataDictionary _
        Implements IViewDataContainer.ViewData
        Get
            Return _viewData
        End Get
        Set(ByVal value As ViewDataDictionary)
            _viewData = value
        End Set
    End Property

End Class

Ya tenemos todo lo necesario para generar el formulario HTML. Así que voy a modificar el código del método Render para que recorra los elementos field del xml y vaya generando los controles HTML correspondientes.
Por cada elemento field voy a generar una etiqueta con el nombre del campo a través del método de extensión Label del HtmlHelper. A continuación, dependiendo de si el campo está marcado como readonly, generaré un control de edición para el campo o simplemente mostraré el valor de éste a través de los métodos Editor y Display del HtmlHelper.
Para cada campo añadiré también una etiqueta en la que mostrar los errores de validación (utilizando el método de extensión ValidationMessage del HtmlHelper), de forma que podamos comprobar que los atributos de validación del modelo realizan su función correctamente.

        public void Render(ViewContext viewContext, TextWriter writer)
        {
            _viewData = viewContext.ViewData;

            string viewPath = viewContext.HttpContext.Server
                .MapPath(string.Format("~/Screens/{0}.xml", _screenName));
            XPathDocument screenXml = new XPathDocument(viewPath);
            XPathNavigator navigator = screenXml.CreateNavigator();
            XPathNodeIterator fields = navigator.Select("/root/field");
            HtmlHelper Html = new HtmlHelper(viewContext, this);
            using (Html.BeginForm())
            {
                while (fields.MoveNext())
                {
                    string fieldName = fields.Current.GetAttribute("name", "");
                    bool readonlyfield = false;
                    if (fields.Current.MoveToAttribute("readonly", ""))
                        readonlyfield = fields.Current.ValueAsBoolean;
                    writer.Write(Html.Label(fieldName));
                    writer.Write(new TagBuilder("br").ToString(TagRenderMode.SelfClosing));
                    if (readonlyfield)
                        writer.Write(Html.Display(fieldName));
                    else
                        writer.Write(Html.Editor(fieldName));
                    writer.Write(new TagBuilder("br").ToString(TagRenderMode.SelfClosing));
                    writer.Write(Html.ValidationMessage(fieldName, new { style="color: red;" }));
                    writer.Write(new TagBuilder("br").ToString(TagRenderMode.SelfClosing));
                }
                TagBuilder submit = new TagBuilder("input");
                submit.MergeAttribute("type", "submit");
                submit.MergeAttribute("value", "Validar");
                writer.Write(submit.ToString(TagRenderMode.SelfClosing));
            }

            _viewData = null;
        }
    Public Sub Render(viewContext As ViewContext, writer As TextWriter) _
    Implements IView.Render
        _viewData = viewContext.ViewData

        Dim viewPath As String = _
            viewContext.HttpContext.Server _
            .MapPath(String.Format("~/Screens/{0}.xml", _screenName))
        Dim screenXml = New XPathDocument(viewPath)
        Dim navigator As XPathNavigator = screenXml.CreateNavigator()
        Dim fields As XPathNodeIterator = navigator.Select("/root/field")
        Dim Html = New HtmlHelper(viewContext, Me)
        Using Html.BeginForm()
            While fields.MoveNext()
                Dim fieldName As String = fields.Current.GetAttribute("name", "")
                Dim readonlyfield = False
                If fields.Current.MoveToAttribute("readonly", "") Then
                    readonlyfield = fields.Current.ValueAsBoolean
                End If
                writer.Write(Html.Label(fieldName))
                writer.Write(New TagBuilder("br").ToString(TagRenderMode.SelfClosing))
                If readonlyfield Then
                    writer.Write(Html.Display(fieldName))
                Else
                    writer.Write(Html.Editor(fieldName))
                End If
                writer.Write(New TagBuilder("br").ToString(TagRenderMode.SelfClosing))
                writer.Write(Html.ValidationMessage(fieldName, New With {.style = "color: red;"}))
                writer.Write(New TagBuilder("br").ToString(TagRenderMode.SelfClosing))
            End While
            Dim submit = New TagBuilder("input")
            submit.MergeAttribute("type", "submit")
            submit.MergeAttribute("value", "validar")
            writer.Write(submit.ToString(TagRenderMode.SelfClosing))
        End Using

        _viewData = Nothing
    End Sub

Además de los controles de formulario he incluido algunos tags br para introducir algunos saltos de línea y un botón submit para enviar los datos al servidor y provocar la validación de los campos.

Si arrancamos la aplicación y navegamos a las urls /Home/Index/PersonaSimple y /Home/Index/PersonaCompleto podemos ver los formularios generados a partir de los archivos xml.

Formularios. Resultado final.
El código completo de este artículo y los tres anteriores, tanto en C# como en Visual Basic, está disponible en:

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

2 comentarios:

  1. Que tal excelente articulo, tengo una duda, como puedo agregar a la vista generada al vuelo que se pinte incluyendo el archivo _Layout.cshtml que tengo en view/shared/_Layout.cshtml

    ResponderEliminar
  2. Hola Paco. Lo que planteas es bastante más complicado. El motor de vistas debería responder a vistas completas (a través del método FindView), no sólo a vistas parciales (a través de FindPartialView) como en mi ejemplo. En el fichero xml (o el formato que quieras utilizar) deberías indicar la vista a utilizar como Layout y, al renderizar la vista, o bien interpretar tú mismo la vista Razor (lo cual puede ser una locura) o integrarlo con el motor de vistas Razor para poder delegar el renderizado de esa parte (habría que ver cómo hacerlo).

    La forma más sencilla de utilizar los layouts sería con el acercamiento del ejemplo: el formato personalizado se muestra como una vista parcial dentro de una vista Razor. De esta forma en la vista Razor puedes indicarle el Layout a utilizar como en cualquier otra vista.

    De hecho si te descargas el código de ejemplo verás que, en este ejemplo, está utilizando siempre el layout de Views/Shared/_Layout.cshtml, lo que pasa es que en mi caso no tiene ningún html (simplemente un @RenderBody). Si lo modificas e incluyes por ejemplo una cabecera verás que se muestra con el formulario.

    ResponderEliminar