web-dev-qa-db-fra.com

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

Mon application WPF génère des ensembles de données pouvant comporter un nombre de colonnes différent à chaque fois. La sortie contient une description de chaque colonne qui sera utilisée pour appliquer le formatage. Une version simplifiée de la sortie pourrait ressembler à ceci:

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

Cette classe est définie en tant que DataContext sur un WPF DataGrid mais je crée en fait les colonnes par programme:

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?

118
Generic Error

Voici une solution de contournement pour la liaison de colonnes dans le DataGrid. Comme 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 du DataGrid chaque fois que la collection change via l’événement CollectionChanged. 

Si nous avons cette collection de DataGridColumn

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

Ensuite, nous pouvons lier BindableColumns à ColumnCollection comme ceci

<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);
    }
}
121
Fredrik Hedblad

J'ai poursuivi mes recherches et je n'ai trouvé aucun moyen raisonnable de le faire. La propriété Columns sur le DataGrid ne peut pas être liée, elle est en lecture seule.

Bryan a suggéré que quelque chose pourrait être fait avec AutoGenerateColumns, alors j'ai jeté un œil. 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. Je pourrais peut-être générer un type à la volée avec une propriété pour chaque colonne, mais il est en train de dévier du chemin.

Comme ce problème est si facilement résolu en code, je vais m'en tenir à une méthode d'extension simple 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);
18
Generic Error

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

Remplissage d'un DataGrid avec des colonnes dynamiques dans une application Silverlight utilisant MVVM

Fondamentalement, elle crée une DataGridTemplateColumn et y place ItemsControl qui affiche plusieurs colonnes.

9
Lukas Cenovsky

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

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 (car, comme indiqué, il n’existe aucun moyen raisonnable de le faire), ni d’une solution qui fonctionnerait directement avec DataGrid.Columns. Il fonctionne réellement avec ItemsSource lié à DataGrid, qui implémente ITypedList et fournit en tant que tel des méthodes personnalisées pour la récupération de PropertyDescriptor. À un endroit du code, vous pouvez définir des "lignes de données" et des "colonnes de données" pour votre grille.

Si vous aviez:

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

vous pouvez 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 une liaison à MyItemsCollection serait remplie avec les colonnes correspondantes. Ces colonnes peuvent être modifiées (nouvelles ajoutées ou existantes supprimées) au moment de l'exécution de façon dynamique et grid actualisera automatiquement sa collection de colonnes.

DynamicPropertyDescriptor mentionné ci-dessus est simplement une mise à niveau de PropertyDescriptor standard et fournit une définition de colonnes fortement typée avec quelques options supplémentaires. DynamicDataGridSource fonctionnerait sinon parfaitement avec PropertyDescriptor de base.

5
doblak

Fait une version de la réponse acceptée qui gère la désinscription.

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);
    }
}
3
Mikhail Orlov

Vous pouvez créer un contrôle utilisateur avec la définition de la grille et définir des contrôles «enfants» avec différentes définitions de colonnes dans xaml. Le parent a besoin d'une propriété de dépendance pour les colonnes et d'une méthode de chargement des 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 délicate consiste à trouver où appeler «LoadGrid».
Je me bats avec cela, mais je dois faire fonctionner les choses en appelant après InitalizeComponent dans le constructeur de fenêtre (childGrid est x: name dans window.xaml):

childGrid.deGrid.LoadGrid();

Entrée de blog connexe

2
Andy

Vous pourrez peut-être faire cela avec AutoGenerateColumns et un DataTemplate. Je ne suis pas certain que si cela fonctionnerait sans beaucoup de travail, il faudrait jouer avec. Honnêtement, si vous avez déjà une solution efficace, je ne ferais pas le changement pour le moment, à moins d'une raison importante. Le contrôle DataGrid est en train de devenir très bon, mais il a encore besoin de travail (et il me reste encore beaucoup à apprendre) pour pouvoir effectuer facilement des tâches dynamiques de ce type.

1
Bryan Anderson

Il y a un exemple de la façon dont je fais par programme:

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; }
}
0
David Soler