Compartir a través de


Controles web de datos anidados (C#)

por Scott Mitchell

Descargar PDF

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.

Each Category, Along with its Products, are Listed

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.

Name the New ObjectDataSource 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.

Configure the ObjectDataSource to Use the CategoriesBLL Class s GetCategories Method

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.

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

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 RepeaterItemactual.

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.

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

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.