Wie binde ich ein WPF DataGrid an eine variable Anzahl von Spalten?

124

Meine WPF-Anwendung generiert Datensätze, die jedes Mal eine andere Anzahl von Spalten haben können. In der Ausgabe ist eine Beschreibung jeder Spalte enthalten, die zum Anwenden der Formatierung verwendet wird. Eine vereinfachte Version der Ausgabe könnte etwa so aussehen:

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

Diese Klasse ist als DataContext in einem WPF-DataGrid festgelegt, aber ich erstelle die Spalten tatsächlich programmgesteuert:

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

Gibt es eine Möglichkeit, diesen Code stattdessen durch Datenbindungen in der XAML-Datei zu ersetzen?

Allgemeiner Fehler
quelle

Antworten:

127

Hier ist eine Problemumgehung für das Binden von Spalten im DataGrid. Da die Columns-Eigenschaft ReadOnly ist, habe ich, wie jeder bemerkt hat, eine Attached-Eigenschaft namens BindableColumns erstellt, die die Columns im DataGrid jedes Mal aktualisiert, wenn sich die Auflistung durch das CollectionChanged-Ereignis ändert.

Wenn wir diese Sammlung von DataGridColumns haben

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

Dann können wir BindableColumns wie folgt an die ColumnCollection binden

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

Die angehängte Eigenschaft 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
quelle
1
schöne Lösung für MVVM-Muster
WPFKK
2
Eine perfekte Lösung! Wahrscheinlich müssen Sie in BindableColumnsPropertyChanged noch einige andere Dinge tun: 1. Überprüfen Sie dataGrid auf null, bevor Sie darauf zugreifen, und lösen Sie eine Ausnahme mit einer guten Erklärung zum Binden nur an DataGrid aus. 2. Überprüfen Sie e.OldValue auf null und melden Sie sich vom CollectionChanged-Ereignis ab, um Speicherverluste zu vermeiden. Nur um dich zu überzeugen.
Mike Eshva
3
Sie registrieren einen Ereignishandler mit dem CollectionChangedEreignis der Spaltensammlung, heben jedoch die Registrierung auf. Auf diese Weise DataGridbleibt das so lange am Leben, wie das Ansichtsmodell existiert, auch wenn die Steuerungsvorlage, die das DataGridursprünglich enthielt, inzwischen ersetzt wurde. Gibt es eine garantierte Möglichkeit, die Registrierung dieses Ereignishandlers erneut aufzuheben, wenn der DataGridnicht mehr benötigt wird?
ODER Mapper
1
@OR Mapper: Theoretisch gibt es, aber es funktioniert nicht: WeakEventManager <ObservableCollection <DataGridColumn>, NotifyCollectionChangedEventArgs> .AddHandler (Spalten, "CollectionChanged", (s, ne) => {switch ....});
Auch
6
Es ist keine Lösung. Der Hauptgrund ist, dass Sie UI-Klassen in ViewModel verwenden. Es funktioniert auch nicht, wenn Sie versuchen, eine Seitenumschaltung zu erstellen. Wenn Sie mit einem solchen Datagrid zur Seite zurückkehren, wird eine Erwartung in der Zeile dataGrid.Columns.Add(column)DataGridColumn mit dem Header 'X' angezeigt, die bereits in der Columns-Auflistung eines DataGrid vorhanden ist. DataGrids können keine Spalten gemeinsam nutzen und keine doppelten Spalteninstanzen enthalten.
Ruslan F.
19

Ich habe meine Forschung fortgesetzt und keinen vernünftigen Weg gefunden, dies zu tun. Die Columns-Eigenschaft im DataGrid kann ich nicht binden, sondern ist schreibgeschützt.

Bryan schlug vor, dass etwas mit AutoGenerateColumns gemacht werden könnte, also habe ich es mir angesehen. Es verwendet eine einfache .NET-Reflexion, um die Eigenschaften der Objekte in ItemsSource zu überprüfen, und generiert für jedes Objekt eine Spalte. Vielleicht könnte ich einen Typ im laufenden Betrieb mit einer Eigenschaft für jede Spalte generieren, aber dies gerät aus der Bahn.

Da dieses Problem so leicht im Code gelöst werden kann, bleibe ich bei einer einfachen Erweiterungsmethode, die ich aufrufe, wenn der Datenkontext mit neuen Spalten aktualisiert wird:

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);
Allgemeiner Fehler
quelle
1
Die am höchsten gewählte und akzeptierte Lösung ist nicht die beste! Zwei Jahre später wäre die Antwort: msmvps.com/blogs/deborahk/archive/2011/01/23/…
Mikhail
4
Nein, würde es nicht. Jedenfalls nicht der bereitgestellte Link, da das Ergebnis dieser Lösung völlig anders ist!
321X
2
Die Lösung von Mealek scheint viel universeller zu sein und ist in Situationen nützlich, in denen die direkte Verwendung von C # -Code problematisch ist, z. B. in ControlTemplates.
EFraim
@Mikhail Link gebrochen
LuckyLikey
3
Hier ist der Link: blogs.msmvps.com/deborahk/…
Mikhail
9

