viernes, 24 de abril de 2015

Windows Forms. DataGridView. Columna con editor de fechas.

El DataGridView es un control con funcionalidad básica que permite una gran extensibilidad. En este artículo mostraré como crear una columna para datos de tipo fecha que utilice un DateTimePicker como editor.


Para crear cualquier columna personalizada en un DataGridView básicamente deberemos trabajar con 3 clases.

Una clase que implemente la interfaz IDataGridViewEditingControl que define el control que utilizaremos para editar los datos.

Una clase que herede de DataGridViewCell que define la plantilla de las celdas de la columna y que se encargará de inicializar el control de edición y de obtener, devolver y formatear los valores.

Una clase que herede de DataGridViewColumn que representará la columna y utilizará la clase de celda como plantilla.

Creando el proyecto de ejemplo

Voy a crear un nuevo proyecto GridExtension del tipo Biblioteca de Clases.

Crear proyecto de Biblioteca de Clases

Al proyecto le agregaré una referencia al ensamblado System.Windows.Forms y System.Drawing.

Referencia a System.Windows.Forms

Al proyecto le voy a añadir un único archivo DateTimeGridColumn.cs/DateTimeGridColumn.vb en la que incluiré las tres clases necesarias para definir la columna.

Creando el editor de fechas


Para generar el control de edición he creado una clase DateTimeEditingControl que hereda de la clase DateTimePicker (control que utilizaré para editar los valores de las celdas) y que implementa la interfaz IDataGridViewEditingControl.

    class DateTimeEditingControl : DateTimePicker, IDataGridViewEditingControl
    {

        private DataGridView grid;
        private bool valueChanged;

        public DateTimeEditingControl()
        {
            this.Format = DateTimePickerFormat.Custom;
        }

        protected override void OnValueChanged(EventArgs eventargs)
        {
            base.OnValueChanged(eventargs);
            SendToGridValueChanged();
        }

        private void SendToGridValueChanged()
        {
            valueChanged = true;
            if (grid != null)
                grid.NotifyCurrentCellDirty(true);
        }

        #region Miembros de IDataGridViewEditingControl

        public void ApplyCellStyleToEditingControl(DataGridViewCellStyle dataGridViewCellStyle)
        {
            this.Font = dataGridViewCellStyle.Font;
            this.CalendarForeColor = dataGridViewCellStyle.ForeColor;
            this.CalendarMonthBackground = dataGridViewCellStyle.BackColor;
        }

        public DataGridView EditingControlDataGridView
        {
            get { return grid; }
            set { grid = value; }
        }

        public object EditingControlFormattedValue
        {
            get
            {
                return this.Value.ToString(this.CustomFormat);
            }
            set
            {
                try { this.Value = DateTime.Parse((string)value); }
                catch { this.Value = DateTime.Now; }
                SendToGridValueChanged();
            }
        }

        public int EditingControlRowIndex { get; set; }

        public bool EditingControlValueChanged
        {
            get { return valueChanged; }
            set { valueChanged = value; }
        }

        public bool EditingControlWantsInputKey(Keys keyData, bool dataGridViewWantsInputKey)
        {
            switch (keyData & Keys.KeyCode)
            {
                case Keys.Left:
                case Keys.Up:
                case Keys.Down:
                case Keys.Right:
                case Keys.Home:
                case Keys.End:
                case Keys.PageDown:
                case Keys.PageUp:
                    return true;
                default:
                    return !dataGridViewWantsInputKey;
            }
        }

        public Cursor EditingPanelCursor
        {
            get { return base.Cursor; }
        }

        public object GetEditingControlFormattedValue(DataGridViewDataErrorContexts context)
        {
            return EditingControlFormattedValue;
        }

        public void PrepareEditingControlForEdit(bool selectAll)
        {
        }

        public bool RepositionEditingControlOnValueChange
        {
            get { return false; }
        }

        #endregion

    }
Class DateTimeEditingControl
    Inherits DateTimePicker
    Implements IDataGridViewEditingControl

    Private grid As DataGridView
    Private valChanged As Boolean

    Public Sub New()
        Me.Format = DateTimePickerFormat.Custom
    End Sub

    Protected Overrides Sub OnValueChanged(eventargs As EventArgs)
        MyBase.OnValueChanged(eventargs)
        SendToGridValueChanged()
    End Sub

    Private Sub SendToGridValueChanged()
        valChanged = True
        If grid IsNot Nothing Then
            grid.NotifyCurrentCellDirty(True)
        End If
    End Sub

