¿Cómo enlazo un WPF DataGrid a un número variable de columnas?

124

Mi aplicación WPF genera conjuntos de datos que pueden tener un número diferente de columnas cada vez. En el resultado se incluye una descripción de cada columna que se utilizará para aplicar el formato. Una versión simplificada de la salida podría ser algo como:

class Data
{
    IList<ColumnDescription> ColumnDescriptions { get; set; }
    string[][] Rows { get; set; }
}

Esta clase se establece como DataContext en un WPF DataGrid pero en realidad creo las columnas mediante programación:

for (int i = 0; i < data.ColumnDescriptions.Count; i++)
{
    dataGrid.Columns.Add(new DataGridTextColumn
    {
        Header = data.ColumnDescriptions[i].Name,
        Binding = new Binding(string.Format("[{0}]", i))
    });
}

¿Hay alguna forma de reemplazar este código con enlaces de datos en el archivo XAML?

Error generico
fuente

Respuestas:

127

Aquí hay una solución para las columnas de enlace en DataGrid. Como la propiedad Columns es ReadOnly, como todos notaron, hice una propiedad adjunta llamada BindableColumns que actualiza las columnas en el DataGrid cada vez que la colección cambia a través del evento CollectionChanged.

Si tenemos esta colección de DataGridColumn's

public ObservableCollection<DataGridColumn> ColumnCollection
{
    get;
    private set;
}

Entonces podemos vincular BindableColumns a ColumnCollection de esta manera

<DataGrid Name="dataGrid"
          local:DataGridColumnsBehavior.BindableColumns="{Binding ColumnCollection}"
          AutoGenerateColumns="False"
          ...>

La propiedad adjunta BindableColumns

public class DataGridColumnsBehavior
{
    public static readonly DependencyProperty BindableColumnsProperty =
        DependencyProperty.RegisterAttached("BindableColumns",
                                            typeof(ObservableCollection<DataGridColumn>),
                                            typeof(DataGridColumnsBehavior),
                                            new UIPropertyMetadata(null, BindableColumnsPropertyChanged));
    private static void BindableColumnsPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
    {
        DataGrid dataGrid = source as DataGrid;
        ObservableCollection<DataGridColumn> columns = e.NewValue as ObservableCollection<DataGridColumn>;
        dataGrid.Columns.Clear();
        if (columns == null)
        {
            return;
        }
        foreach (DataGridColumn column in columns)
        {
            dataGrid.Columns.Add(column);
        }
        columns.CollectionChanged += (sender, e2) =>
        {
            NotifyCollectionChangedEventArgs ne = e2 as NotifyCollectionChangedEventArgs;
            if (ne.Action == NotifyCollectionChangedAction.Reset)
            {
                dataGrid.Columns.Clear();
                foreach (DataGridColumn column in ne.NewItems)
                {
                    dataGrid.Columns.Add(column);
                }
            }
            else if (ne.Action == NotifyCollectionChangedAction.Add)
            {
                foreach (DataGridColumn column in ne.NewItems)
                {
                    dataGrid.Columns.Add(column);
                }
            }
            else if (ne.Action == NotifyCollectionChangedAction.Move)
            {
                dataGrid.Columns.Move(ne.OldStartingIndex, ne.NewStartingIndex);
            }
            else if (ne.Action == NotifyCollectionChangedAction.Remove)
            {
                foreach (DataGridColumn column in ne.OldItems)
                {
                    dataGrid.Columns.Remove(column);
                }
            }
            else if (ne.Action == NotifyCollectionChangedAction.Replace)
            {
                dataGrid.Columns[ne.NewStartingIndex] = ne.NewItems[0] as DataGridColumn;
            }
        };
    }
    public static void SetBindableColumns(DependencyObject element, ObservableCollection<DataGridColumn> value)
    {
        element.SetValue(BindableColumnsProperty, value);
    }
    public static ObservableCollection<DataGridColumn> GetBindableColumns(DependencyObject element)
    {
        return (ObservableCollection<DataGridColumn>)element.GetValue(BindableColumnsProperty);
    }
}
Fredrik Hedblad
fuente
1
buena solución para el patrón MVVM
WPFKK
2
¡Una solución perfecta! Probablemente necesite hacer algunas otras cosas en BindableColumnsPropertyChanged: 1. Verifique que dataGrid no sea nulo antes de acceder a él y arroje una excepción con una buena explicación sobre el enlace solo a DataGrid. 2. Verifique que e.OldValue sea nulo y cancele la suscripción del evento CollectionChanged para evitar pérdidas de memoria. Solo por tu convencimiento.
Mike Eshva
3
Registra un controlador de eventos con el CollectionChangedevento de la colección de columnas, sin embargo, nunca lo anula. De esa manera, DataGridse mantendrá vivo mientras exista el modelo de vista, incluso si la plantilla de control que contenía el DataGriden primer lugar ha sido reemplazada mientras tanto. ¿Hay alguna forma garantizada de cancelar el registro de ese controlador de eventos nuevamente cuando DataGridya no se requiere?
O Mapper
1
@OR Mapper: Teóricamente existe pero no funciona: WeakEventManager <ObservableCollection <DataGridColumn>, NotifyCollectionChangedEventArgs> .AddHandler (columnas, "CollectionChanged", (s, ne) => {switch ....});
también
66
No es la mejor solución. La razón principal es que está utilizando clases de IU en ViewModel. Además, no funcionará cuando intente crear un cambio de página. Al volver a la página con dicha cuadrícula de datos, obtendrá una expection en línea dataGrid.Columns.Add(column)DataGridColumn con el encabezado 'X' ya existe en la colección Columns de una DataGrid. DataGrids no puede compartir columnas y no puede contener instancias de columnas duplicadas.
Ruslan F.
19

