DataExpressions: Filter und berechnete Spalten im Dataset

    • VB.NET

    Es gibt 4 Antworten in diesem Thema. Der letzte Beitrag () ist von DerSmurf.

      DataExpressions: Filter und berechnete Spalten im Dataset

      Ich würd gerne mal etwas eingehen auf ein Feature des typisierten Datasets - ich nenne es "DataExpression".
      DataExpression ist eine eigene kleine Abfragesprache, wie etwa auch reguläre Ausdrücke oder SQL-Commands. Hier die Referenz der Syntax.
      DataExpressions können Daten filtern oder selbstaktualisierende Berechnungen angeben (wie Excel-Formeln).
      Zum Filtern weist man man eine DataExpression der Filter-Property einer BindingSource zu, zB:

      VB.NET-Quellcode

      1. BestellungBindingsource.Filter = "Betrag > 1000"

      Selbstaktualisierende Berechnungen ("berechnete Spalten") erhält man, indem man die Expression-Property einer DataColumn setzt - siehe untiges Bild.
      Filter müssen Wahrheitswerte ergeben, Berechnungen hingegen auf den Datentyp der berechneten DataColumn passen.

      Ich kann hier nicht die ganze Syntax durchmachen - stattdessen zeige und bespreche ich nur ein paar Beispiele am Bild.


      Eine Datatable, deren Spalte "Percent" den prozentualen Stunden-Anteil ("Hours") des aktuellen Datensatzes an den Gesamt-Stunden berechnet.
      Die dafür nötige Formel drückt den mathematischen Zusammenhang quasi in Reinform aus:
      (100*Hours)/sum(Hours)
      Beachte die unterschiedlichen Bedeutungen von Hours und sum(Hours): Hours addressiert den Spaltenwert im jeweiligen Datensatz, und sum(Hours) die Summe aller Hours der DataTable (siehe o.g. Syntax-Referenz).

      Und so mags dann im Grid aussehen - ich habe die Percent-Spalte mal AliceBlue eingestellt, um anzuzeigen: Readonly (sind berechnete Spalten immer).

      Schön an berechneten Spalten findich: die sind gewissermaßen Fakt. Also da muß man nichts updaten etc., sondern einmal eingestellt, verfügt die DataTable eben über eine Spalte, die diesen Prozentsatz anzeigt - und der Wert stimmt immer.


      So richtig schnuckelig werden berechnete Spalten aber erst, wenn man mit ihnen auf andere Tabellen verweist:

      Hier das Dataset einer Kassenabrechnung. Die TabellenStruktur zeigt an: Ein Artikel hat v.a. die Spalten Name und Preis. Ein (Bon-)Posten verweist per Foreignkey auf einen Artikel. "Foreignkey" heißt: Mehrere Posten können auf denselben Artikel verweisen.
      Weiters verweist der Posten aber auch auf den Bon, zu dem er gehört. (Wieder gilt: Mehrere Posten können auf denselben Bon verweisen (denn logisch hat ein Bon mehrere Posten).)
      Wie oben auch drückt die DataExpression für den Posten-Betrag den mathematischen Zusammenhang (fast) ganz einfach aus:
      Anzahl*Parent(FK_Artikel_Posten).Preis
      Nicht wahr: Der Posten-Betrag ist die Anzahl mal dem Artikel-Preis - das steht da ja auch, ziemlich unverblümt.

      Bisserl wüster wirds, wenn im Bon nun die Summe aller Posten-Beträge gebildet werden soll:

      isnull(sum(child(FK_Bon_Posten).Betrag),0)
      sum(child(FK_Bon_Posten).Betrag) ist ja noch logisch - die Summe halt aller Posten-Beträge.
      Eingeschlossen aber ist das in den Ausdruck isnull(<sumFormel>,0). Abgefangen und korrigiert werden muß nämlich, wenn einem Bon kein einziger Posten untergeordnet ist, dass dann die Summenformel nicht Nothing ergibt, sondern 0.

      Jedenfalls das MiniKassDataset mit seinen beiden berechneten Spalten ermöglicht mit minimalem Aufwand, ein durchaus ansehnliches und einsatzfähiges Kassenprogramm hinzuhauen:

      Zu sehen sind 2 Bons, davon der erste mit seinen beiden Posten

      Filtern
      Wie gesagt, eine filternde DataExpression muß einen Bool ergeben, und wird an BindingSource.Filter zugewiesen - und das ist im wesentlichen auch schon alles, was zu tun ist :).
      Das 3. Demo-Projekt ist eine Art Text-Analizer. Ist ganz interessant: Die ursprüngliche Frage war, wie man bei zwei Wortlisten herausbekommt, was in Liste2 fehlt, aber in Liste1 vorhanden ist. Und wie das so ist - Anforderungen wachsen - stellte sich heraus, dass die Worte auch mehrfach vorkommen konnten, und u.U. bestimmte Worte auch in Liste2 häufiger sind als in Liste1 und so Zeugs.
      Ja - habichmir gedacht: In ein Dataset damit, und dann mit Filter-Expressions darauf losgehen.
      Und weil sich immer mehr Varianten auftaten, was man abfragen können mag, habichgleich auch eine Tabelle für die Abfragen dazugepackt.
      Herausgekommen ist fast eine Art Testing-Tool für Filter-Expressions: In der linken Tabelle kann man sich beliebige Expressions ausdenken, und rechts werden sie dann auf den Datenbestand angewendet.

      (Beachte auch, dassich die Schrift der FilterText-Column auf 'Courier New' eingestellt hab, damit die Interpunktions-Zeichen besser sichtbar sind)
      Angewählt ist grad mal ein recht bescheuerter Ausdruck, der Worte sucht, die mit 'd' anfangen, und in Liste1 genau einmal vorkommen ;)
      Aber das Prog ist glaub ganz praktisch, um mal ein bischen mit DataExpressions zu experimentieren :)


      Die Sample-App
      enthält alle Projekte, von denen hier Bilder gezeigt wurden. Um vom einen Projekt auf das andere zu switchen, muß man es als StartProjekt einstellen (ProjektExplorer-MyProject-Kontextmenü)
      Dateien

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

      Filter-Ausdrücke mit PlaceHoldern

      Hier noch eine kleine Erweiterung zum Filtern.
      Bei Datenbank-Abfragen ist ja immer dringend geraten, DbParameter zu verwenden, anstatt sukzessive alle Fehlermöglichkeiten der StringFrickelei selbst auszuprobieren :thumbdown: .

      Aber da beim Filtern mit BindingSource im Grunde dasselbe String-Frickel-Problem besteht (jdfs. was die Konvertierungsprobleme betrifft), habe ich jetzt auch eine Extension für BindingSource gebastelt, welche '?' im Filter-String als ParameterPlatzhalter interpretiert.

      Damit kann das An- und Aus-schalten eines Zeitbereich-Filters zB. so aussehen:

      VB.NET-Quellcode

      1. Private Sub ckFilter_CheckedChanged(ByVal sender As Object, ByVal e As EventArgs) Handles ckFilter.CheckedChanged
      2. If ckFilter.Checked Then
      3. BestellungBindingSource.FilterX("Datum >= ? And Datum <= ?", dtpVon.Value, dtpBis.Value)
      4. Else
      5. BestellungBindingSource.FilterX("")
      6. End If
      7. End Sub

      Ums korrekte Einfrickeln der Werte, unter Berücksichtigung, obs Datum, String oder andere Werte sind, kümmert sich die Extension:

      VB.NET-Quellcode

      1. ''' <summary>
      2. ''' provides a placeholder-syntax for filters. Sample: dv.FilterX("Datum >= ? And ? >= Datum", dtpVon.Value, dtpBis.Value)
      3. ''' </summary>
      4. ''' <exception cref="ArgumentException">expression contains more placeholders than values are passed</exception>
      5. <Extension()> _
      6. Public Sub FilterX(ByVal bs As BindingSource, ByVal expression As String, ByVal ParamArray values() As Object)
      7. bs.Filter = GetFilterString(expression, values)
      8. End Sub
      9. Private Function GetFilterString(ByVal expression As String, ByVal values() As Object) As String
      10. Dim splits = expression.Split("?"c) ' identify placeholder '?'
      11. If splits.Count > values.Length + 1 Then Throw New ArgumentException( _
      12. "expression contains more placeholders than values are passed")
      13. For i = 0 To splits.Length - 2
      14. Dim val = values(i)
      15. Dim sb = New Text.StringBuilder("{0}")
      16. If TypeOf val Is Date Then
      17. sb.Insert(0, "#"c).Append("#"c)
      18. ElseIf TypeOf val Is String Then
      19. With splits(i) ' identify wildcard '*' or '%'
      20. If .Length > 0 AndAlso "*%".Contains(.Chars(.Length - 1)) Then sb.Insert(0, "*"c) : splits(i) = .Remove(.Length - 1, 1)
      21. End With
      22. With splits(i + 1) ' identify wildcard '*' or '%'
      23. If .Length > 0 AndAlso "*%".Contains(.Chars(0)) Then sb.Append("*"c) : splits(i + 1) = .Remove(0, 1)
      24. End With
      25. sb.Insert(0, "'"c).Append("'"c)
      26. End If
      27. splits(i) &= String.Format(Globalization.CultureInfo.InvariantCulture, sb.ToString, val)
      28. Next
      29. Return String.Concat(splits)
      30. End Function

      Anneres Aufrufebeispiel (mit WildCards)

      VB.NET-Quellcode

      1. srcKundeBestellung.FilterX("Empfänger LIKE *?*", txtEmpfFilter.Text)

      ich hoffe, man erkennt, dass die .FilterX()-Extension die übergebenen Argumente auf ihren Datentyp hin untersucht, und dem entsprechene Literale bildet, etwa ein String wird in '<string>' gequotet, ein Datum mit #<datum># - während Zahlen keiner Quotes bedürfen.
      Aus dieser automatischen Anpassung an den Datentyp des übergeben Parameters folgt, dass man die Parameter auch im richtigen Datentyp übergeben muss.
      Angenommen man wolle eine ZahlenSpalte filtern, und versucht zB folgendes:

      VB.NET-Quellcode

      1. MyBindingSource.FilterX("MyNumber > ?", Textbox1.Text)
      Das wird failen, denn Textbox1.Text ist keine Zahl

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

      Filter Asdrücke ---- FilterX

      Hallo ErfinderDesRades,

      ich habe heute Deine FilterX Funktion zum Einsatz gebracht und finde sie sehr praktisch. Danke für Deine Arbeit.

      Solange man mit Strings oder einem Datum arbeitet funktioniert alles wie gewünscht, aber wie gehe ich mit Zahlen um?

      Ich habe in meinem DataGrid Kennzahlen, nach denen gesucht/gefiltert werden soll. Wenn ich die FilterX-Funktion auf Zahlen anwende, dann erhalte ich immer einen Fehler:

      Beispiel: SYSTEM.DATA.SYNTAXERROREXCEPTION: "Die Operation '=' kann nicht an System.Decimal und System.String durchgeführt werden."

      Abgefragt wird:

      VB.NET-Quellcode

      1. BS.FilterX("ID_KNZ = *?*", TextBox1.Text)


      Hast Du eine Idee, was ich falsch mache oder geht das sowieso so nicht?

      Vielen Dank für Deine Hilfe,

      Gruß vom Doc


      Hallo,

      mittlerweile habe ich eine alternative Lösung gefunden, die jetzt separat neben der FilterX Version zum Einsatz kommt. Mir ist nämlich leider nicht klar, wie man die Methode in die FilterX-Methode unterbringen könnte ;( .

      Hier jetzt die Version, die bei Zahlen und dem Einsatz von LIKE funktioniert:

      VB.NET-Quellcode

      1. BS.Filter = String.Format("CONVERT({0}, System.String) LIKE '%{1}%'", Spaltenname, Filtertext)


      Gruß vom Doc

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Marcus Gräfe“ ()

      Sorry - dein Post habich übersehen - jetzt erst drauf gestossen.
      Habich nun gleich ausprobiert, im DatasetOnly-Projekt der SampleSolution - ich fund gar kein Problem:

      VB.NET-Quellcode

      1. srcKundeBestellung.FilterX("Convert(Frachtkosten, System.String) LIKE *?*", txtEmpfFilter.Text)
      funzt ebensogut wie der Filter, der vorher an der Stelle bereits bestehende war.
      Ist interessant - der Convert()-Funktion der DataExpression-Grammatik habich bislang nie Aufmerksamkeit geschenkt - die eröffnet ja nun nochmal paar weitere Möglichkeiten.

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

      Hallo Leute.
      Sorry, dass mich in diesem alten Thread verewige.
      ich habe erfolgreich die FilterX Methode verwendet und Filtere damit die Spalte "Lastname" mittels einer Textbox.

      VB.NET-Quellcode

      1. Private Sub TBSearch_TextChanged(sender As Object, e As EventArgs) Handles TBSearch.TextChanged
      2. DTAddressbookBindingSource.FilterX("Lastname LIKE *?*", TBSearch.Text)
      3. End Sub


      Wie muss ich denn vorgehen, wenn ich jede Spalte in meiner DataTable filtern möchte und nicht nur Lastname?