#Region "Miembros de IDataGridViewEditingControl"

    Public Sub ApplyCellStyleToEditingControl(dataGridViewCellStyle As DataGridViewCellStyle) _
        Implements IDataGridViewEditingControl.ApplyCellStyleToEditingControl
        Me.Font = dataGridViewCellStyle.Font
        Me.CalendarForeColor = dataGridViewCellStyle.ForeColor
        Me.CalendarMonthBackground = dataGridViewCellStyle.BackColor
    End Sub

    Public Property EditingControlDataGridView As DataGridView _
        Implements IDataGridViewEditingControl.EditingControlDataGridView
        Get
            Return grid
        End Get
        Set(value As DataGridView)
            grid = value
        End Set
    End Property

    Public Property EditingControlFormattedValue As Object _
        Implements IDataGridViewEditingControl.EditingControlFormattedValue
        Get
            Return Me.Value.ToString(Me.CustomFormat)
        End Get
        Set(value As Object)
            Try
                Me.Value = DateTime.Parse(CType(value, String))
            Catch ex As Exception
                Me.Value = DateTime.Now
            End Try
            SendToGridValueChanged()
        End Set
    End Property

    Public Property EditingControlRowIndex As Integer _
        Implements IDataGridViewEditingControl.EditingControlRowIndex

    Public Property EditingControlValueChanged As Boolean _
        Implements IDataGridViewEditingControl.EditingControlValueChanged
        Get
            Return valChanged
        End Get
        Set(value As Boolean)
            valChanged = value
        End Set
    End Property

    Public Function EditingControlWantsInputKey(keyData As Keys, dataGridViewWantsInputKey As Boolean) As Boolean _
        Implements IDataGridViewEditingControl.EditingControlWantsInputKey
        Select Case keyData And Keys.KeyCode
            Case Keys.Left, Keys.Up, Keys.Down, Keys.Right, _
                Keys.Home, Keys.End, Keys.PageDown, Keys.PageUp
                Return True
            Case Else
                Return Not dataGridViewWantsInputKey
        End Select
    End Function

    Public ReadOnly Property EditingPanelCursor As Cursor _
        Implements IDataGridViewEditingControl.EditingPanelCursor
        Get
            Return MyBase.Cursor
        End Get
    End Property

    Public Function GetEditingControlFormattedValue(context As DataGridViewDataErrorContexts) As Object _
        Implements IDataGridViewEditingControl.GetEditingControlFormattedValue
        Return EditingControlFormattedValue
    End Function

    Public Sub PrepareEditingControlForEdit(selectAll As Boolean) _
        Implements IDataGridViewEditingControl.PrepareEditingControlForEdit

    End Sub

    Public ReadOnly Property RepositionEditingControlOnValueChange As Boolean _
        Implements IDataGridViewEditingControl.RepositionEditingControlOnValueChange
        Get
            Return False
        End Get
    End Property

#End Region

End Class



Esta clase contiene la mayor parte del código del ejemplo ya que implementa la lógica de edición. Así que, como diría Jack el Destripador: vamos por partes.

En el constructor establecemos la propiedad Format del DateTimePicker a DateTimePickerFormat.Custom ya que estableceremos el formato de la fecha a partir del formato definido en la columna.

Sobrescribimos el método OnValueChanged para notificar al DataGridView que se ha modificado el contenido de la celda a través del método NotifyCurrentCellDirty.

 A continuación implementamos la interfaz IDataGridViewEditingControl.

En el método ApplyCellStyleToEditingControl trasladamos el estilo definido para la celda a las propiedades del DateTimePicker.

En la propiedad EditingControlFormattedValue se realiza la conversión entre el valor tipo DateTime de la celda y el string a visualizar.

En el método EditingControlWantsInputKey se establecen las teclas que necesita gestionar el control DateTimePicker durante la edición del valor.

Definiendo el tipo de celda


