Mostrando las entradas con la etiqueta Data Templates. Mostrar todas las entradas
Mostrando las entradas con la etiqueta Data Templates. Mostrar todas las entradas

viernes, 28 de noviembre de 2014

El Demo del Día: Preview de Imágenes en un TreeView de Directorios en WebForms

Preview de Imágenes en un TreeView de Directorios en WebForms

En este breve post veremos como mostrar los directorios de una cierta carpeta de la Aplicación Web, por ejemplo de las Imágenes. Para esto usaremos el control TreeView para mostrar jerárquicamente los directorios y el control Repeater para listar los archivos y ver las imágenes del directorio seleccionado.

Crear la Aplicación Web en ASP .NET Web Form

Crear un "Nuevo Sitio web vacío de ASP .NET" en C# llamado "TreeViewDirectorios_PreviewImagenes". Crear la siguiente estructura de carpetas en el proyecto:
- Estilos: Contiene el archivo de hoja de estilo ACME.css
- Imagenes: Contiene las carpetas Empleados y Productos
  - Empleados: Contiene archivos de los empleados del 01.jpg al 08..jpg
  - Productos: Contiene las siguientes carpetas
    - Bebidas: Contiene archivos jpg con bebidas.
    - Condimentos: Contiene archivos jpg con condimentos.
    - Confecciones: Contiene archivos jpg con confecciones.
- Paginas: Contiene el formulario web VisorImagenes.aspx

Crear el Archivo de Hoja de Estilo

En la carpeta "Estilos" agregar un archivo de hoja de estilo llamado "ACME.css" y escribir lo siguiente:

body {
    background-color:lightgray;
}
.Titulo {
    background-color:black;
    color:white;
    text-transform:uppercase;
    font-size:xx-large;
    font-weight:bold;
}
.Subtitulo {
    background-color:white;
    color:black;
    text-transform:capitalize;
    font-size:x-large;
    font-weight:bold;
}
.Imagen {
    width:120px;
    height:100px;
    border:solid;
    border-width:20px;
    border-color:lightgray;
}

.Imagen:hover {
    cursor:pointer;
    border:solid;
    border-width:20px;
    border-color:yellow;
}

Crear la Pagina como Formulario WebForm

En la carpeta "Paginas" agregar un formulario WebForm llamado: "VisorImagenes.aspx" y escribir lo siguiente:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="VisorImagenes.aspx.cs" Inherits="TreeViewDirectorios_PreviewImagenes.Paginas.VisorImagenes" %>
<!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="../Estilos/ACME.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <table>
            <tr class="Titulo">
                <td colspan="2">Preview de Imagenes en un TreeView de Directorios</td>
            </tr>
            <tr class="Subtitulo">
                <td colspan="2">Creado Por: Luis Duenas</td>
            </tr>
            <tr>
                <td style="vertical-align:top">
                    <asp:TreeView ID="tvwDirectorio" runat="server"
                        OnSelectedNodeChanged="mostrarImagenesDirectorio"/>
                </td>
                <td>
                    <asp:Repeater ID="rptImagen" EnableViewState="false" runat="server">
                        <ItemTemplate>
                            <img src="<%#obtenerUrl(Container.DataItem.ToString())%>"                              
                                title="<%#obtenerNombre(Container.DataItem.ToString())%>"
                                class="Imagen" alt="" />
                        </ItemTemplate>
                    </asp:Repeater>
                </td>
            </tr>
        </table>
    </div>
    </form>
</body>
</html>

Nota: Es importante que se desactive el ViewState del Repeater para dismunir el HTML generado en el cliente, ya que no es necesario, porque en el servidor se esta enlazando el control Repeater cada vez que se elige un directorio (optmización del HTML enviado al cliente).

El diseño de la pagina debe mostrase similar a la siguiente figura:

Escribir el siguiente código C# para la pagina:

using System;
using System.Drawing;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.IO;

namespace TreeViewDirectorios_PreviewImagenes.Paginas
{
    public partial class VisorImagenes : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!Page.IsPostBack)
            {
                string ruta = Server.MapPath("../Imagenes");
                IEnumerable<string> listaDirectorios = Directory.EnumerateDirectories(ruta);
                TreeNode nodoRaiz = new TreeNode("Imagenes");
                tvwDirectorio.Nodes.Add(nodoRaiz);
                llenarNodos(nodoRaiz, listaDirectorios);
                tvwDirectorio.ExpandAll();
                tvwDirectorio.SelectedNodeStyle.BackColor = Color.Yellow;
                tvwDirectorio.SelectedNodeStyle.ForeColor = Color.Red;
            }
        }

        private void llenarNodos(TreeNode nodoPadre, IEnumerable<string> listaDirectorios)
        {
            foreach(string directorio in listaDirectorios)
            {
                TreeNode nodo = new TreeNode(Path.GetFileName(directorio));
                nodoPadre.ChildNodes.Add(nodo);
                IEnumerable<string> lista = 
                Directory.EnumerateDirectories(Path.Combine(Server.MapPath("../"), 
                nodo.ValuePath));
                if(lista.Count() > 0) llenarNodos(nodo, lista);
            }
        }

        protected void mostrarImagenesDirectorio(object sender, EventArgs e)
        {
            string directorio = Path.Combine(Server.MapPath("../"),
            tvwDirectorio.SelectedNode.ValuePath.Replace(@"/", @"\"));
            IEnumerable<string> archivos = Directory.EnumerateFiles(directorio, "*.jpg");
            rptImagen.DataSource = archivos;
            rptImagen.DataBind();
        }

        protected string obtenerUrl(string archivo)
        {
            return String.Format("../{0}/{1}", 
            tvwDirectorio.SelectedNode.ValuePath,Path.GetFileName(archivo));
        }

        protected string obtenerNombre(string archivo)
        {
            return Path.GetFileName(archivo);
        }
    }
}

Nota: Para leer directorios estamos usando Directory.EnumerateDirectories y para leer archivos estamos usando Directory.EnumerateFiles que es la forma mas eficiente de leer en .NET ya que solo crea un registro a la vez (consume menos memoria y es mas rápido).
Además se usa una función recursiva para llenar el TreeView con los directorios llamada "llenarNodos" que usa la propiedad ValuePath que obtiene la ruta del nodo actual.
También se definen 2 métodos protegidos: "obtenerUrl" y "obtenerNombre" que se usan en el HTML del Repeater.

Probar la Pagina Web

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


Observar como se muestra todas lar carpetas creadas dentro de la carpeta "Imagenes" en el TreeView, ahora seleccionar un directorio, por ejemplo "Empleados" y se mostrará las imágenes que esta contiene, similar a la siguiente figura:


Notar que al poner el mouse sobre una imagen se muestra como tooltip el nombre del archivo de imagen.

Ahora seleccionar otro directorio, por ejemplo "Bebidas" y disminuir el ancho del navegador (como si fuera una Tabla) y se muestra menos columnas pero el Diseño Web es Adaptativo (Responsive Web Design), tal como se ve en la siguiente figura:



Para finalizar, seleccionar otro directorio, por ejemplo "Condimentos" y disminuir aún mas el ancho del navegador (como si fuera un Teléfono Celular) y nuevamente el diseño se adapta mostrando una sola columna, tal como se ve en la siguiente figura:



Comentario Final

En este pequeño demo, hemos visto como llenar un TreeView en WebForms con los directorios de una cierta carpeta usando una función recursiva, además de como hacer el preview de su contenido usando un control Repeater y Plantillas de Datos con código incrustado en el servidor (las 2 funciones obtener). Además para mejorar la apariencia se hace usado hojas de estilo (CSS).

A los visitantes del Blog que no han sido alumnos míos y que a veces tienen pedidos que se hacen en clases en el programa PECI .NET o Web Developer, no es mi intención repetir lo que se ve en clases, sino, publicaríamos un Demo diario y el tiempo no me alcanzaría.

Saludos a todos y recuerden la frase: "Somos lo que pensamos de nosotros mismos".

Descarga


jueves, 13 de noviembre de 2014

El Demo del Día: Filtrar en Cabeceras del GridView con Totales en el Pie y Filas de Color

Filtrar en Cabeceras del GridView con Totales en el Pie y Filas de Color

El post que viene es muy interesante ya que es un 3 en 1. Se trata de una GridView en ASP.NET Web Forms que permita filtrar mediante controles en la cabecera, mostrar el número de registros encontrados en el pie y mostrar de color las palabras buscadas en los filtros en los registros encontrados.

Requerimiento

Se necesita crear una consulta web 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 ver de color rojo la palabra buscada en cada columna donde se ingrese un criterio de búsqueda.
- Se debe mostrar la cantidad de registros encontrados en la parte inferior.

Solución

- Usaremos el control GridView y personalizaremos la cabecera, el pie y los registros usando plantilla de datos en Web Forms (Data Template).

- Además usaremos el Enlace de Datos para conservar los valores de los campos buscados en las cabeceras ya que se pierden al ir al servidor web.

- Usaremos funciones de lado del servidor para formatear la cabecera a mostrar asi como los valores de los registros que coincidan con la palabra buscada. En esta última parte usaremos Reflection para obtener el valor de cada propiedad del objeto cabecera.

Nota: Muchos desarrolladores usan controles de terceros para cubrir este requerimiento porque no conocen bien el tema de plantillas de datos, código incrustado del servidor y por supuesto Reflection.

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_FiltroCabecera_RptaColor".
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_FiltroCabecera_RptaColor
{
    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 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: blue;
    color:white;
}
.FilaDatos {
    background-color: white;
    color:blue;
}

Crear la Pagina ASP .NET como un Formulario Web Form

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

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="ConsultaProductos.aspx.cs" Inherits="GridView_FiltroCabecera_RptaColor.ConsultaProductos" %>
<%@ Import Namespace="GridView_FiltroCabecera_RptaColor" %>
<!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>Consulta de Productos</title>
    <link href="ACME.css" rel="stylesheet" type="text/css" />
    <script>
        function filtrar(event) {
            var keyCode = ('which' in event) ? event.which : event.keyCode;
            if (keyCode == 13) {
                var btn = document.getElementById("btnFiltrar");
                if (btn != null) btn.click();
                return false;
            }
        }
    </script>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <table class="AnchoTotal">
            <tr class="Titulo">
                <td>GridView Filtros Cabecera, Total Pie y Filas Color</td>
            </tr>
            <tr class="Subtitulo">
                <td>Consulta Desconectada de Productos (Autor: Luis Dueñas)</td>
            </tr>
            <tr>
                <td>
                    <asp:GridView ID="gvProducto" AutoGenerateColumns="false" ShowFooter="true"
                        Width="700px" ShowHeaderWhenEmpty="true" runat="server">
                        <HeaderStyle CssClass="FilaCabecera" />
                        <RowStyle CssClass="FilaDatos" />
                        <FooterStyle CssClass="FilaCabecera" />
                        <Columns>
                            <asp:TemplateField HeaderText="" ItemStyle-Width="80px"
                              ItemStyle-HorizontalAlign="Right">
                                <HeaderTemplate>
                                    Código<br />
                                    <asp:TextBox ID="txtIdProducto"
                                      Text="<%#formatearNumero(obeProductoCab.IdProducto)%>"
                                      onkeypress="return filtrar(event);" Width="80px" runat="server" />
                                </HeaderTemplate>
                                <ItemTemplate>
                                    <%#formatearCelda(((beProducto)Container.DataItem).
                                            IdProducto.ToString(),"IdProducto")%>
                                </ItemTemplate>
                            </asp:TemplateField>
                            <asp:TemplateField ItemStyle-Width="300px"
                              FooterStyle-HorizontalAlign="Right">
                                <HeaderTemplate>
                                    Descripción del Producto<br />
                                    <asp:TextBox ID="txtNombre" Text="<%#obeProductoCab.Nombre%>"
                                      onkeypress="return filtrar(event);" Width="300px" runat="server" />
                                </HeaderTemplate>
                                <ItemTemplate>
                                    <%#formatearCelda(((beProducto)Container.DataItem).
                                            Nombre,"Nombre")%>
                                </ItemTemplate>
                                <FooterTemplate>
                                    <b>Total de Registros encontrados: </b>
                                </FooterTemplate>
                            </asp:TemplateField>
                            <asp:TemplateField ItemStyle-Width="80px" ItemStyle-HorizontalAlign="Right"
                              FooterStyle-HorizontalAlign="Right">
                                <HeaderTemplate>
                                    Id Prov<br />
                                    <asp:TextBox ID="txtIdProveedor"
                                      Text="<%#formatearNumero(obeProductoCab.IdProveedor)%>"
                                      onkeypress="return filtrar(event);" Width="80px" runat="server" />
                                </HeaderTemplate>
                                <ItemTemplate>
                                    <%#formatearCelda(((beProducto)Container.DataItem).
                                            IdProveedor.ToString(),"IdProveedor")%>
                                </ItemTemplate>
                                <FooterTemplate>
                                    <b><%#lbeFiltro.Count%></b>
                                </FooterTemplate>
                            </asp:TemplateField>
                            <asp:TemplateField ItemStyle-Width="80px" ItemStyle-HorizontalAlign="Right">
                                <HeaderTemplate>
                                    Id Cat<br />
                                    <asp:TextBox ID="txtIdCategoria"
                                       Text="<%#formatearNumero(obeProductoCab.IdCategoria)%>"
                                      onkeypress="return filtrar(event);" Width="80px" runat="server" />
                                </HeaderTemplate>
                                <ItemTemplate>
                                    <%#formatearCelda(((beProducto)Container.DataItem).
                                            IdCategoria.ToString(),"IdCategoria")%>
                                </ItemTemplate>
                            </asp:TemplateField>
                            <asp:TemplateField ItemStyle-Width="80px"
                               ItemStyle-HorizontalAlign="Right">
                                <HeaderTemplate>
                                    Pre Unit<br />
                                    <asp:TextBox ID="txtPrecioUnitario"
                                      Text="<%#formatearNumero(obeProductoCab.PrecioUnitario)%>"
                                      onkeypress="return filtrar(event);" Width="80px" runat="server" />
                                </HeaderTemplate>
                                <ItemTemplate>
                                    <%#formatearCelda(((beProducto)Container.DataItem).
                                           PrecioUnitario.ToString(),"PrecioUnitario")%>
                                </ItemTemplate>
                            </asp:TemplateField>
                            <asp:TemplateField ItemStyle-Width="80px" ItemStyle-HorizontalAlign="Right">
                                <HeaderTemplate>
                                    Stock<br />
                                    <asp:TextBox ID="txtStock"
                                      Text="<%#formatearNumero(obeProductoCab.Stock)%>"
                                      onkeypress="return filtrar(event);" Width="80px" runat="server" />
                                </HeaderTemplate>
                                <ItemTemplate>
                                    <%#formatearCelda(((beProducto)Container.DataItem).
                                            Stock.ToString(),"Stock")%>
                                </ItemTemplate>
                            </asp:TemplateField>
                        </Columns>
                    </asp:GridView>
                </td>
            </tr>
        </table>
        <asp:Button ID="btnFiltrar" OnClick="filtrarProductos" runat="server" />
    </div>
    </form>
</body>
</html>

Nota: El script del cliente "filtrar" se llama al dar Enter a cualquier control de la cabecera y ejecuta el clic del botón "btnFiltrar:" que es el que permite realizar el filtro y esta oculto por código.

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.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace GridView_FiltroCabecera_RptaColor
{
    public partial class ConsultaProductos : System.Web.UI.Page
    {
        private List<beProducto> lbeProducto;
        protected List<beProducto> lbeFiltro;
        protected beProducto obeProductoCab;

        protected void Page_Load(object sender, EventArgs e)
        {
            btnFiltrar.Attributes.Add("style", "display:none");
            obeProductoCab = new beProducto();
            if (!Page.IsPostBack)
            {
                brProducto obrProducto = new brProducto();
                lbeProducto = obrProducto.listar();
                lbeFiltro = lbeProducto;
                Session["Productos"] = lbeProducto;
                gvProducto.DataSource = lbeFiltro;
                gvProducto.DataBind();
            }
        }

        protected string formatearNumero(dynamic campo)
        {
            string cabecera = "";
            if (campo > -1) cabecera = campo.ToString();
            return (cabecera);
        }

        private bool buscarProductos(beProducto obeProducto)
        {
            bool exitoIdProducto = true;
            bool exitoNombre = true;
            bool exitoIdProveedor = true;
            bool exitoIdCategoria = true;
            bool exitoPrecioUnitario = true;
            bool exitoStock = true;
            if (obeProductoCab.IdProducto>-1) exitoIdProducto = (obeProducto.IdProducto.ToString().Contains(obeProductoCab.IdProducto.ToString()));
            if (!obeProductoCab.Nombre.Equals("")) exitoNombre = (obeProducto.Nombre.ToLower().Contains(obeProductoCab.Nombre.ToLower()));
            if (obeProductoCab.IdProveedor > -1) exitoIdProveedor = (obeProducto.IdProveedor.ToString().Contains(obeProductoCab.IdProveedor.ToString()));
            if (obeProductoCab.IdCategoria > -1) exitoIdCategoria = (obeProducto.IdCategoria.ToString().Contains(obeProductoCab.IdCategoria.ToString()));
            if (obeProductoCab.PrecioUnitario > -1) exitoPrecioUnitario = (obeProducto.PrecioUnitario.ToString().Contains(obeProductoCab.PrecioUnitario.ToString()));
            if (obeProductoCab.Stock > -1) exitoStock = (obeProducto.Stock.ToString().Contains(obeProductoCab.Stock.ToString()));
            return (exitoIdProducto && exitoNombre && exitoIdProveedor && exitoIdCategoria && exitoPrecioUnitario && exitoStock);
        }

        protected void filtrarProductos(object sender, EventArgs e)
        {
            guardarFiltros();
            lbeProducto=(List<beProducto>)Session["Productos"];
            Predicate<beProducto> pred = new Predicate<beProducto>(buscarProductos);
            lbeFiltro = lbeProducto.FindAll(pred);
            gvProducto.DataSource = lbeFiltro;
            gvProducto.DataBind();
        }

        private void guardarFiltros()
        {
            string sIdProducto = ((TextBox)gvProducto.HeaderRow.Cells[0].Controls[1]).Text;
            obeProductoCab.IdProducto = (sIdProducto.Equals("") ? -1 : int.Parse(sIdProducto));
            obeProductoCab.Nombre = ((TextBox)gvProducto.HeaderRow.Cells[1].Controls[1]).Text;
            string sIdProveedor = ((TextBox)gvProducto.HeaderRow.Cells[2].Controls[1]).Text;
            obeProductoCab.IdProveedor = (sIdProveedor.Equals("") ? -1 : int.Parse(sIdProveedor));
            string sIdCategoria = ((TextBox)gvProducto.HeaderRow.Cells[3].Controls[1]).Text;
            obeProductoCab.IdCategoria = (sIdCategoria.Equals("") ? -1 : int.Parse(sIdCategoria));
            string sPrecioUnitario = ((TextBox)gvProducto.HeaderRow.Cells[4].Controls[1]).Text;
            obeProductoCab.PrecioUnitario = (sPrecioUnitario.Equals("") ? -1 : decimal.Parse(sPrecioUnitario));
            string sStock = ((TextBox)gvProducto.HeaderRow.Cells[5].Controls[1]).Text;
            obeProductoCab.Stock = (sStock.Equals("") ? short.Parse("-1") : short.Parse(sStock));
        }

        protected string formatearCelda(string valor,string campo)
        {
            StringBuilder rpta = new StringBuilder();
            string celda = valor.ToLower();
            dynamic cabecera = obeProductoCab.GetType().GetProperty(campo).
            GetValue(obeProductoCab, null);
            if (cabecera != null)
            {
                if (!(cabecera is string) && (cabecera == -1)) return valor;
                if ((cabecera is string) && cabecera.Equals("")) return valor;
                string busca = cabecera.ToString().ToLower();
                int posInicio =0;
                int pos = celda.IndexOf(busca);
                if (pos > -1)
                {
                    while (true)
                    {
                        pos = celda.IndexOf(busca, pos);
                        if (pos > -1)
                        {
                            rpta.Append(valor.Substring(posInicio,pos-posInicio));
                            rpta.Append("<span style='color:red;font-bold:true'>");
                            rpta.Append(valor.Substring(pos, busca.Length));
                            rpta.Append("</span>");
                            posInicio = pos + busca.Length;
                            pos += 1;
                        }
                        else break;
                    }
                    rpta.Append(valor.Substring(posInicio, valor.Length - posInicio));
                }
                else rpta.Append(valor);
            }
            else rpta.Append(valor);
            return (rpta.ToString());
        }
    }
}

Nota: El código mas complejo se encuentra en la función "formatearCelda" que se llama desde cada campo de cada registro mostrado en el filtro y que pasa como parámetro el valor de la celda a presentar y el campo a buscar para la cabecera. Aquí usamos Reflection para obtener el valor de cada propiedad del objeto cabecera e iniciar la búsqueda de la palabra en el valor e ir agregando en un StringBuilder la cadena con formato (negrita y colo rojo).

También es necesario antes de "filtrarProductos" llamar a la función "guardarFiltros" que se encarga de pasar los valores de los controles de la cabecera de la grilla en el objeto enlazado a la cabecera llamado: "obeProductoCab", de lo contrario se perderían los valores ingresados.

Finalmente, hay que aclarar que para poder usar desde el código HTML (aspx) un objeto o variables es necesario definirla como protected (por herencia), este es el caso de "lbeFiltro" y "obeProductoCab".

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 "ConsultaProductos.aspx" y seleccionar "Ver en el explorador". Se mostrará una ventana similar a la siguiente figura:


Se listarán todos los productos de Northwind, ahora escribir sobre el control TextBox de la cabecera del nombre la palabra "ma" y pulsar "Enter" y se filtrarán los registros apareciendo la palabra "ma" de color rojo y en negrita, tal como se muestra a continuación:


Sobre este filtro aumentar un filtro más ingresando sobre la cabecera del código del producto el número "1" y se filtrará por los 2 criterios apareciendo el número "1" de color rojo y en negrita igual que la palabra "ma", tal como se muestra en la figura de abajo:


Aumentar otro filtro para el Id de la Categoría escribiendo el número "4" y se filtrará por 3 criterios, reduciéndose mas el resultado y apareciendo de color rojo y en negrita el número "4" de la categoría, tal como se ve en la figura:


Finalmente, probemos un filtro mas que no muestre ningún registro, para lo cual aumentar como cuarto filtro que el Stock tenga el número "5", lo cual no genera ningún resultado, pero la cabecera se mantiene gracias a la propiedad: ShowHeaderWhenEmpty="true".


Si deseas mostrar todos los registros borrar cada campo de la cabecera y dar Enter en cualquiera.

