Controles web de datos anidados (VB)

por Scott Mitchell

Descargar PDF

En este tutorial se verá cómo usar un control Repeater anidado dentro de otro control Repeater. En los ejemplos se muestra cómo rellenar el control Repeater tanto mediante declaración como mediante programación.

Introducción

Además de la sintaxis HTML estática y del enlace de datos, las plantillas también pueden incluir controles web y controles de usuario. Estos controles web pueden tener sus propiedades asignadas mediante sintaxis declarativa, de enlace de datos o se puede acceder a ellos 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 controles TemplateField en el control GridView, ha visto cómo personalizar la representación de GridView mediante la adición de un control Calendar en un elemento 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, ha visto cómo personalizar las interfaces de edición e inserción mediante la adición de controles de validación, TextBox, DropDownList y otros controles web.

Las plantillas también pueden contener otros controles web de datos. Es decir, puede haber 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 e ObjectDataSource hasta opciones mediante programación.

En este tutorial se verá cómo usar un control Repeater anidado dentro de otro control Repeater. El control Repeater externo contendrá un elemento para cada categoría de la base de datos, y mostrará el nombre y la descripción de la categoría. El control Repeater interno de cada elemento de categoría mostrará información para cada producto que pertenezca a esa categoría (vea la figura 1) en una lista con viñetas. En los ejemplos se muestra cómo rellenar el control Repeater interno tanto mediante declaración como mediante programación.

Each Category, Along with its Products, are Listed

Figura 1: Se muestra cada categoría junto con sus productos (Haga clic para ver la imagen a tamaño completo)

Paso 1: Creación de la lista de categorías

Al crear una página que usa controles web de datos anidados, resulta útil diseñar, crear y probar primero el control web de datos más externo, sin preocuparse por el control anidado interno. Por tanto, primero se verán los pasos necesarios para agregar un control Repeater a la página que muestre el nombre y la descripción de cada categoría.

Para empezar, abra la página NestedControls.aspx en la carpeta DataListRepeaterBasics, agregue un control Repeater a la página y establezca su propiedad ID en CategoryList. En la etiqueta inteligente del control Repeater, elija crear un objeto ObjectDataSource denominado CategoriesDataSource.

Name the New ObjectDataSource CategoriesDataSource

Figura 2: Asignación del nombre CategoriesDataSource a 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.

Configure the ObjectDataSource to Use the CategoriesBLL Class s GetCategories Method

Figura 3: Configuración de 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 control Repeater, es necesario ir a la vista Origen y escribir manualmente la sintaxis declarativa. Agregue un elemento ItemTemplate que muestre el nombre de la categoría en un elemento <h4> y la descripción en un elemento de párrafo (<p>). Además, se separará cada categoría con una regla horizontal (<hr>). Después de realizar estos cambios, la página debe contener sintaxis declarativa para el control Repeater y ObjectDataSource similar a la 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 figura 4 se muestra el progreso cuando se ve en un explorador.

Each Category s Name and Description is Listed, Separated by a Horizontal Rule

Figura 4: Se muestra el nombre y la descripción de cada categoría, separados por una regla horizontal (Haga clic para ver la imagen a tamaño completo)

Paso 2: Adición del control Repeater Product anidado

Una vez que se completa la lista de categorías, la siguiente tarea consiste en agregar un control Repeater a ItemTemplate de CategoryList que muestra información sobre los productos que pertenecen a la categoría adecuada. Hay varias maneras de recuperar los datos de este control Repeater interno, y verás dos en breve. Por ahora, se creará los productos del control Repeater dentro de ItemTemplate del control Repeater CategoryList. En concreto, hará que el control Repeater del producto muestre cada producto en una lista con viñetas y que cada elemento de lista incluya el nombre y el precio del producto.

Para crear este control Repeater, es necesario escribir manualmente la sintaxis declarativa y las plantillas del control Repeater interno en ItemTemplate de CategoryList. Agregue el siguiente marcado en ItemTemplate del control Repeater 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: Enlace de los productos específicos de la categoría al control Repeater ProductsByCategoryList

Si visita la página en un explorador en este momento, la pantalla tendrá el mismo aspecto que en la figura 4, ya que todavía hay que enlazar datos al control Repeater. Hay algunas maneras de obtener los registros de producto adecuados y enlazarlos al control Repeater, 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, con una instancia de ObjectDataSource en el elemento ItemTemplate del control Repeater CategoryList, o bien mediante programación, desde la página de código subyacente de la página ASP.NET. Del mismo modo, estos datos se pueden enlazar al control Repeater interno mediante declaración, con su propiedad DataSourceID, mediante la sintaxis declarativa de enlace de datos o mediante programación si se hace referencia al control Repeater interno en el controlador de eventos ItemDataBound del control Repeater CategoryList, estableciendo mediante programación su propiedad DataSource y llamando a su método DataBind(). Se explorará cada uno de estos enfoques.

