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

jueves, 20 de noviembre de 2014

El Libro del Día: Building Web Applications with SVG

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

Titulo: Building Web Applications with SVG
Autor: David Dailey, Jon Frost, Domenico Strazzullo
Editorial: Microsoft
Nro Paginas: 293

Capítulos:
Chapter 1 SVG Basics
Chapter 2 Creating and Editing SVG Graphics
Chapter 3 Adding Text, Style, and Transforms
Chapter 4 Motion and Interactivity
Chapter 5 SVG Filters
Chapter 6 SVG Tools and Resources
Chapter 7 Building a Web Application: Case Studies

Descarga:
Building_Web_Applications_with_SVG

miércoles, 19 de noviembre de 2014

El Libro del Día: HTML5 Programming for ASP.NET Developers

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

Titulo: HTML5 Programming for ASP.NET Developers
Autor: Bipin Joshi
Editorial: Apress
Nro Paginas: 379

Capítulos:
Chapter 1: Overview of HTML5 and ASP.NET 4.5
Chapter 2: Overview of jQuery
Chapter 3: Working with Audio and Video
Chapter 4: Drawing with the Canvas
Chapter 5: Working with Forms and Controls
Chapter 6: Using History API and Custom Data Attributes
Chapter 7: Storing Data in Web Storage
Chapter 8: Developing Offline Web Applications
Chapter 9: Dealing with Local Files Using the File API
Chapter 10: Multithreading in Web Pages Using Web Workers
Chapter 11: Using the Communication API and Web Sockets
Chapter 12: Finding Location with the Geolocation API
Chapter 13: Styling Web Forms and Views with CSS3
Appendix A: HTML5 Learning Resources

Descarga:
HTML5_Programming_ASPNET_Developers

martes, 18 de noviembre de 2014

El Libro del Día: Pro HTML5 Performance

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

Titulo: Pro HTML5 Performance
Autor: Jay Bryant, Mike Jones
Editorial: Apress
Nro Paginas: 285

Capítulos:
Part 1: Introduction
Chapter 1: Introduction
Part 2: Performance Basics
Chapter 2: Development Principles
Chapter 3: Performance Guidelines
Chapter 4: Responsive Web Design
Chapter 5: Understanding the Web Reuse Pattern
Part 3: Building a Web Site
Chapter 6: Page Template
Chapter 7: Navigation
Chapter 8: Masthead
Chapter 9: Footer
Chapter 10: Fractal Design Patterns
Chapter 11: Link Control
Chapter 12: Sidebox Control
Chapter 13: Button Control
Chapter 14: Price Control
Chapter 15: Product Control
Chapter 16: Table Control
Chapter 17: Tab Control
Chapter 18: Form Controls

Descarga:
Pro_HTML5_Performance

lunes, 17 de noviembre de 2014

El Libro del Día:Practical WPF Charts and Graphics

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

Titulo: Practical WPF Charts and Graphics
Autor: Jack Xu
Editorial: Apress
Nro Paginas: 709

Capítulos:
1. Overview of WPF Programming
2. 2D Transformations
3. WPF Graphics Basics in 2D
4. Colors and Brushes
5. 2D Line charts
6. Specialized 2D Charts
7. Stock Charts
8. Interactive 2D Charts
9. 2D Chart Controls
10. Data Interpolations
11. Curve Fitting
12. 3D Transformations
13. WPF Graphics Basics in 3D
14. 3D Charts with the WPF 3D Engine
15. 3D Charts Without the WPF 3D Engine
16. Specialized 3D Charts

Descarga:
Practical_WPF_Charts_Graphics

domingo, 16 de noviembre de 2014

El Libro del Día: Building Enterprise Applications with WPF and The MVVM Pattern

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

Titulo: Building Enterprise Applications with WPF and The MVVM Pattern
Autor: Raffaele Garofalo
Editorial: Microsoft
Nro Paginas: 225

Capítulos:
1 Introduction to Model View ViewModel and Line of Business Applications
2 Design Patterns
3 The Domain Model
4 The Data Access Layer
5 The Business Layer
6 The UI Layer with MVVM
7 MVVM Frameworks and Toolkits

Descarga:
Building_Enterprise_Applications_with_WPF_and_The_MVVM_Pattern