He continuado mi investigación y no he encontrado ninguna forma razonable de hacerlo. La propiedad Columnas en DataGrid no es algo con lo que pueda vincularme, de hecho, es de solo lectura.

Bryan sugirió que se podría hacer algo con AutoGenerateColumns, así que eché un vistazo. Utiliza la reflexión simple .Net para observar las propiedades de los objetos en ItemsSource y genera una columna para cada uno. Tal vez podría generar un tipo sobre la marcha con una propiedad para cada columna, pero esto se está desviando.

Dado que este problema se soluciona tan fácilmente en el código, seguiré con un método de extensión simple al que llamo cuando el contexto de datos se actualiza con nuevas columnas:

public static void GenerateColumns(this DataGrid dataGrid, IEnumerable<ColumnSchema> columns)
{
    dataGrid.Columns.Clear();

    int index = 0;
    foreach (var column in columns)
    {
        dataGrid.Columns.Add(new DataGridTextColumn
        {
            Header = column.Name,
            Binding = new Binding(string.Format("[{0}]", index++))
        });
    }
}

// E.g. myGrid.GenerateColumns(schema);
Error generico
fuente
1
¡La solución más votada y aceptada no es la mejor! Dos años después, la respuesta sería: msmvps.com/blogs/deborahk/archive/2011/01/23/…
Mikhail
44
No, no lo haría. No es el enlace proporcionado de todos modos, ¡porque el resultado de esa solución es completamente diferente!
321X
2
Parece que la solución de Mealek es mucho más universal y es útil en situaciones donde el uso directo del código C # es problemático, por ejemplo, en ControlTemplates.
EFraim
Enlace @Mikhail roto
LuckyLikey
3
aquí está el enlace: blogs.msmvps.com/deborahk/…
Mikhail
9

Encontré un artículo de blog de Deborah Kurata con un buen truco sobre cómo mostrar un número variable de columnas en un DataGrid:

Rellenar una cuadrícula de datos con columnas dinámicas en una aplicación de Silverlight usando MVVM

Básicamente, crea un DataGridTemplateColumny pone ItemsControldentro que muestra varias columnas.

Lukas Cenovsky
fuente
1
¡De lejos no es el mismo resultado que la versión programada!
321X
1
@ 321X: ¿Podría explicar cuáles son las diferencias observadas (y también especificar qué quiere decir con versión programada , ya que todas las soluciones a esto están programadas), por favor?
O Mapper
Dice "Página no encontrada"
Jeson Martajaya
2
aquí está el enlace blogs.msmvps.com/deborahk/…
Mikhail
¡Esto es increíble!
Ravid Goldenberg
6

Logré hacer posible agregar dinámicamente una columna usando solo una línea de código como esta:

MyItemsCollection.AddPropertyDescriptor(
    new DynamicPropertyDescriptor<User, int>("Age", x => x.Age));

