Crear una interfaz de usuario de ordenación personalizada (C#)

por Scott Mitchell

Descargar PDF

Al mostrar una lista larga de datos ordenados, puede resultar muy útil agrupar los datos relacionados mediante la introducción de filas separadoras. En este tutorial verá cómo crear una interfaz de usuario de ordenación de este tipo.

Introducción

Al mostrar una larga lista de datos ordenados donde solo hay algunos valores diferentes en la columna ordenada, a un usuario final le podría resultar difícil distinguir dónde se producen exactamente los límites de diferencia. Por ejemplo, hay 81 productos en la base de datos, pero solo 9 opciones de categoría diferentes (8 categorías únicas más la opción NULL). Considere el caso de un usuario interesado en examinar los productos que se encuentran en la categoría Seafood. Desde una página en la que se muestran todos los productos en un único control GridView, el usuario podría decidir que la mejor opción consiste en ordenar los resultados por categoría, para agrupar todos los productos de Seafood. Después de ordenar por categoría, el usuario debe buscar en la lista y examinar dónde comienzan y terminan los productos agrupados por Seafood. Como los resultados se ordenan alfabéticamente por el nombre de categoría, no es difícil encontrar los productos Seafood, pero es necesario examinar detenidamente la lista de elementos en la cuadrícula.

Para ayudar a resaltar los límites entre grupos ordenados, en muchos sitios web se utiliza una interfaz de usuario que agrega un separador entre estos grupos. Los separadores como los que se muestran en la figura 1 permiten a un usuario encontrar más rápidamente un grupo determinado e identificar sus límites, así como determinar qué grupos distintos existen en los datos.

Each Category Group is Clearly Identified

Figura 1: Cada grupo de categorías está claramente identificado (Haga clic para ver la imagen a tamaño completo)

En este tutorial verá cómo crear una interfaz de usuario de ordenación de este tipo.

Paso 1: Creación de un control GridView estándar y ordenable

Antes de explorar cómo aumentar el control GridView para proporcionar la interfaz de ordenación mejorada, creará un control GridView estándar y ordenable que enumera los productos. Para empezar, abra la página CustomSortingUI.aspx en la carpeta PagingAndSorting. Agregue un control GridView a la página, establezca su propiedad ID en ProductList y enlácela a un nuevo objeto ObjectDataSource. Configure ObjectDataSource para que utilice el método GetProducts() de la clase ProductsBLL para seleccionar los registros.

A continuación, configure GridView de modo que solo contenga los elementos BoundField ProductName, CategoryName, SupplierName y UnitPrice, y el elemento CheckBoxField Descatalogado. Por último, configure GridView para admitir la ordenación mediante la activación de la casilla Habilitar ordenación en la etiqueta inteligente de GridView (o establezca su propiedad AllowSorting en true). Después de realizar estas adiciones en la página CustomSortingUI.aspx, el marcado declarativo debe ser similar al siguiente:

<asp:GridView ID="ProductList" runat="server" AllowSorting="True"
    AutoGenerateColumns="False" DataKeyNames="ProductID"
    DataSourceID="ObjectDataSource1" EnableViewState="False">
    <Columns>
        <asp:BoundField DataField="ProductName" HeaderText="Product"
            SortExpression="ProductName" />
        <asp:BoundField DataField="CategoryName" HeaderText="Category"
            ReadOnly="True" SortExpression="CategoryName" />
        <asp:BoundField DataField="SupplierName" HeaderText="Supplier"
            ReadOnly="True" SortExpression="SupplierName" />
        <asp:BoundField DataField="UnitPrice" DataFormatString="{0:C}"
            HeaderText="Price" HtmlEncode="False" SortExpression="UnitPrice" />
        <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued"
            SortExpression="Discontinued" />
    </Columns>
</asp:GridView>
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
    OldValuesParameterFormatString="original_{0}" SelectMethod="GetProducts"
    TypeName="ProductsBLL"></asp:ObjectDataSource>

Dedique un momento a ver el progreso en un explorador. En la figura 2 se muestra el control GridView ordenable cuando sus datos se ordenan por categoría en orden alfabético.

The Sortable GridView s Data is Ordered by Category

Figura 2: Los datos de GridView ordenados por categoría (Haga clic para ver la imagen a tamaño completo)

Paso 2: Exploración de técnicas para agregar filas de separador

Con el control GridView genérico y ordenable completado, solo falta poder agregar las filas separadoras en GridView antes de cada grupo ordenado único. ¿Cómo se pueden insertar estas filas en GridView? Básicamente, es necesario iterar por las filas de GridView, determinar dónde se producen las diferencias entre los valores de la columna ordenada y, después, agregar la fila separadora adecuada. Al pensar en este problema, parece evidente que la solución se encuentra en algún lugar del controlador de eventos RowDataBound de GridView. Como se ha descrito en el tutorial Formato personalizado basado en datos, este controlador de eventos se usa normalmente al aplicar formato de nivel de fila en función de los datos de la fila. Pero aquí el controlador de eventos RowDataBound no es la solución, ya que las filas no se pueden agregar a GridView mediante programación desde este controlador de eventos. La colección Rows de GridView, de hecho, es de solo lectura.

Para agregar filas adicionales a GridView, hay tres opciones:

  • Agregue estas filas separadoras de metadatos a los datos reales enlazados a GridView
  • Una vez que se enlaza GridView a los datos, agregue instancias TableRow adicionales a la colección de controles de GridView
  • Cree un control de servidor personalizado que extienda el control GridView e invalide los métodos encargados de construir la estructura de GridView

La creación de un control de servidor personalizado sería el mejor enfoque si esta funcionalidad fuera necesaria en muchas páginas web o en varios sitios web. Pero implicaría gran cantidad de código y una exploración exhaustiva en las profundidades del funcionamiento interno de GridView. Por tanto, esa opción no se considerará para este tutorial.

Las otras dos opciones que agregan filas separadoras a los datos reales que se enlazan a GridView y manipulan la colección de controles de GridView después de que se haya enlazado, abordan el problema de forma diferente y merecen una explicación.

Adición de filas a los datos enlazados a GridView

Cuando GridView está enlazado a un origen de datos, crea una instancia de GridViewRow para cada registro devuelto por el origen de datos. Por tanto, puede insertar las filas separadoras necesarias si agrega registros de separador al origen de datos antes de enlazarlo a GridView. En la figura 3 se ilustra este concepto.

One Technique Involves Adding Separator Rows to the Data Source

Figura 3: Una técnica implica agregar filas separadoras al origen de datos

Aquí se usan los registros separadores de términos entre comillas porque no hay ningún registro separador especial; en su lugar, se debe indicar de alguna manera que un registro determinado del origen de datos actúa como separador en lugar de como una fila de datos normal. En los ejemplos, se enlaza una instancia de ProductsDataTable a GridView, que se compone de ProductRows. Se podría marcar un registro como una fila separadora si se establece su propiedad CategoryID en -1 (ya que este valor no existiría normalmente).

Para usar esta técnica, tendría que seguir estos pasos:

  1. Recuperar mediante programación los datos que se van a enlazar a GridView (una instancia de ProductsDataTable)
  2. Ordenar los datos en función de las propiedades SortExpression y SortDirection de GridView
  3. Iterar por ProductsRows en ProductsDataTable y buscar dónde se encuentran las diferencias en la columna ordenada
  4. En cada límite de grupo, inserte una instancia ProductsRow de registro separador en DataTable, que tenga CategoryID establecido en -1 (o cualquier designación que haya decidido para marcar un registro como un registro separador)
  5. Después de insertar las filas separadoras, enlace mediante programación los datos a GridView

Además de estos cinco pasos, también es necesario proporcionar un controlador de eventos para el evento RowDataBound de GridView. Aquí, se comprobaría cada instancia de DataRow para determinar si es una fila separadora, con el valor CategoryID establecido en -1. Si es así, es probable que quiera ajustar su formato o el texto mostrado en las celdas.

El uso de esta técnica para insertar los límites del grupo de ordenación necesita más de trabajo que el descrito antes, ya que también debe proporcionar un controlador de eventos para el evento Sorting de GridView y realizar el seguimiento de los valores SortExpression y SortDirection.

Manipulación de la colección de controles de GridView después del enlace de datos

En lugar de manipular los datos antes de enlazarlos a GridView, las filas separadoras se pueden agregar después de que los datos se hayan enlazado a GridView. El proceso de enlace de datos crea la jerarquía de controles de GridView, que en realidad es simplemente una instancia de Table compuesta de una colección de filas, cada una formada por una colección de celdas. En concreto, la colección de controles de GridView contiene un objeto Table en su raíz, un objeto GridViewRow (que se deriva de la clase TableRow) para cada registro del elemento DataSource enlazado a GridView y un objeto TableCell en cada instancia de GridViewRow para cada campo de datos de DataSource.

Para agregar filas separadoras entre cada grupo de ordenación, puede manipular directamente esta jerarquía de controles después de crearla. Puede confiar en que la jerarquía de controles de GridView se ha creado por última vez cuando se represente la página. Por tanto, este enfoque invalida el método Render de la clase Page, en cuyo punto se actualiza la jerarquía de controles final de GridView para incluir las filas separadoras necesarias. En la figura 4 se ilustra este proceso.

An Alternate Technique Manipulates the GridView s Control Hierarchy

Figura 4: Una técnica alternativa manipula la jerarquía de controles de GridView (Haga clic para ver la imagen a tamaño completo)

En este tutorial, usará este último enfoque para personalizar la experiencia del usuario de ordenación.

Nota:

El código que se presenta en este tutorial se basa en el ejemplo proporcionado en la entrada de blog de Teemu Keiski, Experimentación con la agrupación de ordenación de GridView.

Paso 3: Adición de las filas separadoras a la jerarquía de controles de GridView

Como solo quiere agregar las filas separadoras a la jerarquía de controles de GridView después de que se haya creado por última vez en esa visita a l página, realizará esta adición al final del ciclo de vida de la página, pero antes de que la jerarquía de controles real de GridView se haya representado en HTML. El último punto posible en el que se puede lograr esto es el evento Render de la clase Page, que puede invalidar en la clase de código subyacente mediante la siguiente firma de método:

protected override void Render(HtmlTextWriter writer)
{
    // Add code to manipulate the GridView control hierarchy
    base.Render(writer);
}

Cuando se invoca el método Render original de la clase Pagebase.Render(writer) se representará cada uno de los controles de la página y se generará el marcado en función de su jerarquía de controles. Por tanto, es fundamental llamar a base.Render(writer), para que la página se represente y manipular la jerarquía de controles de GridView antes de llamar a base.Render(writer), de modo que las filas separadoras se hayan agregado a la jerarquía de controles de GridView antes de que se represente.

Para insertar los encabezados de grupo de ordenación, primero es necesario asegurarse de que el usuario ha solicitado que se ordenan los datos. De manera predeterminada, el contenido de GridView no está ordenado y, por tanto, no es necesario escribir ningún encabezado de ordenación de grupo.

Nota:

Si quiere que GridView se ordene por una columna determinada al cargar la página por primera vez, llame al método Sort de GridView en la primera visita de página (pero no en postbacks posteriores). Para ello, agregue esta llamada al controlador de eventos Page_Load dentro de una instancia de if (!Page.IsPostBack) condicional. Consulte la información del tutorial Paginación y ordenación de datos de informe para más información sobre el método Sort.

Si los datos se han ordenado, la siguiente tarea consiste en determinar por qué columna se han ordenado y, después, examinar las filas para buscar diferencias en los valores de esa columna. El código siguiente garantiza que los datos se han ordenado y busca la columna por la que se han ordenado:

protected override void Render(HtmlTextWriter writer)
{
    // Only add the sorting UI if the GridView is sorted
    if (!string.IsNullOrEmpty(ProductList.SortExpression))
    {
        // Determine the index and HeaderText of the column that
        //the data is sorted by
        int sortColumnIndex = -1;
        string sortColumnHeaderText = string.Empty;
        for (int i = 0; i < ProductList.Columns.Count; i++)
        {
            if (ProductList.Columns[i].SortExpression.CompareTo(ProductList.SortExpression)
                == 0)
            {
                sortColumnIndex = i;
                sortColumnHeaderText = ProductList.Columns[i].HeaderText;
                break;
            }
        }
        // TODO: Scan the rows for differences in the sorted column�s values
}

Si GridView aún no se ha ordenado, no se habrá establecido su propiedad SortExpression. Por tanto, solo querrá agregar las filas separadoras si esta propiedad tiene algún valor. Si es así, a continuación es necesario determinar el índice de la columna por el que se han ordenado los datos. Para ello, se recorre en bucle la colección Columns de GridView y se busca la columna cuya propiedad SortExpression sea igual a la propiedad SortExpression de GridView. Además del índice de la columna, también se toma la propiedad HeaderText, que se usa al mostrar las filas separadoras.

Con el índice de la columna por la que se ordenan los datos, el paso final consiste en enumerar las filas de GridView. Para cada fila es necesario determinar si el valor de la columna ordenada difiere del valor de la columna ordenada de la fila anterior. Si es así, es necesario insertar una nueva instancia de GridViewRow en la jerarquía de controles. Esto se realiza mediante el código siguiente:

protected override void Render(HtmlTextWriter writer)
{
    // Only add the sorting UI if the GridView is sorted
    if (!string.IsNullOrEmpty(ProductList.SortExpression))
    {
        // ... Code for finding the sorted column index removed for brevity ...
        // Reference the Table the GridView has been rendered into
        Table gridTable = (Table)ProductList.Controls[0];
        // Enumerate each TableRow, adding a sorting UI header if
        // the sorted value has changed
        string lastValue = string.Empty;
        foreach (GridViewRow gvr in ProductList.Rows)
        {
            string currentValue = gvr.Cells[sortColumnIndex].Text;
            if (lastValue.CompareTo(currentValue) != 0)
            {
                // there's been a change in value in the sorted column
                int rowIndex = gridTable.Rows.GetRowIndex(gvr);
                // Add a new sort header row
                GridViewRow sortRow = new GridViewRow(rowIndex, rowIndex,
                    DataControlRowType.DataRow, DataControlRowState.Normal);
                TableCell sortCell = new TableCell();
                sortCell.ColumnSpan = ProductList.Columns.Count;
                sortCell.Text = string.Format("{0}: {1}",
                    sortColumnHeaderText, currentValue);
                sortCell.CssClass = "SortHeaderRowStyle";
                // Add sortCell to sortRow, and sortRow to gridTable
                sortRow.Cells.Add(sortCell);
                gridTable.Controls.AddAt(rowIndex, sortRow);
                // Update lastValue
                lastValue = currentValue;
            }
        }
    }
    base.Render(writer);
}

Este código comienza con una referencia mediante programación al objeto Table que se encuentra en la raíz de la jerarquía de controles de GridView y crea una variable de cadena denominada lastValue. lastValue se usa para comparar el valor de columna ordenada de la fila actual con el valor de la fila anterior. A continuación, se enumera la colección Rows de GridView y, para cada fila, el valor de la columna ordenada se almacena en la variable currentValue.

Nota:

Para determinar el valor de la columna ordenada de una fila concreta, se usa la propiedad Text de la celda. Esto funciona bien para controles BoundField, pero no de la forma deseada para instancias de TemplateField, CheckBoxField, etc. Más adelante se verá cómo tener en cuenta campos de GridView alternativos.

Después, se comparan las variables currentValue y lastValue. Si difieren, es necesario agregar una nueva fila de separador a la jerarquía de controles. Esto se logra mediante la determinación del índice de GridViewRowen la colección Rows del objeto Table, la creación de instancias de GridViewRow y TableCell, y la adición de TableCell y GridViewRow a la jerarquía de controles.

Tenga en cuenta que el formato de TableCell de la fila de separador abarca todo el ancho de GridView, se le aplica formato mediante la clase CSS SortHeaderRowStyle y tiene una propiedad Text que muestra el nombre del grupo de ordenación (como Category) y el valor del grupo (como Beverages). Por último, lastValue se actualiza al valor de currentValue.

La clase CSS que se usa para dar formato a la fila de encabezado del grupo de ordenación SortHeaderRowStyle se debe especificar en el archivo Styles.css. No dude en usar cualquier valor de estilo que le resulte atractivo; en este caso se ha usado lo siguiente:

.SortHeaderRowStyle
{
    background-color: #c00;
    text-align: left;
    font-weight: bold;
    color: White;
}

Con el código actual, la interfaz de ordenación agrega encabezados de grupo de ordenación al ordenar por cualquier instancia de BoundField (vea la figura 5, en la que se muestra una captura de pantalla al ordenar por proveedor). Pero al ordenar por cualquier otro tipo de campo (como CheckBoxField o TemplateField), los encabezados de grupo de ordenación no aparecen por ninguna parte (vea la figura 6).

The Sorting Interface Includes Sort Group Headers When Sorting by BoundFields

Figura 5: La interfaz de ordenación incluye encabezados de grupo de ordenación al ordenar por campos BoundField (Haga clic para ver la imagen a tamaño completo)

The Sort Group Headers are Missing When Sorting a CheckBoxField

Figura 6: Faltan los encabezados de grupo de ordenación al ordenar un campo CheckBoxField (Haga clic para ver la imagen a tamaño completo)

La razón por la que faltan los encabezados de grupo de ordenación al ordenar por una instancia de CheckBoxField es porque el código solo usa actualmente la propiedad Text de TableCell para determinar el valor de la columna ordenada para cada fila. En instancias de CheckBoxField, la propiedad Text de TableCell es una cadena vacía; en su lugar, el valor está disponible a través de un control web CheckBox que reside dentro de la colección Controls de TableCell.

Para controlar los tipos de campo distintos de BoundField, es necesario aumentar el código donde se asigna la variable currentValue para comprobar la existencia de un instancia de CheckBox en la colección Controls de TableCell. En lugar de usar currentValue = gvr.Cells[sortColumnIndex].Text, reemplace este código por lo siguiente:

string currentValue = string.Empty;
if (gvr.Cells[sortColumnIndex].Controls.Count > 0)
{
    if (gvr.Cells[sortColumnIndex].Controls[0] is CheckBox)
    {
        if (((CheckBox)gvr.Cells[sortColumnIndex].Controls[0]).Checked)
            currentValue = "Yes";
        else
            currentValue = "No";
    }
    // ... Add other checks here if using columns with other
    //      Web controls in them (Calendars, DropDownLists, etc.) ...
}
else
    currentValue = gvr.Cells[sortColumnIndex].Text;

Este código examina la columna ordenada TableCell de la fila actual para determinar si hay algún control en la colección Controls. Si hay, y el primer control es CheckBox, la variable currentValue se establece en Sí o No, en función de la propiedad Checked de CheckBox. De lo contrario, el valor se toma de la propiedad Text de TableCell. Esta lógica se puede replicar para controlar la ordenación de cualquier instancia de TemplateField que pueda existir en GridView.

Con la adición del código anterior, los encabezados de grupo de ordenación ahora están presentes al ordenar por instancias de CheckBoxField Discontinued (vea la figura 7).

The Sort Group Headers are Now Present When Sorting a CheckBoxField

Figura 7: Ahora aparecen los encabezados de grupo de ordenación al ordenar un campo CheckBoxField (Haga clic para ver la imagen a tamaño completo)

Nota:

Si tiene productos con valores de base de datos NULL para los campos CategoryID, SupplierIDo UnitPrice, esos valores aparecerán como cadenas vacías en GridView de manera predeterminada, lo que significa que el texto de la fila de separador de esos productos con valores NULL se leerá como Category: (es decir, no hay ningún nombre después de Category: como Category: Beverages). Si quiere que aquí se muestre un valor, puede establecer la propiedad NullDisplayText de BoundField en el texto que quiera mostrar, o bien puede agregar una instrucción condicional en el método Render al asignar currentValue a la propiedad Text de la fila separadora.

Resumen

GridView no incluye muchas opciones integradas para personalizar la interfaz de ordenación. Pero con un poco de código de bajo nivel, es posible ajustar la jerarquía de controles de GridView para crear una interfaz más personalizada. En este tutorial ha visto cómo agregar una fila separadora de grupo de ordenación para un control GridView ordenable, que identifica más fácilmente los distintos grupos y esos límites de grupos. Para obtener ejemplos adicionales de interfaces de ordenación personalizadas, vea la entrada de blog Recomendaciones y trucos de ordenación de GridView en ASP.NET 2.0 de Scott Guthrie.

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