Come associare un DataGrid WPF a un numero variabile di colonne?


124

La mia applicazione WPF genera set di dati che possono avere ogni volta un numero diverso di colonne. Nell'output è inclusa una descrizione di ogni colonna che verrà utilizzata per applicare la formattazione. Una versione semplificata dell'output potrebbe essere simile a:

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

Questa classe è impostata come DataContext su un DataGrid WPF ma in realtà creo le colonne a livello di codice:

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

Esiste un modo per sostituire questo codice con associazioni di dati nel file XAML?

Risposte:


127

Ecco una soluzione alternativa per le colonne vincolanti in DataGrid. Dato che la proprietà Columns è ReadOnly, come tutti hanno notato, ho creato una proprietà associata chiamata BindableColumns che aggiorna le colonne in DataGrid ogni volta che la raccolta cambia attraverso l'evento CollectionChanged.

Se abbiamo questa raccolta di DataGridColumn's

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

Quindi possiamo associare BindableColumns a ColumnCollection in questo modo

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

La proprietà associata 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);
    }
}

1
bella soluzione per il modello MVVM
WPFKK,

2
Una soluzione perfetta! Probabilmente devi fare alcune altre cose in BindableColumnsPropertyChanged: 1. Controlla dataGrid per null prima di accedervi e lancia un'eccezione con una buona spiegazione sull'associazione solo a DataGrid. 2. Controllare e.OldValue per null e annullare l'iscrizione all'evento CollectionChanged per evitare perdite di memoria. Solo per il tuo convincere.
Mike Eshva,

3
Registri un gestore eventi con l' CollectionChangedevento della raccolta colonne, ma non lo annulli mai. In questo modo, la DataGridvolontà sarà mantenuta in vita finché esiste il modello di visualizzazione, anche se il modello di controllo che conteneva DataGridin primo luogo è stato sostituito nel frattempo. Esiste un modo garantito per annullare nuovamente la registrazione di quel gestore eventi quando DataGridnon è più necessario?
OR Mapper,

1
@OR Mapper: teoricamente esiste ma non funziona: WeakEventManager <ObservableCollection <DataGridColumn>, NotifyCollectionChangedEventArgs> .AddHandler (colonne, "CollectionChanged", (s, ne) => {switch ....});
anche il

6
Non è una soluzione bes. Il motivo principale è che si stanno utilizzando le classi dell'interfaccia utente in ViewModel. Inoltre non funzionerà quando si tenta di creare un cambio di pagina. Quando si torna alla pagina con tale datagrid si otterrà un'aspettativa in linea dataGrid.Columns.Add(column)DataGridColumn con intestazione 'X' esiste già nella raccolta Columns di un DataGrid. DataGrids non può condividere colonne e non può contenere istanze di colonna duplicate.
Ruslan F.,

19

Ho continuato la mia ricerca e non ho trovato alcun modo ragionevole per farlo. La proprietà Columns su DataGrid non è qualcosa a cui posso legarmi, in realtà è di sola lettura.

Bryan ha suggerito che si potrebbe fare qualcosa con AutoGenerateColumns, quindi ho dato un'occhiata. Utilizza una semplice riflessione .Net per esaminare le proprietà degli oggetti in ItemsSource e genera una colonna per ognuno. Forse potrei generare un tipo al volo con una proprietà per ogni colonna, ma questo sta andando fuori strada.

Dal momento che questo problema è così facilmente sostituito dal codice, mi atterrò con un semplice metodo di estensione che chiamo ogni volta che il contesto dei dati viene aggiornato con nuove colonne:

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

1
La soluzione più votata e accettata non è la migliore! Due anni dopo la risposta sarebbe: msmvps.com/blogs/deborahk/archive/2011/01/23/…
Mikhail,

4
No, non lo sarebbe. Non il collegamento fornito comunque, perché il risultato di quella soluzione è completamente diverso!
321X

2
Sembra che la soluzione di Mealek sia molto più universale ed è utile in situazioni in cui l'uso diretto del codice C # è problematico, ad esempio in ControlTemplates.
EFraim

Collegamento @Mikhail interrotto
LuckyLikey


9

Ho trovato un articolo sul blog di Deborah Kurata con un bel trucco su come mostrare un numero variabile di colonne in un DataGrid:

Popolamento di un DataGrid con colonne dinamiche in un'applicazione Silverlight utilizzando MVVM

Fondamentalmente, crea un DataGridTemplateColumn e mette ItemsControlall'interno che visualizza più colonne.


1
Non è di gran lunga lo stesso risultato della versione programmata !!
321X

1
@ 321X: Potresti per favore approfondire quali sono le differenze osservate (e anche specificare cosa intendi per versione programmata , poiché tutte le soluzioni sono programmate), per favore?
OR Mapper

Dice "Pagina non trovata"
Jeson Martajaya,


Questo è a dir poco sorprendente !!
Ravid Goldenberg,

6

Sono riuscito a rendere possibile l'aggiunta dinamica di una colonna usando solo una riga di codice come questa:

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

Per quanto riguarda la domanda, questa non è una soluzione basata su XAML (poiché, come detto, non esiste un modo ragionevole per farlo), né è una soluzione che opererebbe direttamente con DataGrid.Columns. Funziona effettivamente con ItemsSource associato a DataGrid, che implementa ITypedList e come tale fornisce metodi personalizzati per il recupero di PropertyDescriptor. In un punto del codice è possibile definire "righe di dati" e "colonne di dati" per la griglia.

Se avessi:

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

potresti usare ad esempio:

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) 

e la griglia che utilizza l'associazione a MyItemsCollection verrebbe popolata con le colonne corrispondenti. Queste colonne possono essere modificate (nuove aggiunte o esistenti rimosse) in fase di esecuzione in modo dinamico e la griglia aggiornerà automaticamente la sua raccolta di colonne.

DynamicPropertyDescriptor sopra menzionato è solo un aggiornamento al normale PropertyDescriptor e fornisce una definizione di colonne fortemente tipizzate con alcune opzioni aggiuntive. In caso contrario, DynamicDataGridSource funzionerebbe perfettamente bene con PropertyDescriptor di base.


3

Crea una versione della risposta accettata che gestisce la disiscrizione.

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

2

È possibile creare un controllo utente con la definizione della griglia e definire controlli "figlio" con varie definizioni di colonna in xaml. Il genitore ha bisogno di una proprietà di dipendenza per le colonne e di un metodo per caricare le colonne:

Genitore:


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

Xaml bambino:


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

E infine, la parte difficile è trovare dove chiamare 'LoadGrid'.
Sto lottando con questo, ma ho fatto funzionare le cose chiamando InitalizeComponentnel mio costruttore di finestre (childGrid è x: nome in window.xaml):

childGrid.deGrid.LoadGrid();

Voce di blog correlata


1

Potresti riuscire a farlo con AutoGenerateColumns e un DataTemplate. Non sono sicuro che funzionerebbe senza molto lavoro, dovresti giocarci. Onestamente, se hai già una soluzione funzionante, non farei ancora il cambiamento a meno che non ci sia un grande motivo. Il controllo DataGrid sta diventando molto buono ma ha ancora bisogno di un po 'di lavoro (e mi resta ancora molto da imparare) per poter svolgere compiti dinamici come questo facilmente.


La mia ragione è che da ASP.Net sono nuovo a ciò che si può fare con un'associazione di dati decente e non sono sicuro di dove siano i limiti. Mi divertirò con AutoGenerateColumns, grazie.
Errore generico,

0

C'è un esempio di come faccio a livello di codice:

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; }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.