A continuación he definido la clase DateTimeCell que hereda de DataGridViewTextBoxCell para utilizar como plantilla de celda en la columna:

    public class DateTimeCell: DataGridViewTextBoxCell
    {

        public DateTimeCell() : base() { }

        public override Type EditType {
            get { return typeof(DateTimeEditingControl); } 
        }

        public override Type ValueType
        {
            get { return typeof(DateTime); }
        }

        public override object DefaultNewRowValue
        {
            get
            {
                object defaultValue = base.DefaultNewRowValue;
                if (defaultValue is DateTime)
                    return defaultValue;
                else
                    return DateTime.Now;
            }
        }

        public override void InitializeEditingControl(int rowIndex, object initialFormattedValue, DataGridViewCellStyle dataGridViewCellStyle)
        {
            base.InitializeEditingControl(rowIndex, initialFormattedValue, dataGridViewCellStyle);
            DateTimeEditingControl ctl = DataGridView.EditingControl as DateTimeEditingControl;
            try
            {
                if (this.Value == null)
                    ctl.Value = (DateTime)this.DefaultNewRowValue;
                else
                    ctl.Value = (DateTime)this.Value;

            }
            catch (Exception)
            {
                ctl.Value = (DateTime)this.DefaultNewRowValue;
            }

            if (dataGridViewCellStyle.Format.Length == 1)
            {
                string[] patterns = DateTimeFormatInfo.CurrentInfo.GetAllDateTimePatterns(dataGridViewCellStyle.Format.ToCharArray()[0]);
                if (patterns.Length > 0)
                    ctl.CustomFormat = patterns[0].ToString();
                else
                    ctl.CustomFormat = dataGridViewCellStyle.Format;
            }
            else
                ctl.CustomFormat = dataGridViewCellStyle.Format;
        }
    }
Public Class DateTimeCell
    Inherits DataGridViewTextBoxCell

    Public Sub New()
        MyBase.New()
    End Sub

    Public Overrides ReadOnly Property EditType As Type
        Get
            Return GetType(DateTimeEditingControl)
        End Get
    End Property

    Public Overrides ReadOnly Property ValueType As Type
        Get
            Return GetType(DateTime)
        End Get
    End Property

    Public Overrides ReadOnly Property DefaultNewRowValue As Object
        Get
            Dim defaultValue As Object = MyBase.DefaultNewRowValue
            If TypeOf defaultValue Is DateTime Then
                Return defaultValue
            Else
                Return DateTime.Now
            End If
        End Get
    End Property

    Public Overrides Sub InitializeEditingControl(rowIndex As Integer, initialFormattedValue As Object, _
                                                  dataGridViewCellStyle As DataGridViewCellStyle)
        MyBase.InitializeEditingControl(rowIndex, initialFormattedValue, dataGridViewCellStyle)
        Dim ctl As DateTimeEditingControl = CType(DataGridView.EditingControl, DateTimeEditingControl)
        Try
            If Me.Value = Nothing Then
                ctl.Value = CType(Me.DefaultNewRowValue, DateTime)
            Else
                ctl.Value = CType(Me.Value, DateTime)
            End If
        Catch ex As Exception
            ctl.Value = CType(Me.DefaultNewRowValue, DateTime)
        End Try

        If dataGridViewCellStyle.Format.Length = 1 Then
            Dim patterns As String() = DateTimeFormatInfo.CurrentInfo.GetAllDateTimePatterns( _
                dataGridViewCellStyle.Format.ToCharArray()(0))
            If patterns.Length > 0 Then
                ctl.CustomFormat = patterns(0).ToString()
            Else
                ctl.CustomFormat = dataGridViewCellStyle.Format
            End If
        Else
            ctl.CustomFormat = dataGridViewCellStyle.Format
        End If
    End Sub

End Class

Las propiedades EditType y ValueType definen el tipo del control a utilizar para la edición de la celda y el tipo del valor de la celda respectivamente.

He sobrescrito el método InitializeEditingControl para establecer el valor y el formato del control DateTimePicker.

Definiendo el tipo de columna


Finalmente defino la clase DateTimeGridColumn que hereda de DataGridViewColumn y que define el tipo de columna que asignaremos a las columnas de tipo fecha del DataGridView.

    public class DateTimeGridColumn: DataGridViewColumn
    {

        public DateTimeGridColumn() : base(new DateTimeCell()) { }

        public override DataGridViewCell CellTemplate
        {
            get
            {
                return base.CellTemplate;
            }
            set
            {
                if (value != null &&
                        !value.GetType().IsAssignableFrom(typeof(DateTimeCell)))
                    throw new InvalidCastException("Debe especificar una instancia de DateTimeCell");
                base.CellTemplate = value;
            }
        }
    }
