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
BUENA KONAN
ResponderBorrarBuen demo, como dice usted profe la programación es todo un arte e ingenio.
ResponderBorrarSaludos.
Esta buenazo el ejemplo profe... Gracias..
ResponderBorrar¡Genial!. Recuerdo que usted estuvo por nuestra humilde empresa cuando nos orientó en el desarrollo de una aplicación que gestionaría proyectos de desarrollo, en aquella época tuvimos la necesidad contar con una opción así y veo que ahora es una realidad, muchas gracias por compartirlo y sobre todo por su constancia.
ResponderBorrarSaludos del equipo de GCTI. Siempre con agradecimiento y estima.
esta muy bueno el ejemplo, podrías subir los mismo pero no en c#
ResponderBorrarAlguien tiene el proyecto que me lo pueda mandar por favor?
ResponderBorrar