Ein VB NET DataGridView füllen
Inhaltsverzeichnis
Daten in ein VB NET DataGridView füllen
Willkommen zum heutigen Beitrag „VB NET DataGridView füllen„!
Tja da sind wir am heutigen Tage wieder mit einer neuen Problemstellung, willkommen zurück 😛!
Du möchtest also in Visual Basic NET das typische Tabellen-Steuerelement namens DataGridView mit Daten füllen!?
Dann lass uns auch gleich schon direkt loslegen!
Die erste sich stellende Frage an dieser Stelle ist natürlich die goldene Frage Nr. 1.
Frage Dich also zurst: „Woher kommen meine Daten!?“ – Aus einer Datenbank? Aus einer Textdatei!?
Nur zur Darstellung!
Wie eigentlich immer, sind die Steuerelemente natürlich nur zur letztendlichen visuellen Darstellung da.
Sie selbst sollten für gewöhnlich keine Daten „darstellen“ – wenn Du verstehst was ich meine!
Besonders in anderen Sprachen und „Stacks“ wie z. B. WPF wirst Du diese Herangehensweise kennen- und lieben lernen.
Denke an das Beispiel anhand einer simplen TextBox, Welche einen Vornamen verarbeitet:
Wenn Du einen Vornamen in Deinem Programm verarbeitest, dann sollte nicht die TextBox diesen Vornamen widerspiegeln!
Letztendlich wäre es gut, wenn Du eine dahinter geschaltete Eigenschaft hast, Welche dann den Vornamen beinhaltet.
Die TextBox wird lediglich angewiesen, diesen Vornamen visuell darzustellen.
Sie sagt praktisch: „Hey guck mal, das befindet sich im Hintergrund in den Daten“.
Was Sie allerdings nicht sagen sollte ist: „Ich >bin< die Daten„!
So sollte es auch bei fast allen anderen Steuerelementen und im Optimalfall auch dem DataGridView als Tabelle sein.
Der gute alte Weg – Ein VB NET DataGridView füllen
Kommen wir im ersten Beispiel zum „guten alten Weg„, Welcher es eigentlich nicht ist, also gut..
Warum „er“ dennoch als „gut“ bekannt ist? Naja, weil es wie immer die Routine der Menschen ist, Welche etwas als „gut“ betitelt.
Man ist es durchaus als Anfänger gewohnt, dem DataGridView direkte Kommandos via „Items“-Auflistung zu geben.
Dies kommt – zu meinem Bedauern – viel zu häufig vor, leider ist es nicht so toll, wie es einfach erscheint.
Was Objekte gemeinsam haben..
Das funktioniert deshalb so einfach, weil die „Items“-Auflistung vom Typ „DataGridViewRowCollection“ ist.
Und naja was soll ich sagen, daher ist die Auflistung eine Zusammensetzung aus einzelnen „DataGridViewRows„.
Diese einzelnen Reihen sowie die Auflistung als solches haben somit wie jede Objekt-Instanz:
- Eigenschaften
- Methoden (Funktionen, Subs)
- Ereignisse
- …
Schaue Dir also für das erste Beispiel die Methoden der „DataGridViewRowCollection“-Klasse an.
Dort finden wir die verschiedenen „Add„-Methoden, Welche – wie der Name schon sagt – für das Hinzufügen zuständig ist.
DataGridView zusammenbauen
Bevor wir im ersten Beispiel nun Daten einfügen können, müsstest Du zuerst noch ein DataGridView anlegen.
Erstelle also nun im nächsten Schritt das DataGridView, Welches dann gleich mit Daten gefüllt wird.
Nimm gerne dieses hier als Beispiel:
Beachte jedoch, dass ich wie fast immer folgende Eigenschaften gesetzt habe.
Diese benötige ich persönlich fast in 95% der Projekte..
- AllowUserToAddRows – False
- AllowUserToDeleteRows – False
- AllowUserToResizeRows – False
- AlternatingRowsDefaultCellStyle – BackgroundColor auf LightGray
- BackgroundColor – White
- Dock – Fill
- MultiSelect – False
- ReadOnly – True
- RowHeadersVisible – False
- SelectionMode – FullRowSelect
- ..und dann noch die obigen Spalten!
Daten/Reihen hinzufügen
Schauen wir uns nun die verschiedenen Überladungen der „Add“-Methode an.
Ich werde hier natürlich nicht alle durchgehen, da Du Sie ja in der obigen Dokumentation ansehen kannst.
Folgende ist allerdings die meiner Meinung nach für dich – im ersten Beispiel – die Interessanteste Überladung:
Wie Du anhand der Signatur erkennen kannst, möchte diese Überladung ein (param) Array von Objekten.
Durch die spezielle Definition müssen wir auch nicht erst ein Array erstellen und Werte einfügen.
Stattdessen können wir die Werte direkt getrennt durch Kommas auflisten.
Hardcoded
Passend zu unserem designten DataGridView würde das Hinzufügen von Daten also so aussehen:
dgvArticles.Rows.Add(1, "Article 1", "KG", 9.95, 16.49, 50)
Natürlich könnte man das auch in einer Schleife aufrufen.
Obwohl das zumindest in diesem Beispiel nicht den größten Sinn macht, zeige ich es trotzdem mal.
Wir fügen einfach mal 5 Einträge durch eine zählergesteuerte Schleife hinzu.
Dabei benutze ich meine sehr gemochte „String–Interpolation“ (mit dem Dollar-Zeichen).
Damit kann ich die Strings ein wenig einfacher zusammenbauen, als ständig „&…“ zu benutzen.
Als Schleife – Beispiel
For i = 1 To 5 dgvArticles.Rows.Add(i, $"Article {i}", "KG", 9.95, 16.49, 50) Next
Ich meine normalerweise müssten die Einträge vermutlich sowieso von einer Art Datenquelle kommen.
Ob man nun eine Datenbank, eine Textdatei, oder sonstige Dinge verwendet..
Aus einem Dialog heraus
Nun wird es hier schon ein wenig interessanter, da wir nun einmal die Eingaben aus einem Dialog entgegennehmen.
Füge dem Projekt nun eine neue Form hinzu und benenne Sie z. B. als „frmArticle“.
Danach kannst Du Sie analog zu meinem Beispiel designen (lad Dir am besten das Beispiel herunter):
Danach sorgen wir dafür, dass der Dialog auch wirklich angezeigt und dessen Ergebnis verarbeitet wird.
Gehe dazu in den „Form1„-Code und erstelle einen neuen Ereignishandler für den „Add row“-Button:
Private Sub btnAddRow_Click(sender As Object, e As EventArgs) Handles btnAddRow.Click Dim dialog = New frmArticle() Dim result = dialog.ShowDialog(Me) If result <> DialogResult.OK Then Return End If Dim id = Convert.ToInt32(dialog.nudId.Value) Dim name = dialog.tbName.Text.Trim() Dim unit = dialog.cbbUnit.Text Dim purchasingPrice = dialog.nudPurchasingPrice.Value Dim sellingPrice = dialog.nudSellingPrice.Value Dim stock = Convert.ToInt32(dialog.nudStock.Value) dgvArticles.Rows.Add(id, name, unit, purchasingPrice, sellingPrice, stock) End Sub
Zuerst instanziieren wir hier eine neue Dialog-Instanz.
Danach zeigen wir diese Instanz an, indem wir die „ShowDialog„-Methode aufrufen und „Me“ (also die aktuelle Form) als „Papa“ mitgeben.
Den Rückgabewert der Funktion speichern wir in der Variable „result„, um dann zu checken, ob’s „OK“ war.
Falls nicht, verlassen wir den Ereignishandler mit einem Early-Return.
Anschließend ziehen wir uns die einzelnen Daten aus den jeweiligen Steuerelementen.
Auch hier könnte man mit Datenbindung arbeiten und so mit Eigenschaften arbeiten, aber das lassen wir hier erstmal..
Zum Schluss fügen wir die Daten dem DataGridView wie oben bereits gezeigt hinzu.
Gehen wir aber noch einmal zurück zum Artikel-Dialog selbst.
Dort brauchen wir zuerst den Handler des Abbrechen-Buttons:
Private Sub btnCancel_Click(sender As Object, e As EventArgs) Handles btnCancel.Click DialogResult = DialogResult.Cancel End Sub
Der setzt das „DialogResult“ der Form, woraufhin die Form dann mit diesem Ergebnis geschlossen wird.
Danach legen wir den Code für den „Hinzufügen„-Knopf fest:
Private Sub btnAdd_Click(sender As Object, e As EventArgs) Handles btnAdd.Click If Not ValidateInput() Then Return End If DialogResult = DialogResult.OK End Sub
Dort prüfen wir den Rückgabewert der gleich noch folgenden Funktion und „early returnen“ hier wieder, falls Sie „False“ zurückgibt.
Anschließend setzen wir das „DialogResult“ auf „OK„, woraufhin auch hier die Form geschlossen wird.
Zum Schluss kommt dann hier noch die kleine „ValidateInput„-Funktion:
Private Function ValidateInput() As Boolean ' dont care about id.. Dim name = tbName.Text.Trim() If String.IsNullOrWhiteSpace(name) Then MessageBox.Show("Please provide an article name") Return False End If Dim unit = cbbUnit.Text If String.IsNullOrWhiteSpace(unit) Then MessageBox.Show("Please provide an article unit") Return False End If ' prices are handled by the numeric up downs.. ' stock is handled by numeric up down Return True End Function
Die Id wird hier erstmal nicht weiter geprüft, da das „NumericUpDown“ schon notwendige Eigenschaften gesetzt hat.
Somit übernimmt es die Prüfung für „Min-“ und „MaxValue„, sowie den Dezimalstellen.
Das gilt übrigens auch für die anderen „NumericUpDowns“ auf der Form.
Die einzigen beiden zu prüfenden Werte wären der Name und die Einheit.
Dort checken wir einfach, dass die Werte nicht „leer“ sind.
Wenn alles okay war, kommen wir ans Ende der Funktion und geben „True“ zurück.
Add(x, y, z) sieht blöd aus, oder!?
Ich meine man muss kein Sherlock Holmes sein, um zu sehen, dass das irgendwie blöd aussieht:
dgvArticles.Rows.Add(1, "Article 1", "KG", 9.95, 16.49, 50)
Denkst Du z. B., dass Du auch noch in einem Jahr weißt, was die 50 bedeutet?
Ist es vielleicht die Kilogramm-Anzahl, oder doch eine Mindestbestellmenge?
Was passiert zum Beispiel, wenn sich die Spalten-Reihenfolge ändert?
Möchtest Du dann viele einzelne Stellen im Code anpassen – ne, oder!?
Es muss natürlich auch nicht immer sein, dass Du der einzige Entwickler im Bunde bist.
Denkst Du, dass dies besonders leserlich und einfach zu verstehen für andere Entwickler ist?
Zusammengefasst würde ich mal behaupten, dass es folgendes nicht ist:
- „sauber„
- leserlich
- einfach zu verstehen
- gut zu behalten
Ja aber..
Klar, jetzt wirst Du mir ggf. mit der Ausrede kommen:
„Ja, aber ein Objekt-Konstruktor kann doch auch lang sein und durch Kommas getrennt sein“.
Da hast Du natürlich recht, weshalb das natürlich auch ebenso der Grund ist, weshalb man „lange“ Konstruktoren vermeiden sollte.
Die schmerzende Länge definiert hier mit Sicherheit jedes Entwickler-Team in dessen Konventionen selbst.
Aber zum Glück gibt es genug Beiträge und Bücher darüber, weshalb die relativ einfache Antwort: „Zwischen 2-3 Parametern“ sein kann.
Besonders bei einem Model, oder einer Entität namens „Artikel“ muss man aber wirklich von vielen Eigenschaften ausgehen.
Daher würde ich hier allgemein nicht großartig anfangen einen Konstruktor aufzublähen..
Der modernere Weg – Das VB NET DataGridView füllen
Kommen wir im nächsten Szenario zu einer etwas moderneren Herangehensweise.
Ich würde diese Weise auch durchaus als wesentlich sauberer bezeichnen, warum – siehst Du gleich.
Du kannst auch hierfür die grafische Oberfläche aus dem Beispiel von oben verwenden.
Ggf. kopierst Du das Projekt einmal, um beide Varianten separat vorliegen zu haben.
Im nächsten Beispiel werden wir nun eine datengebundene Variante verwenden.
Das verbessert neben dem oben angesprochenen „Spalten-Reihenfolgen-Problem“ auch noch andere Aspekte.
Sauber(er)es Feeling
Insgesamt fühlt es sich – jedenfalls für mich und viele andere Entwickler – einfach sauberer an.
Das mag vom Empfinden her vermutlich daran liegen, dass man eine klare Trennung zwischen Daten und Oberfläche hat.
Im vorherigen Beispiel haben wir stattdessen die GUI sozusagen mit den Daten vermischt – ihh!
Auch wenn das „DataGridView“ viele Methoden zur Befüllung mit Daten unterstützt, ist Diese die von mir am meisten Genutzte.
Lass uns also im nächsten Schritt die Vorbereitungen dazu treffen!
Klasse erstellen
Yay, endlich ist es soweit und wir können zu dem Punkt kommen, der die Objektorientierung wohl am meisten prägt.
Ich spreche dabei selbstverständlich vom Thema Klassen, Welche die Baupläne von Objekten darstellen.
Erst wenn man diese Pläne „umsetzt„, entstehen daraus konkrete Objekte, mit denen man dann arbeiten kann.
Diese Objekte kann man – wie Du gleich sehen wirst – auch für unser „DataGridView“ verwenden.
In Kombination brauchen wir natürlich noch irgendetwas, was diese Instanzen/Objekte der Klassen dann auflisten kann.
Eine Liste muss her
Wenn man so an Auflistungen denkt, kommen einem vermutlich zuerst Dinge wie das ganz rudimentäre Array in den Sinn.
Als nächstes macht man sich ggf. Gedanken über Listen, bzw. deren generische Variante.
Leider reichen die sehr (auch meinerseits) beliebten Listen nicht aus, da ein entscheidender Faktor fehlt.
Häufig arbeiten wir in solchen Szenarien mit Aktionen, Welche dann Einträge hinzufügen, oder eben auch löschen.
Dabei ist es natürlich von Relevanz, dass die Liste z. B. eine Entfernung eines Elements auch „nach außen kommuniziert„.
Das sollte Sie deshalb im Optimalfall tun, damit andere (vor allem grafische) Elemente, eine Chance haben zu reagieren.
Im konkreten Fall unserer kleinen tabellarischen Darstellung, könnte die Tabelle somit neu gezeichnet werden.
Das geht nur eben schlecht, wenn unsere Tabelle (also das DataGridView) davon gar nichts mitbekommt.
Die Alternative zur List Of
Wie bereits erwähnt benötigen wir eine andere Art Liste, um unsere Arbeit zu erledigen.
Wenn wir ein wenig im NET Framework graben, kommen wir auf die sogenannte „BindingList„.
Diese ist wie der Name schon suggeriert, für die Bindung von Daten geeignet.
Durch Ihre generische Art, können wir die „BindingList“ für alle möglichen Arten von Objekten verwenden.
Eine Artikel-Klasse – unseren Bauplan erstellen
Zunächst gehen wir hin und erstellen im nächsten Schritt den Bauplan für unsere Objekte, also eine Klasse.
Analog zum oben bereits veranschaulichten Beispiel, könnte die Artikel–Klasse so aussehen:
Public Class Article Public Property Id As Integer Public Property Name As String Public Property Unit As String Public Property PurchasingPrice As Decimal Public Property SellingPrice As Decimal Public Property Stock As Integer Sub New() End Sub End Class
Danach müssen wir die definierte Klasse natürlich noch in Kombination mit der „BindingList“ verwenden.
Eine Eigenschaft für die Auflistung/Liste
Gehe also im nächsten Schritt in die Form-Klasse und lege dort eine neue Eigenschaft namens „Articles“ an:
Public Property Articles As BindingList(Of Article)
Anschließend instanziieren wir diese Eigenschaft im Konstruktor der Form.
Ansonsten würden wir natürlich eine Nullverweisausnahme bekommen, sprich ein Objekt verwenden, Welches (noch) „nicht existiert“.
Sub New() InitializeComponent() Articles = New BindingList(Of Article) End Sub
Danach können wir im Load-Ereignishandler dem „DataGridView“ die Datenquelle zuordnen.
Anschließend laden wir mit der gleich folgenden Methode ein paar Testdaten.
Diese würde im Normalfall eventuell aus einer Datenbank, Textdatei, oder woher auch immer kommen.
Für unser Beispiel lade ich die Daten allerdings bewusst aus einer Methode heraus..
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load dgvArticles.DataSource = Articles LoadArticleTestData() End Sub Private Sub LoadArticleTestData() Articles.Add(New Article() With { .Id = 1, .Name = "Article 1", .Unit = "KG", .PurchasingPrice = 9.95, .SellingPrice = 12.99, .Stock = 50 }) Articles.Add(New Article() With { .Id = 2, .Name = "Article 2", .Unit = "l", .PurchasingPrice = 13.95, .SellingPrice = 19.99, .Stock = 0 }) Articles.Add(New Article() With { .Id = 3, .Name = "Article 3", .Unit = "ml", .PurchasingPrice = 3.49, .SellingPrice = 6.99, .Stock = 20 }) End Sub
Beachte bitte, dass die Liste für jeden „Add-Vorgang“ ein getrenntes Ereignis auslöst.
Die Liste sagt also sowas wie „Hey, hier ist was hinzugekommen„.
Wenn Du natürlich nun Daten aus einer Datenbank lädst (30, 300, oder gar 30000 Einträge), spielt dieser Aspekt eine Rolle.
Für mich persönlich ist diese Rolle sogar ganz und gar nicht zu unterschätzen.
Wenn die Auflistung 3000 mal das „ItemAdded“ (oder ähnlich) auslöst, ist das für mich eher weniger effizient.
Stattdessen wäre es ja sinnvoll, wenn die Liste ein einziges Mal nach Abschluss des Ladevorgangs sagt:
„So liebe zuhörenden Steuerelemente, bitte nun neu zeichnen, da ich mich verändert habe“.
Leider besitzt die „BindingList“ von Haus aus keine „AddRange„-Methode.
Zum Glück können wir diese aber relativ einfach und zügig mit einer Extension nacharbeiten.
BindingList AddRange-Extension
Lege hierzu einen neuen Ordner im Projekt an, den Du dann z. B. „Utils“ nennen kannst.
Erstelle darin ein Modul namens „BindingListExtensions“ und füge folgenden Code ein:
Imports System.ComponentModel Imports System.Runtime.CompilerServices Namespace Utils Module BindingListExtensions <Extension()> Public Sub AddRange(Of T)(bindingList As BindingList(Of T), collection As IEnumerable(Of T)) Dim oldRaiseEvents = bindingList.RaiseListChangedEvents bindingList.RaiseListChangedEvents = False Try For Each item In collection bindingList.Add(item) Next Finally bindingList.RaiseListChangedEvents = oldRaiseEvents If bindingList.RaiseListChangedEvents Then bindingList.ResetBindings() End If End Try End Sub End Module End Namespace
Die „BindingList„-Klasse wird hiermit dann um die „AddRange„-Methode erweitert.
In der Methode selbst merken wir uns oben den aktuellen Wert der „RaiseListChangedEvents„-Eigenschaft.
Diese bestimmt, ob für gewisse Vorgänge dann ein „Ich habe mich verändert„-Ereignis ausgelöst werden soll.
Anschließend deaktivieren wir diesen Benachrichtigungs-Mechanismus, damit Dieser pausiert.
Danach fügen wir die einzelnen Elemente in die Auflistung ein, wobei man sich theoretisch das Try-Catch sparen könnte.
Eigentlich kann in diesem Fall nicht viel falsch gehen..
Nachdem wir alle Elemente hinzugefügt haben, setzen wir die „RaiseListChangedEvents„-Eigenschaft wieder auf den vorherigen Wert.
Danach setzen wir die Datenbindungen mit Hilfe der „ResetBindings„-Methode zurück.
Elemente/Artikel mit AddRange hinzufügen
Final konnten wir so dann das zigfache Auslösen der Ereignisse unterbinden und ein effizienteres Hinzufügen umsetzen.
Schauen wir uns nun einmal an, wie ein beispielhafter Aufruf der Methode aussehen könnte.
Wir haben also nach wie vor unsere instanziierte „Articles„-Eigenschaft.
Ebenso rufen im Load-Ereignishandler immer noch die „LoadArticleTestData„-Methode auf.
Diese Methode könnte dann so aussehen:
Private Sub LoadArticleTestData() Dim myArticles = New List(Of Article) myArticles.Add(New Article() With { .Id = 1, .Name = "Article 1", .Unit = "KG", .PurchasingPrice = 9.95, .SellingPrice = 12.99, .Stock = 50 }) myArticles.Add(New Article() With { .Id = 2, .Name = "Article 2", .Unit = "l", .PurchasingPrice = 13.95, .SellingPrice = 19.99, .Stock = 0 }) myArticles.Add(New Article() With { .Id = 3, .Name = "Article 3", .Unit = "ml", .PurchasingPrice = 3.49, .SellingPrice = 6.99, .Stock = 20 }) Articles.AddRange(myArticles) End Sub
Wir erstellen hier also erstmal eine lokale Liste für die Sub, Welche eine normale Liste darstellt.
Danach fügen wir die einzelnen Artikel hinzu und verwenden zum Schluss die definierte „AddRange„-Methode.
Vergiss hier nicht die notwendigen Imports oberhalb der Form hinzuzufügen:
Imports System.ComponentModel Imports VBDataGridViewFillExample.Utils Public Class Form1 ' ... End Class