DBExtensions - allgemeine Lösung der Daten-Persistierung via Datenbanken

    • VB.NET

    Es gibt 7 Antworten in diesem Thema. Der letzte Beitrag () ist von DMO.

      DBExtensions - allgemeine Lösung der Daten-Persistierung via Datenbanken

      Download Sources:
      Ich habe ich mir folgendes ausgedacht zur Update-Benachrichtigung: Ich habe den Download-Link im SourceCode-Austausch eingestellt, und dort hänge ich dann immer einen Post an, wenn ich ein Update verzapft habe.
      Wer also an einer Update-Benachrichtigung interessiert ist, kann den Thread dort abonnieren, und erhält dadurch eine Email, wenns ein neues Update gibt.
      Und dieser Thread hier bleibt erhalten fürs Tutorial und Fragen dazu, und wird nicht mit Update-Posts verunziert - die sich mit der Zeit ja u.U. ziemlich ansammeln können ;)

      Tutorial:
      Es geht um ein paar Extension-Methods, dies Dataset um Befüllung und Rückspeicherung erweitern - wahlweise in eine Datenbank, oder auch direkt auf Platte.
      Design-Strategie ist, einfach ans Dataset eine Connection-Property dranzumachen, und damit sind eigentlich schon alle für den Austausch mit der DB notwendigen Informationen beisammen, und das Gewurstel mit DBConnection, DBCommands, DataAdaptern, DataReadern, DbParametern ist Vergangenheit - ja es muß nicht einmal mehr Sql geschrieben werden.
      Sql schreiben ist natürlich weiterhin möglich, aber nur für sehr wenige Spezialfälle noch sinnvoll. Jedenfalls beim Befüllen des Datasets wäre es gradezu dumm, auf die Unterstützung der DBExtensions zu verzichten, wennman die Dlls eingebunden hat.
      Die Extensions heißen folgerichtig Fill() und Save() - wobei Fill() gibts in Varianten, die einerseits häufige Standardfälle komfortabel unterstützen, andererseits auch die gesamte Funktions-Mächtigkeit von Sql offenhalten:
      • Dataset.Fill(ParamArray Tables)
        löscht das gesamte Dataset und führt ein Reload durch. Über die optionalen Tables kann man bewirken, dass nicht alle Tabellen befüllt werden, sondern nur die angegebenen.
      • DataTable.Fill(conditions As String, ParamArray values() As Object)
        Reload einer einzelnen Tabelle. (Aus datenbänkerischer Logik folgt, dass die untergeordneten Tabellen mit-gelöscht werden.)
        Mittels conditions kann eine "halbe" Sql-Query angegeben werden. Nämlich alles mögliche nach dem SELECT-Abschnitt. Man kann dort WHERE - Klauseln angeben, aber auch INNER-JOINs formulieren - mw. auch ORDER-/GROUP BY - habichnix dagegen ;)
        Nur den SELECT-Abschnitt generiert die DataTable selber, denn ihre Spaltenstruktur gibt ja genauestens vor, welche DB-Spalten selektiert werden müssen.
        conditions kann auch Parameter spezifizieren - Platzhalter-Zeichen hierfür ist '?'. In dem Falle sind die zu übertragenden Parameter-Werte über das values - Argument angegeben. Also bei Bedarf unterstützt DBExtensions den vollen Sql-Leistungsumfang, der auf Datasets anwendbar ist, und zwar auf ordentliche Weise: nämlich unter Verwendung passender DBParameter.
      • parentRow.FillChildTables(dataTables(), conditions, values())
        Eine ParentRow kann nun ihre untergeordneten DataTables befüllen. Angenommen eine KundeRow - die kann nun die BestellungTable füllen mit allen BestellungDatensätzen, die auf diesen Kunden verweisen.
        Es können sogar unter-untergeordnete DataTables mit-befüllt wern, etwa die BestellDetails-DataTable kann zusätzlich zur BestellTable angegeben sein, und würde dann mit allen BestellDetails aller Bestellungen dieses Kunden befüllt.
        Über conditions kann man weitere Bedingungen geltend machen, etwa einen WHERE - Abschnitt, der einen bestimmten Zeitbereich selektiert oder sowas.
      • Dataset.Save(frm As Form) speichert dann alle Änderungen in die DB, wobei vorher das Form auch noch validiert wird - dass nicht ungültige Eingaben übernommen werden, oder gültige Eingaben vergessen.

      Die Arbeit macht ein Objekt namens DatasetAdapter - das muß man erstellen und dem Dataset mitteilen - hiermal ein Beispiel für SqlCe:

      VB.NET-Quellcode

      1. Dim adp = New DatasetAdapter( _
      2. SqlCeProviderFactory.Instance, My.Settings.BestellungenConnectionString, _
      3. ConflictOption.OverwriteChanges)
      4. BestellungenDts.Adapter(adp) 'DatasetAdapter "mitteilen"
      SqlCeProviderFactory ist vielen sicher unbekannt, aber ich kann versichern: Jedes .Net-fähige Datenbanksystem stellt neben Connections, Commands und Zeugs auch eine ProviderFactory bereit, mit der man diese Sachen auch automatisch sich fabrizieren lassen kann ;).
      Dazu muß man natürlich noch einen ConnectionString angeben, aber das hat man ja immer gemußt.
      Und man muß entscheiden, wie mit DBConcurrency-Konflikten umgegangen werden soll, also wenn in einer MultiUserApp 2 Anwender denselben Datensatz bearbeitet haben - wessen Version soll dann letztendlich in die Datenbank?
      Mit diesen 3 Angaben weiß das Dataset alles nötige, um sich selbst befüllen und abspeichern zu können, und nun: "tschüss, Sql!" - wir proggen wieder objektorientiert und mit Compiler-Unterstützung. ;)
      Und: "tschüss, DBCommand, DataAdapter, DataReader, DbParameter!" - wir kümmern uns wieder mehr um die eigentliche Programmlogik, und weniger ums Infrastruktur-Theater.
      Und auch: "tschüss, TableAdapter und TableAdapterManager!" - ihr werdet ab sofort gleich wieder rausgeschmissen, sobald der Dataset-Designer ein typisiertes Dataset aus der Datenbank generiert hat (siehe dazu "Datenbank in 10 Minuten" auf Movie-Tuts).
      Schon um diese bekloppten Komponenten einzusparen lohnt sich das Einbinden von DBExtensions, denn von 10000 Zeilen generierten Codes eines (eher kleinen) typisierten Datasets gehen 5000 aufs Konto dieses Adapter-Brimboriums.

      Unterstützte Datenbanksysteme: Access-OleDB, SqlCe, SqlServer und MySql, SqLite.

      Dahinter steht ein ganzes Monstrum an Code, am Ende sind insgesamt ca. 20 Klassen zusammengekommen, verstreut über 3 Dll-Projekte.
      Die 3 Dlls beackern verschiedene Thematiken:
      • "GeneralHelpers": enthält lauter listigen Kleinkram, der ganz allgemein und in vielen Projekten nützlich sein kann, nicht nur Datenbank-Anwendungen. "GeneralHelpers" hat nichtmal WinForms eingebunden, also könnteman auch in Wpf-Anwendungen verwenden, ohne sich unnützen Ballast ans Bein zu binden.
      • "WinFormHelpers": beschäftigt sich mit Forms und BindingSources - grad BindingSources sind ohne Extension-Methods ziemlich unangenehm zu becoden, findich.
        "WinFormHelpers" löst auch das Problem redundanter Datasets, wenn man mehrere Forms hat (oder mit UserControls arbeitet).
        Und es bietet auch 3 Extension an, wenn mans Dataset einfach als Xml auf Platte schreiben will:
        1. Dataset.DataFile(path) -setzt, wohin die Xml-Datei zu speichern ist.
        2. + 3.: Dataset.Fill() und Dataset.Save() - grad so, als hätte man einen DatasetAdapter zu Diensten ;)
        Also man kann DatenbankProgrammierung auch ganz ohne Datenbanken betreiben - ein enormer Vorteil bei der Entwicklung, und was Portabilität angeht.
      • "DBExtensions": implementiert eingangs vorgestellte Funktionalität

      Also wer sich sicher ist, dasser nie was anneres proggen wird als WinForms, der kann auch alle Klassen von "GeneralHelpers" in die WinFormHelpers schieben, und hat dann eine bischen einfachere Struktur.
      Und wer sich sicher ist, dasser immer nur DB-Anwendungen schreibt, der kann alles gleich in die DBExtensions packen, dann isses nurnoch eine Dll.
      Und wer überhaupt nie in den Code hineingucken will, kanns auch kompilieren, und die Kompilate direkt verwenden - dann kanner nurnix mehr dran rumschrauben.
      Aber ich hab mit so Helper-Dll-Projekten gute Erfahrungen gemacht: Dahinein kann man wiederverwertbaren Code packen, der gelegentlich anfällt, und dann muß man nicht in jedem neuen Projekt das "Rad neu erfinden" ;)

      Ah - die die Sample-Solution!
      Es sind inzw. 5 Sample-Solutions:
      • "Northwind": eine "große" Access-DB (MS' Northwind-Beispiel-DB)
      • "SqlCeSamples": ist eine doppelte Anwendung, mit sageUndSchreibe 11 Datensätzen ;). Wenn man als StartFormular frmIncrementalFill setzt, wird je nach angewählten Kunden nur dessen Bestellungen von der DB abgerufen. Im Ausgabefenster wird das generierte Sql protokolliert.
        Wenn man hingegen mit frmTwoFormMain startet, so sieht man 2 Forms, die dasselbe Dataset präsentieren, und kann sich über die Funktion Formübergreifenden Databindings freuen ;)
      • "DatasetOnly": Ich habe die Northwind-DB in ein Xml-File geschrieben, und dann das Northwind-Projekt ohne Access und ühaupt ohne die DBExtensions-Dll implementiert. Ich kann keinen Unterschied bei der Lade-Geschwindigkeit feststellen, aber der Code ist noch einfacher, und das Konzept bietet halt die genannten Vorteile, was Entwicklung und Portabilität angeht. Etwa Änderungen am Datenmodell sind im Dataset-Designer einfacher umgesetzt, da man nicht zusätzlich die dahinterliegende Datenbank überarbeiten muss.
      • "SqlServerTest": fast dieselbe App wie "Northwind", nur DBProvider ist halt SqlServer
      • "MySqlTester": diese App ist nicht lauffähig. Ich habe sie anhand einer im Internet liegenden MySql-DB entwickelt, aber im Download das Passwort entfernt.

      Wie vlt. schon angeklungen: Zur Einschätzung des Nutzens dieses Frameworks betrachte man vor allem den einfachen Code der Sample-Projekte. Auch die bereits vorgefertigte "Abspeichern?"-Abfrage, wenn bei vorliegenden Änderungen das MainForm geschlossen wird. Die Abfrage kann mit einer Codezeile eingebunden werden:

      VB.NET-Quellcode

      1. AddHandler Me.FormClosing, BestellungenDts.HandleFormClosing

      Hier mal der gesamte(!) Code des Mainforms der SqlCe-Lösung mit den 2 Forms:

      VB.NET-Quellcode

      1. Imports System.Data.Common
      2. Imports System.Data.SqlServerCe
      3. Public Class frmMainM_N
      4. Private Sub Form_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
      5. Dim adp = New DatasetAdapter( _
      6. SqlCeProviderFactory.Instance, My.Settings.BestellungenConnectionString, _
      7. ConflictOption.OverwriteChanges)
      8. Me.AlignOnTop.BestellungenDts.Register(Me).Adapter(adp).Fill()
      9. AddHandler Me.FormClosing, BestellungenDts.HandleFormClosing
      10. End Sub
      11. Private Sub MenuItem_Click(ByVal sender As Object, ByVal e As EventArgs) _
      12. Handles ReloadToolStripMenuItem.Click, SaveToolStripMenuItem.Click
      13. Select Case True
      14. Case sender Is ReloadToolStripMenuItem
      15. BestellungenDts.Fill()
      16. Case sender Is SaveToolStripMenuItem
      17. BestellungenDts.Save(Me)
      18. End Select
      19. End Sub
      20. End Class

      erklärungsbedürftig nur die eine zeile im Flow-Code-Design:

      VB.NET-Quellcode

      1. Me.AlignOnTop.BestellungenDts.Register(Me).Adapter(adp).Fill()

      Flow-Design ist kompakt, und dabei ebensogut oder besser verständlich wie untereinandergeschriebener Code - man gehe einfach alle Methoden von links nach rechts durch:
      1. .AlignOnTop: dockt das Form am oberen Bildschirmrand, dabei die ganze Bildschirmbreite nutzend. Ist oft fürs Debuggen nett, weil da kann man unten das Ausgabefenster sehen (hier aber grad nicht erforderlich).
      2. .BestellungenDts.Register(Me): löst das RedundanzProblem, wenn mehrere Forms dieselben Daten präsentieren sollen: Das Form wird mit Reflection regelrecht abgegrast, und alle gefundenen Datasetse werden durch ein einziges ausgetauscht, und alle BindingSources werden auf dieses einzige umgestöpselt.
      3. .Adapter(adp): teilt dem Dataset seinen DatasetAdapter mit
      4. .Fill(): Dataset komplett befüllen.


      Ich werde die anneren Forms auch noch besprechen, den Code der Dlls aber nicht - das würde zu umfangreich.
      Aber ich freue mich über Nachfragen jeder Art, also was die Benutzung der Dlls angeht, aber auch ihre Funktionsweise im allgemeinen, oder auch von CodeFragmenten.

      Eiglich ists eher eine Prä-Beta-Release, also die Dlls sind überwiegend unkommentiert, und mehr Tests als die gegebenen Samples habich noch garnet gemacht. Ich hoffe nämlich auch auf eure Mitarbeit, also einerseits natürlich BugReports, um die Qualität zu verbessern, aber auch Nachfragen, sodasses eine Art "lebendiges Tutorial" sein könnte.

      Der Download heißt sinnigerweise "AllTogether", denn ich habe alle Samples in eine Solution gestopft. Um ein anderes Sample auszuprobieren muß man es also als Startprojekt festlegen (und bei "SqlCeSamples" hat man noch die 2 Optionen bezüglich des StartFormulars).
      Aber innerhalb der einzelnen Sample-Ordner sind auch spezifische Solution-Dateien, über die man jedes Sample einzeln öffnen kann. So sieht man deutlich, wie die datenbank-basierten Samples alle 3 Dlls einbinden, während "DatasetOnly" mit "nur" 2 Helper-Dlls auskommt.

      Bisherige History:
      • 1.10.11: first posted
      • 7.11.11:
        1. Sql-Generierung sehr optimiert
        2. Code-Design geändert, von DataTable.FillByParentrow(parentRow) auf parentRow.FillChildTables(tables())
        3. Unterstützung für SqlServer zugefügt

      • 8.11.11: Bugfix der DataTable.Fill(conditions, values()) - Extension-Methode
      • 12.11.11: Unterstützung auch von Sql-Funktionen, wie SUM(), BETWEEN, IN() und sowas

      Dieser Beitrag wurde bereits 21 mal editiert, zuletzt von „ErfinderDesRades“ ()

      Hier beide Forms der TwoForm-SqlCe - Lösung:

      Das MainForm zeigt einfach alle Tabellen komplett an, während das ChildForm alle Kunden zeigt, und die Bestellungen nur dieses Kunden. Darüberhinaus zeigt es aber ebenso alle Bearbeiter an - nämlich in der Combobox.
      Dort wählt man also einen Bearbeiter, und es wird ein zusätzlicher Filter gesetzt. Die Bestellungen werden also doppelt gefiltert:
      Zum einen, über die DataRelation, wird Bestellungen nach Kunde gefiltert - das ist bereits durch geeignetes Setzen der BindingSource-DataSource eingerichtet.
      Zum andern, über das Setzen des Filters, wird Bestellungen nach Bearbeiter gefiltert, und das muß bei Änderungen der Bearbeiter-Auswahl codeseitig nachgeführt werden:

      VB.NET-Quellcode

      1. Imports SqlCeSmallSamples.BestellungenDts
      2. '...
      3. Private Sub BearbeiterBindingSource_CurrentChanged(ByVal sender As Object, ByVal e As EventArgs) Handles BearbeiterBindingSource.CurrentChanged
      4. Dim rwBearb = BearbeiterBindingSource.At(Of BearbeiterRow)()
      5. If rwBearb.Null Then Return
      6. scrKundeBestellung.Filter = "BearbeiterID=" & rwBearb.ID
      7. lbFilter.Text = scrKundeBestellung.Filter
      8. End Sub
      Es wird also die aktuelle BearbeiterRow geholt und ein FilterString gebildet und zugewiesen - und hier wird er zusätzlich im Label dokumentiert - (das ist aber eher ein Debug-Feature denn ein User-Feature ;))

      Hier frmIncrementalFill derselben Solution:

      Die Oberfläche ist identisch mit der des o.g. ChildForms, nur die Bestelldaten werden OnTheFly abgerufen, bereits gefiltert auf Kunde und Bearbeiter. Also statt einen Filter zu setzen, wird hier jedesmal mit rwParent.FillChildTable(childTable, condition, value) nur eine kleine Datenmenge abgerufen (Beachte die Debug-Ausgabe im Bildle):

      VB.NET-Quellcode

      1. Imports SqlCeSmallSamples.BestellungenDts
      2. '...
      3. Private Sub BindingSource_CurrentChanged(ByVal sender As Object, ByVal e As EventArgs) _
      4. Handles KundeBindingSource.CurrentChanged, BearbeiterBindingSource.CurrentChanged
      5. 'Datenabruf in Abhängigkeit des gewählten Kundes und Bearbeiters
      6. Dim rwKunde = KundeBindingSource.At(Of KundeRow)()
      7. Dim rwBearb = BearbeiterBindingSource.At(Of BearbeiterRow)()
      8. If rwBearb.Null OrElse rwKunde.Null Then Return
      9. rwKunde.FillChildTable( _
      10. BestellungenDts.Bestellung, "WHERE BearbeiterID=?", rwBearb.ID)
      11. lbSqlFromWhere.Text = "WHERE BearbeiterID=?".Replace("?", rwBearb.ID)
      12. End Sub

      Is klar, oder? Es werden der aktuelle Kunde und Bearbeiter geholt - jeweils aus der entsprechenden BindingSource, und wenn beide gültige Werte haben, kann rwKunde.FillChildTable(Bestellung) abgefahren werden, mit zusätzlich einem WHERE-Abschnitt als condition, und der ID, auf die gefiltert werden soll.
      (Und fürs Label mache ich genau das, was man in Sql niemals machen soll: Ich frickel den Parameter-Wert direkt in den String ein - Label kann halt kein Sql ;) )

      Das ist natürlich Quatsch, für 11 Datensätze eine incrementelle Befüllung zu implementieren. Ist überhaupt Quatsch, dafür eine DB zu hinterlegen - sowas macht man DatasetOnly.

      Die nächste Solution - Northwind - ist immer noch Quatsch: Auch die Daten der 1MB große Northwind.mdb können (und sollten auch) komplett geladen werden - auch hierfür würde DatasetOnly voll ausreichen:

      Aber ich will parentRow.FillChildTable() ja auch bei mehrstufigem ParentChildView zeigen:

      VB.NET-Quellcode

      1. Imports NorthWind.NorthWindDts
      2. '...
      3. Private Sub KundenBindingSource_CurrentChanged(ByVal sender As Object, ByVal e As EventArgs) _
      4. Handles KundenBindingSource.CurrentChanged
      5. Dim rwKunde = KundenBindingSource.At(Of KundenRow)()
      6. If rwKunde.Null Then Return
      7. With NorthWindDts
      8. rwKunde.FillChildTables(.Bestellungen, .Bestelldetails)
      9. End With
      10. End Sub
      Auch klar, oder? die parentRow rwKunde befüllt zunächst die Bestellungen mit untergeordneten Datensätzen (also deren .KundeID mit rwKunde.ID übereinstimmt), und dann auch die BestellDetails all dieser Bestellungen (also die unter-untergeordneten Datensätze).
      Der eine Befehl fährt also folgende 2 Sql-Queries ab:

      SQL-Abfrage

      1. SELECT [Bestellungen].* FROM [Bestellungen] WHERE [Bestellungen].[KundeID]=?
      2. SELECT [Bestelldetails].* FROM ([Bestellungen] INNER JOIN [Bestelldetails]
      3. ON ([Bestellungen].[KundeID]=? AND [Bestellungen].[BestellID]=[Bestelldetails].[BestellID]))


      In DatasetOnly gibts diese Fill-Methoden garnet, denn da alles geladen ist, brauch nix zwischendrin noch angefordert zu werden. Aber es gibt ja noch mehr Logik, die ich im Northwind-Sample unterschlagen habe - hier also der komplette DatasetOnly-Code:

      VB.NET-Quellcode

      1. Imports DatasetOnly.NorthWindDts
      2. Public Class frmMain
      3. Private _TimeRanges As Integer() = New Integer() {99999, 1, 3, 6, 9, 12}
      4. Private Sub frmNorthWind_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
      5. cmbOrderRange.DataSource = _TimeRanges
      6. Me.NorthWindDts.DataFile("..\..\NorthWind.xml").LoadXml()
      7. AddHandler Me.FormClosing, NorthWindDts.HandleFormClosing
      8. End Sub
      9. Private Sub MenuItem_Click(ByVal sender As Object, ByVal e As EventArgs) _
      10. Handles ReloadToolStripMenuItem.Click, SaveToolStripMenuItem.Click, TestToolStripMenuItem.Click
      11. Select Case True
      12. Case sender Is ReloadToolStripMenuItem : NorthWindDts.LoadXml()
      13. Case sender Is SaveToolStripMenuItem : NorthWindDts.SaveXml(Me)
      14. End Select
      15. End Sub
      16. Private Sub SetOrderFilter()
      17. If cmbOrderRange.SelectedIndex < 0 Then Return
      18. Dim n = _TimeRanges(cmbOrderRange.SelectedIndex)
      19. Dim filter = ""
      20. Try
      21. Dim rwBest = srcKundeBestellung.At(Of BestellungenRow)(0)
      22. If rwBest.Null Then Return
      23. Dim dt = rwBest.Bestelldatum
      24. If Date.MinValue.AddMonths(n) > dt Then
      25. Exit Sub 'die nachfolgende Rechnung ergäbe einen Wert < Minvalue - Fehler
      26. End If
      27. filter = "Bestelldatum > ".And("'", dt.AddMonths(-n).ToShortDateString, "'")
      28. Finally
      29. srcKundeBestellung.Filter = filter
      30. lbFilter.Text = filter
      31. End Try
      32. End Sub
      33. Private Sub Something_Changed(ByVal sender As Object, ByVal e As EventArgs) _
      34. Handles cmbOrderRange.SelectedIndexChanged, KundenBindingSource.PositionChanged
      35. SetOrderFilter()
      36. End Sub
      37. End Class

      die Logik fängt mit den _TimeRanges an - ich will nämlich per Combobox einen Filter setzen (wie bei der SqlCe-Lösung), und zwar soll wählbar sein: Alle Bestellungen des Kunden, die des letzten Monats, der letzten 3, 6, 9, 12 Monate.
      Dieser Filter muß immer gesetzt werden, wenn der Kunde gewechselt wird, und türlich auch, wenn die Combo (implementiert ist das in Something_Changed())

      Das Setzen des Filters erwies sich als überraschend kompliziert, aber wie man sieht, habichs geschafft, und bei Interesse beantworte ich Fragen auch dazu.

      Dann eben noch die Dataset-Initialisierung im Flow-Design:

      VB.NET-Quellcode

      1. Me.NorthWindDts.DataFile("..\..\NorthWind.xml").LoadXml()
      Setzung eines DataFiles und dann Laden der Daten.
      Das Registrieren des Datasets auf die einzig erlaubte Instanz erübrigt sich, da's ja nur ein Form gibt.

      Dieser Beitrag wurde bereits 7 mal editiert, zuletzt von „ErfinderDesRades“ ()

      die vier Views in WinForms

      Jetzt nehme ich hier endlich mal die vier Views auch für WinForms durch - am Beispiel der DatasetOnly-Sample-Solution.
      Diese Solution befindet sich im DatasetOnly-Ordner, und bindet weniger Dlls ein - ist also von den Samples die portableste.
      Ich zitiere nochmal aus meim anneren 4-View-Tut:
      Ein wesentliches Konzept der Datenbänkerei ist, was ich "die vier Views" nenne: vier verschiedene Konzepte, wie sich komplexe Daten präsentieren lassen.
      1. Detail-View: Gegeben ist eine Liste von DatenObjekten, deren jedes mehrere Properties hat. In einem ListenControl präsentiert man diese Datenobjekte nun bewusst unvollständig, häufig zeigt man nur eine einzige (signifikante) Property an - etwa den Namen.
        In der Liste wählt man nun ein Objekt aus, und in beigeordneten EinzelControls werden alle weiteren Properties des aktuell angewählten Objekts präsentiert.
        Ein DetailView ist angesagt, wenn Datenobjekte viele Properties beinhalten, oder wenn einzelne Properties (etwa Bilder, oder langer Text) zuviel Platz beanspruchen würden, um in eine Tabellen-Zeile zu passen.

      2. Parent-Child-View: Gegeben sind DatenObjekte, deren jedes weitere DatenObjekte enthalten. Links werden zB. die übergeordneten Objekte angezeigt, und rechts eine Liste oder Tabelle der untergeordneten Objekte.
        Ein Beispiel, was jeder kennt - der DateiBrowser: links wählt man einen Ordner, und rechts finden sich alle Dateien, die in diesem Ordner drin sind.

      3. Joining-View: In einer großen Tabelle werden alle untergeordneten DatenObjekte angezeigt, aber bestimmte Spalten bestehen aus ComboBoxen, die Werte aus übergeordneten Tabellen anzeigen.
        Beispielsweise können alle Artikel angezeigt sein, und in einer Combobox-Spalte wird jeweils die Kategorie angezeigt, der ein Artikel angehört. Der JoiningView sieht prinzipiell aus wie das Ergebnis einer Inner-Join - Abfrage in Sql, jedoch, da die Tabellen getrennt geladen wurden, ist es möglich, über die Comboboxen einen Artikel einer anderen Kategorie zuzuordnen.

      4. m:n-View: vom Prinzip her eine Kombination aus Parent-Child und Joining-View. Angenommen die Daten von Kunden, Kundenbetreuern und Bestellungen.
        Im Parent-Child-View kann ein Kunde aus einer Liste gewählt werden, und auf der Child-Seite sieht man seine Bestellungen. Aber (Joining-View) eine Combobox-Spalte der Bestellung zeigt den KundenBetreuer an, der diese Bestellung entgegengenommen hat.

      Alle diese Views präsentieren die Daten - damit meine ich: prinzipiell ermöglichen sie volle Bearbeitung (Löschen, Zufügen, editieren von Datensätzen) - was natürlich je nach Erforderlichkeit vom Programm her eingeschränkt wird.
      Die Views sind auch beliebig kombinierbar - man kann auch 3-stufige Parent-Child-Views bauen, oder Detail-Präsentation mit den anderen Views kombinieren - wies eben erforderlich und sinnvoll ist.
      Ich empfehle sehr, sich in diese etwas abstrahierende Sichtweise einzudenken, denn dann guckt man auf eine Anwendungs-Oberfläche, oder hört auch nur vom Konzept, und erkennt gleich: "Ah! ParentChildView mit DetailView der ChildTable!". Und damit ist die Lösung bereits vollständig ausgesagt, denn einen ParentChildView zu erstellen und einen DetailView sind absolut triviale Routine-Aufgaben, deren Vorgehensweise immer dieselbe bleibt.
      Was variiert ist die Art, wie die verschiedenen Views miteinander rekombiniert werden.

      Aber guggemol sample-Solution:

      ein komplexer View:
      • Links ein DetailView der Kategorien, namlich das Foto mittm Kuchen ist ein Detail des angewählten Kategorie-Datensatzes.
      • Zusätzlich liegt ein ParentChild vor, nämlich zw. dem KategorieGrid links, und dem ArtikelGrid inne Mitte. Also man wählt eine Kategorie, und das ArtikelGrid zeigt die Artikel dieser Kategorie.
      • Auch das Artikelgrid ist um einen DetailView erweitert - das sind die Einzelcontrols auf dem TableLayoutPanel rechts.
      • Der Artikel-DetailView rechts verfügt auch über einen Joining-Anteil: Da ist ja diese Combo, die den KategorieNamen in den ArtikelView joint.
      • Es liegt auch ein zweiter, paralleler ParentChildView vor: Die Combobox oben Mitte - ebenso gut wie mit dem DataGridview kann man auch damit eine Kategorie wählen, und das ArtikelGrid springt.

      Derlei komplexe Views bastelt man nicht im Code-Editor, sondern, indem man im Form-Designer Databindings einrichtet. Dabei verwendet man geeignet konfigurierte BindingSources, an die die Controls gebunden werden. (Wenn man die Tabellen aus dem Datenfenster zieht, bekommt man Bindingsources hingeneriert - gugge "ParentChildView" und (!!!) "DBEinzelblattView" auf Movie-Tuts.)
      Gewissermaßen komponiert man die Views auf Bindingsource-Ebene, also der ParentChildView "Kategorie->Artikel" benötigt 2 BindingSources: Eine "KategorienSource", mit .DataSource=NorthwindDataset und .Datamember=Kategorie.
      Und eine untergeordnete "KategorieArtikelSource": Deren .DataSource ist die vorgenannte KategorieSource und ihr .Datamember ist die DataRelation zwischen den DataTables Kategorie und Artikel. Diese DataRelation heißt "KategorienArtikel", und kann man sich im FormDesigner aussuchen:

      Für den ParentChildView Kategorie->Artikel stöpseln wir also die ArtikelSource an die KategorieSource an, und nicht ans Dataset.
      Die BindingSources sind beim View-Komponieren das entscheidende - welche Controls daran hängen, ist schnurzepieps.
      Daher habe ich in diesem Fall auch gleich zwei Controls an die KategorienSource gehängt, nämlich das mächtige KategorienDataGridView, und die platzsparende KategorieCombobox:

      zu beachten, dass man bei der Combo den .DisplayMember angeben muß - beim DGV nicht, denn es zeigt ja alle Spalten an.
      Nun steuern beide die KategorienSource, und werden auch von ihr gesteuert. Also wenn per DGV die BindingSource auf den 3. Eintrag gesetzt wird, dann stellt die BindingSource auch die Combo auf den 3.Eintrag ein (und umgekehrt: Databinding eben).
      Vor allem aber setzt die KategorienSource einen Filter auf die an sie angestöpselte KategorieArtikelSource - letztere gibt aufgrund der Verstöpselung nur diejenigen Artikel aus, die der inne übergeordneten KategorienSource eingestellten Kategorie angehören.

      Der Artikel-EinzelblattView ist codetechnisch aufwändiger (aber wir müssens ja nicht coden, wir designen es ;)). Nämlich jedes EinzelControl ist an die KategorieArtikelSource gebunden - jeweils unter Angabe einer anneren Property:

      Zu beachten, dass bei weitem nicht nur die Text-Property eines Controls bindungsfähig ist - das Bild zeigt: bei der Checkbox wars die Checkstate-Property, die an KategorieArtikelSource - AuslaufArtikel gebunden ist, sodass die Checkbox nun aussagt: der Artikel ist AuslaufArtikel oder nicht (oder - Checkstate.Indeterminated - der Wert ist nicht gesetzt).
      Wie gesagt: diese zig Labels und Textboxen schmeißt uns der FormDesigner fertig konfiguriert hin, wenn wir eine Tabelle als DetailView aus dem Datenfenster ziehen:

      Nochmalige Empfehlung: unbedingt "ParentChildView" und "DBEinzelblattView" auf Movie-Tuts angugge - das Datenfenster macht noch mehr. (Kommt mir also nicht mit dumme Fragen, nur weiler diese Tuts zu konsumieren nicht für nötig hieltet) ;).

      Databinding mit Combo-/List-box: JoiningView
      Die drei databindingfähigen MultiItem-Controls sind: DatagridView, Combobox und Listbox. Combo/List-box fasse ich zusammen, die sind absolut identisch zu behandeln - tatsächlich sind sie dasselbe Control, nur in je unterschiedlichem Outfit.
      Wie gesagt: der Funktionalität einer Anwendung ists schnurz, ob an eine BindingSource nun ein DGV angebunden wird oder eine Combo oder beides, und die Combo macht auch dasselbe wie ein DGV:
      Nämlich die Anwahl eines Items steuert den PositionsZeiger der BindingSource und wird von ihm gesteuert - Bindung eben.
      Nur ist Combo eingeschränkt gegenüber DGV, denn sie kann die Items der Anzeige nicht editieren, und kann auch nur eine einzige Datenspalte anzeigen (DGV hingegen kann (muß aber nicht) alle Spalten zeigen). Die eine anzuzeigende Spalte teilt man der Combo mit durch Setzen der Property DisplayMember.
      Aber Combobox kann etwas ganz anneres: sie kann nämlich die ID des angewählten Datensatzes in einen ganz anderen Datensatz aus einer anderen Tabelle schreiben! Das ist das entscheidende Feature des JoiningViews, bei dem Werte der übergeordneten Tabelle in die untergeordnete Tabelle hinein-gejoint werden.
      Hierzu muß man der Combo die ID-Spalte erstmal mitteilen - durch Setzen der ValueMember-Property.
      Und schließlich muß man ein Binding setzen für die SelectedValue-Property der Combobox. Dieses Binding bestimmt eine weitere BindingSource und einen SpaltenNamen als Ziel, in den der SelectedValue geschrieben werden soll.

      Also ohne Beispiel ist das wohl nicht verständlich: Gegeben sind also die DataTables Kategorie und Artikel, und Artikel ist per DataRelation den Kategorien untergeordnet (1:n - Relation Kategorie->Artikel: eine Kategorie enthält mehrere Artikel, ein Artikel verweist auf genau eine Kategorie).
      Nun ein DGV, gebunden an eine ArtikelBindingSource, sodaß das DGV den Positionszeiger der Artikel-BS steuert.
      So, nun eine 2. BindingSource, "KategorienBindingSource", die Kategorien-Datensätze bereitstellt.
      Daran angebunden die "KategorienCombobox", also DataSource=KategorienBindingSource, DisplayMember="KategorieName" - so weit noch die übliche Positionszeiger-Verwaltung, wie man auch beim binden eines DGVs erhielte.
      Jetzt aber teilen wir der Combo die ID-Spalte mit: KategorienCombobox.ValueMember="KategorieID", also die PrimKey-Spalte der KategorienTabelle.
      Und nun setzen wir ein Binding von Combo.SelectedValue nach ArtikelBindingSource.KategorieID, sodaß Combo.SelectedValue nach Artikel.KategorieID geschrieben wird, also in den Fremdschlüsselwert des Current-Datensatzes der ArtikelBindingSource.
      Codetechnisch wäre das so formuliert, aber man stellt das auf jeden Fall besser im FormDesigner ein:

      VB.NET-Quellcode

      1. KategorienCombobox.DataSource=KategorienBindingSource
      2. KategorienCombobox.DisplayMember="KategorieName"
      3. KategorienCombobox.ValueMember="KategorieID"
      4. KategorienCombobox.DataBindings.Add("SelectedValue", ArtikelBindingSource, "KategorieID")
      (beachte, dass KategorieID in Zeile#3 den PrimKey der KategorienTable bezeichnet, aber in #4 den ForeignKey der ArtikelTable, mit dem ein Artikel-Datensatz auf seine Kategorie verweist. Es sind unterschiedliche Spalten in verschiedenen Tabellen, die ich aber gleich benannt habe, um die bestehende DataRelation zu verdeutlichen.)

      Wie gesagt: Niemals auf diese Weise codetechnisch machen - immer im Designer zurechtklicksen! Weil der bietet die Optionen zur Auswahl an, sodaß man viel weniger Fehler machen kann (immer noch reichlich genug), und Blöd-Tipp- oder Syntax-fehler sind so ganz ausgeschlossen.
      Hier ein Bild, wie mans im Designer einrichtet: Der SmartTag der Combo ist geöffnet, und sichtbar sind die entscheidenden 4 Einstellungen (wobei die Binding-Einstellung des SelectedValue selbst nochmal geöffnet ist, um besser sichtbar zu machen, was eingestellt ist).

      Die beteiligten BindingSources heißen etwas anders: Statt "KategorienBindingSource" (übergeordnet) heißts "scrClmKategorie", und statt "ArtikelBindingSource" (untergeordnet) heißts "KategorieArtikelSource" - hat damit zu tun, dass noch viele weitere BindingSources auf dem Form rumfahren und auseinandergehalten werden müssen.
      Ergebnis dieser ganzen Operation ist ein JoiningView: die Combo zeigt den KategorieName des angewählten Artikels an, und bietet als Auswahlmöglichkeit alle anneren KategorieNamen. Wird nun eine annere Kategorie gewählt, so ist der Artikel-Datensatz dadurch wirksam dieser neuen Kategorie zugeordnet (wovon man sich im Sample überzeugen kann: Er verschwindet nämlich stantepede aus der aktuellen Kategorie, und taucht in einer anderen wieder auf).

      Auf der anderen Tabpage der SampleApp habe ich auch den m:n - View implementiert:

      Angezeigt die erste Bestellung des Kunden "ALFKI", die gesendet wurde an den Empfänger "Wilman Kala", mit allen BestellDetails.
      Von den BestellDetail-Daten ist die Anzeige der "ArtikelID"-Spalte doppelt, nämlich einmal als Textbox-, und einmal als Combo-Column. Die ComboColumn ist nun der klassische JoiningView - sie joint den ArtikelNamen in die BestellDetail-Tabelle hinein, damit der User weiß, was bestellt wurde, und nicht nur die Textbox hat, die nur eine dumme Nummer anzeigt.
      Merkregel: Schlüssel-Spalten-Werte, ob ForeignKey oder PrimaryKey, haben in der Anzeige nix verloren. Was soll der User dumme Nummern angugge?
      Hier die Konfiguration der ArtikelID-ComboColumn:

      Man sieht: Ich habe den ColumnType auf DatagridviewComboboxColumn gesetzt, und verfahre identisch wie bei der anneren Joining-Combo, nur jetzt auf Englisch: DataSource ist eine BindingSource, die alle Artikel bereitstellt, DisplayMember ist ArtikelName. ValueMember ist natürlich ArtikelID - der Primkey der Artikel-Datensätze, und was vorhin "Ausgewählter Wert" hieß, heißt hier DataPropertyName.
      Hier gibts einen gewissen Unterschied, denn ich muß kein Binding für Combobox.SelectedValue setzen, sondern nur die Fremdschlüsselspalte angeben, das Binding erstellt die DatagridViewComboboxColumn dann selbst. Weil bei einer DGV-Column ist die Ziel-Tabelle des SelectedValue-Bindings ja vorgegeben: Eine ComboColumn in einem BestellDetail-DatagridView kann logisch nur in BestellDetail-Datensätze schreiben.
      Also die ArtikelID-Combo schreibt die aus der Tabelle Artikel ausgewählte ArtikelID (ihr SelectedValue) in den ForeignKey BestellDetail.ArtikelID des im Grid aktuellen BestellDetail-Datensatzes, und kann damit den bestellten Artikel wirksam ändern, etwa ein BestellDetail von 12 Einheiten Bier verwandeln in 12 Einheiten CurrySauce ;).

      Dieser Beitrag wurde bereits 6 mal editiert, zuletzt von „ErfinderDesRades“ ()

      zusätzliche Samples: m:n-View, DetailView im Popup-Form

      m:n - View:

      naja, jetzt ist glaub klar: jeder Kunde hat viele Bestellungen, und jeder Bearbeiter hat viele Bestellungen, und es sind jeweils dieselben Bestellungen, "nur" bei Kunden und Bearbeitern verschieden zugeordnet (Das Sample findet sich im SqlCe-Projekt).

      DetailView im Popup-Form
      Manchmal scheint es gute Benutzerführung, wenn zum Bearbeiten eines Datensatzes ein extra Form aufpoppt. Ich denkeja, dasses meist keine gute Benutzerführung ist, aber habichjetzt auch gebastelt (im DatasetOnly-Projekt), weil wird viel angefragt:

      Also das große Grid ist jetzt zur Bearbeitung gesperrt, und bei Doppelklick poppt der DetailView auf. Es ist fast derselbe Detailview wie der vom MainForm, nur kann halt nicht adden und removen.
      Code frmArtikels:

      VB.NET-Quellcode

      1. Imports DatasetOnly.NorthWindDts
      2. '...
      3. Private Sub Button_Click(ByVal sender As Object, ByVal e As EventArgs) Handles btAddProduct.Click, btDelete.Click, btEdit.Click
      4. Select Case True
      5. Case sender Is btAddProduct
      6. srcArtikel.AddNew()
      7. LaunchDetails()
      8. Case sender Is btEdit
      9. LaunchDetails()
      10. Case sender Is btDelete : srcArtikel.RemoveCurrent()
      11. End Select
      12. End Sub
      13. Private Sub LaunchDetails()
      14. Using frmDetail = New frmArtikelDetails
      15. 'Besonderheit: Die BindingSource des Detail-Forms wird nicht auf eine DataTable
      16. ' umgesetzt, sondern auf ein einzelnes DataRowView: srcArtikel.Current
      17. frmDetail.srcArtikel.DataSource = srcArtikel.Current
      18. If frmDetail.ShowDialog(Me) = Windows.Forms.DialogResult.OK Then
      19. frmDetail.srcArtikel.EndEdit()
      20. srcArtikel.ResetCurrentItem() 'den von der anneren BS geänderten Datensatz neu einlesen.
      21. Else
      22. srcArtikel.CancelEdit()
      23. End If
      24. End Using
      25. End Sub
      Zu sehen, dass Editieren und adden identisch sind, nur beim Adden bewirkt srcArtikel.AddNew(), dass srcArtikel.Current das DataRowView einer neu erzeugten ArtikelRow ist.
      Der Trick beim Starten des DetailForms besteht im Umstöpseln der ArtikelBindingSource des DetailForms: Diese wird nämlich nicht auf eine DataTable umgestöpselt, sondern auf genau einen Datensatz aus dieser DataTable - und - fertig!

      Hier noch der weltbewegende Code des DetailForms:

      VB.NET-Quellcode

      1. Public Class frmArtikelDetails
      2. Private Sub Form_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
      3. Me.NorthWindDts.Register(Me)
      4. End Sub
      5. End Class
      Also es registriert sein lokales Dataset, damit alle BindingSources sich aufs selbe typisierte Dataset beziehen, und nicht mehr auf die im Designer angelegte lokale Instanz.

      Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „ErfinderDesRades“ ()

      TypParameter 'Of KundeRow' nicht definiert

      Hi,

      habe mir deine DLLs geladen und ins Projekt eingebunden. Mein Dataset hat jetzt ein paar schicke neue Extensions 8-)
      Komme aber noch nicht ganz damit klar. DataSet.Fill und .Save habe ich schon klargemacht... und die ganzen TableAdapters in Rente geschickt (und das bevor die 67 wurden :thumbsup: ).
      Aber nu versuch ich dein SqlCE-Beispiel auf meine Bedürfnisse umzufummeln und zwar dergestallt, das in Abhängigkeit von den Datensätzen der Haupttabelle die passenden Daten von Nebentabellen angezeigt werden (du weißt ja, was ich da für einen Zirkus habe).

      Folgender Code aus deinem Tut ist m.E. genau das Richtige für mich_

      VB.NET-Quellcode

      1. Private Sub BindingSource_CurrentChanged(ByVal sender As Object, ByVal e As EventArgs) _
      2. Handles KundeBindingSource.CurrentChanged, BearbeiterBindingSource.CurrentChanged
      3. 'Datenabruf in Abhängigkeit des gewählten Kundes und Bearbeiters
      4. Dim rwKunde = KundeBindingSource.At(Of KundeRow)()
      5. Dim rwBearb = BearbeiterBindingSource.At(Of BearbeiterRow)()
      6. If rwBearb.Null OrElse rwKunde.Null Then Return
      7. rwKunde.FillChildTable( _
      8. BestellungenDts.Bestellung, "WHERE BearbeiterID=?", rwBearb.ID)
      9. lbSqlFromWhere.Text = "WHERE BearbeiterID=?".Replace("?", rwBearb.ID)
      10. End Sub


      Hab ein Problem mit diesem KundeRow. Bin einfach noch nicht so tief in VB, dass ich sagen kann: issoch völlig klar das Teil. Wenn ich versuche, deinen Beispielcode umzuschreiben, dann scheiter ich daran, dass meine Solution kein entsprechendes Objekt kannt (bei mir müsste es natürlich BuchungRow oder so heißen). Ich hab schon rausgefunden, dass KundeRow ne Klasse ist und die Definition (partial) hatter mir auch angezeigt. Aber mir ist nicht klar, ob ich jetzt zu Fuß für meine Tabellen irgendwelche Klassen/Objekte anlegen muss, damit obiger Mechanismus funktioniert oder was sonst noch zu tun wäre. Wo bekomme ich ein BuchungRow (analog zu deinem KundeRow) her? Habe dazu leider auch nix in deinem Tut gefunden.
      Ich code nur 'just for fun'! Damit kann ich jeden Mist entschuldigen, den mein Interpreter verdauen muss :D

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „ErfinderDesRades“ ()

      Ja, das wird gelegentlich gefragt (wenns das jetzt ist):

      VB.NET-Quellcode

      1. Dim rwKunde = KundeBindingSource.At(Of KundeRow)()

      Der Typ "KundeRow" ist an dieser Stelle nur deshalb bekannt, weil am Datei-Anfang ein Import auf die typisierte Dataset-Klasse gesetzt ist:

      VB.NET-Quellcode

      1. Imports SqlCeSmallSamples.BestellungenDts

      Die Imports - Anweisung macht Namespaces und sogar auch Klassen-Inhalte innerhalb einer Datei bekannt, sodass man nicht immer den vollqualifizierten Typen (hier: SqlCeSmallSamples.BestellungenDts.KundeRow) angeben muß.
      Das macht Code übersichtlicher.

      (ich editiere mal den Post)
      Hallo ErfinderDesRades,

      ich hoffe nach 5 Jahren kannst du mir trotzdem noch helfen.

      Ich stoße auf ein kleines Problem dem ich nicht Herr werde, zumindest in Zusammenhang mit deiner DBExtensions und zwar dieses doofe Combobox in Datagridview Datenladeproblem (ComboboxCell-Wert ist ungültig). Aus deinem Thread Daten laden, speichern, verarbeiten - einfachste Variante kenn ich das Problem schon, und mit der Lösung daraus hat es bisher auch immer funktioniert, zumindest wenn ich schön brav aus VisualStudio die ganzen Dataset, Bindingsource, Tableadapter, ... verwendet habe. Ich dachte ich versuch es jetzt mal mit deiner DbExtensions, allerdings bin ich da wohl zu doof, dieses "Ich lade mal die Tabelle für das DGV vor der Tabelle für die Combobox" - Problem zu umschiffen. Hast du da nen Tip für mich?


      Edit: ursprüngliches Problem hat sich gelöst, man sollte hald schon die richtige Tabellen laden ;) *peinlich in ecke verkriecht*

      Hab aber ein neue Herrausvorderung:
      Ich hab zum Testen ein neues Formular gemacht, Tabelle aus Datenquelle Reingezogen als DGV, neuen DatasetAdapter erstellt und damit versucht das Dataset zu Registrieren

      VB.NET-Quellcode

      1. Dim adp = New DatasetAdapter(
      2. MySql.Data.MySqlClient.MySqlClientFactory.Instance,
      3. My.Settings.db_dispatchingConnectionString,
      4. ConflictOption.OverwriteChanges)
      5. Me.Db_serviceticket_test.Adapter(adp).Register(Me, True)


      soweit alles gut und funktioniert auch. Da ich das Ganze aber in einer Übersicht 20 Mal haben will, also 20 Datagridviews mit Filtern und weiteren Informationen, hatte ich das ursprünglich als Benutzersteuerelement angelegt, versuche ich jetzt das Ganze aus meinem Testformular zu übernehmen bekomm ich bei

      VB.NET-Quellcode

      1. Me.Db_serviceticket_test.Adapter(adp).Register(Me, True)


      einen Fehler da er das Benutzersteuerelement nicht in ein Form konvertieren kann. Gibts hierfür nen workarround?

      Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „DianonForce“ ()

      ErfinderDesRades schrieb:

      Download Sources:
      Und: "tschüss, DBCommand, DataAdapter, DataReader, DbParameter!" - wir kümmern uns wieder mehr um die eigentliche Programmlogik, und weniger ums Infrastruktur-Theater.
      Und auch: "tschüss, TableAdapter und TableAdapterManager!" - ihr werdet ab sofort gleich wieder rausgeschmissen, sobald der Dataset-Designer ein typisiertes Dataset aus der Datenbank generiert hat (siehe dazu "Datenbank in 10 Minuten" auf Movie-Tuts).
      Das Ziel zum Link "Datenbank in 10 Min." ist leider nicht mehr erreichbar. Hast du @ErfinderDesRades das Tut woanders online gestellt?