viernes, 21 de noviembre de 2014

El Demo del Día: Filtrar y Ordenar en Cabeceras del DataGridView en WinForms

Filtrar y Ordenar en Cabeceras del DataGridView en WinForms

En este post aprenderemos como cambiar las cabeceras del control DataGridView de Windows Forms para incluir TextBoxs que permitan filtrar registros, así como etiquetas que permitan ordenar ascendente y descendentemente los datos de cada columna.

De repente muchos preguntaran porque tantos demos con Grillas y Cabeceras sobre estas, tanto en Windows y Web Forms y la respuesta es que este es el control mas usado y estas funcionalidades son muy usadas pero no es tan simple como configurar una propiedad y muchos eligen usar un control de terceros, pero con la ayuda de estos post aprenderás tu mismo a crearlos.

Requerimiento

Se necesita crear una consulta en Windows con los siguientes requerimientos:

- La consulta debe ser desconectada y no ir a a cada momento al servidor de datos.

- Los filtros deben ser por todos los campos mostrados y deben ocupar poco espacio.

- Se debe poder ordenar cada columna en forma ascendente y descendente.

- Se debe mostrar el símbolo de ordenación: "▲" para ascendente y "▼" para descendente.

Solución

- Usaremos el control DataGridView y crearemos columnas personalizadas.

- Crearemos una clase DataGridViewTextBoxHeaderColumn para cada columna de tipo TextBox.

- Crearemos una clase DataGridViewTextBoxHeaderCell para la cabecera de cada columna, la cual contendrá un LinkLabel para ordenar y un TextBox para filtrar.

Crear el Procedimiento Almacenado en la Base de Datos de SQL Server

Create Procedure uspProductsListar
As
Select ProductID,ProductName,SupplierID,CategoryID,UnitPrice,UnitsInStock
From Products

Crear una Aplicación Windows Forms en C#

En Visual Studio crear un proyecto Windows Forms en C# con el nombre de "DataGridView_FiltroOrdenCabecera", luego cambiar el nombre del formulario a "frmConsultaProductos".

Arrastrar un DataGridView llamado "dgvProducto" y acoplarlo en todo el formulario, el diseño se mostrará similar a la siguiente figura:


Crear la Clase Entidad del Negocio

Crear la clase beProducto escribiendo el siguiente código:

using System;
namespace DataGridView_FiltroOrdenCabecera
{
    public class beProducto
    {
        public beProducto()
        {
            IdProducto = -1;
            IdProveedor = -1;
            IdCategoria = -1;
            PrecioUnitario = -1;
            Stock = -1;
        }
        public int IdProducto { get; set; }
        public string Nombre { get; set; }
        public int IdProveedor { get; set; }
        public int IdCategoria { get; set; }
        public decimal PrecioUnitario { get; set; }
        public short Stock { get; set; }
    }
}

Nota: Se ha creado un constructor para iniciar las propiedades numéricas en -1 y asi poder permitir la búsqueda del cero (0) en los filtros.

Crear la Clase de Acceso a Datos

Crear la clase daProducto escribiendo el siguiente código:

using System;
using System.Data; //CommadType
using System.Data.SqlClient; //SqlConnection, SqlCommand, SqlDataReader
using System.Collections.Generic; //List
namespace DataGridView_FiltroOrdenCabecera
{
    public class daProducto
    {
        public List<beProducto> listar(SqlConnection con)
        {
            List<beProducto> lbeProducto = null;
            SqlCommand cmd = new SqlCommand("uspProductsListar", con);
            cmd.CommandType = CommandType.StoredProcedure;
            SqlDataReader drd = cmd.ExecuteReader(CommandBehavior.SingleResult);
            if (drd != null)
            {
                lbeProducto = new List<beProducto>();
                int posIdProducto = drd.GetOrdinal("ProductID");
                int posNombre = drd.GetOrdinal("ProductName");
                int posIdProveedor = drd.GetOrdinal("SupplierID");
                int posIdCategoria = drd.GetOrdinal("CategoryID");
                int posPrecioUnitario = drd.GetOrdinal("UnitPrice");
                int posStock = drd.GetOrdinal("UnitsInStock");
                beProducto obeProducto;
                while (drd.Read())
                {
                    obeProducto = new beProducto();
                    obeProducto.IdProducto = drd.GetInt32(posIdProducto);
                    obeProducto.Nombre = drd.GetString(posNombre);
                    obeProducto.IdProveedor = drd.GetInt32(posIdProveedor);
                    obeProducto.IdCategoria = drd.GetInt32(posIdCategoria);
                    obeProducto.PrecioUnitario = drd.GetDecimal(posPrecioUnitario);
                    obeProducto.Stock = drd.GetInt16(posStock);
                    lbeProducto.Add(obeProducto);
                }
                drd.Close();
            }
            return (lbeProducto);
        }
    }
}

Nota: Tiene que haberse creado el Procedimiento Almacenado "uspProductsListar" en la BD Northwind.

Crear la Clase de Reglas del Negocio

Crear la clase brProducto escribiendo el siguiente código:

using System;
using System.Collections.Generic; //List
using System.Data.SqlClient; //SqlConnection
using System.Configuration; //ConfigurationManager
namespace DataGridView_FiltroOrdenCabecera
{
    public class brProducto
    {
        public List<beProducto> listar()
        {
            List<beProducto> lbeProducto = null;
            string conexion = ConfigurationManager.ConnectionStrings["conNW"].ConnectionString;
            using (SqlConnection con = new SqlConnection(conexion))
            {
                try
                {
                    con.Open();
                    daProducto odaProducto = new daProducto();
                    lbeProducto = odaProducto.listar(con);
                }
                catch (SqlException ex)
                {                
                    foreach (SqlError err in ex.Errors)
                    {
                        //Capturar cada error y grabar un Log
                    }
                }
            } //con.Close(); con.Dispose(); con = null;
            return (lbeProducto);
        }
    }
}

Nota: Hay que hacer referencia a la librería "System.Configuration.dll" para usar la clase ConfigurationManager para leer el archivo de configuración.

Crear una Clase para las Cabeceras de las Columnas del DataGridView

Crear la clase "DataGridViewTextBoxHeaderCell" escribiendo el siguiente código:

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Windows.Forms.VisualStyles;

namespace DataGridView_FiltroOrdenCabecera
{
    public class DataGridViewTextBoxHeaderCell : DataGridViewColumnHeaderCell
    {
        public TextBox txt;
        public LinkLabel lbl;
        public event EventHandler OrdenaCampo;
        public event EventHandler CambiaTexto;

        public DataGridViewTextBoxHeaderCell()
        {
            lbl = new LinkLabel();
            lbl.Click += new EventHandler(ordenaCampo);
            txt = new TextBox();          
            txt.TextChanged += new EventHandler(cambiaTexto);
        }

        protected override void Paint(Graphics graphics, Rectangle clipBounds,Rectangle cellBounds,
            int rowIndex, DataGridViewElementStates dataGridViewElementState,
            object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle,
            DataGridViewAdvancedBorderStyle advancedBorderStyle,
            DataGridViewPaintParts paintParts)
        {
            base.Paint(graphics, clipBounds, cellBounds, rowIndex, dataGridViewElementState,
            value, formattedValue, errorText, cellStyle, advancedBorderStyle, paintParts);
            lbl.Location = new Point(cellBounds.X + 2, cellBounds.Y+5);
            lbl.Size = new Size(cellBounds.Size.Width - 4, 13);
            lbl.BackColor = Color.Transparent;
            lbl.Font=new Font("Arial",8,FontStyle.Bold);
            lbl.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
            txt.Location = new Point(cellBounds.X + 2,cellBounds.Y + 25);
            txt.Size = new Size(cellBounds.Size.Width-4,20);
        }

        public string ValorEtiqueta
        {
            get
            {
                return (lbl.Text);
            }
            set
            {
                lbl.Text = value;
            }
        }

        private void ordenaCampo(object sender, EventArgs e)
        {
            if (OrdenaCampo != null)
            {
                OrdenaCampo(this, e);
            }
        }

        public string ValorTexto
        {
            get
            {
                return (txt.Text);
            }
            set
            {
                txt.Text = value;
            }
        }

        private void cambiaTexto(object sender, EventArgs e)
        {
            if (CambiaTexto != null)
            {
                CambiaTexto(this, e);
            }
        }
    }
}

Nota: La clase creada hereda de "DataGridViewColumnHeaderCell" y crea 2 controles: un LinkLabel "lbl" para ordenar y un TextBox "txt" para filtrar. Al "lbl" se le crea el evento "OrdenaCampo" y al "txt" se le crea el evento "CambiaTexto" para filtrar.
Además se crean 2 propiedades para exponer los valores de las etiquetas de las cabeceras: "ValorEtiqueta" y el valor de los textos: "ValorTexto".

Crear una Clase para las Columnas del DataGridView

Crear la clase "DataGridViewTextBoxHeaderColumn" escribiendo el siguiente código:

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Windows.Forms.VisualStyles;

namespace DataGridView_FiltroOrdenCabecera
{
    public class DataGridViewTextBoxHeaderColumn: DataGridViewTextBoxColumn
    {
        DataGridViewTextBoxHeaderCell textoCabecera = new DataGridViewTextBoxHeaderCell();      
        public DataGridViewTextBoxHeaderColumn()
        {
            this.CellTemplate = new DataGridViewTextBoxCell();
            this.HeaderCell = textoCabecera;          
        }

         protected override void OnDataGridViewChanged()
        {
            if (this.DataGridView != null)
            {
                this.DataGridView.Controls.Add(textoCabecera.lbl);
                this.DataGridView.Controls.Add(textoCabecera.txt);
            }
        }

        public DataGridViewTextBoxHeaderCell Texto
        {
            get
            {
                return this.textoCabecera;
            }
        }
    }
}

Nota: La clase creada hereda de "DataGridViewTextBoxColumn" y crea un objeto "textoCabecera" de tipo "DataGridViewTextBoxHeaderCell" que se configura como propiedad "HeaderCell" de la columna. Este se expone como una propiedad llamada "Texto".
Además en el evento "OnDataGridViewChanged" se agrega los controles creados en la cabecera de la celda: el LinkLabel "lbl" y el TextBox "txt".

Usar las Clases creadas en el DataGridView del Formulario

Regresar al formulario "frmConsultaProductos" y escribir el siguiente código:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;

namespace DataGridView_FiltroOrdenCabecera
{
    public partial class frmConsultaProductos : Form
    {
        private List<beProducto> lbeProducto;
        private List<beProducto> lbeFiltro;
        DataGridViewTextBoxHeaderColumn col1,col2,col3,col4,col5,col6;

        public frmConsultaProductos()
        {
            InitializeComponent();
        }

        private void cargarProductos(object sender, EventArgs e)
        {
            brProducto obrProducto = new brProducto();
            lbeProducto = obrProducto.listar();
            dgvProducto.AutoGenerateColumns = false;
            dgvProducto.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
            dgvProducto.ColumnHeadersHeight = 50;
            dgvProducto.ColumnHeadersHeightSizeMode =
            DataGridViewColumnHeadersHeightSizeMode.DisableResizing;
            lbeFiltro = lbeProducto;
            dgvProducto.DataSource = lbeFiltro;
            crearColumnasCabecera();
        }

        private void crearColumnasCabecera()
        {
            col1 = new DataGridViewTextBoxHeaderColumn();
            col1.DataPropertyName = "IdProducto";          
            col1.Width = 70;
            col1.Texto.ValorEtiqueta = "Código";
            col1.Texto.OrdenaCampo += new EventHandler(ordenarProductos);
            col1.Texto.CambiaTexto += new EventHandler(filtrarProductos);
            dgvProducto.Columns.Add(col1);
         
            col2 = new DataGridViewTextBoxHeaderColumn();
            col2.DataPropertyName = "Nombre";
            col2.Width = 300;
            col2.Texto.ValorEtiqueta = "Descripción del Producto";
            col2.Texto.OrdenaCampo += new EventHandler(ordenarProductos);
            col2.Texto.CambiaTexto += new EventHandler(filtrarProductos);
            dgvProducto.Columns.Add(col2);
         
            col3 = new DataGridViewTextBoxHeaderColumn();
            col3.DataPropertyName = "IdProveedor";
            col3.Width = 70;
            col3.Texto.ValorEtiqueta = "Id Prov";
            col3.Texto.OrdenaCampo += new EventHandler(ordenarProductos);
            col3.Texto.CambiaTexto += new EventHandler(filtrarProductos);
            dgvProducto.Columns.Add(col3);
         
            col4 = new DataGridViewTextBoxHeaderColumn();
            col4.DataPropertyName = "IdCategoria";
            col4.Width = 70;
            col4.Texto.ValorEtiqueta = "Id Cat";
            col4.Texto.OrdenaCampo += new EventHandler(ordenarProductos);
            col4.Texto.CambiaTexto += new EventHandler(filtrarProductos);
            dgvProducto.Columns.Add(col4);
         
            col5 = new DataGridViewTextBoxHeaderColumn();
            col5.DataPropertyName = "PrecioUnitario";
            col5.Width = 70;
            col5.Texto.ValorEtiqueta = "Pre Unit";
            col5.Texto.OrdenaCampo += new EventHandler(ordenarProductos);
            col5.Texto.CambiaTexto += new EventHandler(filtrarProductos);
            dgvProducto.Columns.Add(col5);
         
            col6 = new DataGridViewTextBoxHeaderColumn();
            col6.DataPropertyName = "Stock";
            col6.Width = 70;
            col6.Texto.ValorEtiqueta = "Stock";
            col6.Texto.OrdenaCampo += new EventHandler(ordenarProductos);
            col6.Texto.CambiaTexto += new EventHandler(filtrarProductos);
            dgvProducto.Columns.Add(col6);
        }

        private bool buscarProductos(beProducto obeProducto)
        {
            bool exitoIdProducto = true;
            bool exitoNombre = true;
            bool exitoIdProveedor = true;
            bool exitoIdCategoria = true;
            bool exitoPrecioUnitario = true;
            bool exitoStock = true;
            if (!col1.Texto.Equals("")) exitoIdProducto =
            (obeProducto.IdProducto.ToString().Contains(col1.Texto.ValorTexto));
            if (!col2.Texto.Equals("")) exitoNombre =
            (obeProducto.Nombre.ToLower().Contains(col2.Texto.ValorTexto.ToLower()));
            if (!col3.Texto.Equals("")) exitoIdProveedor =
            (obeProducto.IdProveedor.ToString().Contains(col3.Texto.ValorTexto));
            if (!col4.Texto.Equals("")) exitoIdCategoria =
            (obeProducto.IdCategoria.ToString().Contains(col4.Texto.ValorTexto));
            if (!col5.Texto.Equals("")) exitoPrecioUnitario =
            (obeProducto.PrecioUnitario.ToString().Contains(col5.Texto.ValorTexto));
            if (!col6.Texto.Equals("")) exitoStock =
            (obeProducto.Stock.ToString().Contains(col6.Texto.ValorTexto));
            return (exitoIdProducto && exitoNombre && exitoIdProveedor && exitoIdCategoria
            && exitoPrecioUnitario && exitoStock);
        }

        protected void filtrarProductos(object sender, EventArgs e)
        {
            Predicate<beProducto> pred = new Predicate<beProducto>(buscarProductos);
            lbeFiltro = lbeProducto.FindAll(pred);
            dgvProducto.DataSource = lbeFiltro;
        }

        protected void ordenarProductos(object sender, EventArgs e)
        {
            DataGridViewTextBoxHeaderCell cabecera = (DataGridViewTextBoxHeaderCell)sender;
            DataGridViewTextBoxHeaderColumn columna =
            (DataGridViewTextBoxHeaderColumn )cabecera.OwningColumn;
            string campo = columna.DataPropertyName;
            string etiqueta = cabecera.ValorEtiqueta.Replace("▲","").Replace("▼","").Trim();
            int n = 0;
            string simbolo = "▲";
            if (columna.Tag != null)
            {
                if (columna.Tag.Equals(0))
                {
                    simbolo = "▼";
                    n = 1;
                }
            }
            columna.Tag = n;
            cabecera.ValorEtiqueta = String.Format("{0} {1}", etiqueta, simbolo);
            if(n.Equals(0)) lbeFiltro = lbeFiltro.OrderBy(x=>x.GetType().
                 GetProperty(campo).GetValue(x,null)).ToList();
            else lbeFiltro = lbeFiltro.OrderByDescending(x => x.GetType().
                 GetProperty(campo).GetValue(x, null)).ToList();
            dgvProducto.DataSource = lbeFiltro;
        }
    }
}

Nota: En el evento "load" del formulario se llama a la función controladora "cargarProductos" que obtiene los datos y configura el DataGridView "dgvProducto" sobre todo creando las columnas personalizadas en la función "crearColumnasCabecera".

En el evento "OrdenaCampo" de la cabecera de cada columna se asocia la función "ordenarProductos" y en el evento "CambiaTexto" se asocia a la función "filtrarProductos".

Modificar el archivo App.Config para incluir la Cadena de Conexión

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <connectionStrings>
      <add name="conNW" providerName="System.Data.SqlClient" connectionString="uid=UsuarioNW;pwd=123456;data source=DSOFT\Sqlexpress;initial catalog=Northwind"/>
    </connectionStrings>
    <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
</configuration>

Probar la Aplicación Windows Forms

Grabar la aplicación y ejecutarla con F5. Se mostrará la siguiente ventana:


En las cabeceras de cada columna aparecen unos LinkLabels y TextBoxs, probamos la ordenación dando clic a los LinkLabel y la primera vez se ordenará en forma ascendente y la siguiente en forma descendente apareciendo su respectivo símbolo, tal como se muestra en la siguiente figura:


Escribir en la cabecera del código el "1" y en la del nombre "ma" y cada vez que se escribe se realiza el filtro desconectado, tal como se muestra a continuación:


Comentario Final

En este post hemos aprendido como sobre escribir las clases "DataGridViewColumnHeaderCell" y "DataGridViewTextBoxColumn" para crear columnas con cabeceras personalizadas que contengan controles: LinkLabels para ordenar y TextBox para filtrar.

En Windows Forms crear cabeceras en el DataGridView es un poco mas trabajoso que en ASP.NET WebForms y MVC que usan plantillas y que en WPF que usa estilos y es mas fácil personalizar.

Pero de todas formas no es imposible y lo mejor es que puedes hacer los cambios que deseas ya que tienes el fuente del código. Espero les sirva a todos los visitantes del Blog.

Creo que después de este buen Demo algunos se animarán a ir al Seminario de Buenas Prácticas en .NET, sobre todo los que nunca han llevado cursos conmigo.

Descarga
Demo26_DataGridView_FiltroOrdenCabecera

El Libro del Día: Start Here! Learn HTML5

El Libro del Día: 2014-11-21

Titulo: Start Here! Learn HTML5
Autor: Faithe Wempen
Editorial: Microsoft
Nro Paginas: 360

Capítulos:
Part I Getting Started with HTML
Chapter 1 HTML Basics: The Least You Need to Know
Chapter 2 Setting Up the Document Structure
Chapter 3 Formatting Text with Tags
Chapter 4 Using Lists and Backgrounds
Chapter 5 Creating Hyperlinks and Anchors
Part II Style Sheets and Graphics
Chapter 6 Introduction to Style Sheets
Chapter 7 Formatting Text with CSS
Chapter 8 Formatting Paragraphs with CSS
Chapter 9 Inserting Graphics
Part III Page Layout and Navigation
Chapter 10 Creating Navigational Aids
Chapter 11 Creating Division-Based Layouts
Chapter 12 Creating Tables
Chapter 13 Formatting Tables
Chapter 14 Creating User Forms
Chapter 15 Incorporating Sound and Video
Chapter 16 HTML and Microsoft Expression Web
Part IV Appendices
Appendix A Designing for Usability
Appendix B Designing for Accessibility
Appendix C Quick Reference

Descarga:
Start_Here_Learn_HTML5