martes, 24 de junio de 2014

El Demo del Día: Comparación de Rendimiento al Llenar Grillas Jerárquicas en WinForms

Comparación de Rendimiento al Llenar Grillas Jerárquicas en WinForms

Hoy veremos un caso muy interesante que es crear Grillas Jerárquicas en WinForms para realizar consultas desconectadas de 3 niveles. En el presente Demo veremos como consultar los Detalles de las Ordenes de los Clientes de la Base de Datos Northwind.

Problema con Grillas Jerárquicas en WinForms

En WinForms hay 2 problemas que se presentan cuando queremos trabajar con Grillas Jerárquicas:
1. El Control DataGridView no tiene soporte para listas jerárquicas.
2. Cual es la mejor forma de llenar la grilla:
    2.1. Crear una lista de objetos jerárquicos que tengan como propiedades otras listas de objetos.
    2.2. Crear varias listas de objetos lineales, es decir No jerárquicos y realizar el filtro en la App.

Solución del Problema con Grillas Jerárquicas en WinForms

1. Podemos agregar un Control DataGridView a otro Control DataGridView usando el método Add de su colección Controls, ocultarlo y ubicarlo dentro de la grilla usando el método SetBounds.
2. La mejor forma de llenar una grilla WinForms es la segunda, es decir, tener varias listas en un objeto con todas las listas (lineales No jerárquicas), ya que la carga es más rápido y consume menos memoria.

Creación del Procedimiento Almacenado que devuelva los 3 Selects

Create Procedure [dbo].[uspClienteOrdenDetalleListar]
As
Select CustomerID,CompanyName,Address,IsNull(ContactName,'') As ContactName From Customers
Select OrderID,OrderDate,CustomerID,EmployeeID From Orders
Select d.OrderID,d.ProductID,p.ProductName,d.UnitPrice,d.Quantity
From Order_Details d Inner Join Products p On d.ProductID=p.ProductID

Creación de las Clases de Tipo Entidades del Negocio (be: Business Entities)

Creamos una aplicación WinForms llamada: "Grillas_Jerarquicas_3Niveles" y agregamos las sgtes clases:

1. Clase beDetalle
using System;
namespace Grillas_Jerarquicas_3Niveles
{
    public class beDetalle
    {
        public int IdOrden { get; set; }
        public int IdProducto { get; set; }
        public string NombreProducto { get; set; }
        public decimal PrecioUnitario { get; set; }
        public short Cantidad { get; set; }
        public decimal PrecioTotal {
            get
            {
                return (PrecioUnitario * Cantidad);
            }
        }
    }
}

2. Clase beOrden
using System;
using System.Collections.Generic;

namespace Grillas_Jerarquicas_3Niveles
{
    public class beOrden
    {
        public int IdOrden { get; set; }
        public DateTime FechaOrden { get; set; }
        public string IdCliente { get; set; }
        public int IdEmpleado { get; set; }
        public List<beDetalle> Detalles { get; set; }
    }
}

3. Clase beCliente
using System;
using System.Collections.Generic;
namespace Grillas_Jerarquicas_3Niveles
{
    public class beCliente
    {
        public string IdCliente { get; set; }
        public string Nombre { get; set; }
        public string Direccion { get; set; }
        public string Contacto { get; set; }
        public List<beOrden> Ordenes { get; set; }
    }
}

4. Clase beListas
using System;
using System.Collections.Generic;
namespace Grillas_Jerarquicas_3Niveles
{
    public class beListas
    {
        public List<beCliente> Clientes {get; set;}
        public List<beOrden> Ordenes { get; set; }
        public List<beDetalle> Detalles { get; set; }
    }
}

Nota: Por un tema de simplificación del demo las clases han sido incluidas dentro de la misma aplicación WinForms, pero lo ideal es que estén en Librerías.

