Code zur Anzeige von Daten aus DataSet optimieren (Ausführung dauert zu lange)

  • VB.NET
  • .NET 4.5

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

    magst du mir da auch was geniales basteln`?
    Oder ist hier eine (deutliche) Verbesserung des Codes nicht nötig oder möglich (ich kenn die Antwort glaub ich xD)

    Dann müsste ich den Code ja nur anpassen, und ihn in DtsSettings schmeißen.

    VB.NET-Quellcode

    1. Partial Class DtsSettings
    2. Public IncomeRecord As DailyIncomeRow
    3. Public CustomersRecord As DailyIncomeRow
    4. Public ReadOnly ProductGroupRecords As New List(Of Distribution_TableRow)
    5. Public ReadOnly MonthlyGroupRecords As New List(Of Tuple(Of Date, ProductGroupRow, Integer))
    6. Public UsatzMonatRekord As Tuple(Of Date, Integer)
    7. Public KundenMonatRekord As Tuple(Of Date, Integer)
    8. Private Sub CalculateRekordMonate()
    9. Dim monthInfos = Aggregate tag In DailyIncome
    10. Let dt = tag._Date, dtMonth = New Date(dt.Year, dt.Month, 1)
    11. Group By dtMonth Into umsatz = [Select](tag.DailyIncome)
    12. Select dtMonth, monthSum = umsatz.Sum Into ToArray
    13. Dim maxMonthSum = monthInfos.MaxBy(Function(x) x.monthSum)
    14. UsatzMonatRekord = Tuple.Create(maxMonthSum.dtMonth, maxMonthSum.monthSum)
    15. monthInfos = Aggregate tag In DailyIncome
    16. Let dt = tag._Date, dtMonth = New Date(dt.Year, dt.Month, 1)
    17. Group By dtMonth Into kunden = [Select](tag.CustomerCount)
    18. Select dtMonth, monthSum = kunden.Sum Into ToArray
    19. maxMonthSum = monthInfos.MaxBy(Function(x) x.monthSum)
    20. KundenMonatRekord = Tuple.Create(maxMonthSum.dtMonth, maxMonthSum.monthSum)
    21. End Sub
    22. '...
    Aber die Anzeige musste selbst basteln.

    ErfinderDesRades schrieb:

    Aber die Anzeige musste selbst basteln.

    Vielen Dank! und kein Problem.

    Aber eins haben wir (ich) jetzt vergessen. Den letzten Punkt der TODO Liste.

    DerSmurf schrieb:

    Man sieht aber das Ziel der Ausgabe, nämlich
    • Monat mit höchstem Gesamtumsatz (Umsatz und Monat - also Datum)
    • Monat mit den meisten Kunden
    • Monate mit den höchsten Einnahmen für alle Warengruppen
    • außerdem hätte ich gerne noch eine neue Funktion - den Monat mit höchstem pro Kopf Umsatz
    [list]
    [/list]​
    Die Anzeige des höchsten pro Kopf Umsatzes könnte doch in obige Funktion rein, oder?
    Denn wenn ich es richtig verstehe hält monthinfos bei der ersten Verwendung doch sämtliche Monatsumsätze - im zweiten Zugriff sämtliche Monatskundenzahlen.
    Kann ich diese durchgehen und mir so für jeden Monat den pro Kopf Umsatz ausrechnen, oder gehört das in eine eigene Sub?
    Was meinst du mit "Anzeige" - der Code zeigt doch garnix an?
    Was meinst du mit "obige Funktion" - der gezeigte Code enthält doch nur eine Funktion?

    Vielleicht meinst du, dass man beides - Income und CustomerCount - in einem Durchlauf hätte ermitteln können.
    Ja, da hättest du recht - geht glaub so (ungetestet)

    VB.NET-Quellcode

    1. Private Sub CalculateRekordMonate()
    2. Dim monthInfos = Aggregate tag In DailyIncome
    3. Let dt = tag._Date, dtMonth = New Date(dt.Year, dt.Month, 1)
    4. Group By dtMonth Into umsatz = [Select](tag.DailyIncome), kunden = [Select](tag.CustomerCount)
    5. Select dtMonth, umsatzSum = umsatz.Sum, kundenSum = kunden.Sum Into ToArray
    6. Dim maxUmsatzMonth = monthInfos.MaxBy(Function(x) x.umsatzSum)
    7. Dim maxKundenMonth = monthInfos.MaxBy(Function(x) x.kundenSum)
    8. '...
    9. End Sub
    Das meine ich nicht, aber danke für den Einschub.
    Ein Durchlauf des Dts sollte ja doppelt so schnell sein, wie zwei Durchläufe.
    Ich habe das jetzt ein wenig angepasst - englische Benennung der Variablen:
    Spoiler anzeigen

    VB.NET-Quellcode

    1. Sub CalculateMonthlyRecords()
    2. Dim monthInfos = Aggregate tag In DailyIncome
    3. Let dt = tag._Date, dtMonth = New Date(dt.Year, dt.Month, 1)
    4. Group By dtMonth Into Income = [Select](tag.DailyIncome), customers = [Select](tag.CustomerCount)
    5. Select dtMonth, IncomeSum = Income.Sum, CustomerSum = customers.Sum Into ToArray
    6. Dim maxIncomeMonth = monthInfos.MaxBy(Function(x) x.IncomeSum)
    7. Dim maxCustomerMonth = monthInfos.MaxBy(Function(x) x.CustomerSum)
    8. MonthlyIncomeRecord = Tuple.Create(maxIncomeMonth.dtMonth, maxIncomeMonth.IncomeSum)
    9. MonthlyCustomersRecord = Tuple.Create(maxCustomerMonth.dtMonth, maxCustomerMonth.CustomerSum)
    10. CalculateMonthlyGroupRecords()
    11. End Sub


    Ich muss ja noch für jeden Monat der pro Kopf Umsatz errechnen (Monatsumsatz / Kundenzahl), um den höchsten zu bekommen.

    ErfinderDesRades schrieb:

    Was meinst du mit "Anzeige" - der Code zeigt doch garnix an?
    Was meinst du mit "obige Funktion" - der gezeigte Code enthält doch nur eine Funktion?

    mit "Anzeige" meine ich errechnen. Genau diese eine Funktion meine ich dann natürlich :P
    Also kann ich diese Berechnung in die Sub packen, die ich hier gepostet habe?
    Denn monthInfos hält doch alle benötigten Daten. (ich glaube ich raffe den Code so allmählich)
    Also würde ich noch eine Tuple erstellen: Public PCSRecord As Tuple(Of Date, Double)
    und dem Code folgendes hinzufügen (PCS = per capita sales / eine andere Übersetzung für "pro Kopf Umsatz" habe ich nicht gefunden)

    VB.NET-Quellcode

    1. Dim PCS As Double
    2. For Each row In monthInfos
    3. Dim TempPcs = row.IncomeSum / 100 / row.CustomerSum
    4. If TempPcs > PCS Then
    5. PCS = TempPcs
    6. PCSRecord = Tuple.Create(row.dtMonth, PCS)
    7. End If
    8. Next
    9. MessageBox.Show("Der höchste pro Kopf Umsatz war Monat " & PCSRecord.Item1.ToString("MMMM yyyy") & Environment.NewLine & "Es waren " & PCSRecord.Item2.ToString & "€")


    Die msgbox ist natürlich sinnfrei (und gehört hier auch nicht hin, weils ja eine Anzeige ist - keine Berechnung) - dient nur der eindeutigen Anzeige, was ich möchste, dass die Funktion tut.
    passt das so?

    DerSmurf schrieb:

    Ein Durchlauf des Dts sollte ja doppelt so schnell sein, wie zwei Durchläufe.
    jo - ok - das stimmt in diesem Falle - aber häufig stimmts auch nicht.
    Das Durchlaufen selbst kost eiglich keine Zeit - Es kommt drauf an, was im Durchlauf gemacht wird.
    Würde in einem Durchlauf genau das hintereinander getan, was in zwei Durchläufen einzeln gemacht würde, dann wäre es zeitlich gehopft wie gesprungen.
    Oft sind getrennte Durchläufe auch vonne Code-Verstehbarkeit besser (trotz des bischen mehr Codes).

    Aber hier spart man wohl tatsächlich was, indem die Gruppierung nur einmal stattfindet.
    Ansonsten ist das auch egal: Das eine ist sauschnell, und das anneres ist bischen sauschneller - der Unterschied wird sich nie auswirken.



    Jo - deine Zusatz-Auswertung der MonthInfos finde ich sehr gut. Die stellen sich somit als dreifach wiederverwendbar dar: MonatsUmsatzRekord, MonatsKundenRekord, MonatsProkopfUmsatzRekord.
    Da schmeichel ich mich immer, wenn ich eine Lösung programmiert hab, wo sich später herausstellt, dass sie so gut ist, dass sie auch Probleme löst, die noch garnet bekannt waren. ;)

    Interessanterweise wars ja nicht ganz meine Idee, Kunden und Einkommen in dieselbe monthInfos-Linq-query zu stopfen, sondern entstand ja aus einem Missverständnis dessen, was du gefragt hattest.
    Aber dieses Zusammenstopfen eröffnete die Option nun auch der dritten Art der Auswertung.
    Das issn Konzept, das werde ich mir merken.

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

    ErfinderDesRades schrieb:

    Das issn Konzept, das werde ich mir merken.
    #
    Na, dann hast du ja sogar was von deiner Hilfe :o)

    ErfinderDesRades schrieb:

    Da schmeichel ich mich immer, wenn ich eine Lösung programmiert hab, wo sich später herausstellt, dass sie so gut ist, dass sie auch Probleme löst, die noch garnet bekannt waren.

    und es zeigt, dass du als "Lehrer" taugst, denn der ProKopfUmsatz Code ist ja das erste was ich sinnvolles zur Lösung beitragen konnt, weil ich so allmählich etwas raffe...

    Soho - die Anzeige zum Code aus Post 84 ist fertig. Die Anzeige für den ProKopfUmsatz stellt ja ebenfalls kein Problem dar.
    Also kommen wir zu den letzten Fragen (ich glaube gecodet werden muss - von dir - nix mehr).

    Frage1: wäre noch im zuletzt hochgeladenen Demoprojekt nachvollziehbar
    Ich habe nun sämtliche Rekorde wunderschön in Properties des DataSet. Also kann ich beim speichern einer Tageseinnahme prüfen, ob ein Wert höher ist als der Rekord, es also einen neuen Rekord gibt, damit ich dies dem User mitteilen kann.
    Ich habe mal ein Bildchen der Form zur Eingabe angehangen. Diese findet sich, falls du brauchst im letzten Demoprojekt hinter dem Button "Umsätze eintragen".
    Die Controls oberhalb des schwarzen Striches DateTimePicker, Textbox unter Kunden und Buttons sind dabei im Designer erstellt, die Controls unten (Textboxen Warengruppen) sind per Code erstellt.
    Damit ich gescheit auf diese Textboxen zugreifen kann, werden sie als List(Of Control) gespeichert, die wie folgt gefüllt wird:

    VB.NET-Quellcode

    1. for i = 1 to groupcount
    2. Dim newTB = New System.Windows.Forms.TextBox
    3. With newTB
    4. .Location = New System.Drawing.Point(xPosition, yPosition + 23)
    5. .Font = New Font("Microsoft Sans Serif", 12)
    6. .Name = "TB " & GroupName
    7. .Size = New System.Drawing.Size(200, 26)
    8. .TabIndex = i + 1
    9. .Visible = True
    10. AddHandler .TextChanged, AddressOf TextChangeEvent
    11. AddHandler .Enter, AddressOf TextBox_Enter
    12. End With
    13. TBList.Add(newTB)
    14. next


    Nun ist meine Frage wie ich die suche nach neuen Rekorden und ggf. auch das speichern des neuen Rekordes in der entsprechenden Property veranstalte.
    - wo gehört der Code hin? Ich würde ihn ins DataSet schmeißen, damit ich nicht "von außen" an dessen Properties rumfummeln muss, und nach Abschluss der speichern Sub - also nachdem alle Tageseinnahmewerte im DataSet sind ausführen. Hierfür würde ich dann die Werte aus den Textboxen nehmen und nicht die aus dem DataSet. Das ganze mache ich als einfache Sub. An diese Übergebe ich den Inhalt sämtlicher Textboxen (Warengruppen als Array), sowie das Datum.
    In der Sub wird dann jeder Wert mit den Properties abgeglichen. Ist ein übergebener Wert höher, ändere ich den Wert der Property und öffne eine messagebox.

    Hier bin ich mir an zwei Stellen unsicher. 1. Ich verwende den Inhalt der Textboxen, nicht den ausm DataSet. Ist das ok?
    2. Ich vermische Businesslogik mit Anzeigelogik. (Die Sub die prüft ob es einen neuen Rekord gibt, gibt Rückmeldung in Form einer messagebox. Ist das ok?

    Frage2: bezieht sich auf das Hauptprogramm
    Die zweite Frage bezieht sich auf die Sub, welche das befüllen der Properties im DataSet bewirkt. Wo muss die hin?
    Hierzu musst du wissen, dass es im Hauptprogramm eine Form zur Anzeige der Umsatzrekorde gibt. Diese kann nix außer anzeigen - enthält nur die Codegenerierten Label (genau wie hier im Demoprojekt).
    Und es gibt eine Form zum Eintragen eines neuen Tagesumsatzes (exakt die gleiche wie hier hinter dem Button "Umsatz eintragen")
    Das Programm macht aber natürlich noch einiges mehr. (Ich hoffe das ist soweit vorstellbar - sonst mache ich mal Bilder).
    Nun ist es so, dass ich die Statistik nicht jeden Tag brauche. Ich trage so alle 3 bis 4 Tage die fehlenden Tageseinnahmen ein. Die Rekorde schaue ich mir nur sehr sehr selten an.
    Sollte ich dann das setzen der Properties im DataSet im Form Load Event anstoßen? Denn dann würde ich diese recht oft füllen, ohne sie dann zu nutzen.
    Alternativ könnte ich diese ja auch füllen, wenn ich den Button zum starten der Tageseinnahmeform, bzw. zum starten der RekordeForm drücke.
    Bilder
    • inputForm.jpg

      90,71 kB, 1.152×648, 14 mal angesehen
    also das mit den Monats-Rekorden wird mühsam, wenn du das bei jeder Eingabe prüfen willst.
    Da muss man ja jedesmal den aktuellen Monat neu zusammenrechnen.
    Ja gut, kann man machen.
    Ansonsten würde ich mit den DistributionRows arbeiten, die du zufügst, wenn du ein TagesErgebnis eingibst.
    Die kann man sich ja merken, und der BL zur Überprüfung übergeben.



    Oder du änderst das Konzept, und berechnest die Rekorde nicht beim Einlesen, sondern beim Abspeichern.
    Dann haste immer alles frisch und aktuell durchgerechnet.
    Dann kannste noch INotifyPropertyChanged implementieren - das ist der Standard, wie über Property-Änderungen benachrichtigt wird - Databinding arbeitet auch damit.
    Dann brauchste der BL garnix extra zu übergeben, sondern die meldet sich automatisch, wenn eine der Rekord-Properties sich ändert.
    Hmm - dann muss man aber noch was erfinden, wie man die Rekorde auch ins Dataset abspeichert...

    Nee, ich glaub, beste ist, du rechnest wie gehabt beim Einlesen, und beim Abspeichern rechneste eben alles nochmal.
    Zum Einlesen blockierste dann das PropertyChanged-Event, aber anschliessend aktivierste es, sodass beim Abspeicher-Berechnen die Messageboxes aufpoppen können.
    Ist zwar nicht suuper-performant, aber so brauchste keinerlei erneuten Rekorde-BerechnungsCode schreiben - nichtmal, wenn du dir weitere Rekorde ausdenken tätest.

    Ist aber trotzdem einiges an Arbeit, und besonders deine laufzeit-generierten Labels werden da nicht einfach zu händeln sein.

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

    Aber wenn du auch beim abspeichern berechnen würdest, was spricht dann gegen meinen Lösungsansatz?
    Also ohne das Erstellen eines Events (ich verstehe auch nicht so ganz, was du meinst).
    Einfach Einnahmen an die Sub übergeben und rechnen?
    Da ist ja einiges auszurechnen, welche Rekorde durch was übertroffen werden: UmsatzRekord, KundenRekord, KategorieUmsatzRekord (und von welcher Kategorie), Umsatz/Kunden-Rekord-Monat auch?
    Also bei einer Methode wird das nicht bleiben.

    Und im Dataset muss die Rekord-Property geändert werden - also da müssen neue DailyIncomeRow / Distribution_TableRow eingetragen werden, weil die nächste Berechnung muss ja die neuesten Rekorde zum Vergleich heranziehen können.
    Wo aber willste die DailyIncomeRow / Distribution_TableRow hernehmen, wenn du nur 2 Werte (Datum, Zahl) aus iwelchen Textboxen an die Methode übergibst?

    Naja, vielleicht auch nicht. Es kann ja nur eine Eingabe pro Tag geben, also wird es auch nur eine NeuRekord-Berechnung geben - also kann unsaubererweise evtl. drauf verzichtet werden, die Dataset-Rekord-Properties zu updaten.

    Aber wie siehts eiglich mit Korrigieren von Fehleingaben aus? Was soll passieren, wenn eine nachträgliche Korrektur die Rekord-Werte in Mitleidenschaft zieht?

    Also ich glaub, das mitte Rekord-Meldung wird viel Arbeit machen (kann mich natürlich irren) für vergleichsweise geringen Nutzen. Ich fände es eiglich wichtiger, die Benamungsgeschichte anzugehen.
    Huhu
    Sorry, einmal etwas länger halbwegs Offtopic:
    Also ich gebe dir absolut Recht, dass die Anzeige dieser "Hurra neuer Rekord Messageboxen" für das Programm absolut sinnfrei sind.
    Zumal der Umsatz eines einzelnen Tages ja irgendwie auch total egal ist. Wenn ich in einem Monat 30% Kunden weniger, und 20% Umsatz weniger habe, läuft gewaltig etwas schief.
    Wenn ich dann in diesem Monat in zwei Kategorien den höchsten Tagesumsatz fahre, interessiert das auch keinen.
    Aber von solchen Szenarien ist meine Firma weit weg - dit lööft! und ich freu mich immer wie ein Kullerkeks, wenn das Fenster aufploppt und wieder ein Umsatzrekord gebrochen wurde.
    Daher ist diese - wie gesagt, vollkommen sinnbefreite Funktion - für mich gar nicht so sinnbefreit - denn sie liefern mir ein - 2 Sekunden andauerndes - Glücksgefühl.

    Generell sollte dann natürlich trotzdem das wichtigere zuerst erledigt werden, und dass die Umbenennung wichtiger ist glaube ich dir. (für eine eigene Meinung dazu fehlt mir die Erfahrung).
    Außerdem habe ich ja auch noch die Datenredundanz in DtsSettings.DailyIncome.DailyIncome - wo ich den Tagesumsatz per Code errechne und eintrage und eben (noch) keine errechnete Spalte habe.
    Dann ist da noch DtsSettings.DailyIncome._Date welches umbenannt werden muss.
    Und ich habe auch noch das Thema String Interpolation auf dem Schirm. Das hatten wir beide mal in einem anderen Thread.
    Damit würde ich meinen Code deutlich lesbarer machen.

    Aber! Ich gebe dir wie gesagt recht (oder glaube dir), dass diese Dinge wichtiger sind, als mein UmsatzrekordBenachrichtgungNonsense, dennoch ist es so, dass ich bei all dem oben aufgezählten ja keine Hilfe brauche.
    DataSet umbennen ist unheimlich ätzend. Ich werde ein paar Anläufe brauchen, ich werd den Mist verfluchen, ich werd neu Anfangen, wieder alles verfluchen und irgendwann passts.
    Da kann (braucht) mir keiner bei helfen.
    Das gleiche bei Stringinterpolation - da sollten ein paar Seiten im Netz alles klären, damit ich meinen Code abändern kann.
    Wenn ich früher in meinem VisualBasic Forum in einem Thread geholfen habe, dann habe ich mich mit anspruchsvolleren Themen als Helfer auch tagsüber damit beschäftigt, bis es erledigt war.
    Oder eventuelle Verbesserungen für geposteten Code oder Lösungswege gesucht.
    Erst wenn das Thema erledigt war, habe ich als Helfer damit "abgeschlossen".

    Deswegen habe ich das Thema mit der Rekordbenachrichtigung nun zu ende gedacht und gemacht. Es kann dann nämlich das Thema hier abgehakt werden, weil wir fertig sind.
    Bzw. weil ich beim Rest den ich zum fertigstellen brauche, keine Hilfe mehr brauche (glaube ich zumindest).

    Also zurück zum Thema:
    Ich habe einfach mal den Code fertiggestellt. Zu deinen Bedenken habe ich mir das folgende gedacht:

    ErfinderDesRades schrieb:

    Da ist ja einiges auszurechnen, welche Rekorde durch was übertroffen werden

    ErfinderDesRades schrieb:

    Und im Dataset muss die Rekord-Property geändert werden (...) weil die nächse Berechnung muss ja die neuesten Rekorde zum Vergleich (...)

    Das löse ich beides indem ich, nachdem ein neuer Rekord gefunden wurde, einfach die Rekorde neu berechnen lasse.
    Die Funktionen dafür sind ja im DataSet und laufen in windeseile durch.

    ErfinderDesRades schrieb:

    Aber wie siehts eiglich mit Korrigieren von Fehleingaben aus? Was soll passieren, wenn eine nachträgliche Korrektur die Rekord-Werte in Mitleidenschaft zieht?

    Wenn ich durch das ändern einer Tageseinnahme einen Rekord entferne, stimmt die ganze Geschichte nach einem Neustart des Programmes wieder, denn da werden die Rekorde ja neu eingelesen.
    Da dies nicht so häufig vorkommt, kann ich damit glaube ich leben.
    Alternativ könnte ich, da meine TageseinnahmeSpeichernSub einen Unterschied zwischen "neuer Einnahme" und "Einnahme ändern" kennt, - beim ändern müssen ja keine neuen Rows hinzugefügt werden, bei einer neuen schon - im Falle einer Änderung auch die Rekorde neu berechnen lassen.
    Das gleiche gilt beim löschen einer Einnahme. Nach dem löschen der Rows - einfach Rekorde neu berechnen und gut ist.

    Soho, hier der Code - ich habe ihn grob getestet, schein zu tun was er soll. Ob das ganze so gut - oder wenigestens "in Ordnung" gelöst ist, weiß ich natürlich nicht.
    Ich spare mir mal das hochladen einer Demo, denn ich denke die brauchst du zum beurteilen des Code nicht.
    Wenn doch, mach ich das aber natürlich gerne:

    VB.NET-Quellcode

    1. 'der Aufruf aus der speichern Sub
    2. 'neue Rekorde suchen
    3. Dim ArrDailyGroupincome(0 To TBList.Count - 1) As Double
    4. Dim i = -1
    5. For Each TB As Control In TBList
    6. i = i + 1
    7. ArrDailyGroupincome(i) = Double.Parse(TB.Text)
    8. Next
    9. DtsSettings.FindNewStatisticRecordsDaily(Integer.Parse(TBCustomers.Text), Double.Parse(LBLDailyIncome.Text), ArrDailyGroupincome, IncomeDate)
    10. DtsSettings.FindNewStatisticRecordsMonthly(IncomeDate)
    11. 'und die DataSet Klasse. Den alten Code habe ich mal hinterm Spoiler versteckt:
    12. Partial Class DtsSettings
    13. Public IncomeRecord As DailyIncomeRow
    14. Public CustomersRecord As DailyIncomeRow
    15. Public ReadOnly ProductGroupRecords As New List(Of Distribution_TableRow)
    16. Public ReadOnly MonthlyGroupRecords As New List(Of Tuple(Of Date, ProductGroupRow, Integer))
    17. Public MonthlyIncomeRecord As Tuple(Of Date, Integer)
    18. Public MonthlyCustomersRecord As Tuple(Of Date, Integer)
    19. Public PCSRecord As Tuple(Of Date, Double)

    Spoiler anzeigen

    VB.NET-Quellcode

    1. Public Sub LoadDailyRecords()
    2. IncomeRecord = DailyIncome.MaxBy(Function(x) x.DailyIncome)
    3. CustomersRecord = DailyIncome.MaxBy(Function(x) x.CustomerCount)
    4. Dim rekorde = From dist In Distribution_Table Group By dist.ProductGroupID Into rekord = MaxBy(dist.ProductGroupIncome) Select rekord
    5. ProductGroupRecords.Clear() : ProductGroupRecords.AddRange(rekorde)
    6. End Sub
    7. Private Sub CalculateMonthlyGroupRecords()
    8. Dim tuples = New List(Of Tuple(Of Date, ProductGroupRow, Integer))
    9. Dim kategorieInfos = Aggregate dist In Distribution_Table Group By kategorie = dist.ProductGroupRow Into distsOfKategorie = Group Into ToArray
    10. For Each info In kategorieInfos
    11. Dim monthInfos = Aggregate dist In info.distsOfKategorie
    12. Let dt = dist.DailyIncomeRow._Date, dtMonth = New Date(dt.Year, dt.Month, 1)
    13. Group By dtMonth Into monthlyDists = ToArray(dist)
    14. Select dtMonth, monthSum = monthlyDists.Sum(Function(x) x.ProductGroupIncome) Into ToArray
    15. Dim maxMonthPerKategorie = monthInfos.MaxBy(Function(x) x.monthSum)
    16. tuples.Add(Tuple.Create(maxMonthPerKategorie.dtMonth, info.kategorie, maxMonthPerKategorie.monthSum))
    17. Next
    18. Dim orderedTuples = From kat In ProductGroup Join tpl In tuples On kat Equals tpl.Item2 Select tpl 'Reihenfolge der Tuples in ProductGroup-Reihenfolge bringen
    19. Me.MonthlyGroupRecords.Clear()
    20. Me.MonthlyGroupRecords.AddRange(orderedTuples)
    21. End Sub
    22. Public Sub CalculateMonthlyRecords()
    23. Dim monthInfos = Aggregate tag In DailyIncome
    24. Let dt = tag._Date, dtMonth = New Date(dt.Year, dt.Month, 1)
    25. Group By dtMonth Into Income = [Select](tag.DailyIncome), customers = [Select](tag.CustomerCount)
    26. Select dtMonth, IncomeSum = Income.Sum, CustomerSum = customers.Sum Into ToArray
    27. Dim maxIncomeMonth = monthInfos.MaxBy(Function(x) x.IncomeSum)
    28. Dim maxCustomerMonth = monthInfos.MaxBy(Function(x) x.CustomerSum)
    29. MonthlyIncomeRecord = Tuple.Create(maxIncomeMonth.dtMonth, maxIncomeMonth.IncomeSum)
    30. MonthlyCustomersRecord = Tuple.Create(maxCustomerMonth.dtMonth, maxCustomerMonth.CustomerSum)
    31. Dim PCS As Double
    32. For Each row In monthInfos
    33. Dim TempPcs = row.IncomeSum / 100 / row.CustomerSum
    34. If TempPcs > PCS Then
    35. PCS = TempPcs
    36. PCSRecord = Tuple.Create(row.dtMonth, PCS)
    37. End If
    38. Next
    39. 'MessageBox.Show("Der höchste pro Kopf Umsatz war Monat " & PCSRecord.Item1.ToString("MMMM yyyy") & Environment.NewLine & "Es waren " & PCSRecord.Item2.ToString & "€")
    40. CalculateMonthlyGroupRecords()
    41. End Sub


    VB.NET-Quellcode

    1. 'Sub zum finden neuer Tagesrekorde nach Eingabe
    2. Public Sub FindNewStatisticRecordsDaily(Customers As Integer, DailyIncome As Double, ArrDailyGroupIncome() As Double, Datum As Date)
    3. 'TODO Sub in Function umwandeln: Rückgabewert = List(of string)
    4. 'statt Showmessagebox: List of String mit entsprechenden Werten erstellen
    5. 'Ausgabe der Daten erledigt dann Aufrufsub
    6. Dim NewRecordFound = False
    7. 'Kunden
    8. If Customers > CustomersRecord.CustomerCount Then
    9. ShowMessageBox("Tageskundenrekord", CustomersRecord.CustomerCount.ToString("#,###"), Customers.ToString("#,###"), Datum.ToShortDateString)
    10. NewRecordFound = True
    11. End If
    12. 'Tageseinnahme
    13. If DailyIncome > IncomeRecord.DailyIncome Then
    14. ShowMessageBox("Tageseinnahmerekord", IncomeRecord.DailyIncome.ToString("#,##0.00 €"), DailyIncome.ToString("#,##0.00 €"), Datum.ToShortDateString)
    15. NewRecordFound = True
    16. End If
    17. 'Warengruppeneinnahme
    18. Dim i = -1
    19. For Each record In ProductGroupRecords
    20. i += 1
    21. 'Daten aus DataSet sammeln
    22. Dim ParentProductGroupRow = record.ProductGroupRow
    23. Dim ParentDailyIncomeRow = record.DailyIncomeRow
    24. Dim RecordDate = ParentDailyIncomeRow._Date
    25. Dim ProductGroupName = ParentProductGroupRow.Name
    26. Dim ProductGroupIncome = record.ProductGroupIncome / 100
    27. 'die Prüfung auf neuen Rekord
    28. If ArrDailyGroupIncome(i) > ProductGroupIncome Then
    29. ShowMessageBox("Tagesumsatzrekord " & ProductGroupName, ProductGroupIncome.ToString("#,##0.00 €"), ArrDailyGroupIncome(i).ToString("#,##0.00 €"), RecordDate.ToShortDateString)
    30. NewRecordFound = True
    31. End If
    32. Next
    33. If NewRecordFound Then LoadDailyRecords()
    34. End Sub
    35. Private Sub ShowMessageBox(NameOfRecord As String, OldRecord As String, NewRecord As String, OldDatum As String)
    36. MessageBox.Show("Herzlichen Glückwunsch! Es gibt einen neuen " & NameOfRecord & Environment.NewLine &
    37. "alt: " & OldRecord & " vom " & OldDatum & Environment.NewLine &
    38. "neu: " & NewRecord)
    39. End Sub
    40. 'Sub zum finden neuer Monatsrekorde nach Eingabe
    41. Public Sub FindNewStatisticRecordsMonthly(Datum As Date)
    42. 'TODO Sub in Function umwandeln: Rückgabewert = List(of string)
    43. 'statt Showmessagebox: List of String mit entsprechenden Werten erstellen
    44. 'Ausgabe der Daten erledigt dann Aufrufsub
    45. Dim NewRecordFound = False
    46. '1. Schritt
    47. 'Monatsumsätze des aktuellen Monats zusammenrechnen (enthalten ja die neue Tageseinnahme bereits)
    48. Dim Startdate = New Date(Datum.Year, Datum.Month, 1)
    49. Dim MonthIncome As Double
    50. Dim MonthCustomers As Integer
    51. Dim productGroups = ProductGroup.ToArray
    52. Dim groupcount = productGroups.Length
    53. Dim ArrProductGroupIncome(0 To groupcount - 1) As Double
    54. Dim dt1 = Startdate.AddMonths(1) ' Zeit-Bereich
    55. 'Produktgruppen
    56. Dim distributionTables = From distTbl In productGroups.SelectMany(Function(x) x.GetDistribution_TableRows)
    57. Let dt = distTbl.DailyIncomeRow._Date
    58. Where dt >= Startdate AndAlso dt < dt1 Select distTbl
    59. For Each rwDistrTbl In distributionTables
    60. Dim rwPG = rwDistrTbl.ProductGroupRow
    61. Dim day = rwDistrTbl.DailyIncomeRow._Date.Day
    62. Dim iProdGroup = Array.IndexOf(productGroups, rwPG)
    63. Dim income = rwDistrTbl.ProductGroupIncome / 100
    64. ArrProductGroupIncome(iProdGroup) += income
    65. Next
    66. 'Gesamtumsatz
    67. For Each ProductGroupIncomeIncome In ArrProductGroupIncome
    68. MonthIncome += ProductGroupIncomeIncome
    69. Next
    70. 'Monatskunden
    71. For Each rwIncome In DailyIncome.Where(Function(x) x._Date >= Startdate AndAlso x._Date < dt1)
    72. MonthCustomers += rwIncome.CustomerCount
    73. Next
    74. '2. Schritt
    75. 'Monatsumsätze mit Rekorden vergleichen
    76. 'Monatsumsatz
    77. If MonthIncome > MonthlyIncomeRecord.Item2 / 100 Then
    78. ShowMessageBox("Monatsumsatzrekord", MonthlyIncomeRecord.Item2.ToString("#,##0.00 €"), MonthIncome.ToString("#,##0.00 €"), Datum.ToString("MMMM yyyy"))
    79. NewRecordFound = True
    80. End If
    81. 'Monatskunden
    82. If MonthCustomers > MonthlyCustomersRecord.Item2 Then
    83. ShowMessageBox("Monatskundenrekord", MonthlyCustomersRecord.Item2.ToString("#,###"), MonthCustomers.ToString("#,###"), Datum.ToString("MMMM yyyy"))
    84. NewRecordFound = True
    85. End If
    86. 'Monatsumsatz für jede Warengruppe
    87. Dim i = -1
    88. For Each tpl In MonthlyGroupRecords
    89. i += 1
    90. If ArrProductGroupIncome(i) > tpl.Item3 / 100 Then
    91. ShowMessageBox("Umsatzrekord " & tpl.Item2.Name, (tpl.Item3 / 100).ToString("#,##0.00 €"), ArrProductGroupIncome(i).ToString("#,##0.00 €"), Datum.ToString("MMMM yyyy"))
    92. NewRecordFound = True
    93. End If
    94. Next
    95. If NewRecordFound Then CalculateMonthlyRecords()
    96. End Sub
    97. End Class


    Und noch eine Frage:
    Welche Methode ist geeigneter, wenn ich eine Boolean Variable mit dem Wert False erstellen möchte?

    VB.NET-Quellcode

    1. '1.
    2. Dim var1 as boolean
    3. 'oder 2.
    4. Dim var2 = False

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

    zur Frage: Annähernd schnurz. Der zweiten Variante kann man einen kleinen Vorzug geben, weil da ausdrücklich steht, dass die Variable False sein wird. Bei der ersten Variante weiss man das halt, dass ein uninitialisierter Boolean auf False steht.
    Für mich ists auch eine Gewohnheitssache - ich nehme die zweite Variante, wo immer es geht.
    Ich kann dir nicht gut raten, alles zu löschen. V.a. nicht, wenns tut, was es soll (inklusive Geschwindigkeit) - und davon geh ich mal aus.
    Ich blick den Code auch nicht durch - ist viel zu viel.
    Ich wäre wie gesagt ganz anderen Weg gegangen. Weil die Berechnung der Rekorde aus dem Dataset gibts ja schon - da würde ich nicht eine neue Berechnung aus Textboxen heraus implementieren.
    Sondern ich würde mir die alten Rekorde merken, den neuen Datensatz einspeichern, und die Rekorde neu rechnen lassen - und dann gucken, was sich geändert hat.

    Wirklich elegant könnte das werden, wenn du Databinding implementieren tätest - weil dann brauchste den Umstand mit "vorher merken - hinterher nachgucken" nicht, sondern kriegst ein Event gefeuert, wenn ein Rekord übertroffen wird.
    Ich setze eh immer komplett auf Databinding - aber Controls zur Laufzeit generieren passt da nicht zu.
    Wenn wolle guckst du WinForms Projektentwicklung bzgl. der Rolle des Formulars da hat im Kapitel "MVP" Vaporized ein Databinding-Sample gemacht, und in meiner Antwort ich auch eines.
    Holla
    Den verlinkten Thread von Vaporized finde ich mega interessant.
    Allerdings hebe ich mir diesen für später auf - denn nach dem intensiven lesen und verstehen, habe ich deutlich mehr Code zu ändern, als nur das Beispiel hier...
    Also mache ich mich zunächst an die von dir angesprochenen Dinge.

    ErfinderDesRades schrieb:

    Weil die Berechnung der Rekorde aus dem Dataset gibts ja schon - da würde ich nicht eine neue Berechnung aus Textboxen heraus implementieren.

    Aber das zu ich doch garnicht!?
    Bei Tagesrekorden prüfe ich, ob der Eingegebene Wert (Textbox) größer ist als der Rekord. Wenn ja - messagebox und Rekorde neu rechnen (Methode im DataSet aufrufen).
    Bei den Monatsumsätzen übergebe ich nur das Datum der Eingabe und rechne den Monatsumsatz zusammen (so wie bei der Anzeige der Monatsumsätze - ohne das in diesem Fall unnötige drumherum) und vergleiche die Werte dann entsprechend mit den RekordProperties. Hier auch, wenn Rekord gefunden Messagebox und Prozedur im DataSet aufrufen, um Rekorde neu zu rechnen.
    Es ist zeitlich nicht zu merken, dass der Code ausgeführt wird. Ich klicke auf speichern und fertig ist.
    Also lass ichs dann erstmal so und mach mich ans umbenennen.

    Aber hierzu eine Frage.
    Ich entwickle das Programm auf PC1 (EntwicklerPC) und führe es auf PC2(AusführPC) aus - mittels nUpdate.
    Auf dem AusführPC befinden sich bereits Daten im DataSet.
    Wenn ich nun die Tables im DataSet auf dem EntwicklerPC ändere und das ganze mittels nUpdate auf den AusführPC bringe, ist doch meine dortige xml nicht mehr einlesbar.
    Ich muss also die xml des AusführPC auf den EntwicklerPC bringen. Dann irgendwie eine Prozedur schreiben, die mir eine neue xml - mit aktualisieren Table Namen erstellt, damit diese nicht unbrauchbar wird.
    Gibt es hier einen Trick? Denn ich weiß nicht so genau, wie ich das anstellen soll.

    DerSmurf schrieb:


    Wenn ich nun die Tables im DataSet auf dem EntwicklerPC ändere und das ganze mittels nUpdate auf den AusführPC bringe, ist doch meine dortige xml nicht mehr einlesbar.
    Ich muss also die xml des AusführPC auf den EntwicklerPC bringen. Dann irgendwie eine Prozedur schreiben, die mir eine neue xml - mit aktualisieren Table Namen erstellt, damit diese nicht unbrauchbar wird.
    Gibt es hier einen Trick? Denn ich weiß nicht so genau, wie ich das anstellen soll.
    Jo, dassis lausig.
    Da muss wirklich die Datendatei zurück auf den Entwicklungsrechner, damit man Textersatz über den ganzen Ordner drüberlaufen lassen kann.
    Na toll... noch mehr Try and Error xD
    Nagut ich mach mich mal ran.

    Musste erstmal die neue Helpers Datei pimpen. In der die ich auf dem PC habe, hatte ich mir damals eine Funktion aus der "großen" Helpers eingebaut, um EditNew eine Tuple zu übergeben, damit ich einen Datensatz "kopieren" kann. Aber das müsste hingehauen haben. Bin dann daran gescheitert, dass ich das Framework meiner Programmes von 4.5 auf 4.6.1 anheben muss, weil die Helpers darauf entwickelt wurde.
    Hat ewig gedauert, bis ich den Fehler gefunden habe.
    Oder hast du da mittlerweile etwas anderes implementiert? So siehts bei mir aus, nach Änderung der entsprechenden Prozeduren in den Helpers.
    Spoiler anzeigen

    VB.NET-Quellcode

    1. Dim SelectedOrder = DirectCast(DirectCast(CustomerOrderBindingSource.Current, DataRowView).Row, DtsSettings.CustomerOrderRow)
    2. 'Daten des Artikels an bearbeitenForm übergeben
    3. Dim tpls As New List(Of Tuple(Of DataColumn, Object))
    4. With DtsSettings.CustomerOrder
    5. 'Werte die belegt sein müssen übergeben
    6. tpls.Add(Tuple.Create(Of DataColumn, Object)(.OrderDateColumn, System.DateTime.Now))
    7. tpls.Add(Tuple.Create(Of DataColumn, Object)(.CustomerNameColumn, SelectedOrder.CustomerName))
    8. tpls.Add(Tuple.Create(Of DataColumn, Object)(.ArtNrColumn, SelectedOrder.ArtNr))
    9. tpls.Add(Tuple.Create(Of DataColumn, Object)(.AmountColumn, SelectedOrder.Amount))
    10. CustomerOrderBindingSource.EditNew(Of frmEditCustomerOrder)(tpls)
    11. End With


    Dafür habe ich die Helpers wie folgt geändert. Habe Sorge zuviel (oder was falsch) geändert zu haben:
    Die MoveToRow habe ich eingefügt, den Rest habe ich ersetzt:

    VB.NET-Quellcode

    1. Public Module BindingSourceX
    2. ''' <summary> editiert bs.Current im angegebenen Dialog-Form. </summary>
    3. <Extension()>
    4. Public Function EditNew(Of T As {Form, New})(bs As BindingSource, defaultValues As List(Of Tuple(Of DataColumn, Object)), Optional owner As Form = Nothing, Optional onErrorRetry As Boolean = False) As DialogResult
    5. Dim drv = DirectCast(bs.AddNew, DataRowView)
    6. defaultValues.ForEach(Sub(tpl) drv(tpl.Item1.ColumnName) = tpl.Item2)
    7. Return EditItem(Of T)(bs, drv, onErrorRetry, owner)
    8. End Function
    9. ''' <summary> editiert bs.Current im angegebenen Dialog-Form. </summary>
    10. <Extension()>
    11. Public Function EditNew(Of TEditForm As {Form, New})(bs As BindingSource, Optional onErrorRetry As Boolean = False, Optional owner As Form = Nothing) As DialogResult
    12. Return EditItem(Of TEditForm)(bs, bs.AddNew, onErrorRetry, owner)
    13. End Function
    14. ''' <summary> editiert bs.AddNew im angegebenen Dialog-Form. </summary>
    15. <Extension()>
    16. Public Function EditCurrent(Of TEditForm As {Form, New})(bs As BindingSource, Optional onErrorRetry As Boolean = False, Optional owner As Form = Nothing) As DialogResult
    17. Return EditItem(Of TEditForm)(bs, bs.Current, onErrorRetry, owner)
    18. End Function
    19. Private Function EditItem(Of T As {Form, New})(bs As BindingSource, item As Object, onErrorRetry As Boolean, owner As Form) As DialogResult
    20. EditItem = DialogResult.Retry
    21. Dim tb = DirectCast(item, DataRowView).Row.Table
    22. While EditItem = DialogResult.Retry
    23. Using frm = New T
    24. tb.DataSet.Register(frm, False)
    25. Dim allCtls = New GetChilds(Of Control)(Function(ctl) ctl.Controls).AllAsList(frm)
    26. Dim bindFlags As BindingFlags = BindingFlags.Instance Or BindingFlags.Public _
    27. Or BindingFlags.NonPublic Or BindingFlags.GetField
    28. For Each ctl In allCtls.Where(Function(c) TypeOf c Is ContainerControl AndAlso TypeOf c Is Form OrElse TypeOf c Is UserControl)
    29. For Each fld In ctl.GetType.GetFields(bindFlags).Where(Function(f) f.FieldType Is GetType(BindingSource))
    30. Dim bs2 = DirectCast(fld.GetValue(ctl), BindingSource)
    31. If bs2 Is Nothing Then Continue For
    32. If tb Is bs2.DataTable Then
    33. bs2.DataSource = item
    34. Try
    35. EditItem = frm.ShowDialog(owner)
    36. If EditItem <> DialogResult.Cancel Then
    37. Try
    38. bs2.EndEdit()
    39. ' BusyDelay.SetCallback(Sub() bs2.MoveToRow(DirectCast(item, DataRowView).Row))
    40. Catch ex As Exception
    41. If Not onErrorRetry Then Throw
    42. EditItem = MessageBox.Show(ex.Message, ex.GetType.Name, MessageBoxButtons.RetryCancel)
    43. Continue While
    44. End Try
    45. End If
    46. Finally
    47. If EditItem = DialogResult.Cancel Then
    48. bs.CancelEdit()
    49. Else
    50. If DirectCast(bs.List, DataView).Sort <> "" Then bs.MoveToRow(DirectCast(item, DataRowView).Row)
    51. bs.ResetCurrentItem() 'den von der anneren BS geänderten Datensatz neu einlesen.
    52. End If
    53. End Try
    54. Exit Function
    55. End If
    56. Next
    57. Next
    58. Throw New Exception("could not find a proper BindingSource.")
    59. End Using
    60. End While
    61. End Function
    62. <Extension()>
    63. Public Function MoveToRow(bs As BindingSource, row As DataRow) As Boolean
    64. If row Is bs.At Then Return True
    65. If bs.IsBindingSuspended Then Return False
    66. Dim dv = DirectCast(bs.List, DataView)
    67. For i As Integer = 0 To bs.Count - 1
    68. If dv(i).Row Is row Then
    69. bs.CancelEdit() 'ansonsten setzter u.U. beim Moven die vorherige Row auf Rowstate.Changed
    70. bs.Position = i
    71. Return True
    72. End If
    73. Next
    74. Return False
    75. End Function

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