Saltar al contenido
XamDataGrid: crear y enlazar columnas de datos de forma dinámica con un editor de su elección

XamDataGrid: crear y enlazar columnas de datos de forma dinámica con un editor de su elección

Recientemente, un buen amigo mío me envió un correo electrónico preguntando cómo generar dinámicamente columnas basadas en una colección de objetos con XamDataGrid.  Esto parece ser una tarea muy común que probablemente todas las aplicaciones necesitarán hacer.  Definitivamente no es una tarea poco común.

10 minutos de lectura

Recientemente, un buen amigo mío me envió un correo electrónico preguntando cómo generar dinámicamente columnas basadas en una colección de objetos con XamDataGrid.  Esto parece ser una tarea muy común que probablemente todas las aplicaciones necesitarán hacer.  Definitivamente no es una tarea poco común.

En este caso particular, necesitamos aplanar una colección de objetos para representarlos como columnas en la cuadrícula.  Por ejemplo; Es posible que tenga un objeto que tenga un número de n propiedades o atributos de nivel que no se conocen hasta el tiempo de ejecución, pero desea editar el objeto en una sola fila de una cuadrícula. No desea agregar un montón de propiedades en su objeto como Prop1, Prop2, Prop3, etc., solo para poder vincularlo a su cuadrícula.  No tienes ni idea de cuántos habrá. Desea agregar dinámicamente columnas a la cuadrícula y enlazarlas al objeto correcto de la colección secundaria en tiempo de ejecución.

En este escenario, estoy creando una aplicación de dotación de personal y tengo un objeto "StaffMember" que tiene una colección de objetos "Period" como propiedad secundaria. Mis objetos se ven más o menos así:

public class StaffMember
{
    public String Department { get; set; }
    public String Name { get; set; }
    public IList<Period> Periods { get; set; }

    public StaffMember()
    {
        this.Periods = new List<Period>();
    }
}

public class Period
{
    public string Title { get; set; }
    public int Hours { get; set; }
}

Se trata simplemente de POCO simples que actualmente no implementan INotifyPropertyChanged.  Para esta aplicación de demostración, no necesito notificaciones de propiedades.  En una aplicación de producción, lo más probable es que tenga que implementar la interfaz INotifyPropertyChanged para las notificaciones de cambios.  A continuación, necesitamos un ViewModel.

public class StaffMemberViewModel : INotifyPropertyChanged
{
    ObservableCollection<StaffMember> _staffMembers;
    public ObservableCollection<StaffMember> StaffMembers
    {
        get { return _staffMembers; }
        set
        {
            _staffMembers = value;
            RaisePropertyChanged("StaffMembers");
        }
    }

    public StaffMemberViewModel()
    {
        PopulateStaffMembers();
    }

    void PopulateStaffMembers()
    {
        var list = new ObservableCollection<StaffMember>();
        var rand = new Random();

        for (Int32 i = 1; i < 4; i++)
        {
            var member = new StaffMember { Name = String.Format("Name {0}", i), Department = String.Format("Department {0}", i) };
            for (int j = 1; j < 5; j++)
                member.Periods.Add(new Period { Title = String.Format("Period {0}", j), Hours = rand.Next(0, 160) });
            list.Add(member);
        }

        StaffMembers = list;
    }