Acceso a los datos mediante declaración con un control ObjectDataSource y el controlador de eventos ItemDataBound

Como en esta serie de tutoriales se ha usado ObjectDataSource ampliamente, 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 elemento categoryID especificado. Por tanto, se puede agregar una instancia de ObjectDataSource a ItemTemplate del control Repeater CategoryList y configurarla para acceder a sus datos desde este método de clase.

Desafortunadamente, el control Repeater no permite que sus plantillas se editen desde la vista Diseño, por lo que es necesario agregar la sintaxis declarativa para este control ObjectDataSource manualmente. En la sintaxis siguiente se muestra ItemTemplate del control Repeater 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 de ObjectDataSource, es necesario establecer la propiedad DataSourceID del control Repeater ProductsByCategoryList en 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 control Repeater CategoryList. Recuerde que el evento ItemDataBound se activa una vez para cada elemento enlazado al control Repeater. Por tanto, cada vez que se activa este evento para el control Repeater externo, se puede asignar el valor CategoryID actual al parámetro CategoryID de ObjectDataSource ProductsByCategoryDataSource.

Cree un controlador de eventos para el evento ItemDataBound del control Repeater CategoryList con el código siguiente:

Protected Sub CategoryList_ItemDataBound(sender As Object, e As RepeaterItemEventArgs) _
    Handles CategoryList.ItemDataBound
    If e.Item.ItemType = ListItemType.AlternatingItem _
        OrElse e.Item.ItemType = ListItemType.Item Then
        ' Reference the CategoriesRow object being bound to this RepeaterItem
        Dim category As Northwind.CategoriesRow = _
            CType(CType(e.Item.DataItem, System.Data.DataRowView).Row, _
                Northwind.CategoriesRow)
        ' Reference the ProductsByCategoryDataSource ObjectDataSource
        Dim ProductsByCategoryDataSource As ObjectDataSource = _
            CType(e.Item.FindControl("ProductsByCategoryDataSource"), _
                ObjectDataSource)
        ' Set the CategoryID Parameter value
        ProductsByCategoryDataSource.SelectParameters("CategoryID").DefaultValue = _
            category.CategoryID.ToString()
    End If
End Sub

Este controlador de eventos comienza asegurándose de que se trata de un elemento de datos en lugar del elemento de encabezado, pie de página o separador. A continuación, se hace referencia a la instancia de CategoriesRow real que se acaba de enlazar al RepeaterItem actual. Por último, se hace referencia a ObjectDataSource en ItemTemplate y se asigna su valor de parámetro CategoryID a CategoryID del elemento RepeaterItemactual.

Con este controlador de eventos, el control Repeater ProductsByCategoryList de cada RepeaterItem se enlaza a esos productos de la categoría de RepeaterItem. En la figura 5 se muestra una captura de pantalla de la salida resultante.

The Outer Repeater Lists Each Category; the Inner One Lists the Products for that Category

Figura 5: El control Repeater externo enumera cada categoría; el interno enumera los productos de esa categoría (Haga clic para ver la imagen a tamaño completo)

Acceso a los datos de productos por categoría mediante programación

En lugar de usar ObjectDataSource para recuperar los productos de la categoría actual, podría 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 una instancia de CategoryID. Imagine que tuviera este método en la clase de código subyacente de la página ASP.NET, con el nombre GetProductsInCategory(categoryID). Con este método, se podrían enlazar los productos de la categoría actual al control Repeater interno mediante la siguiente sintaxis declarativa:

<asp:Repeater runat="server" ID="ProductsByCategoryList" EnableViewState="False"
      DataSource='<%# GetProductsInCategory(CType(Eval("CategoryID"), Integer)) %>'>
  ...
</asp:Repeater>

La propiedad DataSource del control Repeater usa la sintaxis de enlace de datos para indicar que sus datos proceden del método GetProductsInCategory(categoryID). Como Eval("CategoryID") devuelve un valor de tipo Object, el objeto se convierte en Integer antes de pasarlo al método GetProductsInCategory(categoryID). Observe que la instancia de CategoryID a la que aquí se accede mediante la sintaxis de enlace de datos es CategoryID en el control Repeater externo (CategoryList), el que está enlazado a los registros de la tabla Categories. Por tanto, sabemos que CategoryID no puede ser un valor NULL de base de datos, por lo que se puede convertir el método Eval sin comprobar si se trata de DBNull.

Con este enfoque, es necesario crear el método GetProductsInCategory(categoryID) y hacer que recupere el conjunto adecuado de productos dado el valor categoryID proporcionado. Se puede hacer si simplemente se devuelve el valor ProductsDataTable devuelto por el método GetProductsByCategoryID(categoryID) de la clase ProductsBLL. Ahora creará el método GetProductsInCategory(categoryID) en la clase de código subyacente para la página NestedControls.aspx. Hágalo con el código siguiente:

Protected Function GetProductsInCategory(ByVal categoryID As Integer) _
    As Northwind.ProductsDataTable
    ' Create an instance of the ProductsBLL class
    Dim productAPI As ProductsBLL = New ProductsBLL()
    ' Return the products in the category
    Return productAPI.GetProductsByCategoryID(categoryID)
End Function

Este método simplemente crea una instancia del método ProductsBLL y devuelve los resultados del método GetProductsByCategoryID(categoryID). Observe que el método se debe marcar como Public o Protected; si el método se marca como 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 en un explorador. La salida debe ser idéntica a cuando se utiliza el enfoque de ObjectDataSource y el controlador de eventos ItemDataBound (consulte la figura 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 control Repeater interno, como: DataSource='<%# ProductsBLL.GetProductsByCategoryID(CType(Eval("CategoryID"), Integer)) %>'? Aunque esta sintaxis no funcionará con la implementación actual de la clase ProductsBLL (ya que 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 Instance() estático 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 ofrece más flexibilidad para trabajar con los datos recuperados, como verá en breve.

Recuperación de toda la información del producto a la vez

Las dos técnicas anteriores que ha examinado obtienen los productos de la categoría actual mediante una llamada al método GetProductsByCategoryID(categoryID) de la clase ProductsBLL (el primer enfoque lo hacía desde ObjectDataSource, el segundo desde el método GetProductsInCategory(categoryID) de la clase de código subyacente). Cada vez que se invoca este método, la capa de 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 necesita N + 1 llamadas a la base de datos: una consulta a la base de datos para obtener todas las categorías y, después, N llamadas para obtener los productos específicos de cada categoría. Pero se pueden recuperar todos los datos necesarios en solo dos llamadas de base de datos: una para obtener todas las categorías y otra para obtener todos los productos. Una vez que tenga todos los productos, se pueden filtrar para que solo los productos que coincidan con el valor CategoryID actual se enlacen al control Repeater interno de esa categoría.

Para proporcionar esta funcionalidad, solo es necesario realizar una ligera modificación del método GetProductsInCategory(categoryID) en la clase de código subyacente de la página ASP.NET. En lugar de devolver a ciegas los resultados del método GetProductsByCategoryID(categoryID) de la clase ProductsBLL, primero se puede acceder a todos los productos (si aún no se han accedido a ellos) y, después, devolver solo la vista filtrada de los productos en función del valor CategoryID pasado.

Private allProducts As Northwind.ProductsDataTable = Nothing
Protected Function GetProductsInCategory(ByVal categoryID As Integer) _
    As Northwind.ProductsDataTable
    ' First, see if we've yet to have accessed all of the product information
    If allProducts Is Nothing Then
        Dim productAPI As ProductsBLL = New ProductsBLL()
        allProducts = productAPI.GetProducts()
    End If
    ' Return the filtered view
    allProducts.DefaultView.RowFilter = "CategoryID = " & categoryID
    Return allProducts
End Function

Observe 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 valor CategoryID coincide con el valor CategoryID especificado. Este enfoque reduce el número de veces que se accede a la base de datos de N + 1 a 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:

Se podría argumentar de forma intuitiva que reducir el número de accesos a la base de datos mejoraría seguramente el rendimiento. Pero podría no ser el caso. Si tiene un gran número de productos cuyo valor 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 se 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 ha visto cómo anidar un control web de datos dentro de otro, se ha examinado específicamente cómo hacer que un control Repeater externo muestre un elemento para cada categoría y un control Repeater 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 se han examinado en este tutorial. En el primer enfoque examinado se ha usado un elemento ObjectDataSource en el valor ItemTemplate del control web de datos externo que estaba enlazado al control web de datos interno mediante su propiedad DataSourceID. En la segunda técnica se ha accedido a los datos mediante un método en la clase de código subyacente de la página ASP.NET. Después, este método se puede enlazar a la propiedad DataSource del control web de datos interno mediante la sintaxis de enlace de datos.

Aunque en la interfaz de usuario anidada que se examina en este tutorial se usa un control Repeater anidado dentro de otro control Repeater, estas técnicas se pueden extender a los demás controles web de datos. Puede anidar un control Repeater dentro de un control GridView o un control GridView dentro de un control DataList, etc.

¡Feliz programación!

Acerca del autor

Scott Mitchell, autor de siete libros de ASP y ASP.NET, y fundador de 4GuysFromRolla.com, trabaja con tecnologías web de Microsoft desde 1998. Scott trabaja como consultor independiente, entrenador y escritor. Su último libro es Sams Teach Yourself ASP.NET 2.0 in 24 Hours. Puede ponerse en contacto con él a través de mitchell@4GuysFromRolla.com. o de su blog, que se puede encontrar en http://ScottOnWriting.NET.

Agradecimientos especiales a

Esta serie de tutoriales fue revisada por muchos revisores. Los revisores principales de este tutorial han sido Zack Jones y Liz Shulok. ¿Le interesaría revisar mis próximos artículos de MSDN? Si es así, escríbame a mitchell@4GuysFromRolla.com.