Creación de la Clase de Acceso a Datos (da: Data Access)

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
namespace Grillas_Jerarquicas_3Niveles
{
    public class daCliente
    {
        public beListas listar(SqlConnection con)
        {
            beListas obeListas = new beListas();
            List<beCliente> lbeCliente = new List<beCliente>();
            List<beOrden> lbeOrden = new List<beOrden>();
            List<beDetalle> lbeDetalle = new List<beDetalle>();
            beCliente obeCliente = null;
            beOrden obeOrden = null;
            beDetalle obeDetalle = null;
            //Llenar las 3 Listas desde el SP de SQL Server
            SqlCommand cmd = new SqlCommand("uspClienteOrdenDetalleListar", con);
            SqlDataReader drd = cmd.ExecuteReader();
            if (drd != null)
            {
                //Leer el Primer Select de Clientes
                while (drd.Read())
                {
                    obeCliente = new beCliente();
                    obeCliente.IdCliente = drd.GetString(0);
                    obeCliente.Nombre = drd.GetString(1);
                    obeCliente.Direccion = drd.GetString(2);
                    obeCliente.Contacto = drd.GetString(3);
                    lbeCliente.Add(obeCliente);
                }
                //Leer el Segundo Select de Ordenes
                if (drd.NextResult())
                {
                    while (drd.Read())
                    {
                        obeOrden = new beOrden();
                        obeOrden.IdOrden = drd.GetInt32(0);
                        obeOrden.FechaOrden = drd.GetDateTime(1);
                        obeOrden.IdCliente = drd.GetString(2);
                        obeOrden.IdEmpleado = drd.GetInt32(3);
                        lbeOrden.Add(obeOrden);
                    }
                }
                //Leer el Tercer Select de Detalles
                if (drd.NextResult())
                {
                    while (drd.Read())
                    {
                        obeDetalle = new beDetalle();
                        obeDetalle.IdOrden = drd.GetInt32(0);
                        obeDetalle.IdProducto = drd.GetInt32(1);
                        obeDetalle.NombreProducto = drd.GetString(2);
                        obeDetalle.PrecioUnitario = drd.GetDecimal(3);
                        obeDetalle.Cantidad = drd.GetInt16(4);
                        lbeDetalle.Add(obeDetalle);
                    }
                }
            }
            //Llenar las 3 Listas al objeto
            obeListas.Clientes = lbeCliente;
            obeListas.Ordenes = lbeOrden;
            obeListas.Detalles = lbeDetalle;
            return (obeListas);
        }
    }
}

Creación de la Clase de Reglas de Negocios (br: Business Rules)

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

namespace Grillas_Jerarquicas_3Niveles
{
    public class brCliente
    {
        public beListas listarLineal()
        {
            beListas obeListas;
            string conexion = ConfigurationManager.ConnectionStrings["conNW"].ConnectionString;
            using (SqlConnection con = new SqlConnection(conexion))
            {
                con.Open();
                daCliente odaCliente = new daCliente();
                obeListas = odaCliente.listar(con);
            } //con.Close(); con.Dispose(); con=null;
            return (obeListas);
        }

        public List<beCliente> listarJerarquico()
        {
            beListas obeListas = listarLineal();
            foreach (beCliente unCliente in obeListas.Clientes)
            {
                foreach (beOrden unaOrden in obeListas.Ordenes)
                {
                    unaOrden.Detalles = obeListas.Detalles.FindAll
                    (x => x.IdOrden.Equals(unaOrden.IdOrden));
                }
                unCliente.Ordenes = obeListas.Ordenes.FindAll
                (x => x.IdCliente.Equals(unCliente.IdCliente));
            }
            return (obeListas.Clientes);
        }
    }
}

Nota: Esta clase tiene 2 métodos: listarLineal() que devuelve un objeto con las 3 listas y listarJerarquico() que devuelve una lista de clientes que a su vez tiene una lista de ordenes y esta a su vez tiene una lista de detalles de cada orden.

Aumentar la cadena de conexión en el archivo de configuración

Abrir el archivo App.Config y debajo de <configuration> escribir lo siguiente:
  <connectionStrings>
    <add name="conNW" providerName="System.Data.SqlClient" 
      connectionString="uid=UsuarioNW;pwd=123456;data source=DSOFT\Sqlexpress;
      initial catalog=Northwind"/>
  </connectionStrings>

Nota: Cambiar el usuario y password, también el data source con el servidor que se tenga disponible.

Formulario de Prueba de Grillas Jerárquicas llenadas con Listas Lineales

Crear un formulario llamado: "frmGrillaListasLineales" con una Grilla llamada "dgvCliente" acoplada a todo el formulario, asociar al evento "Load" del formulario la función: "cargarDatos", tal como se muestra en el siguiente código:

using System.Windows.Forms;
using System.Diagnostics;

namespace Grillas_Jerarquicas_3Niveles
{
    public partial class frmGrillaListasLineales : Form
    {
        private beListas obeListas;
        private DataGridView dgvOrden;
        private DataGridView dgvDetalle;

        public frmGrillaListasLineales()
        {
            InitializeComponent();
        }

        private void cargarDatos(object sender, EventArgs e)
        {
            Stopwatch oReloj = new Stopwatch();
            oReloj.Start();
            brCliente obrCliente = new brCliente();
            obeListas = obrCliente.listarLineal();
            //Configurar Grilla Clientes
            dgvCliente.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
            dgvCliente.CellMouseMove += new DataGridViewCellMouseEventHandler 
            (cambiarPunteroMouse);
            dgvCliente.CellContentClick += new DataGridViewCellEventHandler
            (listarOrdenesPorCliente);
            dgvCliente.KeyPress += new KeyPressEventHandler(ocultarGrillas);
            dgvCliente.DataSource = obeListas.Clientes;
            //Configurar Grilla Ordenes
            dgvOrden = new DataGridView();
            dgvOrden.Visible = false;
            dgvOrden.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
            dgvOrden.CellMouseMove += new DataGridViewCellMouseEventHandler 
            (cambiarPunteroMouse);
            dgvOrden.CellContentClick += new DataGridViewCellEventHandler
            (listarDetallesPorOrden);
            dgvCliente.Controls.Add(dgvOrden);
            //Configurar Grilla Detalles
            dgvDetalle = new DataGridView();
            dgvDetalle.Visible = false;
            dgvDetalle.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
            dgvOrden.Controls.Add(dgvDetalle);
            oReloj.Stop();
            this.Text = String.Format("Tiempo Procesamiento: {0:n0} msg",
            oReloj.Elapsed.TotalMilliseconds);
        }

        private void cambiarPunteroMouse(object sender, DataGridViewCellMouseEventArgs e)
        {
            DataGridView dgv = (DataGridView)sender;
            if (e.ColumnIndex.Equals(0)) dgv.Cursor = Cursors.Hand;
            else dgv.Cursor = Cursors.Default;
        }

        private void listarOrdenesPorCliente(object sender, DataGridViewCellEventArgs e)
        {
            if (e.ColumnIndex.Equals(0))
            {
                string IdCliente = dgvCliente.CurrentCell.Value.ToString();
                dgvOrden.Visible = true;
                dgvOrden.DataSource = obeListas.Ordenes.FindAll
                (obe => obe.IdCliente.Equals(IdCliente));
                int x = dgvCliente.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false).Left + 
                dgvCliente.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false).Width;
                int y = dgvCliente.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false).Top + 
                dgvCliente.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false).Height;
                dgvOrden.SetBounds(x, y, 800, 400);
            }
        }

        private void listarDetallesPorOrden(object sender, DataGridViewCellEventArgs e)
        {
            if (e.ColumnIndex.Equals(0))
            {
                int IdOrden = (int)dgvOrden.CurrentCell.Value;
                dgvDetalle.Visible = true;
                dgvDetalle.DataSource = obeListas.Detalles.FindAll
                (obe => obe.IdOrden.Equals(IdOrden));
                int x = dgvOrden.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false).Left + 
                dgvOrden.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false).Width;
                int y = dgvOrden.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false).Top + 
                dgvOrden.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false).Height;
                dgvDetalle.SetBounds(x, y, 600, 200);
            }
        }

        private void ocultarGrillas(object sender, KeyPressEventArgs e)
        {
            if (((int)e.KeyChar).Equals(27))
            {
                if (dgvDetalle.Visible) dgvDetalle.Visible = false;
                if (dgvOrden.Visible) dgvOrden.Visible = false;
            }
        }
    }
}

