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
{
public interface IViewDataContainer
{
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.
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.
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
ResponderEliminarHola 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).
ResponderEliminarLa 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.