martes, 7 de abril de 2015

ASP.NET MVC. Plantilla de editor para fecha y hora

En este artículo voy a mostrar cómo crear en un proyecto MVC un editor personalizado para los tipos de datos de fecha y fecha y hora. El editor se basa en el plugin DateTimePicker de jQuery.

Creando el proyecto de ejemplo

Para realizar el ejemplo voy a crear un nueva Aplicación ASP.NET MVCDateTimePicker en nuestro Visual Studio, seleccionando la plantilla vacía y marcando el check para incluir las referencias a MVC.


Crear proyecto MVCDateTimePicker

Seleccionar plantilla vacía con MVC

En el proyecto de prueba he añadido una clase Persona en la carpeta Models. Para el ejemplo he creado dos propiedades de tipo fecha: FechaNacimiento y UltimoAcceso. La propiedad FechaNacimiento la he decorado con un atributo DataType para especificar que el tipo de datos es de sólo fecha (sin hora). De esta forma tendremos una propiedad de tipo fecha y otra de tipo fecha/hora con las que poder probar nuestras plantillas.

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; }
    }
}
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
End Class

En la carpeta Shared dentro de Views he creado una nueva página de diseño (layout) _Layout.cshtml/_Layout.vbhtml.
<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
</head>
<body>
    <div>
        @RenderBody()
    </div>
</body>
</html>
<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewData("Title")</title>
</head>
<body>
    <div>
        @RenderBody()
    </div>
</body>
</html>

En la carpeta Controllers he creado un controlador HomeController con una única acción Index que se encarga de enviar una instancia de la clase Persona a la vista.

using MVCDateTimePicker.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace MVCDateTimePicker.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index(Persona persona)
        {
            return View(persona);
        }
    }
}
Imports System.Web.Mvc

Namespace Controllers
    Public Class HomeController
        Inherits Controller

        Function Index(persona As Persona) As ActionResult
            Return View(persona)
        End Function
    End Class
End Namespace

Para terminar he creado la vista Index para la acción del controlador en la carpeta Views. La vista utiliza la clase Persona como modelo y genera un formulario para el objeto recibido. He añadido un botón Validar para devolver los datos al servidor y que se ejecuten las correspondientes validaciones.

Crear vista Index con Persona como clase de modelo

@model MVCDateTimePicker.Models.Persona

@using (Html.BeginForm())
{
    @Html.EditorForModel()
    <br/>
    <input type="submit" value="Validar" />
}
@ModelType MVCDateTimePicker.Persona

@Using (Html.BeginForm())
    @Html.EditorForModel()
    @:<br />
    @:<input type="submit" value="Validar" />
End Using

Si compilamos y arrancamos nuestra aplicación podremos ver el problema: no sólo el editor generado para los campos de tipo fecha resulta pobre, sino que además se muestra un editor diferente dependiendo del tipo de datos y del navegador utilizado. En la siguiente imagen podemos ver el interfaz generado por los navegadores Chrome 41.0 (a la izquierda) e Internet Explorer 10 (a la derecha).

Interfaz generada por Chrome e Internet Explorer

Creando las plantillas para fechas


Voy a basar las plantillas en el plugin DateTimePicker de jQuery. Para ello lo primero que necesitaremos es tener las librerías de jQuery incluidas en el proyecto. Podemos descargarlas de la página de jQuery o incluirlas a través de la administración de paquetes NuGet.

Pulsando con el botón derecho sobre el proyecto en la ventana Explorador de soluciones seleccionamos la opción Administrar Paquetes NuGet.... En la ventana de administración de paquetes seleccionamos la opción En línea (a la derecha), buscamos el paquete correspondiente a jQuery y pulsamos en Instalar:

Añadir paquete NuGet jQuery

A continuación descargamos el plugin DateTimePicker desde GitHub e incluimos el archivo jquery.datetimepicker.js en la carpeta Scripts y el archivo jquery.datetimepicker.css en la carpeta Content.

Añadimos en la página de diseño principal de la aplicación (_Layout.cshtml/_Layout.vbhtml) una referencia tanto a los dos ficheros del plugin como a la librería jQuery en la sección head.

<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>
    <script src="~/Scripts/jquery.datetimepicker.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>
    <script src="~/Scripts/jquery.datetimepicker.js"></script>
</head>



Por convención los editores de MVC para cada tipo de datos se guardan en la carpeta Views/Shared/EditorTemplates con el nombre del tipo de datos para el que es el editor.

Así si queremos crear un editor para el tipo de datos DateTime crearemos una vista parcial DateTime.cshtml/DateTime.vbhtml en la carpeta Views/Shared/EditorTemplates. Y para la propiedad a la que hemos establecido su tipo de datos como DataType.Date a través del atributo DataTypeAttribute deberemos crear una vista Date.cshtml/Date.vbhtml.