Formulario de Prueba de Grillas Jerárquicas llenadas con Listas Jerárquicas

Crear un formulario llamado: "frmGrillaListasJerarquicas" con una Grilla llamada "dgvCliente" acoplada a todo el formulario, asociar al evento "Load" del formulario la función: "cargarDatos", tal como se muestra en el siguiente código:

using System.Windows.Forms;
using System.Diagnostics;

namespace Grillas_Jerarquicas_3Niveles
{
    public partial class frmGrillaListasJerarquicas : Form
    {
        private List<beCliente> lbeCliente;
        private DataGridView dgvOrden;
        private DataGridView dgvDetalle;

        public frmGrillaListasJerarquicas()
        {
            InitializeComponent();
        }

        private void cargarDatos(object sender, EventArgs e)
        {
            Stopwatch oReloj = new Stopwatch();
            oReloj.Start();
            brCliente obrCliente = new brCliente();
            lbeCliente = obrCliente.listarJerarquico();
            //Configurar Grilla Clientes
            dgvCliente.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
            dgvCliente.CellMouseMove += new DataGridViewCellMouseEventHandler 
            (cambiarPunteroMouse);
            dgvCliente.CellContentClick += new DataGridViewCellEventHandler
            (listarOrdenesPorCliente);
            dgvCliente.KeyPress += new KeyPressEventHandler(ocultarGrillas);
            dgvCliente.DataSource = lbeCliente;
            //Configurar Grilla Ordenes
            dgvOrden = new DataGridView();
            dgvOrden.Visible = false;
            dgvOrden.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
            dgvOrden.CellMouseMove += new DataGridViewCellMouseEventHandler 
            (cambiarPunteroMouse);
            dgvOrden.CellContentClick += new DataGridViewCellEventHandler
            (listarDetallesPorOrden);
            dgvCliente.Controls.Add(dgvOrden);
            //Configurar Grilla Detalles
            dgvDetalle = new DataGridView();
            dgvDetalle.Visible = false;
            dgvDetalle.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
            dgvOrden.Controls.Add(dgvDetalle);
            oReloj.Stop();
            this.Text = String.Format("Tiempo Procesamiento: {0:n0} msg",
            oReloj.Elapsed.TotalMilliseconds);
        }

        private void cambiarPunteroMouse(object sender, DataGridViewCellMouseEventArgs e)
        {
            DataGridView dgv = (DataGridView)sender;
            if (e.ColumnIndex.Equals(0)) dgv.Cursor = Cursors.Hand;
            else dgv.Cursor = Cursors.Default;
        }

