En este artículo mostraré cómo podemos gestionar los scripts de plantillas y vistas parciales sin cargar código de más en páginas que no lo necesitan ni repetir código en páginas que muestran varias veces una misma plantilla.
En el artículo anterior (
ASP.NET MVC. Platilla de editor para fecha y hora) mostré cómo crear editores personalizados para los tipos de datos de fecha y fecha/hora.
Al final del artículo se presentaban algunos problemas referentes a los scripts utilizados: el hecho de que el archivo javascript del plugin datetimepicker.js se carga en todas las páginas (sea o no necesario) y que por cada campo de texto creado en una página se ejecutaba el código de inicialización de todos los campos, lo que provocaba que se ejecutara el mismo código varias veces de forma innecesaria.
Comprendiendo el problema
Para mostrar el problema voy a continuar con el código del proyecto que creé en el artículo anterior.
En primer lugar voy a añadir dos nuevas propiedades a la clase Persona: el campo Fecha de Registro de tipo fecha y el campo Última Operación de tipo fecha/hora.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;
namespace MVCDateTimePicker.Models
{
public class Persona
{
public string Nombre { get; set; }
public string Apellido { get; set; }
public string Ciudad { get; set; }
[Display(Name="Fecha de Nacimiento")]
[DataType(DataType.Date)]
public DateTime FechaNacimiento { get; set; }
[Display(Name="Último Acceso")]
public DateTime UltimoAcceso { get; set; }
[DataType(DataType.Date)]
public DateTime FechaRegistro { get; set; }
[Display(Name = "Última Operación")]
public DateTime UltimaOperacion { get; set; }
}
}
Imports System.ComponentModel.DataAnnotations
Public Class Persona
Property Nombre As String
Property Apellido As String
Property Ciudad As String
<Display(Name:="Fecha de Nacimiento")> _
<DataType(DataType.Date)> _
Property FechaNacimiento As DateTime
<Display(Name:="Último Acceso")> _
Property UltimoAcceso As DateTime
<Display(Name:="Fecha de Registro")> _
<DataType(DataType.Date)> _
Property FechaRegistro As DateTime
<Display(Name:="Última Operación")> _
Property UltimaOperacion As DateTime
End Class
A continuación voy a quitar la referencia al plugin datetimepicker de la página de diseño principal y la incluirá en las plantillas de los editores para tener todo el código relacionado con cada editor en su propio fichero y evitar así que se incluya el código del plugin en las páginas en las que no sea necesario.
La cabecera del archivo
_Layout.cshtml/_Layout.vbhtml quedaría:
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
<link href="~/Content/jquery.datetimepicker.css" rel="stylesheet" />
<script src="~/Scripts/jquery-2.1.3.min.js"></script>
</head>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewData("Title")</title>
<link href="~/Content/jquery.datetimepicker.css" rel="stylesheet" />
<script src="~/Scripts/jquery-2.1.3.min.js"></script>
</head>
El editor
Date.cshtml/Date.vbhtml:
@model DateTime
<script src="~/Scripts/jquery.datetimepicker.js"></script>
<script>
$(function () {
$(".datepicker").datetimepicker({
lang: 'es',
format: 'd/m/Y',
formatDate: 'd/m/Y',
dayOfWeekStart: 1,
mask: true,
timepicker: false
});
});
</script>
@Html.TextBox("", (Model == DateTime.MinValue ? null : Model.ToString("dd/MM/yyyy"))
, new { @class = "datepicker" })
@ModelType DateTime
<script src="~/Scripts/jquery.datetimepicker.js"></script>
<script>
$(function () {
$(".datepicker").datetimepicker({
lang: 'es',
format: 'd/m/Y',
formatDate: 'd/m/Y',
dayOfWeekStart: 1,
mask: true,
timepicker: false
});
});
</script>
@Html.TextBox("", IIf(Model = DateTime.MinValue, Nothing, Model.ToString("dd/MM/yyyy")) _
, New With {.class = "datepicker"})
Y el editor
DateTime.cshtml/DateTime.vbhtml:
@model DateTime
<script src="~/Scripts/jquery.datetimepicker.js"></script>
<script>
$(function () {
$(".datetimepicker").datetimepicker({
lang: 'es',
format: 'd/m/Y H:i',
formatDate: 'd/m/Y',
dayOfWeekStart: 1,
mask: true
});
});
</script>
@Html.TextBox("", (Model == DateTime.MinValue ? null : Model.ToString("dd/MM/yyyy hh:mm"))
, new { @class = "datetimepicker" })
@ModelType DateTime
<script src="~/Scripts/jquery.datetimepicker.js"></script>
<script>
$(function () {
$(".datetimepicker").datetimepicker({
lang: 'es',
format: 'd/m/Y H:i',
formatDate: 'd/m/Y',
dayOfWeekStart: 1,
mask: true
});
});
</script>
@Html.TextBox("", IIf(Model = DateTime.MinValue, Nothing, Model.ToString("dd/MM/yyyy hh:mm")) _
, New With {.class = "datetimepicker"})
Si arrancamos la aplicación en el navegador y obtenemos el código HTML del formulario podemos ver cuál es el problema.
Como muestra la imagen, la referencia al plugin se repite por cada campo (hasta un total de 4 veces) y el código de inicialización de los controles también (2 para los de tipo
Date y 2 para los de tipo
DateTime).
Creando el Helper para gestionar los scripts
Para solucionar el problema voy a crear tres métodos de extensión de la clase HtmlHelper.
La idea es sencilla: ir incluyendo todos los scripts en una colección que controle que no se incluyan scripts repetidos y renderizarlos al final de la página. Para ello, voy a crear una nueva clase
ScriptHelper, en una nueva carpeta
Infrastructure en la que definiré tres métodos de extensión, uno para añadir referencias a archivos javascript (
AddScriptFile), otro para añadir código javascript (
AddScript) y un último método que se encargará de escribir los bloques script en la página (
RenderScripts).
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace MVCDateTimePicker.Infrastructure
{
public static class ScriptHelper
{
private const string scriptItemsKey = "ScriptBlocks";
private enum ScriptType { Code, File }
private class ScriptBlock
{
public ScriptType ScriptType { get; set; }
public string Script { get; set; }
}
private static Dictionary<string, ScriptBlock> ScriptCollection(HtmlHelper html)
{
HttpContextBase _httpContext = html.ViewContext.HttpContext;
if (!_httpContext.Items.Contains(scriptItemsKey))
_httpContext.Items[scriptItemsKey] = new Dictionary<string, ScriptBlock>();
return (Dictionary<string, ScriptBlock>)_httpContext.Items[scriptItemsKey];
}
public static MvcHtmlString AddScript(this HtmlHelper html, string key, string scriptCode)
{
Dictionary<string, ScriptBlock> scripts = ScriptCollection(html);
if (!scripts.ContainsKey(key))
scripts.Add(key
, new ScriptBlock { ScriptType = ScriptType.Code, Script = scriptCode });
return MvcHtmlString.Empty;
}
public static MvcHtmlString AddScriptFile(this HtmlHelper html, string key, string scriptFile)
{
Dictionary<string, ScriptBlock> scripts = ScriptCollection(html);
if (!scripts.ContainsKey(key))
scripts.Add(key
, new ScriptBlock { ScriptType = ScriptType.File, Script = scriptFile });
return MvcHtmlString.Empty;
}
public static MvcHtmlString RenderScripts(this HtmlHelper html)
{
Dictionary<string, ScriptBlock> scripts = ScriptCollection(html);
string result = string.Empty;
foreach (ScriptBlock item in scripts.Values)
{
TagBuilder scriptTag = new TagBuilder("script");
scriptTag.MergeAttribute("type", "text/javascript");
if (item.ScriptType == ScriptType.File)
scriptTag.MergeAttribute("src", UrlHelper.GenerateContentUrl(item.Script, html.ViewContext.HttpContext));
else
scriptTag.InnerHtml = Environment.NewLine + item.Script + Environment.NewLine;
result += scriptTag.ToString(TagRenderMode.Normal) + Environment.NewLine;
}
html.ViewContext.HttpContext.Items.Remove(scriptItemsKey);
return MvcHtmlString.Create(result);
}
}
}
Imports System.Web.Mvc
Imports System.Runtime.CompilerServices
Public Module ScriptHelper
Private Const ScriptItemsKey As String = "ScriptBlocks"
Private Enum ScriptType
Code
File
End Enum
Private Class ScriptBlock
Property ScriptType As ScriptType
Property Script As String
End Class
Private Function ScriptCollection(html As HtmlHelper) As Dictionary(Of String, ScriptBlock)
Dim _httpContext As HttpContextBase = html.ViewContext.HttpContext
If Not _httpContext.Items.Contains(ScriptItemsKey) Then
_httpContext.Items(ScriptItemsKey) = New Dictionary(Of String, ScriptBlock)
End If
Return CType(_httpContext.Items(ScriptItemsKey), Dictionary(Of String, ScriptBlock))
End Function
<Extension> _
Public Function AddScript(html As HtmlHelper, key As String, scriptCode As String) As MvcHtmlString
Dim scripts As Dictionary(Of String, ScriptBlock) = ScriptCollection(html)
If Not scripts.ContainsKey(key) Then
scripts.Add(key _
, New ScriptBlock() With {.ScriptType = ScriptType.Code, .Script = scriptCode})
End If
Return MvcHtmlString.Empty
End Function
<Extension> _
Public Function AddScriptFile(html As HtmlHelper, key As String, scriptFile As String) As MvcHtmlString
Dim scripts As Dictionary(Of String, ScriptBlock) = ScriptCollection(html)
If Not scripts.ContainsKey(key) Then
scripts.Add(key _
, New ScriptBlock() With {.ScriptType = ScriptType.File, .Script = scriptFile})
End If
Return MvcHtmlString.Empty
End Function
<Extension> _
Public Function RenderScripts(html As HtmlHelper) As MvcHtmlString
Dim scripts As Dictionary(Of String, ScriptBlock) = ScriptCollection(html)
Dim result As String = String.Empty
For Each item As ScriptBlock In scripts.Values
Dim scriptTag As TagBuilder = New TagBuilder("script")
scriptTag.MergeAttribute("type", "text/javascript")
If item.ScriptType = ScriptType.File Then
scriptTag.MergeAttribute("src", UrlHelper.GenerateContentUrl(item.Script, html.ViewContext.HttpContext))
Else
scriptTag.InnerHtml = Environment.NewLine & item.Script & Environment.NewLine
End If
result = result & scriptTag.ToString(TagRenderMode.Normal) & Environment.NewLine
Next
html.ViewContext.HttpContext.Items.Remove(ScriptItemsKey)
Return MvcHtmlString.Create(result)
End Function
End Module
Los métodos
AddScript y
AddScriptFile crean un objecto
ScriptBlock, cada uno de un tipo diferente, y lo incluyen en una colección
Dictionary. A cada objecto
ScriptBlock le asignamos una key que nos permite evitar incluir en la colección objetos repetidos.
Esta colección
Dictionary se almacena en la colección
Items del objecto
HttpContext al que tenemos acceso a través de la propiedad
ViewContext del
HtmlHelper.
Finalmente el método
RenderScripts genera el código html a enviar a la página para cada uno de los elementos
ScriptBlock de la colección.
Para probarlo voy a incluir una llamada al método
RenderScripts al final de la página principal de diseño (
_Layout.cshtml/_Layout.vbhtml) que se encargará de generar el código de todos los scripts que se hayan ido añadiendo a lo largo del procesamiento de la vista.
@using MVCDateTimePicker.Infrastructure
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
<link href="~/Content/jquery.datetimepicker.css" rel="stylesheet" />
<script src="~/Scripts/jquery-2.1.3.min.js"></script>
</head>
<body>
<div>
@RenderBody()
</div>
@Html.RenderScripts()
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewData("Title")</title>
<link href="~/Content/jquery.datetimepicker.css" rel="stylesheet" />
<script src="~/Scripts/jquery-2.1.3.min.js"></script>
</head>
<body>
<div>
@RenderBody()
</div>
@Html.RenderScripts()
</body>
</html>
Y por último sólo queda modificar las plantillas de editor para incluir los scripts a través de los nuevos métodos
AddScript y
AddScriptFile.
La plantilla
Date.cshtml/Date.vbhtml:
@using MVCDateTimePicker.Infrastructure
@model DateTime
@Html.AddScriptFile("datetimepicker_plugin", "~/Scripts/jquery.datetimepicker.js")
@Html.AddScript("init_Date_Editor"
, @" $(function () {
$('.datepicker').datetimepicker({
lang: 'es',
format: 'd/m/Y',
formatDate: 'd/m/Y',
dayOfWeekStart: 1,
mask: true,
timepicker: false
});
});")
@Html.TextBox("", (Model == DateTime.MinValue ? null : Model.ToString("dd/MM/yyyy"))
, new { @class = "datepicker" })
@ModelType DateTime
@Html.AddScriptFile("datetimepicker_plugin", "~/Scripts/jquery.datetimepicker.js")
@Html.AddScript("init_Date_Editor" _
, "$(function () {" & vbCrLf &
"$('.datepicker').datetimepicker({" & vbCrLf &
"lang: 'es'," & vbCrLf &
"format: 'd/m/Y'," & vbCrLf &
"formatDate: 'd/m/Y'," & vbCrLf &
"dayOfWeekStart: 1," & vbCrLf &
"mask: true," & vbCrLf &
"timepicker: false" & vbCrLf &
"});" & vbCrLf &
"});" & vbCrLf)
@Html.TextBox("", IIf(Model = DateTime.MinValue, Nothing, Model.ToString("dd/MM/yyyy")) _
, New With {.class = "datepicker"})
Y la plantilla
DateTime.cshtml/DateTime.vbhtml:
@using MVCDateTimePicker.Infrastructure
@model DateTime
@Html.AddScriptFile("datetimepicker_plugin", "~/Scripts/jquery.datetimepicker.js")
@Html.AddScript("init_DateTime_Editor"
, @" $(function () {
$('.datetimepicker').datetimepicker({
lang: 'es',
format: 'd/m/Y H:i',
formatDate: 'd/m/Y',
dayOfWeekStart: 1,
mask: true
});
});")
@Html.TextBox("", (Model == DateTime.MinValue ? null : Model.ToString("dd/MM/yyyy hh:mm"))
, new { @class = "datetimepicker" })
@ModelType DateTime
@Html.AddScriptFile("datetimepicker_plugin", "~/Scripts/jquery.datetimepicker.js")
@Html.AddScript("init_DateTime_Editor" _
, "$(function () {" & vbCrLf &
"$('.datetimepicker').datetimepicker({" & vbCrLf &
"lang: 'es'," & vbCrLf &
"format: 'd/m/Y H:i'," & vbCrLf &
"formatDate: 'd/m/Y'," & vbCrLf &
"dayOfWeekStart: 1," & vbCrLf &
"mask: true" & vbCrLf &
"});" & vbCrLf &
"});" & vbCrLf)
@Html.TextBox("", IIf(Model = DateTime.MinValue, Nothing, Model.ToString("dd/MM/yyyy hh:mm")) _
, New With {.class = "datetimepicker"})
Ahora si arrancamos la aplicación y comprobamos el código HTML generado por el formulario veremos que los scripts se agrupan al final de la página y sin repetirse ninguno de los bloques:
El código completo de este artículo y el anterior (
ASP.NET MVC. Platilla de editor para fecha y hora), tanto en C# como en Visual Basic, está disponible en:
Códigos de muestra - Ejemplos MSDN
Muy interesante. Para el caso de helpers personalizados, el registro de scripts seria con la misma clase ScriptHelper? Podria incluirse el script ahi, sin necesidad de picarlo en la vista? Como podria hacerlo para ponerlo en la clase HtmlHelper de la que extiendo y donde escribo mi helper?
ResponderEliminarMuchas preguntas?
Gracias de antemano
Hola PIMPROYECT.
EliminarEfectivamente, aunque la clase ScriptHelper está pensada para evitar repeticiones de scripts utilizados en plantillas y vistas parciales, se puede utilizar en cualquier parte del código para registrar tanto archivos javascript a incluir como bloques de código javascript.
La clase, o el módulo si trabajas en VB, puedes crearlo en cualquier ubicación dentro de tu proyecto o en una librería externa. En este segundo caso simplemente tendrías que tener en tu proyecto web una referencia a la librería.
Para poder utilizarlo en cualquier parte de tu proyecto simplemente deberías incluir una instrucción using para el namespace de la clase Helper.
No sé si he conseguido aclarar las dudas o simplemente liarte más...
Cuando disponga de algo de tiempo haré unas pruebas, muchas gracias por tu respuesta
EliminarUn gran post, me ayudo bastante en el uso de vistas parciales, estuve buen tiempo tratando de encontrar esta solución, muchas gracias ... Y a seguir desarrollando :D
ResponderEliminar