Public Class DateTimeGridColumn
    Inherits DataGridViewColumn

    Public Sub New()
        MyBase.New(New DateTimeCell())
    End Sub

    Public Overrides Property CellTemplate As DataGridViewCell
        Get
            Return MyBase.CellTemplate
        End Get
        Set(value As DataGridViewCell)
            If value IsNot Nothing AndAlso _
                Not value.GetType().IsAssignableFrom(GetType(DateTimeCell)) Then
                Throw New InvalidCastException("Debe especificar una instancia de DateTimeCell")
            End If
            MyBase.CellTemplate = value
        End Set
    End Property
End Class

En el constructor de la columna llamo al constructor de la clase base DataGridViewColumn pasándole una nueva instancia de DateTimeCell para usar como plantilla de la celda.

Por otra lado simplemente he sobrescrito la propiedad CellTemplate para asegurarme de que la plantilla de celda asignada a la columna sea del tipo DateTimeCell.

Probando la columna de fechas

Con esto ya estaría creado el nuevo tipo de columna. Para probarlo voy a añadir a la solución un nuevo proyecto GridTest del tipo Aplicación de Windows Forms.

Crear proyecto de prueba

Al proyecto GridTest le he añadido una referencia al proyecto GridExtension.

Referencia a GridExtension

En el formulario que Visual Studio crea por defecto le he añadido un control DataGridView al que le he agregado dos columnas: una columna Texto del tipo DataGridViewTextBoxColumn y una columna Fecha del tipo DateTimeGridColumn.

Crear columna DateTimeGridColumn

Para seleccionar el formato de fecha a utilizar en la columna he editado la columna Fecha y, en la ventana de edición de la propiedad DefaultCellStyle he establecido el valor de Format a g para establecer el formato de fecha general: fecha corta y hora corta.

Establecer formato de fecha de la columna


Ahora ya podemos arrancar la aplicación para comprobar el funcionamiento del nuevo tipo de columna.

Formulario de prueba

El código completo de este artículo, tanto en C# como en Visual Basic, está disponible en:

Códigos de muestra - Ejemplos MSDN

9 comentarios:

  1. Muchas gracias por tu aporte, me ha servido bastante :)

    ResponderEliminar
  2. hola que tal he estado viendo tus articulos y la verdad es que son muy buenos solo queria preguntarte si se le pude poner como un simbolo predeterminado y cualquier dato que se ponga salgacon ese simolo
    agradeceria tu ayuda

    ResponderEliminar
  3. Buenas noches.
    Gracias Ingeniero , acabo de acomplar las librerias de classes a mi datagridview de mi programa
    y esta funcionando de manera espectacular.
    me despliega el calendario de fechas de forma automatica y selecciono la fecha que deseo sin ningun problema .

    gracias y quedamos a la orden
    Ing. Silvestre Govea Rdz. (Sistemas Monterrey NL)

    ResponderEliminar
  4. Excelente aporte muchas gracias por su compañerismo.
    Espero seguir aprendiendo de usted.

    Jorge Aguilar

    ResponderEliminar
  5. Buenas esta bien tu clase para la fecha en la grilla, pero una consulta como hago cuando seleccione una fecha ejecute un evento, para completar se le agradecería
    Saludos,

    ResponderEliminar
  6. Error 2 'GridExtension2.DateTimeEditingControl' is not accessible in this context because it is 'Friend'. C:\Users\usuario\Desktop\Sistemas\EVSAMOD\EVSAMOD\SUBS\frmcargaextra.Designer.vb 207 23 EVSAMOD como lo resuelvo?

    ResponderEliminar
  7. Respuestas
    1. Basta con que elabores las tres clases y las guardes dentro de tu proyecto.
      Una vez guardadas agregas el datagridview y al agregar una columna de manera automatica encontraras el tipo CalendarColum.

      Saludos!

      Eliminar
  8. Excelente aportación me sirvió mucho.

    Saludos!

    ResponderEliminar