        private void listarOrdenesPorCliente(object sender, DataGridViewCellEventArgs e)
        {
            if (e.ColumnIndex.Equals(0))
            {
                string IdCliente = dgvCliente.CurrentCell.Value.ToString();
                int posCliente = lbeCliente.FindIndex(obe => obe.IdCliente.Equals(IdCliente));
                dgvOrden.Visible = true;
                dgvOrden.DataSource = lbeCliente[posCliente].Ordenes;
                int x = dgvCliente.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false).Left + 
                dgvCliente.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false).Width;
                int y = dgvCliente.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false).Top + 
                dgvCliente.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false).Height;
                dgvOrden.SetBounds(x, y, 800, 400);
            }
        }

        private void listarDetallesPorOrden(object sender, DataGridViewCellEventArgs e)
        {
            if (e.ColumnIndex.Equals(0))
            {
                string IdCliente = dgvCliente.CurrentCell.Value.ToString();
                int posCliente = lbeCliente.FindIndex(obe => obe.IdCliente.Equals(IdCliente));
                int IdOrden = (int)dgvOrden.CurrentCell.Value;
                int posOrden = lbeCliente[posCliente].Ordenes.FindIndex 
                (obe=>obe.IdOrden.Equals(IdOrden));
                dgvDetalle.Visible = true;
                dgvDetalle.DataSource = lbeCliente[posCliente].Ordenes[posOrden].Detalles;
                int x = dgvOrden.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false).Left + 
                dgvOrden.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false).Width;
                int y = dgvOrden.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false).Top + 
                dgvOrden.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false).Height;
                dgvDetalle.SetBounds(x, y, 600, 200);
            }
        }

        private void ocultarGrillas(object sender, KeyPressEventArgs e)
        {
            if (((int)e.KeyChar).Equals(27))
            {
                if (dgvDetalle.Visible) dgvDetalle.Visible = false;
                if (dgvOrden.Visible) dgvOrden.Visible = false;
            }
        }
    }
}

Ejecución y Pruebas de Ambos Formularios

Configurar en el archivo "Program.cs" en la función "Main" lo siguiente: 
Application.Run(new frmGrillaListasLineales());
Pulsar F5 para ejecutar y probar el primer formulario con Listas Lineales y la ventana sale de inmediato (de 200 a 400 msg aprox), luego clic a cualquier celda de la primera columna con el código del cliente y se mostrará sus ordenes, luego clic a cualquier celda de la columna con el código de la orden y se mostrará sus detalles.

Para salir de las grillas pulsar la tecla Escape sobre la grilla de Clientes. Detener la aplicación.

Configurar en el archivo "Program.cs" en la función "Main" lo siguiente: 
Application.Run(new frmGrillaListaJerarquicas());
Pulsar F5 para ejecutar y probar el segundo formulario con Listas Jerárquicas y la ventana demora en mostrarse (mas de 12000 msg aprox), luego clic a cualquier celda de la primera columna con el código del cliente y se mostrará sus ordenes, luego clic a cualquier celda de la columna con el código de la orden y se mostrará sus detalles.

Comentario Final

Hemos demostrado que con un poco de ingenio podemos crear Grillas Jerárquicas en WinForms usando Listas de Objetos y que mejor es usar Listas Lineales y filtrarlas a demanda en vez de crear listas jerárquicas que demoran en cargarse (12,000 msg Versus 200 msg) y consumen mucha memoria. Espero les sea útil sobre a todo los desarrolladores que trabajan en Cliente Servidor o Windows que mucha veces extrañan la facilidad de Visual Foxpro, Power Builder, etc.

Descarga:
Demo06_Grillas_Jerarquicas_3Niveles

5 comentarios:

  1. ¡Excelente!. Particularmente he sido testigo de cómo ha ido usted perfeccionando una aplicación Windows Forms que permita la consulta jerarquizada, primero con el control GridView y en este último nivel con el más usado, el DataGridView.

    Muchas gracias por el demo, de hecho será de utilidad, sobre todo cuando pueda adaptarlo para la consulta de gráficos Gantt, espero no haya inconveniente de su parte.

    Saludos.

    ResponderBorrar
  2. Como dice Juan Carlos, en WinForms hasta el 2003 existía el control DataGrid que soportaba jerarquía pero en el 2005 apareció el DataGridView que no tenia el soporte jerárquico, pero con un poco de creatividad podemos darle esta funcionalidad. Saludos.

    ResponderBorrar
  3. Muy buenoo... como siempre demos de mucha utilidad..

    ResponderBorrar