Introducción:
Si has intentado usar el control ListView en una aplicación de tipo WPF (Windows Presentation Foundation), seguramente te habrás dado cuenta que no tiene nada que ver con el control de las aplicaciones de Windows Forms.
Sí, es más complicado.
Hace unos años (más de 5) ya te comentaba algunos de esos cambios en los puntos 9, 10 y 11 de Equivalencias entre las clases de WPF y las de .NET (1).
Aquí te voy a explicar lo que yo creo que se debe hacer para acceder a los elementos de un ListView, que en realidad no ha cambiado mucho, pero lo que sí ha cambiado es la forma de acceder a los "sub elementos", es decir ya no existe una propiedad / colección subitems que es la forma que tenemos en WinForms para acceder a las diferentes columnas de cada elemento.
Cómo se hace en WinForms
Por ejemplo, si tenemos un ListView con más de una columna, la forma de acceder a la segunda columna de la primera fila es: LisView1.Items(0).SubItems(1).Text
Pero eso no lo podemos hacer en WPF ya que SubItems no existe.
Cómo se hace en WPF
Los controles ListView de WPF para definir columnas deben tener un control del tipo GridView como contenido de la propiedad ListView.View.
Ese GridView define tantas columnas como sean necesarias, esas columnas son del tipo GridViewColumn y en la definición de las propiedades de ese objeto-columna, concretamente en DisplayMemberBinding es donde está "la magia" de cómo acceder a cada uno de los "sub-elementos" de cada fila de datos.
Por ejemplo, si tenemos un formulario tal como el mostrado en la figura 1, ese ListView lo podemos definir tal como vemos en el listado 1.
Figura 1. El formulario de WPF
Este es el código XAML para crear ese ListView (más abajo te mostraré el código completo):
<ListView Name="lvDatos" Margin="0" MinHeight="240"
VerticalAlignment="Stretch" HorizontalAlignment="Stretch" >
<ListView.View>
<GridView AllowsColumnReorder="True">
<!-- Utilizo valores en Binding distintos a los nombres de las cabeceras
para que se vea que no hay relación entre el nombre del Header
y el nombre de Binding -->
<GridViewColumn Header="Nombre y apellidos"
DisplayMemberBinding="{Binding Nombre}" Width="150"/>
<GridViewColumn Header="Correo"
DisplayMemberBinding="{Binding email}" Width="190"/>
<GridViewColumn Header="How old"
DisplayMemberBinding="{Binding Edad}" Width="70"/>
</GridView>
</ListView.View>
</ListView>
Listado 1. El código XAML del control ListView
Como puedes comprobar en cada columna hay tres asignaciones a otras tantas propiedades:
Width que es para indicar el ancho, Header que es para indicr el texto a mostrar en la cabecera de la columna y DisplayMemberBinding que es la propiedad que enlaza con el dato a mostrarse en esa columna.
Como puedes comprobar el valor que le estoy asignando a esa propiedad "enlazada" es: {Binding Nombre}.
¿Qué significa eso?
Eso significa que lo que se muestre en esa columna será un "campo" (o propiedad) del dato que se agregue a una fila.
Y tu dirás... ¡pos vale! pero no me entero...
Eso quiere decir que cada dato que añadas a cada fila del ListView debe ser un objeto que defina una propiedad con el mismo nombre que indicamos después de Binding. En ese ejemplo, será una propiedad llamada Nombre.
Es decir, en WPF el ListView no funciona "tan fácil" como lo hace en WinForms, en WPF los elementos agregados deben ser (o deberían ser) de un tipo concreto y las columnas (o cabeceras) deben hacer referencia a las propiedades que queremos mostrar.
En el ejemplo que he puesto del ListView para WPF podemos usar una clase que defina al menos tres propiedades (no campos públicos) con los nombres usados después de Binding, en este ejemplo esas propiedades deben ser: Nombre, email y Edad.
Nota:
Los nombres usados en Binding deben coincidir "exactamente" con los nombres de las propiedades, es decir, no deben diferenciarse en las mayúsculas/minúsculas... ¡ni siquiera en Visual Basic!
Además deben estar definidos como propiedades, no como métodos ni campos públicos.
Código de cómo agregar y recuperar los elementos
Veamos cómo agregar elementos al ListView y después veremos cómo recuperar esos elementos.
Te voy a mostrar dos formas de hacerlo:
1.- usando tipos anónimos (no es recomendable, pero si sabes lo que haces, es fácil de utilizar, aunque a la hora de recuperar los datos en Visual Basic deberías hacerlo usando Option Strict Off al menos en el fichero de código en el que estés usando el tipo anónimo. En C# tendrás que acceder a ese dato usando dynamic.
2.- usando un tipo de datos previamente definido con esas tres propiedades.
Agregar datos usando un tipo anónimo
Para agregar los datos en este ejemplo, voy a usar un bucle que agregue 10 objetos al ListView. Como son objetos de tipo anónimo usamos un tipo anónimo que defina las propiedades que necesitamos.
VB:
' Rellenar el listview con datos
lvDatos.Items.Clear()
For i = 1 To 10
' Asignamos un tipo anónimo
' Si las columnas (GridViewColumn) tienen un Binding a las propiedades
' los valores se asignarán a cada columna, si no están enlazadas
' se agregará el valor de ToString a cada columna (estará el texto repetido)
lvDatos.Items.Add(New With
{.Nombre = "Nombre anónimo" & i.ToString,
.email = "correo" & i.ToString & "@outlook.com",
.Edad = 17 + i
})
Next
C#:
// Rellenar el listview con datos
lvDatos.Items.Clear();
for (var i = 1; i <= 10; i++)
{
// Asignamos un tipo anónimo
// Si las columnas (GridViewColumn) tienen un Binding a las propiedades
// los valores se asignarán a cada columna, si no están enlazadas
// se agregará el valor de ToString a cada columna (estará el texto repetido)
lvDatos.Items.Add(new
{
Nombre = "Nombre anónimo" + i.ToString(),
email = "correo" + i.ToString() + "@outlook.com",
Edad = 17 + i
});
}
Recuperar los datos usando un tipo anónimo
La forma de recuperar esos datos es usando una variable de tipo Object en Visual Basic y en el caso de C# esa variable debe ser de tipo dynamic, ya que en ambos lenguajes vamos a usar late-binding o enlace tardío, es decir, hasta que no se ejecute el código no se sabrá si esas propiedades están definidas o no en el objeto recuperado del ListView.
Es importante que leas y pruebes el comentario que te hago al final del código sobre qué ocurriría si quisiéramos acceder a una propiedad que no está definida en el "tipo anónimo".
VB:
' Si tenemos Option Strict Off podemos hacer esto:
Dim v = lvDatos.SelectedItem
txtMostar.Text = v.Nombre & " (" & v.email & ")"
' El problema es que queramos acceder a una propiedad que no exista
' (esto dará error)
'txtMostar.Text = v.Nombre & " (" & v.cagonto & ")"
C#:
// En C# debemos definir la variable como dynamic:
dynamic v = lvDatos.SelectedItem;
txtMostar.Text = v.Nombre + " (" + v.email + ")";
// El problema es que queramos acceder a una propiedad que no exista
// (esto dará error)
//txtMostar.Text = v.Nombre + " (" + v.cagonto + ")";
Agregar datos usando un tipo definido (Colega)
Como podrás comprobar, el código de agregar los datos es muy parecido al anterior, la diferencia está en que después de "NEW" usamos el tipo Colega en vez de no indicar qué tipo de datos estamos usando.
VB:
' Rellenar el listview con datos
lvDatos.Items.Clear()
For i = 1 To 10
' Asignamos un valor de un tipo definido (Colega)
' que tiene como mínimo las propiedades que vamos a usar
' en el listiView
lvDatos.Items.Add(New Colega With
{.Nombre = "Nombre " & i.ToString,
.email = "email" & i.ToString & "@outlook.com",
.Edad = 17 + i
})
Next
C#:
// Rellenar el listview con datos
lvDatos.Items.Clear();
for (var i = 1; i <= 10; i++)
{
// Asignamos un valor de un tipo definido (Colega)
// que tiene como mínimo las propiedades que vamos a usar
// en el listiView
lvDatos.Items.Add(new Colega
{
Nombre = "Nombre " + i.ToString(),
email = "email" + i.ToString() + "@outlook.com",
Edad = 17 + i
});
}
Recuperar los datos usando un tipo definido (Colega)
En este caso, como estamos usando un tipo definido previamente, lo único que tenemos que hacer a la hora de recuperar el elemento seleccionado es hacer un "cast", es decir, una conversión al tipo Colega, ya que el valor devuelto por SelectedItem es del tipo object, ya que los elementos de un ListView pueden ser de cualquier tipo, incluso tipos mezclados... aunque esto último no lo he probado, pero hacer se puede hacer... otra cosa es hacerlo bien, jeje.
VB:
Dim v = TryCast(lvDatos.SelectedItem, Colega)
txtMostar.Text = v.Nombre & " (" & v.email & ")"
' como ahora usamos un tipo previamente definido
' si usamos esto:
' txtMostar.Text = v.Nombre & " (" & v.cagonto & ")"
' nos dirá que "cagonto" no es un miembro de Colega
C#:
var v = ((Colega)lvDatos.SelectedItem);
txtMostar.Text = v.Nombre + " (" + v.email + ")";
// como ahora usamos un tipo previamente definido
// si usamos esto:
// txtMostar.Text = v.Nombre + " (" + v.cagonto + ")";
// nos dirá que "cagonto" no es un miembro de Colega
Y esto es todo... espero que te haya aclarado algunos conceptos y dudas... y si quieres saber más sobre los ListView, los GridView y cómo hacer maravillas con el contenido de los ListView, te recomiendo que le eches un vistazo a los enlaces que te he puesto en cada una de esos tipos de datos.
Un poco más abajo tienes el código compelto tanto del XAML como el de Visual Basic y C#.
Ese código lo he hecho con Visual Studio 2012, pero supongo que será válido en otras versiones anteriores: en el caso de VB desde el VS2008, y en el caso de C# desde Visual Studio 2010.
Nota:
En ese código hay comentadas un par de pruebas que puedes hacer.
Entre ellas está la de no asignar ningún valor a la propiedad Binding de los GridViewColumn y asignar cadenas normales a los elementos del ListView, después al ejecutar el programa y al recuperar el contenido del elemento seleccionado verás que... no te lo cuento... así lo tendrás que probar ;-)
Espero que te sea de utilidad.
Nos vemos.
Guillermo
El código Xaml
En VB usa esta definición de la ventana:
<Window x:Class="MainWindow"
En C# usa esta definición de la ventana (o pon el nombre del espacio de nombres del proyecto delante de MainWindows):
<Window x:Class="WpfListView_cs.MainWindow"
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
Height="380" Width="525">
<Grid>
<StackPanel Orientation="Vertical" Margin="10,10">
<ListView Name="lvDatos" Margin="0" MinHeight="240"
VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
SelectionChanged="lvDatos_SelectionChanged" >
<ListView.View>
<GridView AllowsColumnReorder="True">
<!-- Utilizo valores en Binding distintos a los nombres de las cabeceras
para que se vea que no hay relación entre el nombre del Header
y el nombre de Binding -->
<GridViewColumn Header="Nombre y apellidos"
DisplayMemberBinding="{Binding Nombre}" Width="150"/>
<GridViewColumn Header="Correo"
DisplayMemberBinding="{Binding email}" Width="190"/>
<GridViewColumn Header="How old"
DisplayMemberBinding="{Binding Edad}" Width="70"/>
</GridView>
</ListView.View>
</ListView>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="10">
<Button Content="Rellenar" Click="Button_Click_1" />
<Label />
<Button Name="btnMostrar" Content="Mostrar seleccionado"
IsEnabled="False" Click="btnMostrar_Click" />
<Label />
<CheckBox Name="chkAnonimo" IsChecked="True"
Content="Usar tipo anónimo (si no, tipo definido)" />
</StackPanel>
<TextBlock Name="txtMostar" Text="Una fila"/>
<TextBlock Name="txtMostar2" Text="Una fila"/>
</StackPanel>
</Grid>
</Window>
El código para Visual Basic .NET
'------------------------------------------------------------------------------
' Ejemplo de ListView usando WPF para Desktop (27/Dic/12)
' y cómo acceder a las columnas (sin usar SubItems)
'
' ©Guillermo 'guille' Som, 2012
'------------------------------------------------------------------------------
Option Strict Off
Option Infer On
Class MainWindow
Private Sub Button_Click_1(sender As Object, e As RoutedEventArgs)
' Rellenar el listview con datos
lvDatos.Items.Clear()
For i = 1 To 10
'' Esto añadirá el mismo texto a todas las columnas
'' siempre y cuando no hayamos usado ningún Binding
'' si no, esto no añade nada de nada...
'.Items.Add("Item número " & i.ToString("00"))
If chkAnonimo.IsChecked Then
' Asignamos un tipo anónimo
' Si las columnas (GridViewColumn) tienen un Binding a las propiedades
' los valores se asignarán a cada columna, si no están enlazadas
' se agregará el valor de ToString a cada columna (estará el texto repetido)
lvDatos.Items.Add(New With
{.Nombre = "Nombre anónimo" & i.ToString,
.email = "correo" & i.ToString & "@outlook.com",
.Edad = 17 + i
})
Else
' Asignamos un valor de un tipo definido (Colega)
' que tiene como mínimo las propiedades que vamos a usar
' en el listiView
lvDatos.Items.Add(New Colega With
{.Nombre = "Nombre " & i.ToString,
.email = "email" & i.ToString & "@outlook.com",
.Edad = 17 + i
})
End If
Next
End Sub
Private Sub btnMostrar_Click(sender As Object, e As RoutedEventArgs)
' Mostar en el TextBlock el elemento seleccionado
' Dependiendo de que sea el tipo anónimo o el definido
' el valor mostrado será diferente.
' En el caso de la clase Colega, si no tenemos sobrecargado el método ToString
' mostrará el nombre de la clase: WpfListView_vb.Colega
txtMostar2.Text = lvDatos.SelectedItem.ToString
If chkAnonimo.IsChecked Then
' Si tenemos Option Strict Off podemos hacer esto:
Dim v = lvDatos.SelectedItem
txtMostar.Text = v.Nombre & " (" & v.email & ")"
' El problema es que queramos acceder a una propiedad que no exista
' (esto dará error)
'txtMostar.Text = v.Nombre & " (" & v.cagonto & ")"
Else
Dim v = TryCast(lvDatos.SelectedItem, Colega)
txtMostar.Text = v.Nombre & " (" & v.email & ")"
' como ahora usamos un tipo previamente definido
' si usamos esto:
' txtMostar.Text = v.Nombre & " (" & v.cagonto & ")"
' nos dirá que "cagonto" no es un miembro de Colega
End If
End Sub
Private Sub lvDatos_SelectionChanged(sender As Object, e As SelectionChangedEventArgs)
' habilitar según haya o no elementos seleccionados
btnMostrar.IsEnabled = (lvDatos.SelectedItems.Count > 0)
End Sub
End Class
''' <summary>
''' Tipo Colega, con las popiedades (o campos)
''' que tendrán las columnas del ListView.
''' Aunque puede tener más propiedades
''' aunque no estén enlazadas con las columnas
''' </summary>
Class Colega
Public Property Nombre As String
Public Property email As String
Public Property Edad As Integer
' Podemos tener más propiedadas
Public Property Apellidos As String
End Class
El código para C#
//-----------------------------------------------------------------------------
// Ejemplo de ListView usando WPF para Desktop (27/Dic/12)
// y cómo acceder a las columnas (sin usar SubItems)
//
// ©Guillermo 'guille' Som, 2012
//-----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace WpfListView_cs
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
// Rellenar el listview con datos
lvDatos.Items.Clear();
for (var i = 1; i <= 10; i++)
{
//' Esto añadirá el mismo texto a todas las columnas
//' siempre y cuando no hayamos usado ningún Binding
//' si no, esto no añade nada de nada...
//.Items.Add("Item número " + i.ToString("00"));
if ((bool)chkAnonimo.IsChecked)
{
// Asignamos un tipo anónimo
// Si las columnas (GridViewColumn) tienen un Binding a las propiedades
// los valores se asignarán a cada columna, si no están enlazadas
// se agregará el valor de ToString a cada columna (estará el texto repetido)
lvDatos.Items.Add(new
{
Nombre = "Nombre anónimo" + i.ToString(),
email = "correo" + i.ToString() + "@outlook.com",
Edad = 17 + i
});
}
else
{
// Asignamos un valor de un tipo definido (Colega)
// que tiene como mínimo las propiedades que vamos a usar
// en el listiView
lvDatos.Items.Add(new Colega
{
Nombre = "Nombre " + i.ToString(),
email = "email" + i.ToString() + "@outlook.com",
Edad = 17 + i
});
}
}
}
private void btnMostrar_Click(object sender, RoutedEventArgs e)
{
// Mostar en el TextBlock el elemento seleccionado
// Dependiendo de que sea el tipo anónimo o el definido
// el valor mostrado será diferente.
// En el caso de la clase Colega, si no tenemos sobrecargado el método ToString
// mostrará el nombre de la clase: WpfListView_vb.Colega
txtMostar2.Text = lvDatos.SelectedItem.ToString();
if ((bool)chkAnonimo.IsChecked)
{
// En C# debemos definir la variable como dynamic:
dynamic v = lvDatos.SelectedItem;
txtMostar.Text = v.Nombre + " (" + v.email + ")";
// El problema es que queramos acceder a una propiedad que no exista
// (esto dará error)
//txtMostar.Text = v.Nombre + " (" + v.cagonto + ")";
}
else
{
var v = ((Colega)lvDatos.SelectedItem);
txtMostar.Text = v.Nombre + " (" + v.email + ")";
// como ahora usamos un tipo previamente definido
// si usamos esto:
// txtMostar.Text = v.Nombre + " (" + v.cagonto + ")";
// nos dirá que "cagonto" no es un miembro de Colega
}
}
private void lvDatos_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// habilitar según haya o no elementos seleccionados
btnMostrar.IsEnabled = (lvDatos.SelectedItems.Count > 0);
}
}
/// <summary>
/// Tipo Colega, con las popiedades (o campos)
/// que tendrán las columnas del ListView.
/// Aunque puede tener más propiedades
/// aunque no estén enlazadas con las columnas
/// </summary>
class Colega
{
public string Nombre { get; set; }
public string email { get; set; }
public int Edad { get; set; }
// Podemos tener más propiedadas
public string Apellidos { get; set; }
}
}
Espacios de nombres usados en el código de este artículo:
System.Windows
System.Windows.Controls