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

124

Mon application WPF génère des ensembles de données qui peuvent avoir un nombre différent de colonnes à chaque fois. La sortie comprend une description de chaque colonne qui sera utilisée pour appliquer la mise en forme. 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 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 à la place?

Erreur générique
la source

Réponses:

127

Voici une solution de contournement pour la liaison de colonnes dans le DataGrid. 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 le 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);
    }
}
Fredrik Hedblad
la source
1
belle solution pour le modèle MVVM
WPFKK
2
Une solution parfaite! Vous devez probablement faire quelques autres choses dans BindableColumnsPropertyChanged: 1. Vérifiez dataGrid pour null avant d'y accéder et lancez une exception avec une bonne explication sur la liaison uniquement à DataGrid. 2. Vérifiez e.OldValue pour null et désabonnez-vous de l'événement CollectionChanged pour éviter les fuites de mémoire. Juste pour convaincre.
Mike Eshva
3
Vous enregistrez un gestionnaire d'événements avec l' CollectionChangedévénement de la collection de colonnes, mais vous ne l'annulez jamais. De cette façon, le DataGridsera maintenu en vie aussi longtemps que le modèle de vue existe, même si le modèle de contrôle qui contenait le DataGriden premier lieu a été remplacé entre-temps. Existe-t-il un moyen garanti de désinscrire à nouveau ce gestionnaire d'événements lorsque le DataGridn'est plus nécessaire?
OR Mapper
1
@OR Mapper: Théoriquement il y en a mais cela ne fonctionne pas: WeakEventManager <ObservableCollection <DataGridColumn>, NotifyCollectionChangedEventArgs> .AddHandler (colonnes, "CollectionChanged", (s, ne) => {switch ....});
trop
6
Ce n'est pas une solution. La raison principale est que vous utilisez des classes d'interface utilisateur dans ViewModel. De plus, cela ne fonctionnera pas lorsque vous essayez de créer un changement de page. Lorsque vous revenez à la page avec une telle grille de données, vous obtiendrez une attente dans la ligne dataGrid.Columns.Add(column)DataGridColumn avec l'en-tête «X» déjà existant dans la collection Columns d'un DataGrid. DataGrids ne peut pas partager de colonnes et ne peut pas contenir des instances de colonne en double.
Ruslan F.
19

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

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

Étant donné que ce problème est si facilement résolu dans le code, je m'en tiendrai à 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);
Erreur générique
la source
1
La solution la plus votée et acceptée n'est pas la meilleure! Deux ans plus tard, la réponse serait: msmvps.com/blogs/deborahk/archive/2011/01/23/…
Mikhail
4
Non, ce ne serait pas le cas. Pas le lien fourni de toute façon, car le résultat de cette solution est complètement différent!
321X
2
On dirait que la solution de Mealek est beaucoup plus universelle, et est utile dans les situations où l'utilisation directe du code C # est problématique, par exemple dans ControlTemplates.
EFraim
@Mikhail link broken
LuckyLikey
3
voici le lien: blogs.msmvps.com/deborahk/…
Mikhail
9

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

Remplissage d'un DataGrid avec des colonnes dynamiques dans une application Silverlight à l'aide de MVVM

Fondamentalement, elle crée un DataGridTemplateColumnet met à l' ItemsControlintérieur qui affiche plusieurs colonnes.

Lukas Cenovsky
la source
1
Ce n'est de loin pas le même résultat que la version programmée !!
321X
1
@ 321X: Pourriez-vous s'il vous plaît préciser quelles sont les différences observées (et préciser également ce que vous entendez par version programmée , car toutes les solutions à cela sont programmées), s'il vous plaît?
OR Mapper
Il dit "Page non trouvée"
Jeson Martajaya
2
voici le lien blogs.msmvps.com/deborahk/…
Mikhail
C'est tout simplement incroyable !!
Ravid Goldenberg le
6

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 mentionné, il n'y a pas de moyen raisonnable de le faire), ni d'une solution qui fonctionnerait directement avec DataGrid.Columns. Il fonctionne en fait avec ItemsSource liés à DataGrid, qui implémente ITypedList et en tant que tel fournit des méthodes personnalisées pour la récupération de PropertyDescriptor. En un seul 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 la liaison à MyItemsCollection serait remplie avec les colonnes correspondantes. Ces colonnes peuvent être modifiées (ajoutées ou supprimées existantes) au moment de l'exécution de manière dynamique et la grille actualisera automatiquement sa collection de colonnes.

DynamicPropertyDescriptor mentionné ci-dessus est juste une mise à niveau vers PropertyDescriptor standard et fournit une définition de colonnes fortement typées avec quelques options supplémentaires. DynamicDataGridSource fonctionnerait autrement très bien avec PropertyDescriptor de base.

doblak
la source
3

Réalisation d'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);
    }
}
Mikhail Orlov
la source
2

Vous pouvez créer un contrôle utilisateur avec la définition de la grille et définir des contrôles «enfants» avec des définitions de colonne variées en 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 délicate est de trouver où appeler «LoadGrid».
J'ai du mal avec cela mais j'ai obtenu que les choses fonctionnent en appelant après InitalizeComponentdans mon constructeur de fenêtre (childGrid est x: nom dans window.xaml):

childGrid.deGrid.LoadGrid();

Entrée de blog associée

Andy
la source
1

Vous pourrez peut-être le faire avec AutoGenerateColumns et un DataTemplate. Je ne suis pas sûr que cela fonctionne sans beaucoup de travail, il faudrait jouer avec. Honnêtement, si vous avez déjà une solution qui fonctionne, je ne ferais pas encore le changement à moins qu'il n'y ait une grande raison. Le contrôle DataGrid devient très bon, mais il a encore besoin de travail (et il me reste beaucoup à apprendre) pour pouvoir effectuer facilement des tâches dynamiques comme celle-ci.

Bryan Anderson
la source
Ma raison est que venant d'ASP.Net, je suis nouveau dans ce qui peut être fait avec une liaison de données décente et je ne sais pas où se trouvent ses limites. Je vais jouer avec AutoGenerateColumns, merci.
Erreur générique du
0

Il existe un exemple de la façon dont je fais par programmation:

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
la source