viernes, 22 de abril de 2011

Creando un control de fechas con calendario desplegable usando CalendarExtender del AjaxControlToolkit


En esta publicación, vamos a ver cómo podemos crear un control de usuario para el ingreso de fechas, con un calendario desplegable, utilizando los componentes del AJAX control toolkit.
El objetivo, es tener un control donde se pueda ingresar una fecha, tanto por teclado en un campo de texto con máscara, como seleccionando la misma en un calendario desplegable, y utilizando validadores del lado del cliente y del servidor para validar la fecha y mostrar los mensajes de error. 



Empecemos por crear una nueva solución en Visual Studio y agregar un proyecto de tipo ASP.NET Web Application llamado “CalendarControlExample”. Luego, agregamos una referencia a la dll del Ajax control toolkit. Y agregamos un nuevo control de usuario llamado “DateTimePicker”.
También creamos una carpeta llamada “css” y dentro de la misma copiamos el archivo de imagen para la imagen del calendario del control y una hoja de estilo llamada “DateTimePicker.css”.

Continuemos armando el control. En el ascx, creamos un DIV y dentro agregamos:
Un control textbox con las siguientes propiedades:
        ID="txtDateTime"
            Width=”130px”
;
Un control ImageButton con las siguientes propiedades:
ID="ImgBntCalc"
ImageUrl="/css/calendar_icon.jpg"

Un control MaskedEditExtender con las siguientes propiedades:
ID="dateMaskedEditExtender"
TargetControlID="txtDateTime"
Mask="99/99/9999"
MessageValidatorTip="true"
MaskType="Date"
ErrorTooltipEnabled="True"


Un control MaskedEditValidator con las siguientes propiedades:
            ID="dateMaskedEditValidator"
            ControlExtender="dateMaskedEditExtender"
            ControlToValidate="txtDateTime"
            EmptyValueMessage="Date is required"
            InvalidValueMessage="Date is invalid"
            Display="Dynamic"
            TooltipMessage=""

Para hacer más llamativa la interfaz de usuario, usaremos un ValidatorCalloutExtender asociado al MaskedEditValidator para mostrar los mensajes de error en un popup:
            ID="dateMaskedEditValidator_vce"
            TargetControlID="dateMaskedEditValidator"
            Enabled="True"

Por último agregamos el  CalendarExtender:
            ID="cexCalendarExtender"
            TargetControlID="txtDateTime"
            CssClass="cal_Theme1"
            PopupButtonID="ImgBntCalc"

En este punto ya podemos correr la aplicación y probar nuestro control. Para eso agregamos el control a la página Default.aspx que usaremos como página de prueba.

Lo primero que pueden notar, si tienen la configuración regional de Windows en español, es que el calendario se muestra en inglés, y la fecha seleccionada también adopta el formato mm/dd/aaaa pero el validador, sin embargo, la detecta como inválida. 

Esto se debe a que ambos toman la configuración de cultura de diferentes lugares al no estar definidas las opciones de localización. Vamos a agregar entonces una propiedad al control para definirle la cultura que debe usar  y en el Load, asignaremos al calendar extender y al masked edit dicha cultura para evitar este error.


public CultureInfo Culture { get; set; }

protected void Page_Load(object sender, EventArgs e)
{
    if (this.Culture == null)
         this.Culture = CultureInfo.CurrentCulture;

    this.dateMaskedEditExtender.CultureName = this.Culture.Name
    this.cexCalendarExtender.Format = this.Culture.DateTimeFormat.ShortDatePattern;
}


Además, debemos asegurarnos de agregar estos dos atributos en el  ToolkitScriptManager

<ajaxtoolkit:ToolkitScriptManager ID="ToolkitScriptManager1" runat="server" EnableScriptGlobalization="true" EnableScriptLocalization="true" 
/>

Ahora vamos a agregar algunas propiedades adicionales para completar el control:
        public string ValidationGroup
        {
            get { return this.txtDateTime.ValidationGroup; }
            set
            {
                this.txtDateTime.ValidationGroup = value;
                this.dateMaskedEditValidator.ValidationGroup = value;
            }
        }
  public bool IsRequired
        {
            set { this.dateMaskedEditValidator.IsValidEmpty = !value; }
        }

        public bool Enabled
        {
            get { return this.txtDateTime.Enabled; }
            set
            {
                this.txtDateTime.Enabled = value;
                this.cexCalendarExtender.Enabled = value;
                this.cexCalendarExtender.EnabledOnClient = value;
                this.ImgBntCalc.Enabled = value;
            }
        }
        public DateTime Value
        {
            get
            {
                if (String.IsNullOrEmpty(this.txtDateTime.Text))
                {
                    return DateTime.MinValue;
                }
                else
                {
                    DateTime date;
                    DateTime.TryParse(this.txtDateTime.Text, out date);
                    return date;
                }
            }
            set
            {
                this.txtDateTime.Text = value.ToShortDateString();
            }
        }