    public event PropertyChangedEventHandler PropertyChanged;
    void RaisePropertyChanged(String propertyName)
    {
        var handler = this.PropertyChanged;
        if (handler != null)
            handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

Este ViewModel implementa la interfaz INotifyPropertyChanged, al igual que todos los ViewModels.  Como puede ver, tenemos una sola propiedad que expone una colección de StaffMembers.  También tenemos un método que genera algunos datos ficticios para nosotros.  Lo siguiente que necesitamos es una vista.

<Window x:Class="XamDataGridDynamicColumns.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:igWPF="http://schemas.infragistics.com/xaml/wpf"
        xmlns:local="clr-namespace:XamDataGridDynamicColumns"
        Title="MainWindow" Height="350" Width="525">

    <Window.DataContext>
        <local:StaffMemberViewModel />
    </Window.DataContext>

    <Grid>
        <igWPF:XamDataGrid DataSource="{Binding Path=StaffMembers}"
                           FieldLayoutInitialized="xamDataGrid_FieldLayoutInitialized">
            <igWPF:XamDataGrid.FieldLayoutSettings>
                <igWPF:FieldLayoutSettings AutoGenerateFields="False"/>
            </igWPF:XamDataGrid.FieldLayoutSettings>
            <igWPF:XamDataGrid.FieldLayouts>
                <igWPF:FieldLayout>
                    <igWPF:Field Name="Name"/>
                    <igWPF:Field Name="Department"/>
                </igWPF:FieldLayout>
            </igWPF:XamDataGrid.FieldLayouts>
        </igWPF:XamDataGrid>
    </Grid>
</Window>

En primer lugar, observe que hemos definido dos espacios de nombres en nuestra vista.  Uno es para nuestros objetos locales y el otro es para los controles Infragistics que usaremos.  Establezca DataContext de View en una instancia de nuestro StaffMemberViewModel.  Puedes hacerlo como quieras.  Lo hice en XAML.  Ahora tenemos que declarar un XamDataGrid y enlazarlo a nuestra colección de StaffMembers en nuestro ViewModel.  Ahora la idea es que sepamos que nuestro StaffMember tiene un Nombre y un Departamento que siempre estarán disponibles.  Estas no son columnas dinámicas, por lo que podemos seguir adelante y crear nuestra vista declarándolas en nuestro FieldLayout.  e queremos asegurarnos de que establecemos AutoGenerateField = false, porque estaremos a cargo de qué columnas crear.

Entonces, ¿cómo empezamos a generar columnas?  Bueno, primero agregue un controlador de eventos al evento XamDataGrid.FieldLayoutInitialized.  Aquí es donde se producirá toda la magia para crear, vincular datos, elegir un editor y agregar las columnas.  Agregué una pequeña propiedad a nuestro ViewModel para darme acceso a cualquier dato que pueda necesitar.

public StaffMemberViewModel ViewModel
{
    get { return this.DataContext as StaffMemberViewModel; }
}

private void xamDataGrid_FieldLayoutInitialized(object sender, Infragistics.Windows.DataPresenter.Events.FieldLayoutInitializedEventArgs e)
{

}

La propiedad ViewModel simplemente me da el DataContext de View en forma de nuestro StaffMemberViewModel (lo usaré para hacer trampa en un minuto).  Necesitamos crear nuestras columnas en función del número de períodos en la colección de períodos del miembro del personal.  Recuerde, no sabemos cuántos períodos tendremos hasta el tiempo de ejecución, primero debemos obtener esta información.  Aquí es donde voy a hacer trampa con fines de demostración.

private void xamDataGrid_FieldLayoutInitialized(object sender, Infragistics.Windows.DataPresenter.Events.FieldLayoutInitializedEventArgs e)
{
    //a cheat to get the number of columns to create.
    var staffMember = this.ViewModel.StaffMembers.First();

    for (Int32 i = 0; i < staffMember.Periods.Count; i++)
    {
        var field = new UnboundField
        {
            Name = staffMember.Periods[i].Title,
            BindingMode = BindingMode.TwoWay,
            BindingPath = new PropertyPath(String.Format("Periods[{0}].Hours", i))
        };

        e.FieldLayout.Fields.Add(field);
    }
}

En esta demostración, estoy usando el primer StaffMember de la colección para determinar el número de columnas que se van a crear.  Ahora, por supuesto, en el mundo real, no querría usar el primer índice de la colección secundaria para averiguar cuántas columnas construir. Recomendaría algún tipo de objeto de definición que defina qué columnas y cuántas columnas construir.  A continuación, creamos un bucle con el número correcto de interacciones para hacer y creamos un nuevo UnboundField.  Queremos establecer tres propiedades importantes.  El primero es el Nombre.  Esto nos va a dar el encabezado de nuestra columna.  A continuación se muestra BindingMode.  Queremos que nuestros enlaces de datos sean bidireccionales.  Por último, creamos un BindingPath.  Observe cómo estoy creando una ruta de enlace que usa un indexador ([ ]).  Esto nos permite crear enlaces para los objetos en índices específicos de la colección que estamos aplanando.  Finalmente, todo lo que tenemos que hacer es agregar el campo recién creado a nuestro FieldLayout.  Ejecute la aplicación y esto es lo que obtiene.

Ejecute la aplicación y esto es lo que obtiene.

Muy bonito.  Hemos aplanado con éxito nuestro gráfico de objetos y hemos creado los enlaces de datos adecuados para cada celda a cada propiedad del objeto subyacente.

Choosing Your Editor

Ahora sé lo que estás pensando.  Pero Brian, el editor predeterminado es un TextBlock.  ¿Qué pasa si quiero usar un editor diferente como XamNumericEditor?  Bueno, por suerte para ti es tan sencillo de hacer.  Solo necesitamos agregar algo de código y un poco de XAML.

private void xamDataGrid_FieldLayoutInitialized(object sender, Infragistics.Windows.DataPresenter.Events.FieldLayoutInitializedEventArgs e)
{
    //a cheat to get the number of columns to create.
    var staffMember = this.ViewModel.StaffMembers.First();

    for (Int32 i = 0; i < staffMember.Periods.Count; i++)
    {
        var field = new UnboundField
        {
            Name = staffMember.Periods[i].Title,
            BindingMode = BindingMode.TwoWay,
            BindingPath = new PropertyPath(String.Format("Periods[{0}].Hours", i))
        };

        field.Settings.EditAsType = typeof(Int32);
        field.Settings.EditorStyle = (Style)Resources["HoursFieldStyle"];

        e.FieldLayout.Fields.Add(field);
    }
}

Agregamos dos líneas de código a nuestro controlador de eventos.  Al establecer la propiedad Field.Settings.EditAsType, estamos indicando al editor que estamos usando cómo manejar el tipo de datos.  Dado que nuestra propiedad Hours es de tipo Int, establecemos la propiedad en consecuencia.  Ahora, eso no nos va a dar el XamNumericEditor automáticamente.  Para eso, necesitamos proporcionar un EditorStyle.  Por lo tanto, agregamos una línea de código que le dice a la propiedad Field.Settings.EditorStyle que obtenga su valor de un recurso que estamos a punto de crear llamado "HoursFieldStyle".

<Window.Resources>
    <Style x:Key="HoursFieldStyle" TargetType="{x:Type igWPF:XamNumericEditor}">
        <Setter Property="Mask" Value="###" />
    </Style>
</Window.Resources>

Abra nuestro XAML y defina un estilo dentro de la propiedad Window.Resources de nuestra vista.  Lo que estamos haciendo aquí es especificar que usaremos XamNumericEditor y, a continuación, establecer la propiedad Mask en ese editor en 3 dígitos.  Ahora, tenga en cuenta que nuestros datos aleatorios tienen menos de 3 dígitos, pero esto solo se usa como ejemplo.  Ejecutamos la aplicación y vamos a ver qué obtenemos.

Ejecutamos la aplicación y vamos a ver qué obtenemos.

Ahora estamos usando XamNumericEditor con una máscara de ###. ¿Qué sucede si intentamos ingresar un valor que no coincide con la máscara ### que definimos?

¿Qué sucede si intentamos ingresar un valor que no coincide con la máscara ### que definimos?

¡Así es!  Nuestro editor funciona como debería, advirtiéndole que el nuevo valor no coincide con la máscara requerida para una entrada válida.  Genial, ¿verdad?  Y es muy fácil.

Bueno, ¿qué tal un ComboBox?

Está bien, Brian, nos enseñaste las cosas simples, pero ¿qué tal algo más complicado?  Quiero usar un XamComboEditor y rellenarlo con valores para elegir la forma de cada celda.  ¿Estás tratando de desafiarme?  Traer. En. En.

Muy bien, lo primero es lo primero.  Necesitamos una fuente de datos para nuestro XamComboEditor.  Hay algunos enfoques para esto, así que solo elegiré uno.  Necesito un objeto que represente los valores de los que seleccionar y una clase para usar como fuente de datos.  Podría ser un ViewModel independiente o dentro del mismo ViewModel que tenemos actualmente.  Usaré uno separado.  Estas son las clases que representan mi origen de datos para XamComboEditor:

public class HoursDataSource
{
    public ObservableCollection<DataItem> Hours { get; set; }

    public HoursDataSource()
    {
        PopulateHours();
    }

    private void PopulateHours()
    {
        var list = new ObservableCollection<DataItem>();

        for (int i = 1; i < 160; i++)
        {
            list.Add(new DataItem() { Name = i.ToString(), Value = i });
        }

        Hours = list;
    }
}

public class DataItem
{
    public string Name { get; set; }
    public int Value { get; set; }
}

Muy sencillo.  No hay mucho que hacer.  Ahora necesitamos agregar algo de código a nuestro XAML.

<Window.Resources>
    <local:HoursDataSource x:Key="hoursDataSource" />
    <igWPF:ComboBoxItemsProvider x:Key="hoursProvider" ItemsSource="{Binding Hours, Source={StaticResource hoursDataSource}}" DisplayMemberPath="Name" ValuePath="Value" />
    <Style x:Key="HoursFieldStyle" TargetType="{x:Type igWPF:XamComboEditor}">
        <Setter Property="ItemsProvider" Value="{StaticResource hoursProvider}" />
    </Style>
    <!--<Style x:Key="HoursFieldStyle" TargetType="{x:Type igWPF:XamNumericEditor}">
        <Setter Property="Mask" Value="###" />
    </Style>-->
</Window.Resources>

Comenzamos comentando el primer estilo que creamos.  A continuación, debemos agregar una instancia de nuestro objeto HoursDataSource.  A continuación, cree un ComboBoxItemsProvider.  Los datos enlazan ItemsSource a la propiedad Hours que existe en nuestra instancia de HoursDataSource.  No olvide establecer DsiplayMemberPath=Name y ValuePath=Value.  A continuación, reemplace el estilo que teníamos anteriormente por uno nuevo que establezca el TargetType os en XamComboEditor.  Defina un Setter que establezca la propiedad ItemsProvider en nuestro recurso ComboBoxItemsProvider que acabamos de crear.  Ejecute la aplicación y veamos qué sucede.

Ejecute la aplicación y veamos qué sucede.

Por supuesto, siempre puede hacer que esto sea más funcional incluyendo lógica para determinar qué editor usar para qué columna, o qué tipo de valor se debe editar para diferentes columnas.

Por favor, siéntase libre de descargar el código fuente.  Si tienes alguna pregunta puedes contactarme a través de mi blog en http://brianlagunas.com, en Twitter (@BrianLagunas), o simplemente dejar un comentario a continuación.

Solicitar una demostración