Der ultimative WPF DataGrid MVVM Guide in 2024 – C# & VB.NET
Inhaltsverzeichnis
- 1 Daten im MVVM tabellarisch darstellen – mit WPF DataGrid
- 2 Im Videoformat ansehen
- 3 Ordnerstruktur vorbereiten
- 4 Das MainViewModel für die WPF DataGrid Bindung
- 5 Den DataContext des MainWindows setzen
- 6 Unser Model – womit arbeiten wir überhaupt?
- 7 Eine erste Datenbindung
- 8 Die Datengebundene Liste befüllen
- 9 Eine ObservableRangeCollection – AddRange zur Hand!
- 10 Mit ausgewähltem Item arbeiten
Daten im MVVM tabellarisch darstellen – mit WPF DataGrid
Ich glaube jeder Entwickler wird regelmäßig eine Art Tabellen-Steuerelement, also z. B. das WPF DataGrid benutzen müssen. Unter Windows Forms hieß es damals noch „DataGridView“, aber letztendlich hat sich vom offensichtlichen Zweck nichts geändert: Wir möchten dem Nutzer Daten anhand einer Tabellenstruktur darstellen. Diese Daten werden anhand von Reihen, Welche aus verschiedenen Spalten bestehen gegliedert dargestellt.
Daher beschäftigen wir uns im heutigen Beitrag damit, was sich seit den Windows Forms (DataGridView) geändert hat und wie man im heutigen MVVM- (Model-View-Viewmodel-) Entwurfsmuster mit Tabellen arbeitet. Auch wenn wir hier natürlich nicht zu tief in die Details von MVVM selbst abtauchen können, werde ich einen Teil wiederholen.
Gerne kannst Du in meinem Beitrag über das Model-View-ViewModel-Entwurfsmuster mehr Details erfahren. Hierbei spielt es auch keine Rolle, ob man VB.NET oder C# verwendet, denn die Prinzipien sind die Gleichen! Für diesen Beitrag ist auch das Verständnis über die „INotifyPropertyChanged“-Schnittstelle essenziell, also schau auch einmal dort vorbei!
Im Videoformat ansehen
Falls Du keine Lust auf Text hast, kannst Du Dir auch gerne mein passendes Video ansehen. Ich würde Dir jedoch trotzdem empfehlen, den ein oder anderen Blick auf diesen Beitrag hier zu werfen, da ich hier noch mehr im Detail auf das Thema eingehen kann.
Ordnerstruktur vorbereiten
Die meisten Entwickler lieben es, alles sauber zu strukturieren, es gehört ja auch irgendwie zum Beruf, oder? Daher gehen wir im nächsten Schritt hin und beginnen mit unserer Projekt-Strukturierung. Achte darauf, dass Du die Ordner nicht aus Versehen in der Projektmappe, sondern tatsächlich im Projektordner erstellst!
Erstelle dazu im ersten Schritt den wichtigsten Ordner in Deinem Projekt, namens „ViewModels“. Danach kannst Du noch den Ordner „Models“ und einen Weiteren namens „Utils“ erstellen. Danach sind wir startklar und können passende ViewModels dort ablegen und darauf zugreifen. Deine Struktur sollte zu diesem Zeitpunkt wie folgt aussehen:
Allerdings ist dies auch nicht schlimm, da es praktisch im Kern darum geht, wo man die Daten her bekommt und wie man Sie anschließend durch Datenbindungen darstellt.
Das MainViewModel für die WPF DataGrid Bindung
Damit wir gleich irgendwann mit sogenannten Datenbindungen im WPF-Bereich arbeiten können, müssen wir vorerst noch andere Vorbereitungen treffen. Da im MVVM-Entwurfsmuster mit ViewModels und dazugehörigen Views gearbeitet wird, legen wir im nächsten Schritt unser erstes ViewModel an.
Dies wird praktisch das „Haupt-Ding“ unserer Anwendung, daher nennen wir es passenderweise auch „MainViewModel“. Weil wir natürlich sauber arbeiten, achte bitte darauf, dass Du die Klassen-Datei nicht nur im korrekten Ordner (ViewModels) anlegst, sondern auch mit einem korrekten Namespace versiehst!
namespace DataGridBindingBeispiel.ViewModels; public class MainViewModel { public MainViewModel() { } }
Namespace ViewModels Public Class MainViewModel Sub New() End Sub End Class End Namespace
Den DataContext des MainWindows setzen
Nachdem Du nun das „MainViewModel“ erstellt hast, müssen wir Dieses, bzw. eine Instanz davon im nächsten Schritt verwenden. Ansonsten wüsste das „MainWindow“ nicht, wo es die zugrundeliegenden Daten für die Datenbindungs-Anweisungen findet. Lass uns den „DataContext“ des „MainWindows“ nun im Code-Behind-File des Views setzen.
Dorthin gelangst Du, indem Du einfach die F7-Taste drückst, während Du im XAML-Designer des „MainWindows“ bist:
using DataGridBindingBeispiel.ViewModels; class MainWindow { public MainWindow() { InitializeComponent(); DataContext = new MainViewModel(); } }
Imports DataGridBindingBeispiel.ViewModels Class MainWindow Sub New() InitializeComponent() DataContext = New MainViewModel() End Sub End Class
Grundsätzlich weiß unser Hauptfenster also nun, wo es seine Daten finden kann, wenn wir eine Datenbindung verwenden. Das nächste Problem ist aktuell natürlich noch, dass im Datenkontext noch gar keine Daten vorhanden sind, an Welche gebunden werden könnte. Lass uns das also gleich erledigen, vorerst müssen wir leider noch etwas „erfinden“ – dazu im nächsten Abschnitt mehr.
Unser Model – womit arbeiten wir überhaupt?
Bevor wir gleich überhaupt „etwas“ anzeigen können, müssen wir uns über dieses „Etwas“ erst einmal Gedanken machen. In unserem heutigen Beispiel möchten wir z. B. Rechnungen im DataGrid darstellen. Dazu müssen wir unserem Programm jedoch zuerst beibringen, was eine Rechnung überhaupt ist – woher soll es das sonst wissen..
Bringen wir also im nächsten Schritt unserem Programm bei, was eine Rechnung ist. Erstelle dazu im Ordner „Models“ eine Klassen-Datei namens „Bill“ und schreibe den folgenden Code hinein. Achte auch hier selbstverständlich auf den korrekten Namespace!
using System; namespace Models { public class Bill { public int Id { get; set; } public DateTime Date { get; set; } public Bill(int id, DateTime date) { Id = id; Date = date; } } }
Namespace Models Public Class Bill Public Property Id As Integer Public Property [Date] As Date Sub New(id As Integer, [date] As Date) Me.Id = id Me.Date = [date] End Sub End Class End Namespace
So, nun weiß unser Programm, was eine Rechnung ist und der nächste Schritt wäre dann, Rechnungen darzustellen. Dies besteht natürlich wieder aus einzelnen Schritten.
Eine erste Datenbindung
Gehe nun zurück in die „MainWindow.xaml“-Datei, dort werden wir jetzt die Tabelle, also das WPF DataGrid erstellen. Natürlich werden wir hier jetzt keine ausgefeilte UI bauen, sondern uns auf das DataGrid allein fokussieren. Schreibe wie für XAML üblich also nun ein öffnendes und schließendes DataGrid-Tag in den XAML Code.
Zusätzlich geben wir auch direkt die erste Datenbindung an:
<!-- MainWindow Rest ... --> <DataGrid ItemsSource="{Binding Bills}"> </DataGrid> <!-- ... -->
Damit sagen wir dem XAML-Code erstmal: „Hey, stelle hier bitte ein DataGrid dar und die dazugehörigen Einträge/Reihen, findest Du in der Bills-Eigenschaft“.
Der Code hätte aktuell außer einer Fehlermeldung noch nichts zur Folge. Wo schauen die Datenbindungs-Anweisungen nochmal nach? Richtig! Im DataContext, sprich unserer „MainViewModel“-Instanz! Dort gibt es bisher noch keine Art „Liste“ von Rechnungen. Aktuell kommt so nur ein Fehler namens „Kann Bills auf blabla nicht finden“.
Automatisch generierte Spalten im WPF DataGrid
Da wir gerade aber beim Thema XAML sind, stell‘ Dir einfach mal vor, wir hätten diese „Bills“ schon drin (dazu kommen wir gleich selbstverständlich auch noch). Aktuell würde das „DataGrid“ alle Spalten anhand der Eigenschaften der zugrundeliegenden Objekte automatisch generieren. Dies sähe erstmal so aus:
Ich sag mal so: Cool, aber auch irgendwie uncool.. Die meisten deutschen Nutzer würden sich direkt beschweren, was denn bitte ein „Date“ sein soll (bitte arg deutsch aussprechen – dann kommt der Effekt besser rüber). Ebenso ist die Darstellung, bzw. das Format des Datums nicht wirklich toll – aber um beides kümmern wir uns nun!
Spalten & Formate selbst bestimmen
Bevor wir uns über die Darstellungs-Formate der jeweiligen Spalten Gedanken machen, müssen wir erstmal dafür sorgen, dass manuelle Spalten vorhanden sind. Das geht im WPF DataGrid ganz einfach, indem wir – eben XAML-typisch – eine Eigenschaft des DataGrids festlegen: „DataGrid.ColumnDefinitions“.
Gehe also wieder in den XAML-Code und zwischen die beiden DataGrid-Tags, dort erstellst Du ein öffnendes und schließendes Tag für die „ColumnDefinitions“-Eigenschaft. Dort drin fügen wir unsere Spalten-Definitionen hinzu. Für unser Beispiel haben wir erstmal eine simple Text-Spalte und eine Weitere mit einer Format-Angabe.
Der Kopf-Text der Spalte
Wir geben der jeweiligen Spalte einen „Header“-Wert, damit wir eine Spaltenüberschrift bekommen und danach noch die Datenbindungs-Anweisung. Bei der Id ist es relativ simple, da wir hier nur sagen: „Stelle hier einfach die Zahl der Id als Text dar“. Beim Datum sieht es anders aus, hier sagen wir erstmal, dass er das Datum nehmen, aber speziell formatieren soll. Dies erreichen wir innerhalb der Binding-Anweisung mit der Eigenschaft „StringFormat“.
Formatierung
Dort schreibst Du einfach zwei geschweifte Klammern-Paare, pass jedoch dabei auf, die Autovervollständigung grätscht hier öfters mal dazwischen. Im zweiten Paar geben wir zuerst den zu verwendenden Wert an (in diesem Fall den ersten Wert = Index 0), Welchen wir dann mit „dd.MM.yy HH:mm“ formatieren möchten.
<DataGrid ItemsSource="{Binding Bills}"> <DataGrid.Columns> <DataGridTextColumn Header="Nr." Binding="{Binding Id}" /> <DataGridTextColumn Header="Datum" Binding="{Binding Date, StringFormat={}{0:dd.MM.yy HH:mm}}" /> </DataGrid.Columns> </DataGrid>
Automatische Spalten-Generierung deaktivieren
Das Ganze sieht dann nun wie gleich folgend aus. Wie Du sicherlich sehen wirst, haben wir das Problem, dass nun 4 Spalten sichtbar sind. Das liegt daran, dass das DataGrid nach wie vor die Spalten generiert, dies müssen wir deaktivieren!
Die automatische Generierung der Spalten deaktivierst Du mit der „AutoGenerateColumns“-Eigenschaft des WPF DataGrids. Mit ausgeschalteter Spalten-Generierung sieht es dann final so aus:
Selbstverständlich hält Dich auch hier nichts davon ab, die „Header“-Eigenschaft zu binden und dynamischen Inhalt wie z. B. Übersetzungen darzustellen.
Die Datengebundene Liste befüllen
Nachdem wir nun alle Vorbereitungen soweit abgeschlossen haben, kümmern wir uns um die eigentlichen Rechnungen. Einerseits werden wir hierfür eine passende Art von Liste erstellen und zusätzlich befüllen. Leider reicht eine normale Liste, also eine gängige „List(Of T) & List<T>“ nicht aus, da Diese keine Änderungen kommunizieren kann.
Kommunizieren von Listen-Änderungen
Für gewöhnlich nimmt man hier die „ObservableCollection“, bzw. einen Listen-Typ, Welcher die „INotifyCollectionChanged“-Schnittstelle implementiert. Elemente, Welche Listen-Änderungen an das gebundene Steuerelement kommunizieren möchten, können diese Schnittstelle implementieren. Um dies zu tun, lösen die implementierenden Klassen das „CollectionChanged“-Ereignis aus.
MainViewModel erweitern
Nun fügen wir dem „MainViewModel“ die passende Eigenschaft hinzu. Beachte, dass ich hier nur einen „Getter“ definiere, da wir die Liste nicht mehrfach setzen werden. Wir instanziieren Diese nur einmal im Konstruktor und befüllen Sie anschließend bei Bedarf mit Daten. Natürlich können wir die Liste auch zu passenden Zeitpunkten leeren.
Nach der Instanziierung der Liste fügen wir noch ein paar Einträge (natürlich in Form von Rechnungen) hinzu.
using System.Collections.ObjectModel; using DataGridBindingBeispiel.Models; namespace DataGridBindingBeispiel.ViewModels; public class MainViewModel { public ObservableCollection<Bill> Bills { get; } public MainViewModel() { Bills = new ObservableCollection<Bill>(); Bills.Add(new Bill(1, DateTime.Now)); Bills.Add(new Bill(2, DateTime.Now)); } }
Imports System.Collections.ObjectModel Imports DataGridBindingBeispiel.Models Namespace DataGridBindingBeispiel.ViewModels Public Class MainViewModel Public ReadOnly Property Bills As ObservableCollection(Of Bill) Public Sub New() Bills = New ObservableCollection(Of Bill)() Bills.Add(new Bill(1, Date.Now)) Bills.Add(new Bill(2, Date.Now)) End Sub End Class End Namespace
Im besten Fall sammelt man vorher einmal alle Items und fügt Diese in einem Schritt hinzu. Das hat den offensichtlichen Vorteil, dass die eigentliche (datengebundene) Liste nur ein einziges Mal „Hey, ich hab mich geändert“ auslöst – statt 200x für 200 Einträge.. Dies könnte im ersten Schritt z. B. durch eine Art Service passieren, oder im Testfall wie folgt:
var list = new List<Bill>(); // triggert kein CollectionChanged! list.Add(new Bill(1, DateTime.Now)); // triggert auch kein CollectionChanged! list.Add(new Bill(2, DateTime.Now)); // triggert CollectionChanged!! // Achtung: Gibt es leider nicht standardmäßig! Bills.AddRange(list);
Dim list = New List(Of Bill)() ' triggert kein CollectionChanged! list.Add(New Bill(1, Date.Now)) ' triggert auch kein CollectionChanged! list.Add(New Bill(2, Date.Now)) ' triggert CollectionChanged!! ' Achtung: Gibt es leider nicht standardmäßig! Bills.AddRange(list)
Eine ObservableRangeCollection – AddRange zur Hand!
Wie im vorherigen Abschnitt erwähnt, gibt es Performance und – naja es fühlt sich halt nicht richtig an – Sauberkeits-Probleme, wenn wir die normale „ObservableCollection“ verwenden. Sie triggert für jeden hinzugefügten oder gelöschten Eintrag das „CollectionChanged“-Ereignis, was blöd ist, wenn das 200x in wenigen Millisekunden passiert.
Daher kannst Du Dich an folgender Klasse orientieren, Welche eine „AddRange“-Methode anbietet. Diese triggert das „CollectionChanged“-Ereignis bei der Verwendung der Methode nur ein einziges Mal! Google dazu am besten einfach mal nach „ObservableRangeCollection“.
Mit ausgewähltem Item arbeiten
Jetzt, wo die Liste an sich erstmal funktioniert, kümmern wir uns im letzten Schritt um eine weitere wichtige Funktionalität: Die Auswahl eines Eintrages. Hierbei spielt es natürlich auch eine Rolle, ob wir den Eintrag aus dem Code, bzw. durch die UI heraus setzen. Also, grundsätzlich zumindest, wir können es uns aber ziemlich einfach machen, da wir mit Zweiwege-Datenbindungen arbeiten.
Bevor wir das allerdings ans Laufen bekommen, fehlt vorher noch eine weitere Sache. Auch hierbei wäre es wieder ratsam, wenn Du Dir zum tieferen Verständnis meinen Beitrag über die „INotifyPropertyChanged“-Schnittstelle anschaust.
Das PropertyChangedBase-Helferlein
Der Einfachheit halber, werden wir von der dort gezeigten „PropertyChangedBase“-Klasse erben. Diese Klasse vereinfacht uns das Auslösen des „PropertyChanged“-Ereignisses, Welches der grafischen Oberfläche alles Notwendige signalisiert. Kopiere Dir die Klasse daher gerne in einen „Utils“-Ordner und packe Sie in den entsprechenden Namespace:
using System.Runtime.CompilerServices; using System.ComponentModel; namespace Utils { public abstract class PropertyChangedBase : INotifyPropertyChanged { protected void NotifyOfPropertyChange([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } public event PropertyChangedEventHandler PropertyChanged; } }
Imports System.ComponentModel Imports System.Runtime.CompilerServices Namespace Utils Public MustInherit Class PropertyChangedBase Implements INotifyPropertyChanged Protected Sub NotifyOfPropertyChange(<CallerMemberName> Optional propertyName As String? = Nothing) RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName)) End Sub Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged End Class End Namespace
Eine SelectedBill-Eigenschaft
Füge im nächsten Schritt nun eine „SelectedBill“-Eigenschaft im ViewModel hinzu. Diese erlaubt uns die Datenbindung der ausgewählten Rechnung aus dem Datagrid. Das schöne ist, es funktioniert in beide Richtungen! Bedenke hier, dass Du das oben im blauen Hinweis angegebene Interface implementierst, oder von meine Beispiel-Basisklasse erbst!
Die „SelectedBill“-Eigenschaft hat hier einen speziellen „Setter“. Falls der sich hinter dem Feld befindliche Wert nicht von dem via „Setter“ übermittelte Wert unterscheiden sollte, springen wir heraus. Warum sollten wir auch „es hat sich was geändert“ auslösen, wenn dem nicht so ist. Wenn sich allerdings was geändert hat, kommunizieren wir dies via „PropertyChanged“-Ereignis nach außen.
Somit können alle interessierten Objekte auf ihre Art – wie das DataGridView – darauf reagieren.
using System.Collections.ObjectModel; using DataGridBindingBeispiel.Models; using DataGridBindingBeispiel.Utils; namespace ViewModels { public class MainViewModel : PropertyChangedBase { public ObservableCollection<Bill> Bills { get; set; } private Bill _selectedBill; public Bill SelectedBill { get { return _selectedBill; } set { if (_selectedBill == value) return; _selectedBill = value; NotifyOfPropertyChange(); } } public MainViewModel() { Bills = new ObservableCollection<Bill>(); Bills.Add(new Bill(1, DateTime.Now)); Bills.Add(new Bill(2, DateTime.Now)); } } }
Imports System.Collections.ObjectModel Imports DataGridBindingBeispiel.Models Imports DataGridBindingBeispiel.Utils Namespace ViewModels Public Class MainViewModel Inherits PropertyChangedBase Public Property Bills As ObservableCollection(Of Bill) Private _selectedBill As Bill Public Property SelectedBill As Bill Get Return _selectedBill End Get Set(value As Bill) If _selectedBill Is value Then Return End If _selectedBill = value NotifyOfPropertyChange() End Set End Property Sub New() Bills = New ObservableCollection(Of Bill)() Bills.Add(New Bill(1, Date.Now)) Bills.Add(New Bill(2, Date.Now)) End Sub End Class End Namespace
Das Setzen der ausgewählten Rechnung aus dem Code heraus könnte so aussehen:
// zweite Rechnung standardmäßig auswählen SelectedBill = Bills[1];
' zweite Rechnung standardmäßig auswählen SelectedBill = Bills(1)
Datenbindung anpassen
Final können wir nun noch einmal in den XAML-Code gehen, um die bisherige Datenbindungen des DataGrids zu verändern. Bisher weiß das WPF DataGrid natürlich nichts von der „SelectedBill“-Eigenschaft in unserem Viewmodel. Dies ändern wir nun mit der folgenden Bindung an die „SelectedItem“-Eigenschaft des Datagrids:
<!-- restlicher MainWindow.xaml Code.. --> <DataGrid ItemsSource="{Binding Bills}" SelectedItem="{Binding SelectedBill}" AutoGenerateColumns="False"> <DataGrid.Columns> <DataGridTextColumn Header="Nr." Binding="{Binding Id}" /> <DataGridTextColumn Header="Datum" Binding="{Binding Date, StringFormat={}{0:dd.MM.yy HH:mm}}" /> </DataGrid.Columns> </DataGrid> <!-- restlicher Code.. -->