Con respecto a la pregunta, esta no es una solución basada en XAML (ya que, como se mencionó, no hay una forma razonable de hacerlo), tampoco es una solución que funcione directamente con DataGrid.Columns. Realmente opera con DataGrid enlazado ItemsSource, que implementa ITypedList y, como tal, proporciona métodos personalizados para la recuperación de PropertyDescriptor. En un lugar en el código puede definir "filas de datos" y "columnas de datos" para su cuadrícula.

Si tuvieras:

IList<string> ColumnNames { get; set; }
//dict.key is column name, dict.value is value
Dictionary<string, string> Rows { get; set; }

podrías usar por ejemplo:

var descriptors= new List<PropertyDescriptor>();
//retrieve column name from preprepared list or retrieve from one of the items in dictionary
foreach(var columnName in ColumnNames)
    descriptors.Add(new DynamicPropertyDescriptor<Dictionary, string>(ColumnName, x => x[columnName]))
MyItemsCollection = new DynamicDataGridSource(Rows, descriptors) 

y la cuadrícula que usa el enlace a MyItemsCollection se completará con las columnas correspondientes. Esas columnas se pueden modificar (nuevas agregadas o eliminadas existentes) en tiempo de ejecución dinámicamente y la cuadrícula actualizará automáticamente su colección de columnas.

DynamicPropertyDescriptor mencionado anteriormente es solo una actualización del PropertyDescriptor normal y proporciona una definición de columnas fuertemente tipada con algunas opciones adicionales. DynamicDataGridSource de lo contrario funcionaría muy bien con un PropertyDescriptor básico.

doblak
fuente
3

Hizo una versión de la respuesta aceptada que maneja la cancelación de la suscripción.

public class DataGridColumnsBehavior
{
    public static readonly DependencyProperty BindableColumnsProperty =
        DependencyProperty.RegisterAttached("BindableColumns",
                                            typeof(ObservableCollection<DataGridColumn>),
                                            typeof(DataGridColumnsBehavior),
                                            new UIPropertyMetadata(null, BindableColumnsPropertyChanged));

    /// <summary>Collection to store collection change handlers - to be able to unsubscribe later.</summary>
    private static readonly Dictionary<DataGrid, NotifyCollectionChangedEventHandler> _handlers;

    static DataGridColumnsBehavior()
    {
        _handlers = new Dictionary<DataGrid, NotifyCollectionChangedEventHandler>();
    }

    private static void BindableColumnsPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
    {
        DataGrid dataGrid = source as DataGrid;

        ObservableCollection<DataGridColumn> oldColumns = e.OldValue as ObservableCollection<DataGridColumn>;
        if (oldColumns != null)
        {
            // Remove all columns.
            dataGrid.Columns.Clear();

            // Unsubscribe from old collection.
            NotifyCollectionChangedEventHandler h;
            if (_handlers.TryGetValue(dataGrid, out h))
            {
                oldColumns.CollectionChanged -= h;
                _handlers.Remove(dataGrid);
            }
        }

        ObservableCollection<DataGridColumn> newColumns = e.NewValue as ObservableCollection<DataGridColumn>;
        dataGrid.Columns.Clear();
        if (newColumns != null)
        {
            // Add columns from this source.
            foreach (DataGridColumn column in newColumns)
                dataGrid.Columns.Add(column);

            // Subscribe to future changes.
            NotifyCollectionChangedEventHandler h = (_, ne) => OnCollectionChanged(ne, dataGrid);
            _handlers[dataGrid] = h;
            newColumns.CollectionChanged += h;
        }
    }

    static void OnCollectionChanged(NotifyCollectionChangedEventArgs ne, DataGrid dataGrid)
    {
        switch (ne.Action)
        {
            case NotifyCollectionChangedAction.Reset:
                dataGrid.Columns.Clear();
                foreach (DataGridColumn column in ne.NewItems)
                    dataGrid.Columns.Add(column);
                break;
            case NotifyCollectionChangedAction.Add:
                foreach (DataGridColumn column in ne.NewItems)
                    dataGrid.Columns.Add(column);
                break;
            case NotifyCollectionChangedAction.Move:
                dataGrid.Columns.Move(ne.OldStartingIndex, ne.NewStartingIndex);
                break;
            case NotifyCollectionChangedAction.Remove:
                foreach (DataGridColumn column in ne.OldItems)
                    dataGrid.Columns.Remove(column);
                break;
            case NotifyCollectionChangedAction.Replace:
                dataGrid.Columns[ne.NewStartingIndex] = ne.NewItems[0] as DataGridColumn;
                break;
        }
    }