Ich habe einen Blog-Artikel von Deborah Kurata mit einem schönen Trick gefunden, wie man eine variable Anzahl von Spalten in einem DataGrid anzeigt:

Auffüllen eines DataGrid mit dynamischen Spalten in einer Silverlight-Anwendung mit MVVM

Grundsätzlich erstellt sie ein DataGridTemplateColumnund fügt ein ItemsControl, das mehrere Spalten anzeigt.

Lukas Cenovsky
quelle
1
Es ist bei weitem nicht das gleiche Ergebnis wie die programmierte Version !!
321X
1
@ 321X: Könnten Sie bitte näher auf die beobachteten Unterschiede eingehen (und auch angeben, was Sie unter programmierter Version verstehen , da alle Lösungen dafür programmiert sind)?
ODER Mapper
Es heißt "Seite nicht gefunden"
Jeson Martajaya
2
Hier ist der Link blogs.msmvps.com/deborahk/…
Mikhail
Das ist einfach unglaublich !!
Ravid Goldenberg
6

Ich habe es geschafft, eine Spalte dynamisch mit nur einer Codezeile wie der folgenden hinzuzufügen:

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

In Bezug auf die Frage handelt es sich weder um eine XAML-basierte Lösung (da es, wie erwähnt, keinen vernünftigen Weg gibt), noch um eine Lösung, die direkt mit DataGrid.Columns funktioniert. Es arbeitet tatsächlich mit DataGrid-gebundener ItemsSource, die ITypedList implementiert und als solche benutzerdefinierte Methoden für das Abrufen von PropertyDescriptor bereitstellt. An einer Stelle im Code können Sie "Datenzeilen" und "Datenspalten" für Ihr Raster definieren.

Wenn Sie hätten:

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

Sie könnten zum Beispiel verwenden:

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) 

und Ihr Raster, das die Bindung an MyItemsCollection verwendet, wird mit entsprechenden Spalten gefüllt. Diese Spalten können zur Laufzeit dynamisch geändert (neu hinzugefügt oder vorhanden entfernt) werden, und das Raster aktualisiert automatisch die Spaltensammlung.

Der oben erwähnte DynamicPropertyDescriptor ist nur ein Upgrade auf den regulären PropertyDescriptor und bietet eine stark typisierte Spaltendefinition mit einigen zusätzlichen Optionen. DynamicDataGridSource würde ansonsten mit dem grundlegenden PropertyDescriptor einwandfrei funktionieren.

Doblak
quelle
3

Es wurde eine Version der akzeptierten Antwort erstellt, die das Abbestellen behandelt.

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

Sie können eine Benutzersteuerung mit der Rasterdefinition erstellen und untergeordnete Steuerelemente mit verschiedenen Spaltendefinitionen in xaml definieren. Das übergeordnete Element benötigt eine Abhängigkeitseigenschaft für Spalten und eine Methode zum Laden der Spalten:

Elternteil:


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

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

Und schließlich ist der schwierige Teil, herauszufinden, wo man 'LoadGrid' aufruft.
Ich habe InitalizeComponentProbleme damit, aber ich habe Dinge zum Laufen gebracht, indem ich in meinem Fensterkonstruktor nach aufgerufen habe (childGrid ist x: name in window.xaml):

childGrid.deGrid.LoadGrid();

Verwandter Blogeintrag

Andy
quelle
1

Möglicherweise können Sie dies mit AutoGenerateColumns und einer DataTemplate tun. Ich bin mir nicht sicher, ob es ohne viel Arbeit funktionieren würde, man müsste damit herumspielen. Ehrlich gesagt, wenn Sie bereits eine funktionierende Lösung haben, würde ich die Änderung noch nicht vornehmen, es sei denn, es gibt einen großen Grund. Das DataGrid-Steuerelement wird sehr gut, aber es muss noch einige Arbeit geleistet werden (und ich muss noch viel lernen), um solche dynamischen Aufgaben problemlos ausführen zu können.

Bryan Anderson
quelle
Mein Grund ist, dass ich von ASP.Net aus weiß, was mit anständiger Datenbindung möglich ist, und ich bin mir nicht sicher, wo die Grenzen liegen. Ich werde mit AutoGenerateColumns spielen, danke.
Generischer Fehler
0

Es gibt ein Beispiel für meine programmatische Vorgehensweise:

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
quelle