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

El Libro del Día: ASP.NET MVC 4 In Action

El Libro del Día: 2014-06-24

Titulo: ASP.NET MVC 4 In Action
Autor: Jeffrey Palermo, Jimmy Bogard, Eric Hexter, 
           Matthew Hinze, Jeremy Skinner
Editorial: Manning
Nro Paginas: 440

Capítulos:
PART 1 HIGH-SPEED FUNDAMENTALS
1 Introduction to ASP.NET MVC
2 Hello MVC world
3 View fundamentals
4 Action-packed controllers
PART 2 WORKING WITH ASP.NET MVC
5 View models
6 Validation
7 Ajax in ASP.NET MVC
8 Security
9 Controlling URLs with routing
10 Model binders and value providers
11 Mapping with AutoMapper
12 Lightweight controllers
13 Organization with areas
14 Third-party components
15 Data access with NHibernate
PART 3 MASTERING ASP.NET MVC
16 Extending the controller
17 Advanced view techniques
18 Dependency injection and extensibility
19 Portable areas
20 Full system testing
21 Hosting ASP.NET MVC applications
22 Deployment techniques
23 Upgrading to ASP.NET MVC 4
24 ASP.NET Web API

Descarga:
ASPNET_MVC4_InAction