Controles web de datos anidados (C#)
por Scott Mitchell
En este tutorial exploraremos cómo usar un repetidor anidado dentro de otro repetidor. En los ejemplos se muestra cómo rellenar el repetidor interno tanto mediante declaración como mediante programación.
Introducción
Además de la sintaxis HTML estática y de enlace de datos, las plantillas también pueden incluir controles web y controles de usuario. Estos controles web pueden tener sus propiedades asignadas a través de la sintaxis declarativa, de enlace de datos o se puede tener acceso mediante programación en los controladores de eventos del lado servidor adecuados.
Al insertar controles dentro de una plantilla, la apariencia y la experiencia del usuario se pueden personalizar y mejorar. Por ejemplo, en el tutorial Uso de TemplateFields en el Control GridView, vimos cómo personalizar la visualización del GridView añadiendo un control Calendar en un TemplateField para mostrar la fecha de contratación de un empleado; en los tutoriales Adición de Controles de Validación a las Interfaces de Edición e Inserción y Personalización de la Interfaz de Modificación de Datos, vimos cómo personalizar las interfaces de edición e inserción añadiendo controles de validación, TextBoxes, DropDownLists y otros controles Web.
Las plantillas también pueden contener otros controles web de datos. Es decir, podemos tener un objeto DataList que contenga otro DataList (o Repeater, GridView o DetailsView, etc.) dentro de sus plantillas. El desafío con esta interfaz es enlazar los datos adecuados al control web de datos interno. Hay varios enfoques disponibles, desde opciones declarativas que utilizan el ObjectDataSource hasta opciones de programación.
En este tutorial exploraremos cómo usar un repetidor anidado dentro de otro repetidor. El repetidor externo contendrá un elemento para cada categoría de la base de datos, mostrando el nombre y la descripción de la categoría. Cada elemento de categoría del repetidor interno mostrará información para cada producto que pertenezca a esa categoría (véase la ilustración 1) en una lista con viñetas. En nuestros ejemplos se muestra cómo rellenar el repetidor interno tanto mediante declaración como mediante programación.
Ilustración 1: cada categoría, junto con sus productos, se enumera (Haga clic para ver la imagen a tamaño completo)
Paso 1: crear la lista de categorías
Al compilar una página que usa controles web de datos anidados, me resulta útil diseñar, crear y probar primero el control web de datos más externo, sin preocuparme siquiera por el control anidado interno. Por lo tanto, comencemos por los pasos necesarios para agregar un repetidor a la página que muestra el nombre y la descripción de cada categoría.
Para empezar, abra la página NestedControls.aspx
en la carpeta DataListRepeaterBasics
y agregue un control Repeater a la página, estableciendo su propiedad ID
en CategoryList
. En la etiqueta inteligente del repetidor, elija crear un objeto ObjectDataSource denominado CategoriesDataSource
.
Ilustración 2: asigne al nuevo CategoriesDataSource
ObjectDataSource (Haga clic para ver la imagen a tamaño completo)
Configure ObjectDataSource para que extraiga sus datos del método GetCategories
de la clase CategoriesBLL
.
Ilustración 3: configurar ObjectDataSource para usar el método GetCategories
de la clase CategoriesBLL
(Haga clic para ver la imagen a tamaño completo)
Para especificar el contenido de la plantilla del repetidor, es necesario ir a la vista Origen y escribir manualmente la sintaxis declarativa. Agregue un ItemTemplate
que muestre el nombre de la categoría en un elemento <h4>
y la descripción de la categoría en un elemento de párrafo (<p>
). Además, vamos a separar cada categoría con una regla horizontal (<hr>
). Después de realizar estos cambios, la página debe contener sintaxis declarativa para el repetidor y el ObjectDataSource similar a lo siguiente:
<asp:Repeater ID="CategoryList" DataSourceID="CategoriesDataSource"
EnableViewState="False" runat="server">
<ItemTemplate>
<h4><%# Eval("CategoryName") %></h4>
<p><%# Eval("Description") %></p>
</ItemTemplate>
<SeparatorTemplate>
<hr />
</SeparatorTemplate>
</asp:Repeater>
<asp:ObjectDataSource ID="CategoriesDataSource" runat="server"
OldValuesParameterFormatString="original_{0}"
SelectMethod="GetCategories" TypeName="CategoriesBLL">
</asp:ObjectDataSource>
En la ilustración 4 se muestra el progreso cuando se ve a través de un explorador.
Ilustración 4: cada nombre y descripción de categoría se muestra, separado por una regla horizontal (Haga clic para ver la imagen a tamaño completo)
Paso 2: agregar el repetidor de producto anidado
Una vez completada la lista de categorías, nuestra siguiente tarea consiste en agregar un repetidor a la ItemTemplate
de CategoryList
que muestra información sobre esos productos que pertenecen a la categoría adecuada. Hay varias maneras de recuperar los datos de este repetidor interno, dos de los cuales exploraremos en breve. Por ahora, vamos a crear los productos del repetidor dentro del ItemTemplate
del repetidor CategoryList
. En concreto, vamos a hacer que el repetidor del producto muestre cada producto en una lista con viñetas con cada elemento de lista, incluido el nombre y el precio del producto.
Para crear este repetidor, es necesario escribir manualmente la sintaxis declarativa y las plantillas del repetidor interno en el ItemTemplate
de CategoryList
. Agregue el siguiente marcado en el ItemTemplate
del repetidor de CategoryList
:
<asp:Repeater ID="ProductsByCategoryList" EnableViewState="False"
runat="server">
<HeaderTemplate>
<ul>
</HeaderTemplate>
<ItemTemplate>
<li><strong><%# Eval("ProductName") %></strong>
(<%# Eval("UnitPrice", "{0:C}") %>)</li>
</ItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</asp:Repeater>
Paso 3: enlazar los productos específicos de la categoría al repetidor ProductsByCategoryList
Si visita la página a través de un explorador en este momento, la pantalla tendrá el mismo aspecto que en la ilustración 4, ya que todavía tenemos que enlazar los datos al repetidor. Hay algunas maneras de obtener los registros de producto adecuados y enlazarlos al repetidor, algunas más eficientes que otras. El principal desafío aquí es recuperar los productos adecuados para la categoría especificada.
Se puede acceder a los datos que se van a enlazar al control Repeater interno mediante declaración, a través de un ObjectDataSource en el ItemTemplate
del repetidor CategoryList
, o mediante programación, desde la página de código subyacente de la página de ASP.NET. Del mismo modo, estos datos se pueden enlazar al repetidor interno mediante declaración, a través de la propiedad interna DataSourceID
del repetidor o a través de la sintaxis declarativa de enlace de datos o mediante programación haciendo referencia al repetidor interno en el controlador de eventos ItemDataBound
del repetidor CategoryList
, estableciendo mediante programación su propiedad DataSource
y llamando a su método DataBind()
. Vamos a explorar cada uno de estos enfoques.
Acceso a los datos mediante declaración con un control ObjectDataSource y el controlador de eventos ItemDataBound
Puesto que hemos usado ObjectDataSource ampliamente en esta serie de tutoriales, la opción más natural para acceder a los datos de este ejemplo es seguir con ObjectDataSource. La clase ProductsBLL
tiene un método GetProductsByCategoryID(categoryID)
que devuelve información sobre los productos que pertenecen al categoryID
especificado. Por lo tanto, podemos agregar un ObjectDataSource al ItemTemplate
del repetidor CategoryList
y configurarlo para acceder a sus datos desde este método de clase.
Desafortunadamente, el repetidor no permite que sus plantillas se editen a través de la vista Diseño, por lo que es necesario agregar la sintaxis declarativa para este control ObjectDataSource manualmente. La sintaxis siguiente muestra el ItemTemplate
del repetidor CategoryList
después de agregar este nuevo ObjectDataSource (ProductsByCategoryDataSource
):
<h4><%# Eval("CategoryName") %></h4>
<p><%# Eval("Description") %></p>
<asp:Repeater ID="ProductsByCategoryList" EnableViewState="False"
DataSourceID="ProductsByCategoryDataSource" runat="server">
<HeaderTemplate>
<ul>
</HeaderTemplate>
<ItemTemplate>
<li><strong><%# Eval("ProductName") %></strong> -
sold as <%# Eval("QuantityPerUnit") %> at
<%# Eval("UnitPrice", "{0:C}") %></li>
</ItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</asp:Repeater>
<asp:ObjectDataSource ID="ProductsByCategoryDataSource" runat="server"
SelectMethod="GetProductsByCategoryID" TypeName="ProductsBLL">
<SelectParameters>
<asp:Parameter Name="CategoryID" Type="Int32" />
</SelectParameters>
</asp:ObjectDataSource>
Cuando se usa el enfoque ObjectDataSource, es necesario establecer la propiedad DataSourceID
del repetidor ProductsByCategoryList
en la ID
de ObjectDataSource (ProductsByCategoryDataSource
). Además, observe que ObjectDataSource tiene un elemento <asp:Parameter>
que especifica el valor de categoryID
que se pasará al método GetProductsByCategoryID(categoryID)
. ¿Pero cómo se especifica este valor? Lo ideal sería establecer simplemente la propiedad DefaultValue
del elemento <asp:Parameter>
mediante la sintaxis de enlace de datos, de la siguiente manera:
<asp:Parameter Name="CategoryID" Type="Int32"
DefaultValue='<%# Eval("CategoryID")' />
Por desgracia, la sintaxis de enlace de datos solo es válida en los controles que tienen un evento DataBinding
. La clase Parameter
carece de este evento y, por tanto, la sintaxis anterior no es válida y producirá un error en runtime.
Para establecer este valor, es necesario crear un controlador de eventos para el evento ItemDataBound
del repetidor CategoryList
. Recuerde que el evento ItemDataBound
se activa una vez para cada elemento enlazado al repetidor. Por lo tanto, cada vez que se activa este evento para el repetidor externo, podemos asignar el valor CategoryID
actual al parámetro CategoryID
del ObjectDataSource ProductsByCategoryDataSource
.
Cree un controlador de eventos para el evento ItemDataBound
del repetidor CategoryList
con el código siguiente:
protected void CategoryList_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
if (e.Item.ItemType == ListItemType.AlternatingItem ||
e.Item.ItemType == ListItemType.Item)
{
// Reference the CategoriesRow object being bound to this RepeaterItem
Northwind.CategoriesRow category =
(Northwind.CategoriesRow)((System.Data.DataRowView)e.Item.DataItem).Row;
// Reference the ProductsByCategoryDataSource ObjectDataSource
ObjectDataSource ProductsByCategoryDataSource =
(ObjectDataSource)e.Item.FindControl("ProductsByCategoryDataSource");
// Set the CategoryID Parameter value
ProductsByCategoryDataSource.SelectParameters["CategoryID"].DefaultValue =
category.CategoryID.ToString();
}
}
Este controlador de eventos comienza asegurándonos de que estamos tratando con un elemento de datos en lugar del elemento de encabezado, pie de página o separador. A continuación, hacemos referencia a la instancia CategoriesRow
real que acaba de estar enlazada al RepeaterItem
actual. Por último, hacemos referencia a ObjectDataSource en el ItemTemplate
y asignamos su valor de parámetro CategoryID
al CategoryID
del RepeaterItem
actual.
Con este controlador de eventos, el repetidor ProductsByCategoryList
de cada RepeaterItem
está enlazado a esos productos de la categoría RepeaterItem
. En la ilustración 5 se muestra una captura de pantalla de la salida resultante.
Ilustración 5: el repetidor exterior enumera cada categoría; el interior enumera los productos de esa categoría (Pulse para ver la imagen a tamaño completo)
Acceso a los productos por datos de categoría mediante programación
En lugar de usar un ObjectDataSource para recuperar los productos de la categoría actual, podríamos crear un método en la clase de código subyacente de la página ASP.NET (o en la carpeta App_Code
o en un proyecto de biblioteca de clases independiente) que devuelva el conjunto adecuado de productos cuando se pasa en un CategoryID
. Imagine que teníamos este método en nuestra clase de código subyacente de página ASP.NET y que se llamaba GetProductsInCategory(categoryID)
. Con este método en su lugar, podríamos enlazar los productos de la categoría actual al repetidor interno mediante la siguiente sintaxis declarativa:
<asp:Repeater runat="server" ID="ProductsByCategoryList" EnableViewState="False"
DataSource='<%# GetProductsInCategory((int)(Eval("CategoryID"))) %>'>
...
</asp:Repeater>
La propiedad DataSource
del repetidor usa la sintaxis de enlace de datos para indicar que sus datos proceden del método GetProductsInCategory(categoryID)
. Dado que Eval("CategoryID")
devuelve un valor de tipo Object
, convertiremos el objeto en un Integer
antes de pasarlo al método GetProductsInCategory(categoryID)
. Tenga en cuenta que el CategoryID
al que se accede aquí a través de la sintaxis de enlace de datos es el CategoryID
en el repetidor externo (CategoryList
), el que está enlazado a los registros de la tabla Categories
. Por lo tanto, sabemos que CategoryID
no puede ser un valor de base de datos NULL
, por lo que podemos convertir ciegamente el método Eval
sin comprobar si estamos tratando con un DBNull
.
Con este enfoque, es necesario crear el método GetProductsInCategory(categoryID)
y hacer que recupere el conjunto adecuado de productos dado el categoryID
proporcionado. Podemos hacerlo simplemente devolviendo el ProductsDataTable
devuelto por el método GetProductsByCategoryID(categoryID)
de la clase ProductsBLL
. Vamos a crear el método GetProductsInCategory(categoryID)
en la clase de código subyacente para nuestra página NestedControls.aspx
. Hágalo con el código siguiente:
protected Northwind.ProductsDataTable GetProductsInCategory(int categoryID)
{
// Create an instance of the ProductsBLL class
ProductsBLL productAPI = new ProductsBLL();
// Return the products in the category
return productAPI.GetProductsByCategoryID(categoryID);
}
Este método simplemente crea una instancia del método ProductsBLL
y devuelve los resultados del método GetProductsByCategoryID(categoryID)
. Tenga en cuenta que el método debe marcarse Public
o Protected
; si el método está marcado Private
, no será accesible desde el marcado declarativo de la página ASP.NET.
Después de realizar estos cambios para usar esta nueva técnica, dedique un momento a ver la página a través de un explorador. La salida debe ser idéntica a la salida cuando se utiliza el enfoque ObjectDataSource y el controlador de eventos ItemDataBound
(consulte la ilustración 5 para ver una captura de pantalla).
Nota:
Puede parecer complicado crear el método GetProductsInCategory(categoryID)
en la clase de código subyacente de la página ASP.NET. Después de todo, este método simplemente crea una instancia de la clase ProductsBLL
y devuelve los resultados de su método GetProductsByCategoryID(categoryID)
. ¿Por qué no llamar a este método directamente desde la sintaxis de enlace de datos en el repetidor interno, como: DataSource='<%# ProductsBLL.GetProductsByCategoryID((int)(Eval("CategoryID"))) %>'
? Aunque esta sintaxis no funcionará con nuestra implementación actual de la clase ProductsBLL
(ya que el método GetProductsByCategoryID(categoryID)
es un método de instancia), puede modificar ProductsBLL
para incluir un método GetProductsByCategoryID(categoryID)
estático o hacer que la clase incluya un método estático Instance()
para devolver una nueva instancia de la clase ProductsBLL
.
Aunque estas modificaciones eliminarían la necesidad del método GetProductsInCategory(categoryID)
en la clase de código subyacente de la página ASP.NET, el método de clase de código subyacente nos ofrece más flexibilidad para trabajar con los datos recuperados, como veremos en breve.
Recuperación de toda la información del producto a la vez
Las dos técnicas anteriores que hemos examinado obtienen los productos de la categoría actual haciendo una llamada al método GetProductsByCategoryID(categoryID)
de la clase ProductsBLL
(el primer enfoque lo hacía a través de un ObjectDataSource, el segundo a través del método GetProductsInCategory(categoryID)
de la clase de código subyacente). Cada vez que se invoca este método, la capa lógica de negocios llama a la capa de acceso a datos, que consulta la base de datos con una instrucción SQL que devuelve filas de la tabla Products
cuyo campo CategoryID
coincide con el parámetro de entrada proporcionado.
Dadas N categorías en el sistema, este enfoque requiere N + 1 llamadas a la base de datos: una consulta a la base de datos para obtener todas las categorías y, a continuación, N llamadas para obtener los productos específicos de cada categoría. Sin embargo, podemos recuperar todos los datos necesarios en solo dos llamadas de base de datos para obtener todas las categorías y otra para obtener todos los productos. Una vez que tenemos todos los productos, podemos filtrar esos productos para que solo los productos que coincidan con el CategoryID
actual estén enlazados a esa categoría del repetidor interno.
Para proporcionar esta funcionalidad, solo es necesario realizar una ligera modificación del método GetProductsInCategory(categoryID)
en nuestra clase de código subyacente de página ASP.NET. En lugar de devolver ciegamente los resultados del método GetProductsByCategoryID(categoryID)
de la clase ProductsBLL
, podemos acceder primero todas las de los productos (si aún no se han accedido) y, a continuación, devolver solo la vista filtrada de los productos en función del CategoryID
pasado.
private Northwind.ProductsDataTable allProducts = null;
protected Northwind.ProductsDataTable GetProductsInCategory(int categoryID)
{
// First, see if we've yet to have accessed all of the product information
if (allProducts == null)
{
ProductsBLL productAPI = new ProductsBLL();
allProducts = productAPI.GetProducts();
}
// Return the filtered view
allProducts.DefaultView.RowFilter = "CategoryID = " + categoryID;
return allProducts;
}
Tenga en cuenta la adición de la variable de nivel de página, allProducts
. Contiene información sobre todos los productos y se rellena la primera vez que se invoca el método GetProductsInCategory(categoryID)
. Después de asegurarse de que el objeto allProducts
se ha creado y rellenado, el método filtra los resultados de DataTable de modo que solo se puedan acceder a las filas cuyo CategoryID
coincide con el CategoryID
especificado. Este enfoque reduce el número de veces que se accede a la base de datos desde N + 1 hasta dos.
Esta mejora no introduce ningún cambio en el marcado representado de la página, ni devuelve menos registros que el otro enfoque. Simplemente reduce el número de llamadas a la base de datos.
Nota:
Uno podría razonar de forma intuitiva que reducir el número de accesos a la base de datos mejoraría seguramente el rendimiento. Sin embargo, esto podría no ser el caso. Si tiene un gran número de productos cuyo CategoryID
es NULL
, por ejemplo, la llamada al método GetProducts
devuelve una serie de productos que nunca se muestran. Además, devolver todos los productos puede ser un desperdicio si solo muestra un subconjunto de las categorías, lo que podría ser el caso si ha implementado la paginación.
Como siempre, cuando se trata de analizar el rendimiento de dos técnicas, la única medida segura es realizar pruebas controladas adaptadas a los escenarios habituales de su aplicación.
Resumen
En este tutorial hemos visto cómo anidar un control web de datos dentro de otro, examinando específicamente cómo hacer que un repetidor externo muestre un elemento para cada categoría con un repetidor interno que muestra los productos de cada categoría en una lista con viñetas. El principal desafío en la creación de una interfaz de usuario anidada radica en el acceso y enlace de los datos correctos al control web de datos interno. Hay una variedad de técnicas disponibles, dos de las cuales examinamos en este tutorial. El primer enfoque examinado usó un ObjectDataSource en el ItemTemplate
del control web de datos externo que estaba enlazado al control web de datos interno a través de su propiedad DataSourceID
. La segunda técnica accedió a los datos a través de un método en la clase de código subyacente de la página ASP.NET. A continuación, este método se puede enlazar a la propiedad DataSource
del control web de datos interno a través de la sintaxis de enlace de datos.
Aunque la interfaz de usuario anidada que se examina en este tutorial usó un repetidor anidado dentro de un repetidor, estas técnicas se pueden extender a los demás controles web de datos. Puede anidar un repetidor dentro de GridView o GridView dentro de una DataList, etc.
¡Feliz programación!
Acerca del autor
Scott Mitchell, autor de siete libros de ASP/ASP.NET y fundador de 4GuysFromRolla.com, ha estado trabajando con tecnologías web de Microsoft desde 1998. Scott trabaja como consultor independiente, instructor y escritor. Su último libro es Sams Teach Yourself ASP.NET 2.0 in 24 Hours. Contacte con el vía mitchell@4GuysFromRolla.com. o a través de su blog, que se puede encontrar en http://ScottOnWriting.NET.
Agradecimientos especiales a
Esta serie de tutoriales fue revisada por muchos revisores que fueron de gran ayuda. Los revisores principales de este tutorial fueron Zack Jones y Liz Shulok. ¿Le interesa revisar mis próximos artículos de MSDN? Si fuera así, escríbame a mitchell@4GuysFromRolla.com.