Der ultimative WPF DataGrid MVVM Guide in 2024 – C# & VB.NET

WPF Datagrid MVVM - Der ultimative Guide
WPF Datagrid MVVM – Der ultimative Guide

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!

💡 Hinweis: Beachte bitte, dass es nicht „den einen Weg“ gibt. Im heutigen Beitrag schauen wir uns eine Möglichkeit an, alles mehr oder weniger selfmade zu machen, da ich das Grundverständnis wichtig finde. In einem zukünftigen Beitrag, bzw. in einem Update für diesen Beitrag, werde ich dann noch auf die Möglichkeiten mit einem Werkzeug wie Caliburn Micro eingehen. Derartige Frameworks helfen uns Entwicklern bei der Umsetzung und sparen uns Einiges an Arbeit.

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:

Ordnerstruktur des Projekts – WPF DataGrid MVVM Beispiel
Ordnerstruktur des Projekts – WPF DataGrid MVVM Beispiel

💡 Hinweis: Beachte hier, dass wir uns nicht an alles halten, sprich, wir werden keinen „Views“-Ordner anlegen, da hier die Verwendung des DataGrids im Vordergrund steht! Ich möchte Dich hier nicht unnötig damit verwirren, wie man dann das MainWindow inkl. der Application-Datei auch noch umgestalten müsste, etc.

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!

💡 Hinweis: Für das C#-Snippet verwende ich hier die neue Namespace-Variante, also ohne die „{}“ und einen zusätzlichen Einschub (ich liebe dieses Feature). Wenn Du noch mit der alten Syntax unterwegs bist, kannst Du Diese natürlich genauso gut verwenden. Umschließe dazu einfach das „MainViewModel“ – wie üblich – mit dem „namespace“-Schlüsselwort und „{}“ und setze die Klasse dort rein.

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!

💡 Hinweis: Natürlich werden wir hier keine vollständige Rechnung nachbauen, sondern nur eine für das Beispiel komprimierte Klasse/Variante. Beachte auch, dass wir beim VB.NET-Beispiel eckige Klammern brauchen, da „Date“ ein reserviertes Schlüsselwort, bzw. ein „DateTime“-Alias ist.

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:

DataGrid mit automatischen Spalten – WPF DataGrid MVVM Beispiel
DataGrid mit automatischen Spalten – WPF DataGrid MVVM Beispiel

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!

DataGrid mit manuellen Spalten – WPF DataGrid MVVM Beispiel
DataGrid mit manuellen Spalten – WPF DataGrid MVVM Beispiel

Die automatische Generierung der Spalten deaktivierst Du mit der „AutoGenerateColumns“-Eigenschaft des WPF DataGrids. Mit ausgeschalteter Spalten-Generierung sieht es dann final so aus:

Ausgeschaltete Spaltengenerierung
Ausgeschaltete Spaltengenerierung

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

💡 Hinweis: Wir benutzen die aufeinanderfolgenden „Add“-Aufrufe nur, weil es hier mehr oder weniger die Basis ist. In einem realen Projekt sollte man eine bessere Implementierung einer INotifyCollectionChanged-Schnittstelle verwenden, da jeder „Add“-Aufruf hier einmal „Hey, es hat sich was geändert“ schreit. Dies ist natürlich völliger Quatsch! Schaue für eine beispielhafte Implementierung in den nächsten Abschnitt.

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

💡 Hinweis: Lasse nun Dein „MainViewModel“ von dieser Basis-Klasse erben, oder implementiere das „INotifyPropertyChanged“-Interface manuell!

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

💡 Hinweis: Wenn Du eine Standard-Auswahl, bzw. eine Auswahl aus dem Code heraus treffen möchtest, kannst Du der Eigenschaft einfach einen Wert zuweisen.

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

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert