Mostrando las entradas con la etiqueta DataGridView. Mostrar todas las entradas
Mostrando las entradas con la etiqueta DataGridView. Mostrar todas las entradas

miércoles, 20 de julio de 2016

El Demo del Día: Leer Archivos de Excel 2007 usando Archivos XML

Leer Archivos de Excel 2007 usando Archivos XML

Este Demo es muy interesante ya que nos permite leer archivos Excel 2007 sin necesidad de tener instalado ni Office (para Automatización COM) ni los Componentes de Acceso a Datos de Office (para OLEDB), ya que solo debemos tener instalado .NET Framework para ejecutar la aplicación (el cual ya viene instalado en la mayoría de Windows).

En el Demo al Descomprimir el archivo de Excel No se crean archivos en disco sino se guardan en memoria usando una lista de objetos XmlDocument ya que se van a leer varios nodos de cada archivo XML. Es por eso que la velocidad es superior a las otras 2 técnicas, sobre todo con gran cantidad de datos.

1. Formatos Office Open XML

Quizás la mayoría no se ha dado cuenta que los archivos de Office a partir de la versión 2007 cumplen con el estándar Open XML que quiere decir que cada archivo es un comprimido (zip) que contiene archivos XML con Metadatos y datos.

Antes los archivos de Office (versión 2003 e inferiores) eran archivos binarios definidos por Microsoft, pero a partir de la versión 2007 son archivos XML comprimidos en una extensión que termina en X (Xml) o M (Macros), por ejemplo:

- En Word: Documentos (docx), Documentos con Macros (docm), Plantillas (dotx), Plantillas con Macros (dotm).

- En Excel: Libros (xlsx), Libros con Macros (xlsm), Plantillas (xltx), Plantillas con Macros (xltm).

- En PowerPoint: Presentaciones (pptx), Presentaciones con Macros (pptm), Plantillas (potx), Plantillas con Macros (potm).

- Otros: XML Paper Specification (XPS), Open XPS (OXPS), etc.

Como experimento solo basta cambiar la extensión de cualquiera de estos archivos a rar y descomprimirlos para ver que contienen archivox XML, por ejemplo, en el caso de uno de mis archivos de Excel 2007:

- Carpeta _rels: archivo .rels

- Carpeta docProps: archivos app.xml y core.xml

- Carpeta xl:
  -) SubCarpeta _rels: archivo workbook.xml.rels
  -) SubCarpeta theme: archivo theme1.xml
  -) SubCarpeta worksheets: sheet1.xml, sheet2.xml, ...
  -) Archivo sharedStrings.xml
  -) Archivo styles.xml
  -) Archivo workbook.xml

- Archivo [Content_Types].xml

2. Crear una Aplicación Windows Forms en C#

Abrir Visual Studio y crear una aplicación Windows Forms en C# llamada "Excel_Leer_XML", cambiarle de nombre al formulario por "frmVisorExcel" y realizar el diseño similar a la figura mostrada:

Diseño del Formulario "frmVisorExcel"

En el diseño hay una etiqueta (lblArchivo), un cuadro de texto (txtArchivo), 2 botones (btnAbrir y btnNuevo) y un control Tab (tabExcel) sin ninguna página ya que estas se crean en tiempo de ejecución en función al número de hojas.

3. Escribir el Código de la Aplicación en C#

Abrir el formulario "frmVisorExcel" y escribir el siguiente código:
using System;
using System.Text;
using System.Data;
using System.IO;
using System.IO.Compression;
using System.Xml;
using System.Windows.Forms;
using System.Collections.Generic;

namespace Excel_Leer_XML
{
    public partial class frmVisorExcel : Form
    {
        public frmVisorExcel()
        {
            InitializeComponent();
        }

        private void abrirExcel(object sender, EventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog();
            ofd.Title = "Selecciona el archivo de Excel a abrir";
            ofd.Filter = "Archivo de Excel 2007|*.xlsx";
            ofd.InitialDirectory = @"C:\Data\NET\Practicas\Archivos\Excel";
            if (ofd.ShowDialog().Equals(DialogResult.OK))
            {
                string archivo = ofd.FileName;
                txtArchivo.Text = archivo;
                tabExcel.Controls.Clear();
                List<XmlDocument> docs = new List<XmlDocument>();
                List<string> nombres = new List<string>();
                List<string> valores = new List<string>();
                //Descomprimir el archivo Excel y guardar solo los archivos xml necesarios
                //en una Lista de objetos de tipo XmlDocument
                using (var archivoExcel = File.OpenRead(archivo))
                using (var zip = new ZipArchive(archivoExcel, ZipArchiveMode.Read))
                {                  
                    XmlDocument doc;
                    foreach (var archivoXml in zip.Entries)
                    {
                        if (archivoXml.Name == "workbook.xml" ||
                             archivoXml.Name == "sharedStrings.xml" ||
                             archivoXml.Name.StartsWith("sheet") &&
                             Path.GetExtension(archivoXml.Name)==".xml")
                        {
                            using (Stream stream = archivoXml.Open())
                            {
                                doc = new XmlDocument();
                                doc.Load(stream);
                                docs.Add(doc);
                                nombres.Add(archivoXml.Name);
                            }
                        }
                    }
                }
                if (docs.Count > 0)
                {
                    //Leer el archivo xml donde se guardan los datos de tipo cadena
                    int pos = nombres.IndexOf("sharedStrings.xml");
                    if (pos > -1)
                    {
                        XmlDocument xdStrings = docs[pos];
                        XmlElement nodoRaizStrings = xdStrings.DocumentElement;
                        XmlNodeList nodosValores = nodoRaizStrings.ChildNodes;
                        if (nodosValores != null)
                        {
                            foreach (XmlNode nodoValor in nodosValores)
                            {
                                valores.Add(nodoValor.FirstChild.FirstChild.Value);
                            }
                        }
                    }
                    //Leer el archivo xml workbook donde se guardan la lista de hojas
                    pos = nombres.IndexOf("workbook.xml");
                    if (pos > -1)
                    {
                        XmlDocument xdLibro = docs[pos];
                        XmlElement nodoRaizHojas = xdLibro.DocumentElement;
                        XmlNodeList nodosHojas = nodoRaizHojas.GetElementsByTagName("sheet");
                        string id, hoja;
                        if (nodosHojas != null)
                        {
                            foreach (XmlNode nodoHoja in nodosHojas)
                            {
                                id = nodoHoja.Attributes["sheetId"].Value;
                                hoja = nodoHoja.Attributes["name"].Value;
                                //Leer cada archivo xml con la hoja
                                pos = nombres.IndexOf("sheet" + id + ".xml");
                                if (pos > -1)
                                {
                                    XmlDocument xdHoja = docs[pos];
                                    XmlElement nodoRaizHoja = xdHoja.DocumentElement;
                                    XmlNodeList nodosFilas =
                                    nodoRaizHoja.GetElementsByTagName("row");
                                    DataTable tabla = new DataTable();
                                    DataRow fila = null;
                                    int indice;
                                    string celda, valor;
                                    XmlAttribute tipoString;
                                    int cf = 0; //contador de filas
                                    int cc = 0; //contador de columnas
                                    string nombreCol; //nombre de la columna: A, B, C, ...
                                    if (nodosFilas != null)
                                    {
                                        foreach (XmlNode nodoFila in nodosFilas)
                                        {
                                            XmlNodeList nodoCeldas = nodoFila.ChildNodes;
                                            if (nodoCeldas != null)
                                            {
                                                cc = 0;
                                                foreach (XmlNode nodoCelda in nodoCeldas)
                                                {                              
                                                    celda = nodoCelda.Attributes["r"].Value;
                                                    tipoString = nodoCelda.Attributes["t"];                              
                                                    valor = "";
                                                    if (tipoString != null)
                                                    {
                                                        if (valores != null && valores.Count > 0)
                                                        {
                                                            indice = int.Parse(nodoCelda.FirstChild.
                                                            FirstChild.Value);
                                                            valor = valores[indice];
                                                        }
                                                    }
                                                    else
                                                    {
                                                        if(nodoCelda.FirstChild!=null &&  nodoCelda.FirstChild.
                                                        FirstChild!=null)
                                                          valor = nodoCelda.FirstChild.FirstChild.Value;
                                                    }
                                                    if (cf == 0)
                                                    {
                                                        nombreCol = ((char)(65+cc)).ToString();
                                                        tabla.Columns.Add(nombreCol);
                                                    }
                                                    if (cc == 0)
                                                    {
                                                        fila = tabla.NewRow();
                                                    }
                                                    if (cc < tabla.Columns.Count)
                                                    {
                                                        fila[cc] = valor;
                                                    }
                                                    cc++;
                                                }
                                            }
                                            tabla.Rows.Add(fila);                      
                                            cf++;
                                        }
                                        DataGridView dgv = new DataGridView();
                                        dgv.Dock = DockStyle.Fill;
                                        dgv.ReadOnly = true;
                                        dgv.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
                                        dgv.AllowUserToAddRows = false;
                                        dgv.DataSource = tabla;
                                        dgv.CellFormatting += new
                                        DataGridViewCellFormattingEventHandler(cambiarCabeceraFilas);
                                        TabPage pagina = new TabPage();
                                        pagina.Text = hoja;
                                        pagina.Controls.Add(dgv);
                                        tabExcel.Controls.Add(pagina);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        private void cambiarCabeceraFilas(object sender, DataGridViewCellFormattingEventArgs e)
        {
            DataGridView dgvHoja = (DataGridView)sender;
            if (e.RowIndex > -1 && e.ColumnIndex == 0)
            dgvHoja.Rows[e.RowIndex].HeaderCell.Value = (e.RowIndex + 1).ToString();
        }

        private void nuevaLectura(object sender, EventArgs e)
        {
            txtArchivo.Clear();
            tabExcel.Controls.Clear();
        }
    }
}

Nota: El método "abrirExcel" estaasociado al evento click del botón "btnAbrir" y el método "nuevaLectura" esta asociado al evento click del botón "btnNuevo".

4. Ejecutar y Probar la Aplicación Windows Forms

Grabar la aplicación y pulsar F5 para ejecutarla, luego clic al botón "..." y se mostrará el diálogo de abrir mostrando los archivos de Excel 2007 (xlsx), tal como se muestra en la siguiente figura:

Dialogo de Abrir archivos de Excel

Seleccionar un archivo del diálogo y se cargarán todas las hojas del libro, mostrándose en una página del control Tab una grilla, tal como se muestra en la siguiente figura:

Primer Tab con la primera Hoja

Nota: Observar que la grilla muestra en las cabeceras de columnas las letras: A, B, C, D, etc. y en la cabecera de las filas los números: 1, 2, 3, 4, etc. Además en este Demo el orden de las hojas si es el verdadero (en el Demo anterior de OLEDB era como se leía primero).

Como en mi caso abrí un archivo con 2 hojas, la primera que tiene Frameworks de JavaScript y la segunda con algunos Videos de JavaScript, tal como se muestra en la siguiente figura:

Segundo Tab con la segunda Hoja

Si se desea se puede abrir cualquier otro archivo dando click nuevamente al botón "...", tal como se muestra en la siguiente figura:

Archivo Excel con solo una Hoja

En este último caso se abrió un archivo con solo una Hoja de Productos.

5. Comentario Final

En este Demo hemos visto como Leer todas las hojas de un libro de Excel 2007 usando archivos XML, para lo cual primero descomprimimos en memoria el archivo usando la clase "ZipArchive" y usando la colección de "Entries" obtuvimos solo los archivos xml necesarios: workbook.xml, sharedStrings.xml y todos los sheets.xml.

Primero se procedió a leer el archivo "sharedStrings.xml" donde se guardan los datos de tipo cadena y luego se procedió a leer el archivo "workbook.xml" donde se guardan la lista de hojas, finalmente para cada hoja se procedió a leer su archivo xml con los datos de la hoja, los cuales se almacenaron en un DataTable y se mostró en una grilla dentro de un tabPage en el tabControl.

Esta aplicación solo es una demostración y no incluye el formato, fórmulas, gráficos y demás objetos que también pueden leerse del archivo Excel xlsx, pero es un buen punto de partida para todos aquéllos que quieran entender "como funcionan las cosas", en este caso saber el contenido del archivo excel.

El que desea saber todo sobre la especificación de Excel 2007 les comparto el documento que lo encuentran también en la web de Microsoft Protocols junto con los demás formatos:
Especificación de Excel Open XML (XLSX)

Espero que les guste el Demo que es una primicia ya que no creo que lo encuentren en ningún lugar, sobre todo explicado, además si "entendieron" la técnica también la pueden usar para Aplicaciones Web y solo en el Cliente sin ir al Servidor, podemos usar JavaScript para descomprimir el archivo Excel y leer los archivos XML para mostrar su contenido. Esto lo haré en otro post, siempre y cuando valoren los demos, les den Like y los compartan en sus redes sociales (claro esta incluyendo al autor: Luis Dueñas).

6. Descarga la Aplicación Windows

2016_07_20_Demo_Excel_Leer_XML

martes, 19 de julio de 2016

El Demo del Día: Leer Archivos de Excel 2007 usando OLEDB

Leer Archivos de Excel 2007 usando OLEDB

Vamos a retomar nuevamente los Demos, con algo muy solicitado que es leer Archivos de Excel 2007 (XSLX), esta forma de lectura la usaba hace muchos años, pero ahora la comparto para los que no han llevado un curso conmigo.

1. Técnicas para Leer Archivos de Excel

Para leer un archivo de Excel con todas sus hojas hay varias formas que describo brevemente a continuación:

- Automatización ActiveX o COM

Consiste en abrir una instancia de Excel desde otra aplicación (en este caso en .NET) y obtener el valor de cada celda o de un rango de celda por cada hoja. Esta forma es la mas usada y es la de peor rendimiento y requiere tener instalado el Excel. Esta forma es la mas antigua cuando todavía no existía ADO.

- Usando OLEDB

Consiste en usar el proveedor de OLEDB en ADO.NET para conectarse a un archivo de Excel 12.0 (XSLX) para lo cual es necesario tener instalado "Microsoft Access Database Engine 2010 Redistributable" el cual puede descargarse de la siguiente dirección:
https://www.microsoft.com/en-us/download/details.aspx?id=13255
En esta página habrán 2 ejecutables:
- AccessDatabaseEngine.exe (25.3 MB) para equipos con S.O. de 32 bits
- AccessDatabaseEngine_X64.exe (27.3 MB) para equipos con S.O. de 64 bits
Esta forma es la que veremos en este Demo.

- Usando Archivos

Es la forma mas óptima y consiste en descomprimir el archivo XLSX y leer los XML donde se encuentran definidos los datos y las hojas. Esto se puede hacer de forma nativa o usando Librerías de Terceros como SpreedSheet (DevExpress), NPOI, EPPlus, ExcelDataReader, etc.
En el siguiente Demo que subiré trataré como leer Excel en forma nativa usando archivos XML.

2. Crear una Aplicación Windows Forms en C#

Abrir Visual Studio y crear una aplicación Windows Forms en C# llamada "Excel_Leer_OLEDB", cambiarle de nombre al formulario por "frmVisorExcel" y realizar el diseño similar a la figura mostrada:

En el diseño hay una etiqueta (lblArchivo), un cuadro de texto (txtArchivo), 2 botones (btnAbrir y btnNuevo) y un control Tab (tabExcel) sin ninguna página ya que estas se crean en tiempo de ejecución en función al número de hojas.

3. Escribir el Código de la Aplicación en C#

Abrir el formulario "frmVisorExcel" y escribir el siguiente código:

using System;
using System.Data;
using System.Data.OleDb;
using System.Windows.Forms;

namespace Excel_Leer_OLEDB
{
    public partial class frmVisorExcel : Form
    {
        public frmVisorExcel()
        {
            InitializeComponent();
        }

        private void abrirExcel(object sender, EventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog();
            ofd.Title = "Selecciona el archivo de Excel a abrir";
            ofd.Filter = "Archivo de Excel 2007|*.xlsx";
            ofd.InitialDirectory = @"C:\Data\NET\Practicas\Archivos\Excel";
            if (ofd.ShowDialog().Equals(DialogResult.OK))
            {
                string archivo = ofd.FileName;
                txtArchivo.Text = archivo;
                tabExcel.Controls.Clear();
                using (OleDbConnection con = new OleDbConnection
                ("provider=Microsoft.Ace.oledb.12.0;data source=" + archivo + ";
                 extended properties=Excel 12.0"))
                {
                    try
                    {
                        con.Open();
                        DataTable tblHojas = con.GetOleDbSchemaTable(OleDbSchemaGuid.Tables,
                        new object[] { null, null, null, "TABLE" });
                        foreach (DataRow fila in tblHojas.Rows)
                        {
                            if (fila["TABLE_NAME"].ToString().EndsWith("$"))
                            {
                                string nombreHoja = fila["TABLE_NAME"].ToString();
                                DataTable tblHoja = new DataTable();
                                using (OleDbDataAdapter dap = new OleDbDataAdapter
                                ("Select * From [" + nombreHoja + "]", con))
                                {
                                    dap.Fill(tblHoja);
                                }
                                DataGridView dgv = new DataGridView();
                                dgv.Dock = DockStyle.Fill;
                                dgv.ReadOnly = true;
                                dgv.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
                                dgv.AllowUserToAddRows = false;
                                dgv.DataSource = tblHoja;
                                TabPage pagina = new TabPage();
                                pagina.Text = nombreHoja.Replace("$","");
                                pagina.Controls.Add(dgv);
                                tabExcel.Controls.Add(pagina);
                            }
                        }                      
                    }
                    catch (Exception ex)
                    {
                        MessageBox.Show(ex.Message,"Error al leer el Excel");
                    }
                }
            }
        }

        private void nuevaLectura(object sender, EventArgs e)
        {
            txtArchivo.Clear();
            tabExcel.Controls.Clear();
        }
    }
}

Nota: El método "abrirExcel" estaasociado al evento click del botón "btnAbrir" y el método "nuevaLectura" esta asociado al evento click del botón "btnNuevo".

4. Ejecutar y Probar la Aplicación Windows Forms

Grabar la aplicación y pulsar F5 para ejecutarla, luego clic al botón "..." y se mostrará el diálogo de abrir mostrando los archivos de Excel 2007 (xlsx), tal como se muestra en la siguiente figura:

Dialogo de Abrir archivos de Excel

Seleccionar un archivo del diálogo y se cargarán todas las hojas del libro, mostrándose en una página del control Tab una grilla, tal como se muestra en la siguiente figura:

Primer Tab con la primera Hoja

En mi caso abrí un archivo con 2 hojas, la primera que tiene Personajes de JavaScript y la segunda con algunos términos de programación, tal como se muestra en la siguiente figura:

Segundo Tab con la segunda Hoja

Si se desea se puede abrir cualquier otro archivo dando click nuevamente al botón "...", tal como se muestra en la siguiente figura:

Archivo Excel con solo una Hoja

En este último caso se abrió un archivo con solo una Hoja de Productos.

5. Comentario Final

En este Demo hemos visto como Leer todas las hojas de un libro de Excel 2007 usando OLEDB, para lo cual se uso el método "GetOleDbSchemaTable()" con la restricción de tablas para obtener todos los nombres de tablas pero solo mostramos las que terminan en $, que son las Hojas de Excel.

Se uso un "OleDbDataAdapter" para ejecutar el comando Select para obtener los datos de la hoja en un "DataTable" y luego se creó dinámicamente una grilla, un tabPage y se agregó la grilla al tabPage y el tabPage al tabControl para que se vea los datos de cada hoja.

El que desea conservar todos los datos para después trabajarlos y luego guardarlos en una base de datos, en ves de usar un DataTable local (en el DataAdapter) debe usar un DataSet global (a nivel de clase).

Espero que les sea útil este Demo y en estos días publicaré una técnica más rápida que es leer archivos xml dentro del paquete (xlsx) la cual es más rápida aunque mas laboriosa y que algunos usan sin darse cuenta al usar librerías de lectura de archivos Excel.

6. Descarga la Aplicación Windows

2016_07_19_Demo_Excel_Leer_OLEDB

viernes, 6 de febrero de 2015

El Demo del Día: Agrupación de Celdas del DataGridView en WinForms

Agrupación de Celdas del DataGridView en WinForms

Requerimiento

Se necesita agrupar los valores repetidos de ciertas columnas del DataGridView en una Aplicación Windows en .NET, por ejemplo para ver los productos por categorías o por proveedores sin tener que repetir varias veces el mismo valor.

Solución

Crearemos una clase personalizada llamada Grilla que herede del DataGridView y que sobre escriba los eventos "OnCellPainting" y "OnCellFormatting".
En el primer evento (CellPainting) colocaremos los bordes solo cuando el valor de la columna sea diferente y en el segundo evento (CellFormatting) borraremos los valores repetidos de cada columna.
En ambos casos solo se considera si es una columna a combinar, para lo cual crearemos una propiedad llamada "GroupColumns".

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

Para el ejemplo usaremos la Base de Datos Northwind de SQL Server, en la cual crearemos el siguiente Procedimiento almacenado:

Create Procedure uspProductsListarStock
As
Select p.ProductName,p.CategoryID,c.CategoryName,UnitsInStock
From Products p
Inner Join Categories c On c.CategoryID=p.CategoryID
Order By 2,3,1

Crear una Aplicación Windows Forms en C#

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

Crear la Clase Entidad del Negocio

Crear la clase beProducto escribiendo el siguiente código:

namespace DataGridView_Agrupado
{
    public class beProducto
    {
        public int IdCategoria { get; set; }
        public string NombreCategoria { get; set; }
        public string NombreProducto { get; set; }
        public short Stock { get; set; }
    }
}

Crear la Clase de Acceso a Datos

Crear la clase daProducto escribiendo el siguiente código:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;

namespace DataGridView_Agrupado
{
    public class daProducto
    {
        public List<beProducto> listarStock(SqlConnection con)
        {
            List<beProducto> lbeProducto = null;
            SqlCommand cmd = new SqlCommand("uspProductsListarStock", con);
            cmd.CommandType = CommandType.StoredProcedure;
            SqlDataReader drd = cmd.ExecuteReader(CommandBehavior.SingleResult);
            if (drd != null)
            {
                lbeProducto = new List<beProducto>();
                int posNombreProducto = drd.GetOrdinal("ProductName");
                int posIdCategoria = drd.GetOrdinal("CategoryID");
                int posNombreCategoria = drd.GetOrdinal("CategoryName");
                int posStock = drd.GetOrdinal("UnitsInStock");
                beProducto obeProducto;
                while (drd.Read())
                {
                    obeProducto = new beProducto();
                    obeProducto.NombreProducto = drd.GetString(posNombreProducto);
                    obeProducto.IdCategoria = drd.GetInt32(posIdCategoria);
                    obeProducto.NombreCategoria = drd.GetString(posNombreCategoria);
                    obeProducto.Stock = drd.GetInt16(posStock);
                    lbeProducto.Add(obeProducto);
                }
                drd.Close();
            }
            return (lbeProducto);
        }
    }
}

Nota: Tiene que haberse creado el Procedimiento Almacenado "uspProductsListarStock" 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;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;

namespace DataGridView_Agrupado
{
    public class brProducto
    {
        public List<beProducto> listarStock()
        {
            List<beProducto> lbeProducto = null;
            string CadenaConexion = ConfigurationManager.ConnectionStrings 
                                                      ["conNW"].ConnectionString;
            using (SqlConnection con = new SqlConnection(CadenaConexion))
            {
                try
                {
                    con.Open();
                    daProducto odaProducto = new daProducto();
                    lbeProducto = odaProducto.listarStock(con);
                }
                catch (Exception ex)
                {
                    //grabarLog(ex);
                }
            }
            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 Combinar las Celdas del DataGridView

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

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

namespace DataGridView_Agrupado
{
    public class Grilla : DataGridView
    {
        public string[] GroupColumns { get; set; }

        private bool esRepetidoValorCelda(int fila, int columna)
        {
            bool exito = false;
            int c = fila - 1;
            dynamic valorCelda = this.Rows[fila].Cells[columna].Value;
            dynamic valorComparar;
            while (c > -1)
            {
                valorComparar = this.Rows[c].Cells[columna].Value;
                if (valorCelda.Equals(valorComparar))
                {
                    exito = true;
                    break;
                }
                c--;
            }
            return (exito);
        }

        private bool esColumnaCombinar(string nombreColumna)
        {
            bool exito = false;
            foreach (string columna in GroupColumns)
            {
                if (columna.Equals(nombreColumna))
                {
                    exito = true;
                    break;
                }
            }
            return (exito);
        }

        protected override void OnCellPainting(DataGridViewCellPaintingEventArgs e)
        {
            base.OnCellPainting(e);
            e.AdvancedBorderStyle.Bottom = DataGridViewAdvancedCellBorderStyle.None;
            if (e.RowIndex < 1 || e.ColumnIndex < 0) return;
            string nombreColumna = this.Columns[e.ColumnIndex].Name;
            bool esCombinada = (GroupColumns!=null && esColumnaCombinar(nombreColumna));
            if ((esCombinada && !esRepetidoValorCelda(e.RowIndex, e.ColumnIndex)) 
            || !esCombinada) e.AdvancedBorderStyle.Top = AdvancedCellBorderStyle.Top;
            else e.AdvancedBorderStyle.Top = DataGridViewAdvancedCellBorderStyle.None;
        }

        protected override void OnCellFormatting(DataGridViewCellFormattingEventArgs e)
        {
            base.OnCellFormatting(e);
            bool esNumero = (e.Value is Int16||e.Value is Int32||e.Value is Int64||e.Value is int
                                          ||e.Value is decimal);
            if (esNumero)
            {
                e.CellStyle.Alignment = DataGridViewContentAlignment.MiddleRight;
                bool esDecimal = (e.Value is decimal);
                if (esDecimal) e.CellStyle.Format = "n2";
            }
            string nombreColumna = this.Columns[e.ColumnIndex].Name;
            bool esCombinada = (GroupColumns != null && esColumnaCombinar(nombreColumna));
            if (esCombinada&&esRepetidoValorCelda(e.RowIndex, e.ColumnIndex))
            {
                e.Value = string.Empty;
                e.FormattingApplied = true;
            }
        }
    }
}

Nota: La clase Grilla hereda del DataGridView y sobre escribe los eventos "CellPainting" y "CellFormatting". Ademas se crea una propiedad llamada "GroupColumns" para especificar las columnas que se desean combinar o agrupar sus valores repetidos, para eso creamos 2 funciones: "esRepetidoValorCelda" y "esColumnaCombinar".

Usar la Clase Grilla en el Formulario

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

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

namespace DataGridView_Agrupado
{
    public partial class frmListaProductos : Form
    {
        public frmGraficoProductos()
        {
            InitializeComponent();
        }

        private void listarProductos(object sender, EventArgs e)
        {
            brProducto obrProducto = new brProducto();
            List<beProducto> lbeProducto = obrProducto.listarStock();
            Grilla dgvProducto = new Grilla();
            dgvProducto.Dock = DockStyle.Fill;
            dgvProducto.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
            //dgvProducto.GroupColumns = new string[] { "IdCategoria", "NombreCategoria"};
            dgvProducto.DataSource = lbeProducto;
            this.Controls.Add(dgvProducto);
        }
    }
}

Nota: En el evento "load" del formulario se llama a la función "listarProductos" que obtiene los datos de los productos y lo almacena en una lista de objetos, luego esta se enlaza al control Grilla creado.
Inicialmente la línea de código donde se configura la propiedad GroupColumns esta comentada.

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:


Observar que los valores de las columnas IdCategoria y NombreCategoria salen repetidos, que es la forma normal como funciona el DataGridView.

Detener la ejecución de la aplicación, descomentar la línea comentada:
dgvProducto.GroupColumns = new string[] { "IdCategoria", "NombreCategoria"};

Probar nuevamente y se mostrará una ventana similar a la siguiente figura:


Observar que las columnas IdCategoria y NombreCategoria no se repite los valores ya que se agrupo mediante la propiedad "GroupColumns".

Detener nuevamente la ejecución de la aplicación y eliminar el campo "NombreCategoria" de la propiedad GroupColumns, tal como sigue:
dgvProducto.GroupColumns = new string[] { "IdCategoria" };

Probar nuevamente y se mostrará una ventana similar a la siguiente figura:


Observar que solo la primera columna esta agrupada. Si deseas puedes aumentar nuevos productos con nombres iguales y aumentar a la propiedad "GroupColumns" el campo "NombreProducto".

Comentario Final

En este post hemos visto como agrupar celdas repetidas en una o varias columnas del DataGridView de Windows Forms para lo cual sobre escribimos los eventos CellPainting y CellFormatting que son los 2 principales eventos de dicho control.

En realidad la agrupación de valores repetidos No ha combinado las celdas sino solo ha borrado los valores repetidos y ha puesto el borde superior. Si deseas combinar las celdas se tendría que programar la escritura en el evento Paint causando demora al renderizar, es decir, la técnica que estamos usando es la mas eficiente aunque no se vea fusionada la celda.

Descarga

lunes, 12 de enero de 2015

Entrenamiento - Tercer Seminario .NET (Reflection y Creación de Librerías de Controles WinForms)

Tercer Seminario .NET: Reflection y Creación de Librerías de Controles WinForms

Este Miércoles 14 de Enero continuaremos con los Seminarios de .NET, esta vez para conocer mas a fondo "Reflection" y su uso en la "Creación de Librería de Controles".

Los que asistan verán muchas Demos reales de controles que le serán útiles en su desarrollo, tales como:

- Log de Objetos Recursivo
- Buscador de Miembros en Múltiples Assemblies
- Texto con Formato Numérico, Cadena, Fecha
- ListView con DataSource
- TreeView con DataSource
- DataGridView Paginado
- Búsqueda de Datos
- Ubigeo (Dpto, Prov, Dist)

La inversión del Seminario es de 40 soles sin certificado y 60 soles con certificado.

El lugar es en el Auditorio CEFAS en Jr Manuel Segura 271 Santa Beatriz (Lima) altura de la cuadra 15 de la Av Arequipa.

La entrada es a partir de las 6:30 pm y el seminario es de 7:00 pm a 10:00 pm.

Los interesados deberán enviar un correo a: Luis.duenash@gmail.com con copia a informes@hte.com.pe confirmando su participación.

Los esperamos.

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