martes, 4 de noviembre de 2014

El Demo del Día: Paginación en un GridView con Columna de CheckBoxs

Paginación en un GridView con Columna de CheckBoxs

Después de mas de 2 meses retomamos los posts de ASP .NET WebForms, en esta ocasión publicaré un caso práctico muy útil.

Requerimiento

Se desea listar registros en forma paginada usando el GridView pero mostrando una columna con Checks que permita seleccionar los registros para un uso posterior como grabar datos en el servidor.

Problema

Si creamos una grilla sin paginar con una columna de tipo CheckBox no hay problemas, pero si paginamos, al seleccionar los checks de una pagina y cambiar de pagina se perderán debido a que la grilla siempre se tiene que enlazar nuevamente para mostrar la pagina seleccionada.

Solución

Para solucionar el problema debemos enlazar la columna checkbox a una propiedad de la entidad del negocio de tipo boolean, luego debemos almacenar el valor seleccionado, lo cual se puede hacer de 2 formas:
1. Guardar el valor cada vez que se selecciona el check, ya sea para marcar o desmarcar.
2. Guardar los valores seleccionados al regresar al servidor ya sea al cambiar de página o al hacer PostBack mediante algún botón.

Definitivamente, la mejor opción es la segunda, es decir, cada vez que ocurra un Postback guardaremos los checks seleccionados de la pagina actual.

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 la Aplicación Web en ASP .NET Web Form

Crear un "Nuevo Sitio web vacío de ASP .NET" en C# llamado "GridView_Paginado_Checks".
Para simplificar el Demo no he creado librerías de clases y las clases las he incluido dentro de la aplicación Web.

Crear la Clase Entidad del Negocio

Crear la clase beProducto escribiendo el siguiente código:

using System;
namespace GridView_Paginado_Checks
{
    public class beProducto
    {
        public bool Seleccion { get; set; }
        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: La propiedad Seleccion es la que se va a enlazar al check para guardar el valor seleccionado.

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 GridView_Paginado_Checks
{
    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 GridView_Paginado_Checks
{
    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 el Archivo de Hoja de Estilo

Agregar un archivo de hoja de estilo llamado ACME.css y escribir lo siguiente:

body {
    background-color:aqua;
}
.Titulo {
    background-color:black;
    color:white;
    text-transform:uppercase;
    font-size:xx-large;
    font-weight:bold;
}
.AnchoTotal {
    width:100%;
}
.Centrado {
    text-align:center;
}
.Subtitulo {
    background-color:white;
    color:blue;
    text-transform:capitalize;
    font-size:x-large;
    font-weight:bold;
}
.FilaCabecera {
    background-color: lightgray;
    color:black;
}
.FilaDatos {
    background-color: white;
    color:blue;
}

Crear la Pagina ASP .NET como un Formulario Web Form

Agregar un Formulario Web Form al proyecto llamado: "ListaProductos.aspx" y escribir lo siguiente:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="ListaProductos.aspx.cs" Inherits="GridView_Paginado_Checks.ListaProductos" %>
<%@ Import Namespace="GridView_Paginado_Checks" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
    <link href="ACME.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <table class="AnchoTotal">
            <tr class="Titulo">
                <td>Paginacion en un GridView con Columna de Checks</td>
            </tr>
            <tr class="Subtitulo">
                <td>Selecciona de la Lista de Productos</td>
            </tr>
            <tr>
                <td>
                    <asp:GridView ID="gvProducto" AllowPaging="true" AutoGenerateColumns="false"
                        Width="800px" runat="server" OnPageIndexChanging="paginarProductos">
                        <HeaderStyle CssClass="FilaCabecera" />
                        <RowStyle CssClass="FilaDatos" />
                        <PagerStyle CssClass="FilaCabecera" />
                        <Columns>
                            <asp:TemplateField ItemStyle-Width="20px">
                                <ItemTemplate>
                                    <asp:CheckBox ID="chkSeleccion" runat="server"
                                      Checked="<%#((beProducto)Container.DataItem).Seleccion%>"  />
                                </ItemTemplate>
                            </asp:TemplateField>
                            <asp:TemplateField HeaderText="Código" ItemStyle-Width="80px">
                                <ItemTemplate>
                                    <%#((beProducto)Container.DataItem).IdProducto%>
                                </ItemTemplate>
                            </asp:TemplateField>
                            <asp:TemplateField HeaderText="Descripción del Producto"
                              ItemStyle-Width="200px">
                                <ItemTemplate>
                                    <%#((beProducto)Container.DataItem).Nombre%>
                                </ItemTemplate>
                            </asp:TemplateField>
                            <asp:TemplateField HeaderText="Id Prov" ItemStyle-Width="80px">
                                <ItemTemplate>
                                    <%#((beProducto)Container.DataItem).IdProveedor%>
                                </ItemTemplate>
                            </asp:TemplateField>
                            <asp:TemplateField HeaderText="Id Cat" ItemStyle-Width="80px">
                                <ItemTemplate>
                                    <%#((beProducto)Container.DataItem).IdCategoria%>
                                </ItemTemplate>
                            </asp:TemplateField>
                            <asp:TemplateField HeaderText="Pre Unit" ItemStyle-Width="80px">
                                <ItemTemplate>
                                    <%#((beProducto)Container.DataItem).PrecioUnitario.ToString("n2")%>
                                </ItemTemplate>
                            </asp:TemplateField>
                            <asp:TemplateField HeaderText="Stock" ItemStyle-Width="80px">
                                <ItemTemplate>
                                    <%#((beProducto)Container.DataItem).Stock%>
                                </ItemTemplate>
                            </asp:TemplateField>
                        </Columns>
                    </asp:GridView>
                </td>
            </tr>
            <tr>
                <td class="Centrado">
                    <asp:Button ID="btnGrabar" Text="Grabar" runat="server"
                      OnClick="grabarProductosSeleccionados" />
                </td>
            </tr>
            <tr>
                <td><asp:Label ID="lblMensaje" Font-Bold="true" runat="server"/></td>
            </tr>
        </table>
    </div>
    </form>
</body>
</html>

El preview del diseño se mostrará similar a la siguiente figura:


Escribir el siguiente código C# en la página:

using System;
using System.Text; //StringBuilder
using System.Collections.Generic; //List
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace GridView_Paginado_Checks
{
    public partial class ListaProductos : System.Web.UI.Page
    {
        List<beProducto> lbeProducto;

        protected void Page_Load(object sender, EventArgs e)
        {
            if (!Page.IsPostBack)
            {
                brProducto obrProducto = new brProducto();
                lbeProducto = obrProducto.listar();
                Session["Productos"] = lbeProducto;
                gvProducto.DataSource = lbeProducto;
                gvProducto.DataBind();
            }
            else
            {
                guardarChecksPagina();
            }
        }

        protected void paginarProductos(object sender, GridViewPageEventArgs e)
        {
            lbeProducto = (List<beProducto>)Session["Productos"];
            gvProducto.PageIndex = e.NewPageIndex;
            gvProducto.DataSource = lbeProducto;
            gvProducto.DataBind();
        }

        private void guardarChecksPagina()
        {
            lbeProducto = (List<beProducto>)Session["Productos"];
            int indice;
            for (int i = 0; i < gvProducto.Rows.Count; i++)
            {
                indice = (gvProducto.PageIndex * gvProducto.PageSize) + i;
                lbeProducto[indice].Seleccion =
                ((CheckBox)gvProducto.Rows[i].Cells[0].Controls[1]).Checked;
            }
            Session["Productos"] = lbeProducto;
        }

        protected void grabarProductosSeleccionados(object sender, EventArgs e)
        {
            StringBuilder sb = new StringBuilder("Ids Seleccionados: ");
            lbeProducto = (List<beProducto>)Session["Productos"];
            //List<beProducto> lbeSeleccion = lbeProducto.FindAll(x => x.Seleccion.Equals(true));
            for (int i = 0; i < lbeProducto.Count; i++)
            {
                if (lbeProducto[i].Seleccion)
                {
                    sb.Append(lbeProducto[i].IdProducto);
                    sb.Append(", ");
                }
            }
            sb = sb.Remove(sb.Length - 2, 2);
            lblMensaje.Text = sb.ToString();
        }
    }
}

Nota: La principal función es "guardarChecksPagina" que se llama en los PostBacks y permite guardar todos los checks de la pagina actual en la lista de objetos.
Además en la función controladora "grabarProductosSeleccionados" asociada al click del botón "btnGuardar" se usa un StringBuilder para concatenar los códigos de todos los checks seleccionados y mostrarlos en el Label "lblMensaje".
En esta función hay una linea comentada que permite crear una lista solo con los productos seleccionados, para nuestro caso solo se mostrará en pantalla, pero si se debe enviar al servidor solo los productos seleccionados usar esta linea y comentar el resto.

Modificar el Archivo Web.Config para especificar la cadena de conexión a Northwind

<?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>
    <system.web>
      <compilation debug="true" targetFramework="4.5" />
      <httpRuntime targetFramework="4.5" />
    </system.web>
</configuration>

Probar la Pagina Web

Guardar el Sitio Web, clic derecho a la Pagina "ListaProductos.aspx" y seleccionar "Ver en el explorador". Se mostrará una ventana similar a la siguiente figura:


Seleccionar checks de diferentes paginas y verificar que se han quedado seleccionados al cambiar de pagina, luego click al botón "Guardar" para confirmar los códigos solo de los productos seleccionados, tal como se muestra en la siguiente figura:


Comentario Final

En este post hemos visto como mantener los registros seleccionados usando una columna tipo check en una grilla paginada, la cual usa plantillas de datos para las columnas y asocia la propiedad checked del checkbox a una propiedad Seleccion del modelo o entidad del negocio.

Descarga:
GridView_Paginado_Checks

El Libro del Día: jQuery 2 Recipes

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

Titulo: jQuery 2 Recipes
Autor: Arun K. Pande
Editorial: Apress
Nro Paginas: 621

Capítulos:
Chapter 1: Introduction
Chapter 2: jQuery Fundamentals
Chapter 3: jQuery Selectors
Chapter 4: jQuery Selectors Filtering and Expansion
Chapter 5: DOM Traversing
Chapter 6: DOM Manipulation
Chapter 7: Event Handling
Chapter 8: jQuery Effects and Animation
Chapter 9: jQuery AJAX
Chapter 10: jQuery UI
Chapter 11: jQuery Mobile
Chapter 12: jqWidgets Framework
Appendix A: Basic HTML5 and CSS3
Appendix B: Appendix B – Web Console
Appendix C: Deploy Web Application
Appendix D: Logging, Error Handling, and Debugging

Descarga:
jQuery2_Recipes