Crear Vista Parcial para editor DateTime


 Éste es el código del archivo DateTime.cshtml/DateTime.vbhtml.

@model DateTime

<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>
    $(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"})

En el editor utilizo el método TextBox del HtmlHelper para generar un elemento html del tipo input type="text" en lugar del input type="datetime" que genera el editor por defecto del tipo DateTime. De esta forma evito que el navegador intente utilizar su propia implementación para la edición de fechas.

Al método TextBox le pasó tres parámetros:

  • El primero establece el nombre del elemento html. Como MVC ya le va a asignar el nombre al elemento en función del nombre de la propiedad a la que se enlace, lo dejo vacío.
  • El segundo establece el valor del elemento. Paso el valor del modelo, con el mismo formato que utilizo en la inicialización del datetimepicker, salvo que el valor sea DateTime.MinValue, en tal caso dejo el valor vacío.
  • Por último le asigno el estilo datetimepicker a través del atributo class para poder identificar los controles a los que aplicar el plugin.
El script de javascript simplemente selecciona los elementos del tipo datetimepicker e inicializa el plugin con una serie de valores iniciales. Se puede consultar la lista completa de opciones de personalización en la documentación del plugin.

Para el editor para el tipo de datos Date simplemente voy a cambiar el formato de visualización de la fecha (tanto en la creación del TextBox como en la inicialización del plugin). Además he cambiado el estilo del control por datepicker y he añadido un nuevo valor de inicialización al plugin para que no muestre la selección de horas: 

@model DateTime

<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>
    $(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"})

Si trabajas con Visual Basic .Net, en Visual Studio 2013, si se intentas añadir la vista Date a través del formulario de Scaffolding recibirás un error indicando que no se puede crear porque Date es una palabra reservada. Puedes crear la vista utilizando las plantillas de Visual Studio o creándola con otro nombre y modificándolo después. Este es un error que, por lo que he visto, persiste en la versión preview de Visual Studio 2015.

Si arrancamos la aplicación veremos cómo se ha modificado el interfaz de edición de fechas, y éste es independiente del navegador.

Editor de fechas personalizado

Cuestiones pendientes


Hemos creado los editores para fecha y fecha/hora. Sin embargo la solución desarrollada presenta algunos problemas. Para empezar he incluido la referencia al plugin datetimepicker en la página de diseño de la aplicación (_Layout.cshtml/_Layout.vbhtml) por lo que el archivo se carga en todas las páginas de la aplicación, independientemente de si existen controles de fecha o no.

El segundo problema también tiene que ver con el código javascript y es más grave aún. En cada uno de los editores he incluido código para inicializar los controles de edición de fechas. En una sola instrucción inicializa todos los controles del mismo tipo que haya en la página. Sin embargo, si existen varios controles del mismo tipo, la instrucción se cargará y ejecutará tantas veces como controles existan en la página. Evidentemente no es una solución nada óptima.

Para solucionar este segundo problema podría meter las dos instrucciones de los editores en un archivo js e incluirlo en la página de diseño principal. De esta forma se ejecutarían una única vez en todas las páginas, independientemente de que existan controles o no. Esta solución también tiene otro problema, dividimos la funcionalidad de los editores en diferentes ubicaciones del proyecto, con lo que esto penaliza a la mantenibilidad del código (no hay más que pensar en las modificaciones a realizar si necesitamos cambiar los nombres de los estilos utilizados).

Estos problemas pueden resultar asumibles en sitios pequeños pero, en grandes proyectos con cientos de scripts, pueden desembocar en un problema importante de rendimiento y mantenibilidad.

En el próximo artículo (ASP.NET MVC. Gestión de Scripts en Plantillas y Vistas Parciales) explicaré cómo podemos resolver estos problemas, consiguiendo que los scripts se carguen únicamente en las pantallas en las que son necesarios y ejecutándose una única vez.

El código completo de este artículo y el anterior (ASP.NET MVC. Gestión de Scripts en Plantillas y Vistas Parciales), tanto en C# como en Visual Basic, está disponible en:

Códigos de muestra - Ejemplos MSDN

2 comentarios:

  1. Hola Buen día, y para hacerlo funcionar en una pagina que no es MVC
    como lo debo llamar en el Diseño o en el Script

    ResponderEliminar
  2. ¿A qué te refieres con "una página que no es MVC"? Si te refieres a utilizar el plugin DateTimePicker en una página WebForms puedes consultar el artículo: Integrating JQuery UI Datepicker with model binding and web forms

    ResponderEliminar