121 votes

Comment lier un DataGrid WPF à un nombre variable de colonnes ?

Mon application WPF génère des ensembles de données qui peuvent avoir un nombre de colonnes différent à chaque fois. La sortie comprend une description de chaque colonne qui sera utilisée pour appliquer le formatage. Une version simplifiée de la sortie pourrait être quelque chose comme :

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

Cette classe est définie comme le DataContext d'une grille de données WPF, mais je crée en fait les colonnes de manière programmatique :

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))
    });
}

Existe-t-il un moyen de remplacer ce code par des liaisons de données dans le fichier XAML ?

127voto

Fredrik Hedblad Points 42772

Voici une solution de rechange pour lier les colonnes dans la grille de données. Puisque la propriété Columns est ReadOnly, comme tout le monde l'a remarqué, j'ai créé une propriété attachée appelée BindableColumns qui met à jour les colonnes dans la grille de données chaque fois que la collection change grâce à l'événement CollectionChanged.

Si nous avons cette collection de DataGridColumn's

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

Ensuite, nous pouvons lier les BindableColumns à la ColumnCollection comme suit

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

La propriété attachée 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);
    }
}

18voto

Generic Error Points 2049

J'ai poursuivi mes recherches et je n'ai pas trouvé de moyen raisonnable de le faire. La propriété Columns du DataGrid n'est pas une propriété que je peux lier, elle est en fait en lecture seule.

Bryan a suggéré que l'on pouvait faire quelque chose avec AutoGenerateColumns, alors j'ai jeté un coup d'oeil. Il utilise une simple réflexion .Net pour examiner les propriétés des objets dans ItemsSource et génère une colonne pour chacun d'eux. Je pourrais peut-être générer un type à la volée avec une propriété pour chaque colonne, mais je m'éloigne du sujet.

Puisque ce problème est si facilement résolu en code, je vais m'en tenir à une simple méthode d'extension que j'appelle chaque fois que le contexte de données est mis à jour avec de nouvelles colonnes :

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);

9voto

Lukas Cenovsky Points 2425

J'ai trouvé un article de blog de Deborah Kurata avec une belle astuce pour afficher un nombre variable de colonnes dans un DataGrid :

Remplir une grille de données avec des colonnes dynamiques dans une application Silverlight en utilisant MVVM

En gros, elle crée un DataGridTemplateColumn et met ItemsControl à l'intérieur qui affiche plusieurs colonnes.

6voto

doblak Points 1578

J'ai réussi à rendre possible l'ajout dynamique d'une colonne en utilisant juste une ligne de code comme ceci :

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

En ce qui concerne la question, il ne s'agit pas d'une solution basée sur XAML (puisque, comme indiqué, il n'existe pas de moyen raisonnable de le faire), ni d'une solution qui fonctionnerait directement avec DataGrid.Columns. Elle fonctionne en fait avec DataGrid bound ItemsSource, qui implémente ITypedList et, en tant que tel, fournit des méthodes personnalisées pour la récupération des PropertyDescriptor. En un seul endroit du code, vous pouvez définir les "lignes de données" et les "colonnes de données" de votre grille.

Si vous l'aviez fait :

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

que vous pourriez utiliser par exemple :

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) 

et votre grille utilisant la liaison à MyItemsCollection sera peuplée des colonnes correspondantes. Ces colonnes peuvent être modifiées (ajout de nouvelles colonnes ou suppression de colonnes existantes) au moment de l'exécution de manière dynamique et la grille rafraîchira automatiquement sa collection de colonnes.

DynamicPropertyDescriptor mentionné ci-dessus n'est qu'une mise à niveau du PropertyDescriptor ordinaire et fournit une définition des colonnes fortement typées avec quelques options supplémentaires. DynamicDataGridSource fonctionnerait autrement très bien avec le PropertyDescriptor de base.

2voto

Andy Points 21

Vous pouvez créer un usercontrol avec la définition de la grille et définir des contrôles 'enfants' avec des définitions de colonnes variées dans le xaml. Le parent a besoin d'une propriété de dépendance pour les colonnes et d'une méthode pour charger les colonnes :

Parent :


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);
  }
}

Enfant 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>

Et enfin, la partie la plus délicate est de trouver où appeler 'LoadGrid'.
J'ai du mal avec cela mais j'ai réussi à faire fonctionner les choses en appelant après InitalizeComponent dans le constructeur de ma fenêtre (childGrid est x:name dans window.xaml) :

childGrid.deGrid.LoadGrid();

Article de blog connexe

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X