Ahora, vamos a agregar un botón a la página y una Label para ver cómo se recupera del lado del servidor el valor seleccionado.  En el Load de la página de prueba, simplemente agregamos lo siguiente:

        this.Label1.Text = this.DateTimePicker1.Value.ToString();

y ejecutamos la aplicación. Al seleccionar una fecha en el calendario o al ingresar a mano una fecha en el cuadro de texto, y al hacer click en el botón, luego del postback, el label mostrará la fecha ingresada.
Bien, ahora supongamos que queremos validar que el usuario no ingrese una fecha menor al 01/01/1950. Agregamos este valor como el valor mínimo en el MaskedEdit:

<ajaxtoolkit:MaskedEditValidator ID="dateMaskedEditValidator" runat="server"                     ControlExtender="dateMaskedEditExtender" ControlToValidate="txtDateTime" EmptyValueMessage="Date is required" InvalidValueMessage="Date is invalid"
        Display="Dynamic" TooltipMessage="" MinimumValue="01/01/1950" />

Como vemos si ingresamos un valor menor al 1950, digamos 12/03/1834, el validador valida correctamente:

Pero si ingresamos un año que comience con “00” como se ve en la imagen, el validador, no lo valida y permite hacer el postback, pudiendo tomar dicho valor en el lado del servidor. Luego del postback el control recupera el valor en la interfaz de usuario, pero cambia el siglo por el siglo 1900.




Este es un error del control y para evitarlo, utilizaremos un simple script de javascript para que cuando el usuario ingrese un año que comience con “00”, este sea reemplazado por la menor fecha aceptada por el control


    function DateText_EditClientValidation(sender, args) {
        var year = args.Value.split("/")[2];
        if (year.substr(0, 2) == "00") {
            $get("<%=this.txtDateTime.ClientID %>").value = "<%= this.dateMaskedEditValidator.MinimumValue %>";
        }
    }

Agregaremos este script por código. Para esto, en el Load agregamos las siguientes lineas de código:
        protected void Page_Load(object sender, EventArgs e)
        {
            if (this.Culture == null)
                this.Culture = CultureInfo.CurrentCulture;

            this.dateMaskedEditExtender.CultureName = this.Culture.Name;
            this.cexCalendarExtender.Format = this.Culture.DateTimeFormat.ShortDatePattern;

            var csType = this.GetType();
            var csName = this.ClientID + "_ClientValidator";

            var sb = new StringBuilder();

            sb.AppendLine("<script type=\"text/javascript\">");
            sb.AppendLine("function " + csName + "(sender, args) {");
            sb.AppendLine("var year = args.Value.split('/')[2];");
            sb.AppendLine("if (year.substr(0, 2) == '00')");
            sb.AppendLine(" $get('" + this.txtDateTime.ClientID + "').value = '" + this.dateMaskedEditValidator.MinimumValue + "';");
            sb.AppendLine("}");
            sb.AppendLine("</script>");

            ScriptManager.RegisterClientScriptBlock(this, csType, csName, sb.ToString(), false);

            this.dateMaskedEditValidator.ClientValidationFunction = csName;
        }

Esto añadirá el script a la página y hará que se ejecute cada vez que se ingrese una fecha.
Si al ejecutar la aplicación y al cambiar la fecha, les da este error:

Microsoft JScript runtime error: 'value' is undefined

Deberán agregar este atributo al ToolkitScriptManager

<ajaxToolkit:ToolkitScriptManager runat="server" ID="ScriptManager1" CombineScripts="false"
/>

Esto también se debe a un error del control y el workaround es no utilizar la versión minimizada de los scripts.
Con esto terminamos el control, con las funcionalidades básicas.

Referencias a los bugs mencionados:

2 comentarios:

  1. me pasas el codigo que falta.. como el css por ejemplo...
    estoy con trabajando sobre vb y ya se complica un poco

    ResponderEliminar
    Respuestas
    1. Hola. Lamentablemente no tengo más el código de este ejemplo. El aspecto visual del control se puede modificar sobreescribiendo los estilos por defecto en tu propia hoja de estilos. Más información acá ->
      http://www.asp.net/AjaxLibrary/AjaxControlToolkitSampleSite/Calendar/Calendar.aspx.

      Justamente la hoja que yo incluía redefinía muchos de estos estilos para darle el aspecto que se ve en la captura. Si buscás hay css disponibles para bajar y darle estilos más vistosos al Calendar del AjaxControlToolkit.
      Creo que los estilos del ejemplo están tomados de acá:
      http://www.ezineasp.net/post/Customized-AJAX-Calendar-Control-CSS-Example.aspx.
      Espero te sirva.
      Saludos!

      Eliminar