Comentario Final

En este post, vimos como se puede crear en un GridView cabeceras y pies personalizados, en este caso para filtrar en forma desconectada usando plantillas de datos y código incrustado del servidor, también vimos como resaltar la palabra buscada en cada columna, aunque se aprecia mejor en columnas de tipo cadena que numérico.

Espero les guste el Demo y recuerden que No es tan complicado crear su propio control Grilla Personalizada, aunque muchos No les gusta hacerse problemas y usan sus controles jGrid, trueGrid, softGrid, superGrid, etc, pero lo mejor seria el yourSelfGrid (Grid de ti mismo).

Nota: En mis cursos creo en la cabecera DropDownList (Combos) para filtrar por Proveedor y Categoría, pero no pinto de colores los registros encontrados.

PD: Los que llevan el curso de MVC los Sábados ya tienen una ayudita para terminar lo que les deje.

Descarga:
GridView_FiltroCabecera_RptaColor

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

jueves, 14 de agosto de 2014

El Demo del Día: Plantilla Jerárquica con el Control Web Repeater

Plantilla Jerárquica con el Control Web Repeater

Requerimiento

Se desea mostrar una consulta jerárquica de productos por categoría en una grilla en ASP .NET. La consulta debe ser desconectada.

Solución

No existe un control Jerárquico que muestre varias columnas tipo TreeGrid. El control TreeView muestra solo un nodo y el control GridView no soporta jerarquía.
La solución es usar plantillas jerárquicas en cualquiera de los controles enlazados a datos: GridView, DataList o Repeater.
Nosotros elegimos el Repeater y crearemos una plantilla jerárquica, es decir un control Repeater con otro control Repeater dentro.

Nota: En este caso quitaremos el ViewState en los dos controles Repeater para disminuir el HTML enviado al cliente ya que los filtros se harán en el servidor pero se mostraran en el cliente con Javascript.

Crear el Procedimiento Almacenado en la Base de Datos de SQL Server

Create Procedure [dbo].[uspCategoriesProductsListar]
As
Select CategoryID,CategoryName From Categories
Select ProductID,ProductName,SupplierID,CategoryID,UnitPrice,UnitsInStock From Products

Crear una Librería de Clases con las Entidades del Negocio

Crear un proyecto de tipo Librería de Clases en C# llamado: "Northwind.Librerias.EntidadesNegocio" y modificar el código de la clase como sigue:

using System;
namespace Northwind.Librerias.EntidadesNegocio
{
    public class beCategoria
    {
        public int IdCategoria { get; set; }
        public string Nombre { get; set; }
    }
}

Agregar una nueva clase al proyecto y llamarla "beProducto":

using System;
namespace Northwind.Librerias.EntidadesNegocio
{
    public class beProducto
    {
        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; }
    }
}

Agregar otra clase al proyecto y llamarla "beCategoriaProducto":

using System;
using System.Collections.Generic;
namespace Northwind.Librerias.EntidadesNegocio
{
    public class beCategoriaProducto
    {
        public List<beCategoria> ListaCategorias { get; set; }
        public List<beProducto> ListaProductos { get; set; }
    }
}

Crear una Librería de Acceso a Datos

Crear un proyecto de tipo Librería de Clases en C# llamado: "Northwind.Librerias.AccesoDatos", primero hacer una referencia a la Librería de Entidades del Negocio y luego modificar el código de la clase como sigue:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using Northwind.Librerias.EntidadesNegocio;

namespace Northwind.Librerias.AccesoDatos
{
    public class daCategoriaProducto
    {
        public beCategoriaProducto obtenerListas(SqlConnection con)
        {
            beCategoriaProducto obeCategoriaProducto = new beCategoriaProducto();
            List<beCategoria> lbeCategoria = null;
            List<beProducto> lbeProducto = null;

            SqlCommand cmd = new SqlCommand("uspCategoriesProductsListar", con);
            cmd.CommandType = CommandType.StoredProcedure;
            SqlDataReader drd = cmd.ExecuteReader(); //Varios Selects
            if (drd != null)
            {
                //Llenar las Categorias
                lbeCategoria = new List<beCategoria>();
                int posIdCat = drd.GetOrdinal("CategoryID");
                int posNomCat = drd.GetOrdinal("CategoryName");
                beCategoria obeCategoria;
                while (drd.Read())
                {
                    obeCategoria = new beCategoria();
                    obeCategoria.IdCategoria = drd.GetInt32(posIdCat);
                    obeCategoria.Nombre = drd.GetString(posNomCat);
                    lbeCategoria.Add(obeCategoria);
                }
                //Avanzar al siguiente Select
                if (drd.NextResult())
                {
                    //Llenar los Productos
                    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();
                obeCategoriaProducto.ListaCategorias = lbeCategoria;
                obeCategoriaProducto.ListaProductos = lbeProducto;
            }
            return (obeCategoriaProducto);
        }
    }
}

Crear una Librería de Reglas del Negocio

Crear un proyecto de tipo Librería de Clases en C# llamado: "Northwind.Librerias.ReglasNegocio", hacer una referencia a la Librería de Entidades del Negocio, a la Librería de Acceso a Datos y a System.Configuration y luego modificar el código de la clase como sigue:

using System;
using System.Configuration; //ConfigurationManager
namespace Northwind.Librerias.ReglasNegocio
{
    public class brGeneral
    {
        //Propiedad
        public string Conexion { get; set; }

        //Constructor
        public brGeneral()
        {
            Conexion = ConfigurationManager.ConnectionStrings["conNW"].ConnectionString;
        }
    }
}

Agregar otra clase llamada: "brCategoriaProducto:" y escribir el siguiente código:

using System;
using System.Collections.Generic; //List
using System.Data.SqlClient; //SqlConnection
using Northwind.Librerias.EntidadesNegocio; //beCategoriaProducto
using Northwind.Librerias.AccesoDatos; //daCategoriaProducto

namespace Northwind.Librerias.ReglasNegocio
{
    public class brCategoriaProducto:brGeneral
    {
        public beCategoriaProducto obtenerListas()
        {
            beCategoriaProducto obeCategoriaProducto = null;
            using (SqlConnection con = new SqlConnection(Conexion))
            {
                try
                {
                    con.Open();
                    daCategoriaProducto odaCategoriaProducto = new daCategoriaProducto();
                    obeCategoriaProducto = odaCategoriaProducto.obtenerListas(con);
                }
                catch (SqlException ex)
                {
                    //Capturar el error y grabar un Log
                }
            } //con.Close(); con.Dispose(); con = null;
            return (obeCategoriaProducto);
        }
    }
}

Nota: La clase "brCategoriaProducto" hereda de la clase "brGeneral" la cadena de conexión para no pasarla como parámetro en cada método de la clase o en el constructor de cada clase se hace una sola vez.

Crear la Aplicación Web en ASP .NET Web Form

Crear un "Nuevo Sitio web vacío de ASP .NET" en C# llamado "Demo15" y crear las siguientes carpetas:
- Estilos: Para el archivo de hoja de estilo (CSS).
- Imagenes: Conteniendo 2 archivos: Contraer.png y Expandir.png
- Paginas: Para contener la pagina de lista de empleados.

Crear el Archivo de Hoja de Estilo (CSS)

Seleccionar la carpeta "Estilos" y agregar un archivo de hoja de estilos con el nombre de: "ACME.css" y modificar el código como sigue:

body {
    background-color:aqua;
}
.Titulo {
    background-color:black;
    color:white;
    text-transform:uppercase;
    font-size:xx-large;
    font-weight:bold;
}
.Subtitulo {
    background-color:white;
    color:blue;
    text-transform:capitalize;
    font-size:x-large;
    font-weight:bold;
}

Crear la Pagina ASP .NET como un Formulario Web Form

Seleccionar la carpeta "Paginas" y agregar un Formulario Web Form llamado: "CategoriaProductos.aspx", para empezar a diseñar la pagina hay que hacer referencia a las Librerías de Negocio (que copia todas sus dependencias).

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="CategoriaProductos.aspx.cs" Inherits="Paginas_CategoriaProducto" %>
<%@ Import Namespace="Northwind.Librerias.EntidadesNegocio" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
    <link href="../Estilos/ACME.css" rel="stylesheet" type="text/css" />
    <script>
        function mostrarProductos(img, indice) {
            var fila = document.getElementById("fila" + indice);
            if (img.title == "Expandir") {
                fila.style.display = "table-row";
                img.src = "../Imagenes/Contraer.png";
                img.title = "Contraer";
            }
            else {
                fila.style.display = "none";
                img.src = "../Imagenes/Expandir.png";
                img.title = "Expandir";
            }
        }
    </script>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <table style="width:100%">
            <tr>
                <td class="Titulo">Plantillas Jerarquicas en el Repeater</td>
            </tr>
            <tr>
                <td class="Subtitulo">Consulta de Productos x Categoria</td>
            </tr>
            <tr>
                <td>
                    <asp:Repeater ID="rpCategoria" EnableViewState="false" runat="server">
                        <HeaderTemplate>
                            <table style="width:100%">
                                <tr style="background-color:blue;color:white">
                                    <td style="width:10%">Código</td>
                                    <td style="width:90%">Nombre de la Categoria</td>
                                </tr>
                        </HeaderTemplate>
                        <ItemTemplate>
                            <tr style="background-color:white;color:blue">
                                <td>
                                    <img id="imgSimbolo" alt="" title="Expandir"
                                       src="../Imagenes/Expandir.png"
                                        width="20" height="20" style="cursor:pointer"
                                        onclick="mostrarProductos(this,<%#Container.ItemIndex%>);"/>
                                    &nbsp;&nbsp;
                                    <%#((beCategoria)Container.DataItem).IdCategoria%>
                                </td>
                                <td>
                                    <%#((beCategoria)Container.DataItem).Nombre%>
                                </td>
                            </tr>
                            <tr id="fila<%#Container.ItemIndex%>" style="display:none">
                                <td></td>
                                <td>
                                    <asp:Repeater ID="rpProducto" DataSource="<%#filtrarProductos
                                      (((beCategoria)Container.DataItem).IdCategoria)%>"
                                      EnableViewState="false" runat="server">
                                         <HeaderTemplate>
                                            <table style="width:100%">
                                                <tr style="background-color:red;color:white">
                                                    <td style="width:20%">Código</td>
                                                    <td style="width:40%">Nombre del Producto</td>
                                                    <td style="width:20%">Precio</td>
                                                    <td style="width:20%">Stock</td>
                                                </tr>
                                        </HeaderTemplate>
                                        <ItemTemplate>
                                            <tr style="background-color:white;color:red">
                                                <td><%#((beProducto)Container.DataItem).IdProducto%></td>
                                                <td><%#((beProducto)Container.DataItem).Nombre%></td>
                                                <td><%#((beProducto)Container.DataItem).
                                                          PrecioUnitario.ToString("n2")%></td>
                                                <td><%#((beProducto)Container.DataItem).Stock%></td>
                                            </tr>
                                        </ItemTemplate>
                                        <FooterTemplate>
                                            </table>
                                        </FooterTemplate>
                                    </asp:Repeater>
                                </td>
                            </tr>
                        </ItemTemplate>
                        <FooterTemplate>
                            </table>
                        </FooterTemplate>
                    </asp:Repeater>
                </td>
            </tr>
        </table>
    </div>
    </form>
</body>
</html>

Nota: Inicialmente los controles Repeaters de productos creados por cada categoría están ocultos (display:none), pero por código Javascript lo mostramos usando: display:"table-row". No usamos display:inline ya que no se vería bien en algunos navegadores, en cambio, usando "table-row" se muestra sin problemas en todos los navegadores.

El diseño de la pagina se mostrará como en la siguiente figura:


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

using System;
using System.Collections.Generic;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using Northwind.Librerias.EntidadesNegocio;
using Northwind.Librerias.ReglasNegocio;

public partial class Paginas_CategoriaProducto : System.Web.UI.Page
{
    private List<beProducto> lbeProducto;

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!Page.IsPostBack)
        {
            brCategoriaProducto obrCategoriaProducto = new brCategoriaProducto();
            beCategoriaProducto obeCategoriaProducto = obrCategoriaProducto.obtenerListas();
            lbeProducto = obeCategoriaProducto.ListaProductos;
            rpCategoria.DataSource = obeCategoriaProducto.ListaCategorias;
            rpCategoria.DataBind();
        }
    }

    protected List<beProducto> filtrarProductos(int idCategoria)
    {
        return (lbeProducto.FindAll(x => x.IdCategoria.Equals(idCategoria)));
    }
}

Nota: En el evento load se carga las 2 listas de categorías y productos, luego se enlaza las categorías al primer control Repeater y el segundo se enlaza a una función "filtrarProductos" que solo selecciona las productos de cada categoría.

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="SQLServer" connectionString="uid=UsuarioNW;pwd=123456;
      data source=DSOFT\Sqlexpress; database=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 "CategoriaProductos.aspx" y seleccionar "Ver en el explorador".


Desplegar cada código de categoría dando clic a la imagen de Expandir (+) y se mostrarán los productos de dicha categoría y para ocultarla usar la imagen de Contraer (-).

Comentario Final

En este post hemos visto como crear una presentación jerárquica usando plantillas en el control Repeater. Hemos trabajado solo con 2 niveles pero podría ser de varios niveles. Como comentario final, si aprendemos bien a usar plantillas de datos (Data Templates) en cualquier control web (enlazado a datos) podemos darle la apariencia que deseemos, sin necesidad de tener un control especifico para cada funcionalidad.

Espero les sirva y será hasta la siguiente semana que veremos como trabajar con el GridView: Paginar, Ordenar, Filtrar en las cabeceras, Pies, etc.

Descarga:
Demo15_Plantilla_Jerarquica_Repeater