    public static void SetBindableColumns(DependencyObject element, ObservableCollection<DataGridColumn> value)
    {
        element.SetValue(BindableColumnsProperty, value);
    }

    public static ObservableCollection<DataGridColumn> GetBindableColumns(DependencyObject element)
    {
        return (ObservableCollection<DataGridColumn>)element.GetValue(BindableColumnsProperty);
    }
}
Mikhail Orlov
fuente
2

Puede crear un control de usuario con la definición de cuadrícula y definir controles 'secundarios' con definiciones de columna variadas en xaml. El padre necesita una propiedad de dependencia para las columnas y un método para cargar las columnas:

Padre:


public ObservableCollection<DataGridColumn> gridColumns
{
  get
  {
    return (ObservableCollection<DataGridColumn>)GetValue(ColumnsProperty);
  }
  set
  {
    SetValue(ColumnsProperty, value);
  }
}
public static readonly DependencyProperty ColumnsProperty =
  DependencyProperty.Register("gridColumns",
  typeof(ObservableCollection<DataGridColumn>),
  typeof(parentControl),
  new PropertyMetadata(new ObservableCollection<DataGridColumn>()));

public void LoadGrid()
{
  if (gridColumns.Count > 0)
    myGrid.Columns.Clear();

  foreach (DataGridColumn c in gridColumns)
  {
    myGrid.Columns.Add(c);
  }
}

Niño Xaml:


<local:parentControl x:Name="deGrid">           
  <local:parentControl.gridColumns>
    <toolkit:DataGridTextColumn Width="Auto" Header="1" Binding="{Binding Path=.}" />
    <toolkit:DataGridTextColumn Width="Auto" Header="2" Binding="{Binding Path=.}" />
  </local:parentControl.gridColumns>  
</local:parentControl>

Y finalmente, la parte difícil es encontrar dónde llamar a 'LoadGrid'.
Estoy luchando con esto, pero hice que las cosas funcionen llamando después InitalizeComponenten mi constructor de ventanas (childGrid es x: nombre en window.xaml):

childGrid.deGrid.LoadGrid();

Entrada de blog relacionada

Andy
fuente
1

Es posible que pueda hacer esto con AutoGenerateColumns y un DataTemplate. No estoy seguro si funcionaría sin mucho trabajo, tendrías que jugar con eso. Honestamente, si ya tienes una solución que funcione, no haría el cambio todavía a menos que haya una gran razón. El control DataGrid se está volviendo muy bueno, pero aún necesita algo de trabajo (y me queda mucho por aprender) para poder realizar tareas dinámicas como esta fácilmente.

Bryan Anderson
fuente
Mi razón es que, desde ASP.Net, soy nuevo en lo que se puede hacer con un enlace de datos decente y no estoy seguro de dónde están sus límites. Jugaré con AutoGenerateColumns, gracias.
Error genérico el
0

Hay una muestra de la forma en que lo hago programáticamente:

public partial class UserControlWithComboBoxColumnDataGrid : UserControl
{
    private Dictionary<int, string> _Dictionary;
    private ObservableCollection<MyItem> _MyItems;
    public UserControlWithComboBoxColumnDataGrid() {
      _Dictionary = new Dictionary<int, string>();
      _Dictionary.Add(1,"A");
      _Dictionary.Add(2,"B");
      _MyItems = new ObservableCollection<MyItem>();
      dataGridMyItems.AutoGeneratingColumn += DataGridMyItems_AutoGeneratingColumn;
      dataGridMyItems.ItemsSource = _MyItems;

    }
private void DataGridMyItems_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
        {
            var desc = e.PropertyDescriptor as PropertyDescriptor;
            var att = desc.Attributes[typeof(ColumnNameAttribute)] as ColumnNameAttribute;
            if (att != null)
            {
                if (att.Name == "My Combobox Item") {
                    var comboBoxColumn =  new DataGridComboBoxColumn {
                        DisplayMemberPath = "Value",
                        SelectedValuePath = "Key",
                        ItemsSource = _ApprovalTypes,
                        SelectedValueBinding =  new Binding( "Bazinga"),   
                    };
                    e.Column = comboBoxColumn;
                }

            }
        }

}
public class MyItem {
    public string Name{get;set;}
    [ColumnName("My Combobox Item")]
    public int Bazinga {get;set;}
}

  public class ColumnNameAttribute : Attribute
    {
        public string Name { get; set; }
        public ColumnNameAttribute(string name) { Name = name; }
}
David Soler
fuente