Clasificar el contenido de un ListViewSegún la columna pulsadaPublicado el 23/Jun/2002 Nota del 20/Dic/2002:
|
Introducción:
Es una técnica habitual que al pulsar en una columna de un control ListView en modo "Detalle", se clasifique el contenido de dicho ListView por el contenido de los elementos de esa columna.
De forma predeterminada, se puede clasificar dicho contenido usando un código como el siguiente:
Private Sub ListView1_ColumnClick(ByVal sender As Object, _ ByVal e As System.Windows.Forms.ColumnClickEventArgs) _ Handles ListView1.ColumnClick ' ' Asignar el orden de clasificación If ListView1.Sorting = SortOrder.Ascending Then ListView1.Sorting = SortOrder.Descending Else ListView1.Sorting = SortOrder.Ascending End If ListView1.Sort() End SubPero esto sólo clasifica el contenido de la primera columna, además de que lo hace considerando que el contenido de la misma es del tipo String. Esto no supone ningún problema si lo que guardamos en esa columna es de tipo cadena, pero puede ser que en lugar de datos de tipo cadena, tengamos datos del tipo numérico o de fecha.
En estos casos, a pesar del tipo de datos que tengamos, se clasificarán como si fuesen del tipo String y puede que ese orden de clasificación no sea el esperado. Por ejemplo:
Si tenemos estos valores: 1, 2, 7, 10, 15, 20, se ordenarán de la siguiente forma: 1, 10, 15, 2, 20 y 7, que no es el que nos gustaría.
Lo mismo ocurre con las fechas, si tenemos estas fechas: 23/06/02, 01/07/02 y 18/12/02, se clasificarán de esta forma: 01/07/02, 18/12/02 y 23/06/02, que tampoco es la que nos gustaría que fuese.En las versiones anteriores de Visual Basic, teníamos que convertir esos datos en el tipo adecuado para que se clasificaran de forma correcta. Esto se conseguía cambiando el contenido antes de clasificar y después volviendo a ponerlos como estaban originalmente... pero esta es otra historia.
El problema con Visual Basic .NET, es que, aunque el contenido de los elementos de cualquier columna sea de tipo cadena, aunque pulsemos en otra columna, siempre se clasifica por el contenido de la primera columna.
En Visual Basic "clásico", se podía indicar que columna queríamos usar para clasificar, asignando el número de columna a la propiedad SortKey, pero en VB .NET no existe esa propiedad, por tanto no podemos seleccionar la columna que se tendrá en cuenta para efectuar la clasificación de los elementos... al menos de forma tan directa y fácil como en las versiones clásicas de Visual Basic.
¿Cómo solucionar este "problemilla"?
La solución no es fácil. O si, dependiendo del nivel que tengas, (si te hablo a ti, que estás leyendo esto).
Para comprender la solución hay que saber cómo funcionan las interfaces y esas cosillas, en particular la interfaz IComparer, aunque aquí no voy a entrar en detalles profundos, simplemente voy a exponer la solución.Nota publicitaria:
Si quieres aprender más sobre interfaces y cómo implementarlas, te recomiendo que compres el libro Manual Imprescindible de Visual Basic .NET que he escrito y próximamente publicará Anaya Multimedia, (a la hora de escribir estas líneas, la fecha prevista de publicación es Septiembre de 2002)
Nota del 20/Dic/02:
Ya está publicado y a la venta... así que... ya sabes... ;-)
Si quieres saber el contenido del libro, pulsa este link.
Si quieres hacer tu pedido por internet o quieres saber los distribuidores de los países de Latinoamérica en los que se vende, pulsa este otro.Esto es lo que habría que hacer:
El control ListView que acompaña a Visual Basic.NET, tiene un propiedad llamada ListViewItemSorter que contiene un objeto del tipo IComparer. Dicho objeto es el que se encarga de clasificar los elementos del ListView.
La solución es crear una clase que implemente esa interfaz y se encargue de clasificar los elementos dependiendo de la columna pulsada y del tipo de datos que contenga esa columna.Para no complicar mucho la cosa, vamos a ver primero una implementación sencilla de la clase que asignaremos a la propiedad ListViewItemSorter, (que será la encargada de comparar el contenido de cada uno de los elementos a clasificar), en la que se supone que todos los datos son del tipo String, después veremos otra implementación de la clase que se encarga de clasificar y que tendrá en cuenta cómo clasificar los datos, dependiendo que el tipo de datos contenido en la columna sea de tipo cadena, numérico o fecha/hora.
Este es el código de la clase "simple", para usar esta clase.
'------------------------------------------------------------------------------ ' Clase para clasificar las columnas de un ListView (23/Jun/02) ' Versión simple que no considera el tipo de datos de cada columna ' ' ©Guillermo 'guille' Som, 2002 '------------------------------------------------------------------------------ Option Strict On Public Class ListViewColumnSortSimple ' Esta clase implementa la interfaz IComparer Implements IComparer ' ' La columna por la que queremos clasificar Public ColumnIndex As Integer = 0 ' El tipo de clasificación a realizar Public Sorting As SortOrder = SortOrder.Ascending ' ' Función que se usará para comparar los dos elementos Public Overridable Function Compare(ByVal a As Object, _ ByVal b As Object) As Integer _ Implements IComparer.Compare ' ' Esta función devolverá: ' -1 si el primer elemento es menor que el segundo ' 0 si los dos son iguales ' 1 si el primero es mayor que el segundo ' Dim menor, mayor As Integer Dim s1, s2 As String ' ' Los objetos pasados a esta función serán del tipo ListViewItem. ' Convertir el texto en el formato adecuado ' y tomar el texto de la columna en la que se ha pulsado s1 = CType(a, ListViewItem).SubItems(ColumnIndex).Text s2 = CType(b, ListViewItem).SubItems(ColumnIndex).Text ' ' Asignar cuando es menor o mayor, dependiendo del orden de clasificación Select Case Sorting Case SortOrder.Ascending ' Esta es la forma predeterminada menor = -1 mayor = 1 Case SortOrder.Descending ' invertimos los valores predeterminados menor = 1 mayor = -1 Case SortOrder.None ' Todos los elementos se considerarán iguales menor = 0 mayor = 0 End Select ' ' Realizamos la comparación y devolvemos el valor esperado If s1 < s2 Then Return menor ElseIf s1 = s2 Then Return 0 Else Return mayor End If End Function End ClassLo que esta clase hace es implementar la interfaz IComparer. Dicha interfaz sólo tiene un método: Compare, que es una función que devuelve un valor -1, 0 o 1 dependiendo del resultado de comparar los dos objetos indicados indicados en los parámetros.
Debido a que esta clase debe comparar el contenido de dos objetos del tipo ListViewItem, los objetos pasados como parámetro, son de ese tipo, por tanto, tenemos que hacer una conversión para acceder al SubItem adecuado.
He de aclarar que en el control ListView de .NET Framework, el contenido de SubItems(0).Text hace referencia al texto de la primera columna de cada uno de los elementos que contiene, ese mismo valor también se puede obtener usando la propiedad Text del elemento en cuestión.
La propiedad ColumnIndex de esta clase debería contener el índice de la columna en la que se ha pulsado, mientras que la propiedad Sorting contendrá el orden de clasificación, la enumeración usada es la misma que utiliza la propiedad del mismo nombre que tiene el control ListView.
En el código usado en el evento ColumnClick veremos cómo asignar estas dos propiedades.Debido a que hay que tener en cuenta el orden de clasificación, usamos unas variables para que devuelva el valor correcto según los elementos comparados sean menor o mayor.
Cuando se comprueban los elementos, se devuelve un valor -1 si el primer objeto es menor que el segundo, un cero si los dos son iguales y un 1 si el primer elemento es mayor que el segundo. Al menos esto es así si se ordenan de forma ascendente, en caso de que se ordenen de forma descendente, los valores devueltos debería ser invertidos. Y en el caso de que no haya que hacer ninguna comparación, se devuelve un cero, que es sinónimo de que los dos elementos son iguales, por tanto no se realiza ninguna acción.Este será el código que habrá que implementar en el evento ColumnClick del control ListView.
Private Sub ListView1_ColumnClick(ByVal sender As Object, _ ByVal e As System.Windows.Forms.ColumnClickEventArgs) _ Handles ListView1.ColumnClick ' ' ======================================== ' Usando la clase ListViewColumnSortSimple ' ======================================== ' ' Crear una instancia de la clase que realizará la comparación Dim oCompare As New ListViewColumnSortSimple() ' ' Asignar el orden de clasificación If ListView1.Sorting = SortOrder.Ascending Then oCompare.Sorting = SortOrder.Descending Else oCompare.Sorting = SortOrder.Ascending End If ListView1.Sorting = oCompare.Sorting ' ' La columna en la que se ha pulsado oCompare.ColumnIndex = e.Column ' Asignar la clase que implementa IComparer ' y que se usará para realizar la comparación de cada elemento ListView1.ListViewItemSorter = oCompare ' ' Cuando se asigna ListViewItemSorter no es necesario llamar al método Sort 'ListView1.Sort() End SubEn este evento, creamos una nueva instancia de la clase que se encarga de clasificar los elementos; asignamos el orden en el que se clasificarán los elementos, valor que asignamos al ListView para que podamos saber cual era el último valor asignado; asignamos a la propiedad ColumnIndex de la clase, la columna que tendremos en cuenta para clasificar y por último asignamos a la propiedad ListViewItemSorter el objeto de la clase que se encargará de efectuar las comparaciones correspondientes.
Si todo esto no lo has comprendido, lo que sigue puede que te resulte aún más complicado, así que... utilízalo aunque no lo entiendas, espero que cuando sigas indagando en esto de las interfaces y la herencia, te resulte más obvio.
Ahora veamos la clase que tendrá en cuenta el tipo de datos y la forma de usarla.
Public Class ListViewColumnSort Implements IComparer ' Public Enum TipoCompare Cadena Numero Fecha End Enum Public CompararPor As TipoCompare Public ColumnIndex As Integer = 0 Public Sorting As SortOrder = SortOrder.Ascending ' ' Constructores Sub New() ' no es necesario indicar nada, ' ya que implícitamente se llama a MyBase.New End Sub Sub New(ByVal columna As Integer) ColumnIndex = columna End Sub ' Public Overridable Function Compare(ByVal a As Object, _ ByVal b As Object) As Integer _ Implements IComparer.Compare ' ' Esta función devolverá: ' -1 si el primer elemento es menor que el segundo ' 0 si los dos son iguales ' 1 si el primero es mayor que el segundo ' Dim menor As Integer = -1, mayor As Integer = 1 Dim s1, s2 As String ' If Sorting = SortOrder.None Then Return 0 End If ' ' Convertir el texto en el formato adecuado ' y tomar el texto de la columna en la que se ha pulsado s1 = DirectCast(a, ListViewItem).SubItems(ColumnIndex).Text s2 = DirectCast(b, ListViewItem).SubItems(ColumnIndex).Text ' ' Asignar cuando es menor o mayor, ' dependiendo del orden de clasificación If Sorting = SortOrder.Descending Then menor = 1 mayor = -1 End If ' Select Case CompararPor Case TipoCompare.Fecha ' Si da error, se comparan como cadenas Try Dim f1 As Date = DateTime.Parse(s1) Dim f2 As Date = DateTime.Parse(s2) If f1 < f2 Then Return menor ElseIf f1 = f2 Then Return 0 Else Return mayor End If Catch 'Return s1.CompareTo(s2) * mayor Return System.String.Compare(s1, s2, True) * mayor End Try Case TipoCompare.Numero ' Si da error, se comparan como cadenas Try Dim n1 As Decimal = Decimal.Parse(s1) Dim n2 As Decimal = Decimal.Parse(s2) If n1 < n2 Then Return menor ElseIf n1 = n2 Then Return 0 Else Return mayor End If Catch Return System.String.Compare(s1, s2, True) * mayor End Try Case Else 'Case TipoCompare.Cadena Return System.String.Compare(s1, s2, True) * mayor End Select End Function End ClassLa diferencia principal con el código de la clase anterior es que tenemos una nueva propiedad que usaremos para saber el tipo de datos. Después tenemos en cuenta ese tipo de datos y usamos unas variables intermedias para que la comparación realizada se haga de la forma adecuada, ya que no es lo mismo comparar dos cadenas que dos fechas, pero de ese tipo de detalles se encarga el propio .NET Framework.
Aunque se supone que el usuario (o el programador) asignará el valor correcto, hacemos una captura de errores, por si el contenido no fuese el adecuado, en caso de que se produzca una excepción al convertir los datos en otro tipo, hacemos la comparación como si fuesen datos del tipo String.Para usar esta clase, veamos el código que tendríamos que usar en el evento ColumnClick:
Private Sub ListView1_ColumnClick(ByVal sender As Object, _ ByVal e As System.Windows.Forms.ColumnClickEventArgs) _ Handles ListView1.ColumnClick ' ' Crear una instancia de la clase que realizará la comparación Dim oCompare As New ListViewColumnSort() ' ' Asignar el orden de clasificación If ListView1.Sorting = SortOrder.Ascending Then oCompare.Sorting = SortOrder.Descending Else oCompare.Sorting = SortOrder.Ascending End If ListView1.Sorting = oCompare.Sorting ' ' La columna en la que se ha pulsado oCompare.ColumnIndex = e.Column ' El tipo de datos de la columna en la que se ha pulsado Select Case e.Column Case 0 oCompare.CompararPor = ListViewColumnSort.TipoCompare.Cadena Case 1 oCompare.CompararPor = ListViewColumnSort.TipoCompare.Numero Case 2 oCompare.CompararPor = ListViewColumnSort.TipoCompare.Fecha End Select ' Asignar la clase que implementa IComparer ' y que se usará para realizar la comparación de cada elemento ListView1.ListViewItemSorter = oCompare ' ' Cuando se asigna ListViewItemSorter no es necesario llamar al método Sort 'ListView1.Sort() End SubEste código también es muy parecido al anterior, lo único que ahora tenemos en cuenta es el tipo de clasificación que tendremos que hacer, por supuesto estas asignaciones es porque en el código usado para este ejemplo, la primera columna es del tipo String, la segunda de tipo numérico y la tercera de tipo fecha/hora.
Veamos el código del evento Load en el que se asignan algunos valores por defecto al control ListView:
Private Sub Form1_Load(ByVal sender As Object, _ ByVal e As System.EventArgs) _ Handles MyBase.Load ' Configurar el listview With ListView1 .View = View.Details .FullRowSelect = True .GridLines = True .LabelEdit = False .Columns.Clear() .Items.Clear() .Columns.Add("Cadena", 90, HorizontalAlignment.Left) .Columns.Add("Número", 90, HorizontalAlignment.Right) .Columns.Add("Fecha", 90, HorizontalAlignment.Left) ' Asignar algunos valores al listview Dim i As Integer Dim r As New System.Random() For i = 0 To 12 With .Items.Add("Elemento " & i.ToString) .SubItems.Add(((i + 1) * 2500).ToString("###,###")) Select Case r.Next(1, 9) Case Is > 6 .SubItems.Add((Now.AddYears(i).ToString("dd/MM/yyyy"))) Case Is > 3 .SubItems.Add((Now.AddMonths(i).ToString("dd/MM/yyyy"))) Case Else .SubItems.Add((Now.AddDays(i).ToString("dd/MM/yyyy"))) End Select End With Next ' Por defecto se clasifica por la primera columna .Sorting = SortOrder.Ascending .Sort() End With End SubEn la primera columna se asignan valores de tipo cadena, en la segunda columna se asignan valores de tipo numérico y en la tercera se asignan valores de fechas. Para asignar aleatoriamente los valores de fechas, se utiliza un número aleatorio, el cual se consigue usando el método Next de la clase System.Random.
Espero que todo esto no te haya parecido demasiado complicado y que ahora sepas cómo puedes clasificar el contenido de un ListView según en la columna en la que se haya pulsado.
Mejoras al código de la clase:
En la clase se podría implementar un constructor en el que se indicara la columna a tener en cuenta. Esto sería conveniente para que no dejemos el valor asignado por defecto a esa propiedad y así obligarnos a asignarla.
Si sólo creamos un constructor al que haya que pasar ese parámetro, sólo podremos crear una instancia de esa clase especificando ese parámetro.
Veamos el código del constructor y el que habría que usar en el evento ColumnClick para crear una instancia de la clase:' Constructor Sub New(ByVal columna As Integer) ColumnIndex = columna End Sub' Crear una instancia de la clase que realizará la comparación ' indicando la columna en la que se ha pulsado Dim oCompare As New ListViewColumnSort(e.Column)Si hacemos esto, no es necesario hacer la asignación a la propiedad ColumnIndex, ni tampoco podremos crear una instancia sin indicar el parámetro.
Otra cosa a tener en cuenta es que si indicamos sólo un constructor en una clase, sólo podremos crear instancia de esa clase usando ese constructor.
Si queremos que también se puedan crear instancias de esa clase sin necesidad de indicar un parámetro, igual que hemos visto anteriormente, tendremos que indicar de forma explícita ese constructor, aunque no contenga código:Sub New() ' no es necesario indicar nada, ' ya que implícitamente se llama a MyBase.New End Sub
Algunas capturas del proyecto en ejecución.
Estas son tres capturas del proyecto en ejecución, en el cual se muestra cómo quedarían los elementos al pulsar en cada una de las columnas.
Los elementos clasificados por la primera columna (cadena)
Los elementos clasificados por la segunda columna (número)
Los elementos clasificados por la tercera columna (fecha)
Bueno, ya está bien por hoy... espero que todo esto te sea de utilidad.
Nos vemos.
Guillermo
Este es el código para C#, aunque es una versión reducida, sin detección de errores, etc.
El código de la clase para clasificar los elementos del ListView:
using System; using System.Collections; using System.Windows.Forms; ////// Clase para clasificar las columnas de un ListView /// public class ListViewColumnSort : IComparer { public enum TipoCompare { Cadena, Numero, Fecha } public TipoCompare CompararPor; public int ColumnIndex = 0; public SortOrder Sorting = SortOrder.Ascending; public ListViewColumnSort() { // // TODO: agregar aquí la lógica del constructor // } public ListViewColumnSort(int columna) { ColumnIndex = columna; } public int Compare(Object a, Object b) { /* ' Esta función devolverá: ' -1 si el primer elemento es menor que el segundo ' 0 si los dos son iguales ' 1 si el primero es mayor que el segundo */ int menor = -1, mayor = 1; String s1, s2; // if (Sorting == SortOrder.None) return 0; /* ' Convertir el texto en el formato adecuado ' y tomar el texto de la columna en la que se ha pulsado */ s1 = ((ListViewItem)a).SubItems[ColumnIndex].Text; s2 = ((ListViewItem)b).SubItems[ColumnIndex].Text; // // Asignar cuando es menor o mayor, dependiendo del orden de clasificación if (Sorting == SortOrder.Descending) { menor = 1; mayor = -1; } // switch(CompararPor) { case TipoCompare.Fecha: try { DateTime f1, f2; f1 = DateTime.Parse(s1); f2 = DateTime.Parse(s2); // if( f1 < f2 ) return menor; else if( f1 == f2 ) return 0; else return mayor; } catch { return System.String.Compare(s1, s2, true) * mayor; } //break; case TipoCompare.Numero: try { decimal n1, n2; n1 = decimal.Parse(s1); n2 = decimal.Parse(s2); if( n1 < n2 ) return menor; else if( n1 == n2 ) return 0; else return mayor; } catch { return System.String.Compare(s1, s2, true) * mayor; } //break; default: //case TipoCompare.Cadena: return System.String.Compare(s1, s2, true) * mayor; //break; } } } El código del formulario:
private void Form1_Load(object sender, System.EventArgs e) { // Configurar el listview listView1.View = View.Details; listView1.FullRowSelect = true; listView1.GridLines = true; listView1.LabelEdit = false; listView1.Columns.Clear(); listView1.Items.Clear(); listView1.Columns.Add("Cadena", 90, HorizontalAlignment.Left); listView1.Columns.Add("Número", 90, HorizontalAlignment.Right); listView1.Columns.Add("Fecha", 90, HorizontalAlignment.Left); // Asignar algunos valores al listview System.Random r = new System.Random(); for( int i = 0; i <= 12; i++ ) { ListViewItem lvi = listView1.Items.Add("Elemento " + i.ToString()); lvi.SubItems.Add(((i + 1) * 2500).ToString("###,###")); switch( r.Next(1, 3) ) { case 3: lvi.SubItems.Add((DateTime.Now.AddYears(i).ToString("dd/MM/yyyy"))); break; case 2: lvi.SubItems.Add((DateTime.Now.AddMonths(i).ToString("dd/MM/yyyy"))); break; case 1: lvi.SubItems.Add((DateTime.Now.AddDays(i).ToString("dd/MM/yyyy"))); break; } } // Por defecto se clasifica por la primera columna listView1.Sorting = SortOrder.Ascending; listView1.Sort(); } private void listView1_ColumnClick(object sender, System.Windows.Forms.ColumnClickEventArgs e) { /* ' ================================================== ' Usando la clase ListViewColumnSort con constructor ' ================================================== ' ' Crear una instancia de la clase que realizará la comparación ' indicando la columna en la que se ha pulsado */ ListViewColumnSort oCompare = new ListViewColumnSort(e.Column); /* ' La columna en la que se ha pulsado ' esto sólo es necesario si no se utiliza el contructor parametrizado 'oCompare.ColumnIndex = e.Column */ /* Asignar el orden de clasificación */ if( listView1.Sorting == SortOrder.Ascending ) oCompare.Sorting = SortOrder.Descending; else oCompare.Sorting = SortOrder.Ascending; listView1.Sorting = oCompare.Sorting; /* El tipo de datos de la columna en la que se ha pulsado */ switch( e.Column ) { case 0: oCompare.CompararPor = ListViewColumnSort.TipoCompare.Cadena; break; case 1: oCompare.CompararPor = ListViewColumnSort.TipoCompare.Numero; break; case 2: oCompare.CompararPor = ListViewColumnSort.TipoCompare.Fecha; break; } // Asignar la clase que implementa IComparer // y que se usará para realizar la comparación de cada elemento listView1.ListViewItemSorter = oCompare; // }
Para evitar problemas al añadir elementos a un ListView (20/Dic/2002)
Cuando añadimos nuevos elementos a un ListView, si este está utilizando una clase para clasificar los elementos, puede que tengamos problemas al hacerlo, sobre todo si la columna a clasificar no es la primera.
Esto es así debido a que cada vez que añadimos un elemento a la colección Items, el objeto que hemos indicado para la clasificación, intentará clasificar usando esa columna, pero si dicha columna no es la primera, se producirá una excepción, por la sencilla razón de que al añadir el nuevo elemento, sólo asignamos el Subitem(0), que es el que corresponde al contenido de la primera columna.
Realmente, este problema sólo surge si añadimos los elementos de la forma "habitual", (al menos la habitual para mi):
With ListView1.Items.Add("texto nuevo")
.Subitems.Add("el texto del subitem")
End With
Pero si usas un objeto del tipo ListViewItem, no tendrás ningún problema:
ListView1.Items.Add(elItem)De todas formas, para evitar "problemas", te recomiendo lo siguiente:
Cuando vayas a añadir nuevos elementos a un ListView, desactiva el objeto que se encarga de clasificar los elementos.
¿Cómo se hace eso?
Muy simple, asigna un valor nulo a la propiedad ListViewItemSorter.En Visual Basic .NET sería esto: ListView1.ListViewItemSorter = Nothing
En C# sería esto otro: ListView1.ListViewItemSorter = null;Espero que esta aclaración te evite algunos quebraderos de cabeza.
Además de esta aclaración, la actualización de hoy también implica un cambio en el código.
No son cambios importantes, sólo son pequeños cambios que, posiblemente, mejorarán el rendimiento y cambios para usar, en la medida de lo posible (al menos con VB), clases propias del .NET Framework, en lugar de las que incluyen el Visual Basic.
Las funciones (o instrucciones) afectadas son:
CType, se cambia por DirectCast, ya que esta última rinde mejor cuando el objeto a convertir es del tipo esperado.
Los CDate y CDec se cambian por DateTime.Parse y Decimal.Parse respectivamente.
El StrComp de VB y el s1.CompareTo de C#, se cambia por String.Compare, ya que este método permite especificar, (al igual que StrComp), si se tendrá en cuenta la diferencia entre mayúsculas/minúsculas.Los ficheros con el código, (tanto para VB como para C#), tienen las nuevas versiones de la clase ListViewColumnSort.
Aquí están los dos proyectos, el de Visual Basic y C#: clasificarListView.zip 17.7 KB