Neuerstellen eines umfangreichen Projektes - aus schlechtem Code mach guten Code

  • VB.NET

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

    DerSmurf schrieb:

    Nun siehst du das Auswahlfenster. Hier habe ich einfach das UCLieferungen hergenommen.
    Hmnee … Du hast dlgLieferantenAuswahl hergenommen. Ansonsten würde folgender Abschnitt keinen Sinn ergeben:

    DerSmurf schrieb:

    Hier habe ich einfach das UCLieferungen hergenommen. Ausgewählt wird nun eine Rechnung durch Klick auf den Auswählen Button oben rechts in der Ecke. Diesen Button würde ich gerne entfernen und durch einen Button ersetzen, der sich auf der UCLieferungen befindet.

    Oder vielleicht versteh ich auch das Vorgehen falsch.
    • Programmstart
    • Klick auf das [PDF]-Symbol
    • eine PDF aus dem Verzeichnis Theo\bin\Debug\Daten\Rechnungen\Lieferant 3 auswählen und öffnen
    • auf [Rechnung] klicken
    • Lieferant auswählen-Dialog erscheint, man stellt es auf Lieferant 3 (oder lässt es bei Futterkiste) und klickt auf [auswählen]
    • es kommt das Fenster Rechnung auswählen und oben rechts ist ein Button [auswählen] und darunter Buttons mit ner Diskette und nem Haken
    Und was soll jetzt (nicht) passieren?
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.
    Das Vorgehen ist soweit richtig. Es ist allerdings wurscht welche PDF du auswählst.
    Und nur zu Lieferant 3 habe ich Bestellungen gespeichert. (Nur da kannst du was Auswählen).

    Auf der Form die du am Ende siehst (bin gerade am Handy) die mit dem Button Rechnung Auswählen. Um die geht es. Hier habe ich die UCLieferungen verwendet, um eine Rechnung auszuwählen.
    Wenn du also eine Rechnung aus dem DGV auswählst, und auf Rechnung Auswählen klickst, wird die raufgezogene pfd unter dem Namen "Rechnungsnummer" vom "Rechnungsdatum" gespeichert.
    Ich hätte aber gerne dass der Code hinter dem Button "Rechnung Auswählen" (Auf der Form) ausgeführt wird, wenn der User auf den Button mit dem Haken (auf dem UC) klickt.
    Also der Haken Button soll den Rechnung Auswählen Button ersetzen, damit dieser gelöscht werden kann.
    Vielleicht versteh ich das Problem falsch, aber leg doch in dlgRechnungsAuswahl.vb folgende Methode an:

    VB.NET-Quellcode

    1. Private Sub UcLieferungen_OrderSelected() Handles UcLieferungen.OrderSelected
    2. SelectSupplierNameAndInvoiceAndCloseForm()
    3. End Sub

    Dann wird der Dialog informiert, wenn eine Bestellung selektiert wurde, also dieser Häkchen-Button gedrückt wurde. Du feuerst bisher zwar das OrderSelected-Event, aber es ist niemand da, der es hört. Aber da dlgRechnungsAuswahl eine UcLieferungen-Instanz beherbergt/hostet, kann es also einen passenden EventHandler dafür haben.
    Das BS-Current brauchst Du anscheinend gar nichtzu übergeben, da Du ja eh UcLieferungen.BSLieferant.At(Of DtsDaten.LieferantRow) hernimmst.
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.
    Oh Mist. manchmal bin ich aber auch ein Bleppo. Logisch gehts so wie du sagst (und genau das ist auch das was ich wollte).
    Ich bin daran gescheitert, weil ich innerhalb der Sub das Event abbonieren wollte - aber ich habs da nicht reinbekommen :o)
    Ich mach das jetzt noch hübsch und bastel eine Funktion dazu.
    Wenn du Zeit hättest, wäre es ein Traum, wenn du mal über meine Drag and Drop Geschichte rüber schauen könntest. (Beginnt auf der frmHaupt bei Zeile 183).
    Wenn der Upload dran ist, schreib ich noch kurz einen Edit.

    Edit: So, der Upload hängt dran. Wie immer hab ich keine Eile. Wenn du es aktuell aus Zeitgründen nicht machen kannst, oder einfach keine Lust hast (womit ich natürlich auch vollkommen Fine bin), gib bitte Bescheid (damit ich nicht umsonst warte).
    Wenn ich nix hör, warte ich einfach :o)

    Edit: und noch eine Quickfrage zum Datum.
    Ich möchte in meinen Bestellungen nur die Daten speichern (also Bestelldatum, Lieferdatum, etc.) - ohne Uhrzeit. Um nicht die von dir genannten Probleme, beim rechnen mit Daten zu bekommen.

    VB.NET-Quellcode

    1. 'Hierzu lese ich ein Datum (ohne Zeit) aus dem DTP
    2. Dim Orderdate = DTPBestelldatum.Value.date
    3. 'mach noch ein bisschen was anderes
    4. ' und speichere nur das Datum ins Dts:
    5. With SelectedOrder
    6. .Bestelldatum = Orderdate
    7. End with

    Und wenn das Control mit Datum kein DTP, sondern ein Nullable DTP ist, siehts so aus:

    VB.NET-Quellcode

    1. Dim DeliverDateIsSet = NDTPLieferdatum.Checked
    2. '...
    3. If DeliverDateIsSet Then
    4. DeliverDate = CDate(NDTPLieferdatum.Value).date
    5. DeliverDuration = DeliverDate.Subtract(Orderdate).TotalDays 'hier rechne ich ohne Uhrzeit?
    6. 'heute um 23.59 Uhr bestellt - morgen um 00:01 geliefert = Lieferzeit 1 Tag?
    7. End If
    8. If DeliverDateIsSet Then
    9. .Lieferdatum = DeliverDate.Date
    10. .Lieferzeit = DeliverDuration
    11. Else
    12. .SetLieferdatumNull()
    13. .SetLieferzeitNull()
    14. End If


    In meiner xml erscheint dann: 2022-08-26T00:00:00+02:00. Ist das +02:00 die Zeitzone (welche ja egal sein müsste), oder hab ich was verraft?
    Spoiler anzeigen
    <Lieferung>
    <ID>-23</ID>
    <Bestelldatum>2022-08-26T00:00:00+02:00</Bestelldatum>
    <Lieferdatum>2022-08-26T00:00:00+02:00</Lieferdatum>
    <Lieferzeit>-2.0646693287037037E-05</Lieferzeit>
    <Rechnungserhalt>2022-08-26T00:00:00+02:00</Rechnungserhalt>
    <Rechnungsdatum>2022-08-26T00:00:00+02:00</Rechnungsdatum>
    <Rechnungsnummer />
    <Netto19>0</Netto19>
    <Netto7>0</Netto7>
    <Gesamt>0</Gesamt>
    <Notiz />
    <LieferantID>-10</LieferantID>
    </Lieferung>
    Dateien
    • Theo.zip

      (18,23 MB, 32 mal heruntergeladen, zuletzt: )

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

    Das ist die Zeitzone plus Sommerzeitverschiebung. Im Vergleich zu Greenwich ohne Sommerzeit (also mit normaler Winterzeit) sind wir grad 2 Stunden mit der Uhrzeit weiter.

    ##########

    DerSmurf schrieb:

    heute um 23.59 Uhr bestellt - morgen um 00:01 geliefert = Lieferzeit 1 Tag?
    Nee, wenn Du mit TotalDays arbeitest, so wie es im Code schon steht, kommt für DeliverDuration ein entsprechender Wert raus: 0.0013888888888888887 = 2 Minuten

    ##########

    DerSmurf schrieb:

    Wenn du Zeit hättest, wäre es ein Traum, wenn du mal über meine Drag and Drop Geschichte rüber schauen könntest.
    Um was genau zu machen? Da Deine Projektstruktur anders ist als meine, kann ich schlecht Optimierungstipps geben. Oder ist da irgendwo ein Funktionsfehler drin?
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.

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

    Ne. Jetzt bin ich verwirrt, oder habe einen Denkfehler.
    Heute am 27.08. lege ich um 23.59 eine neue Bestellung an.
    Dann ist DTPBstDatum.Value = 27.08.2022T23:59:00 02:00
    Nun nehme ich ja aber nicht DTPBstDatum.Value, sonder .Date davon.
    Das macht doch dann den 27.08.2022T00:00:00 02:00
    Gleiches mit dem Datum des Liefereinganges (am 28.08 um 00:01 Uhr)
    DTPErhalt.Value.Date = 28.08.2022T00:00:00 02:00
    und dann ergibt doch:
    DeliverDuration = DeliverDate.Subtract(Orderdate).TotalDays = 1

    Also meine Überlegung ist folgende? Stimmt das so?

    VB.NET-Quellcode

    1. Dim Orderdate = DTPBestelldatum.Value.date ' DTP.Value = 27.08.22 23:59 Uhr | .Date davon ist 27.08.22 00:00Uhr
    2. Dim DeliverDate = CDate(NDTPLieferdatum.Value).date 'DTP.Value = 28.08.22 00:01 Uhr | .Date davon ist 28.08.22 00:00 Uhr
    3. Dim DeliverDuration = DeliverDate.Subtract(Orderdate).TotalDays '=1, wenn ich mit .Date arbeite | =0,00138, wenn ich mit den DTP.Values arbeite.


    ###########################

    Ich dachte, wenn du Code in dieser Art siehst, bzw. grob überfliegst, siehst du grobe Schnitzer im Code, oder dessen Struktur.
    Sowie bei meiner Artikelsuche. Die hast du ja bestimmt auch "nur" (will hier nix runterspielen) überflogen.
    Trotzdem kam z.B. der Vorschlag diese Suche in eine Klasse auszulagern.
    Jou, mit dem Datum hast Du natürlich recht. Unter Verwendung der reinen Datumsangaben kommt natürlich als Tagesdifferenz 1 raus.

    ##########

    Insgesamt finde ich (persönlich!), dass Deine Methoden zuviel tun. Da sind Arbeitsaufteilungen möglich, also sinnvolle Einzelschritte isolieren.
    Was Du m.E. auf jeden Fall aufteilen solltest, ist OrderOrInvoiceDropped in OrderDropped und InvoiceDropped. Ja, Teile der Sub treffen auf beide Dokumentsorten zu. Aber die solltest Du in Methoden auslagern. Wenn eine Methode zwei komplexe (!), unterschiedliche Dinge behandelt, sollte die Methode m.E. aufgespaltet werden:
    Spoiler anzeigen

    VB.NET-Quellcode

    1. Private Sub OrderDropped(DroppedFileInfo As FileInfo, DeleteOriginalFile As Boolean)
    2. Dim SupplierData = GetSupplierData(DroppedFileInfo)
    3. If SupplierData.SelectedInvoice Is Nothing Then Return
    4. Dim OrderFileData = GetOrderFileDataFrom(DroppedFileInfo, SupplierData)
    5. StoreFileInAppFileSystem(OrderFileData, DroppedFileInfo, DeleteOriginalFile)
    6. End Sub
    7. Private Function GetSupplierData(DroppedFileInfo As FileInfo) As (Suppliername As String, SelectedInvoice As DtsDaten.LieferungRow)
    8. Dim SelectedSupplierPosition As Integer = SelectSupplierByPosition()
    9. If SelectedSupplierPosition = -1 Then Return Nothing
    10. Using InvoiceSelect As New dlgRechnungsAuswahl
    11. With InvoiceSelect
    12. .UcLieferungen.BSMain.DataSource = DtsDaten
    13. .UcLieferungen.BSLieferant.Position = SelectedSupplierPosition
    14. .UcLieferungen._FireEvents = False
    15. If PossibleToShowFileInWebView(DroppedFileInfo) Then
    16. .WebView2.Source = New Uri(DroppedFileInfo.FullName)
    17. Else
    18. ShowAutoClosingMessageBox("Die Datei kann nicht angezeigt werden.")
    19. End If
    20. If .ShowDialog = DialogResult.OK Then Return (._Suppliername, ._SelectedInvoice)
    21. End With
    22. End Using
    23. Return Nothing
    24. End Function
    25. Private Function GetOrderFileDataFrom(DroppedFileInfo As FileInfo, SupplierData As (Suppliername As String, SelectedInvoice As DtsDaten.LieferungRow)) As (Targetpath As DirectoryInfo, NewFileName As String, NewFileNameWithoutExtension As String, SuccessText As String)
    26. Dim Targetpath As DirectoryInfo
    27. Dim NewFileName As String
    28. Dim NewFileNameWithoutExtension As String
    29. Dim SuccessText As String
    30. Targetpath = New DirectoryInfo($"{My.Application.Info.DirectoryPath}\Daten\Bestellungen\{SupplierData.Suppliername}")
    31. NewFileNameWithoutExtension = $"{SupplierData.SelectedInvoice.Bestelldatum.ToShortDateString}"
    32. NewFileName = $"{NewFileNameWithoutExtension}{DroppedFileInfo.Extension}"
    33. SuccessText = $"Bestellung vom {SupplierData.SelectedInvoice.Bestelldatum.ToShortDateString} von {SupplierData.Suppliername} gespeichert."
    34. Return (Targetpath, NewFileName, NewFileNameWithoutExtension, SuccessText)
    35. End Function
    36. Private Sub StoreFileInAppFileSystem(FileData As (Targetpath As DirectoryInfo, NewFileName As String, NewFileNameWithoutExtension As String, SuccessText As String), DroppedFileInfo As FileInfo, DeleteOriginalFile As Boolean)
    37. Directory.CreateDirectory(FileData.Targetpath.FullName)
    38. Dim NewFullFilePath = $"{FileData.Targetpath.FullName}\{FileData.NewFileName}"
    39. Do While File.Exists(NewFullFilePath)
    40. If Not ShowMessageBoxAndAskUserYesOrNo($"Die Datei {FileData.NewFileName} existiert bereits.{Environment.NewLine}Wollen Sie {FileData.NewFileName} umbennen?") Then Exit Do
    41. FileData.NewFileName = SetNewFilename(FileData.NewFileName, DroppedFileInfo.Extension, FileData.NewFileNameWithoutExtension)
    42. NewFullFilePath = $"{FileData.Targetpath.FullName}\{FileData.NewFileName}"
    43. Loop
    44. DroppedFileInfo.CopyTo(NewFullFilePath, True)
    45. ShowAutoClosingMessageBox(FileData.SuccessText)
    46. If DeleteOriginalFile Then DroppedFileInfo.Delete()
    47. End Sub
    48. Private Sub InvoiceDropped(DroppedFileInfo As FileInfo, DeleteOriginalFile As Boolean)
    49. Dim SupplierData = GetSupplierData(DroppedFileInfo)
    50. If SupplierData.SelectedInvoice Is Nothing Then Return
    51. Dim InvoiceFileData = GetInvoiceFileDataFrom(DroppedFileInfo, SupplierData)
    52. If InvoiceFileData.Targetpath Is Nothing Then Return
    53. StoreFileInAppFileSystem(InvoiceFileData, DroppedFileInfo, DeleteOriginalFile)
    54. End Sub
    55. Private Function GetInvoiceFileDataFrom(DroppedFileInfo As FileInfo, SupplierData As (Suppliername As String, SelectedInvoice As DtsDaten.LieferungRow)) As (Targetpath As DirectoryInfo, NewFileName As String, NewFileNameWithoutExtension As String, SuccessText As String)
    56. Dim Targetpath As DirectoryInfo
    57. Dim NewFileName As String
    58. Dim NewFileNameWithoutExtension As String
    59. Dim SuccessText As String
    60. Targetpath = New DirectoryInfo($"{My.Application.Info.DirectoryPath}\Daten\Rechnungen\{SupplierData.Suppliername}")
    61. If SupplierData.SelectedInvoice.IsRechnungsdatumNull OrElse SupplierData.SelectedInvoice.Rechnungsnummer.IsEmpty Then
    62. ShowAutoClosingMessageBox("ungültige Rechnung ausgewählt")
    63. Return Nothing
    64. End If
    65. Dim OriginalInvoiceNumber = SupplierData.SelectedInvoice.Rechnungsnummer
    66. Dim InvoiceNumber = OriginalInvoiceNumber.RemoveCharacters((Path.GetInvalidFileNameChars() & Path.GetInvalidPathChars()).ToArray.Distinct.ToArray)
    67. NewFileNameWithoutExtension = $"{InvoiceNumber} vom {SupplierData.SelectedInvoice.Rechnungsdatum.ToShortDateString}"
    68. NewFileName = $"{NewFileNameWithoutExtension}{DroppedFileInfo.Extension}"
    69. SuccessText = $"Rechnung {OriginalInvoiceNumber} vom {SupplierData.SelectedInvoice.Rechnungsdatum.ToShortDateString} von {SupplierData.Suppliername} gespeichert."
    70. Return (Targetpath, NewFileName, NewFileNameWithoutExtension, SuccessText)
    71. End Function

    Aus ca 60 sind 80 Zeilen geworden. 8|
    Und es sind nun mehr Methoden.
    Und es müssen zwischen den Methoden einige Parameter hin- und hergereicht werden.
    Aber es wird nun klarer, was genau passiert, weil die beiden Methoden OrderDropped und InvoiceDropped nur noch dem Leser mitteilen, was sie tun, nicht wie sie es tun. »Tell what, not how.«, lautet meine Devise.

    Solche Methodenaufteilungen sind häufig auch der erste Schritt, um Codeduplikate zu vermeiden, weil man ggf. weitere Bestandteile der sich unterscheidenden Methoden in gemeinsame Methoden auslagern kann. Ja, es werden dann immer mehr und immer kürzere Methoden und ja, man kann auch übertreiben. Aber man sollte es wagen. Irgendwann kommt man dann bei IOSP an. Und an dem Punkt arbeite ich dann auch mit Partial files. So ziemlich all meine Projekte haben für jedes Form viele Teildateien: In einer Datei geht es nur um GUI-Manipulation, in einer anderen nur um Integration, in der nächsten sind nur EventHandler usw.
    Das erleichtert mir extrem die Sucharbeit, weil ich weiß, wo was steht. Und wenn ich was am Programmverhalten ändern will, weiß ich schnell, wo ich was ändern muss.
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.

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

    Alles klar.
    Dann habe ich dich keinen Denkfehler und sollte beim Rechnen mit diesen Daten (und der Anzeige wann welche Lieferung vorraussichtlich eintrifft) ja keine komischen Ergebnisse bekommen.

    Kannst du zu dem anderen was sagen, oder funktioniert das nicht so, wie ich es mir denke?
    (Dass du da raufguckst und siehst was Mist ist)
    Hab grad noch ein Edit nachgeschoben :rolleyes:
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.
    Soho. Ich habe dir noch ein paar Tage extra Frei von mir gegönnt und an eher trivialen (zum größten Teil dank dir - trivialen) Dingen weiter gemacht.
    Meine Methoden (Drag and Drop)sind befördert worden. Sie machen nun sehr viel weniger und delegieren einen großteil ihrer Arbeit. Es ist mit einem Blick ersichtlich was da passiert.
    Auch wenn meine weiteren Veränderungen nicht so cool aussehen wie deine, ist es mir trotzdem recht gut gelungen - wie ich finde. (Mit deiner Version habe ich auch sehr lange zum Verstehen gebraucht - ich glaube weil mir Tuples noch nicht in Fleisch und Blut sind).
    Außerderm befindet sich die gesamte Drag and Drop Geschichte nun in einer "CodeDatei" (ist das der richtige Container?), als Partial Class

    Hinzugekommen ist die Erstellung meines "HomeScreens" (in der Datei frmHauptStartDatenLaden
    Hier habe ich sogar eine Klasse erstellt und (ich glaube) die Erstellung der Daten (also das Sammeln der Daten aus dem Dts), recht schön vom Schreiben der Daten auf die Form getrennt.
    Wenn du hier mal rüber schauen könntest, wäre das ein Traum.
    Eventuell sollte ich die Daten aus dem Dts nicht in einer List(of String) speichern, sondern in einer Tuple?

    Und eine Sache am Rande. Dein junger Padawan macht Fortschritte!!!
    Spoiler anzeigen
    Bei der Lösung eines Problems habe ich daran gedacht eine Extension zu erstellen (bzw. eine deiner Extensions zu modifizieren) und eine Tuple zurück zu geben!!111!!!einseinseins

    VB.NET-Quellcode

    1. [ <Extension> Public Function CheckForIllegalChars(Text As String, IllegalChars As String) As (ContainesIllegalChar As Boolean, IllegalChar As String)
    2. If String.IsNullOrEmpty(Text) Then Return Nothing
    3. For Each Character In Text
    4. If IllegalChars.Contains(Character) Then Return (True, Character.ToString)
    5. Next
    6. Return (False, "")
    7. End Function
    Dateien
    • Theo.zip

      (19,13 MB, 24 mal heruntergeladen, zuletzt: )
    Da war ja noch was …
    Bezüglich der Extension: Return Nothing ist bei Tupels ein Stück weit »gefährlich« (wenn man nicht weiß, was dabei rauskommt). Es gibt kein echtes Nothing bei Tupels, sondern alle Einzelwerte werden mit Default-Werten besetzt. Wenn Du das weißt, ist alles ok. Aber sowas würde nicht gehen:

    VB.NET-Quellcode

    1. If MeinText.CheckForIllegalChars(…) Is Nothing Then 'das wird nie der Fall sein!

    In ReclamationDropped verwendest Du die Tuple-Klasse, in Deiner Extension die verbesserte Version.

    DerSmurf schrieb:

    Eventuell sollte ich die Daten aus dem Dts nicht in einer List(of String) speichern, sondern in einer Tuple?
    Wo meinst Du? Ich benutze für Daten, die man längerfristig verwendet (will heißen: mehr als nur die Situation, in der eine Methode ein paar Tupelwerte zurückgibt und diese unverzüglich ausgewertet werden), immer eigene Objekte. Tupels sind mir aufgrund des Structure-Charakters (also diese nicht-Nothing-Fähigkeit) nicht für meine Programmierzwecke in großem Maße verwendbar.

    Dein Projekt wird immer größer und (für mich!) unübersichtlicher. Eine Gesamtbewertung fällt daher zwangsläufig flach.
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.
    Jau. Mich gibts auch noch :o)

    VaporiZed schrieb:

    Dein Projekt wird immer größer und (für mich!) unübersichtlicher. Eine Gesamtbewertung fällt daher zwangsläufig flach.

    Das verstehe ich natürlich. Ich werde ab jetzt meine Fragen so konkret (und Programmunabhängig) wie möglich halten. Ich habe allerdings auch den Großteil von dem was du mir hier beigebracht hast, verinnerlicht.
    Zum Start kommen hier gleich ein Paar :o)
    1. Ich habe einiges an Code aus meiner frmHaupt ausgelagert in Partial Class frmHaupt. z.B. die gesamte DragandDrop Funktionalität, oder alles was ich brauche, um die Startseite (welche sich direkt auf der Hauptform - nicht in einem UC befindet) zu beschreiben. Hierzu habe ich ein neues Element hinzugefügt und CodeDatei ausgewählt. Ist das der richtige Container für eine Partial Class, oder sollte ich einen anderen wählen?
    2. Ich bin gerade dabei eine Exportfunktion zu bauen. Hierfür habe ich eine Klasse erstellt, welche sich aus einem ausgewählten DGV die Bindingsource (eventuell die gefilterte BindingSource) und damit die Daten aus dem DataSet holt und dann auswählbare Spalten der DataTable in eine csv oder xlsx schreibt. Der Aufruf sieht so aus:

    VB.NET-Quellcode

    1. Public Class DieAufrufKlasse
    2. Private Sub keineLustNamenauszudenken
    3. '[...] DGVToExport = Das DGV, was der User gerade sieht
    4. Dim ExportClass As New Export(DGVToExport)
    5. ExportClass.CollectUserDataForDGVExport()
    6. End Sub
    7. End Class
    8. Public Class Export
    9. Private _DGVToExport As DataGridView
    10. Private _BindingSource As BindingSource
    11. Private _Dts As DtsDaten
    12. Public Sub New(DGVToExport As DataGridView)
    13. _DGVToExport = DGVToExport
    14. _BindingSource = DirectCast(_DGVToExport.DataSource, BindingSource)
    15. _Dts = DirectCast(_BindingSource.DataSource, DtsDaten) 'hier ist das Problem!
    16. End Sub
    17. 'das _Dts brauche ich, um einen Verweis in einer DataTable auf eine andere DataTable, in etwas brauchbares zum exportieren umzuwandeln. z.B. CategoryID in deren Namen:
    18. Public Function GetCategoryName(ID As Integer) As String
    19. Dim CategoryRows = _Dts.Kategorie
    20. Dim FoundCategory = CategoryRows.FirstOrDefault(Function(z) z.ID = ID)
    21. Return FoundCategory.Name
    22. End Function

    Allerdings macht diese Zeile: _Dts = DirectCast(_BindingSource.DataSource, DtsDaten) Probleme.
    Zur Laufzeit tritt ein Fehler auf, den kann ich aber gerade nicht reproduzieren (siehe 3.). Aber ich verstehe nicht warum, denn in meinen UserControls mache ich es doch genauso:
    Dim Dts = DirectCast(BSMain.DataSource, DtsDaten) in meiner Klasse ist dann eben die BSMain, die _BindingSource.
    Edit: Der Fehler ist: Das Objekt des Typs "System.Windows.Forms.BindingSource" kann nicht in Typ "Theo.DtsDaten" umgewandelt werden."
    Aber ich wandel, doch garnicht eine BindingSource um, sondern BindingSource.DataSource deren DataSource Oo
    Falls du das ausführen möchtest, eine TabPage öffnen, auf der es ein DGV gibt (z.B. das blaue @Telefon) und auf Icon Nr. 9( v.l.n.r - der Pfeil nach rechts) klicken und Tabelle exportieren auswählen.
    und 3. (hat sich erledigt)
    Spoiler anzeigen
    meine Solution ist gerade nicht lauffähig... Beim Starten kommt der Fehler: Zwei Ausgabedateinamen wurden zum selben Ausgabepfad aufgelöst: "obj\Debug\Theo.frmHaupt.resources" Theo
    Hierfür hänge ich die Solution (ohne Daten) mal ran. Ich habe nämlich keine Ahnung, was VS von mir will.
    Ich habe den Code der frmHauptDragAndDrop (Partial Class) in den Editor kopiert. Dann den Container gelöscht und neu erstellt und Code wieder einkopiert. Nun gehts wieder.
    Dateien
    • Theo.zip

      (6,92 MB, 20 mal heruntergeladen, zuletzt: )

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

    Von hinten angefangen: Lösche die frmHauptDragAndDrop.resx und es geht wieder. Wenn Du es richtig machen willst, musst Du in der Theo.vbproj angeben, dass die Datei frmHauptDragAndDrop.vb nur ein weiterer Teil von frmHaupt.vb ist. So, wie es jetzt ist, sind es zwei unabhängige Forms mit dem gleichen Klassennamen, aber in unterschiedlich benannten Dateien. In der vbproj muss stehen:

    XML-Quellcode

    1. <Compile Include="frmHauptDragAndDrop.vb">
    2. <DependentUpon>FrmHaupt.vb</DependentUpon>
    3. <SubType>Form</SubType>
    4. </Compile>
    Dann wird die Datei als Unterdatei von frmHaupt anerkannt - genauso wie die frmHaupt.Designer.vb

    Zum DGV: Der Compiler hat Recht: Im beschriebenen Beispiel ist die DataSource der BS namens BSAdressen auch eine DataSource, nämlich BSMain. Dessen DataSource ist Dein tDS.
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.

    DerSmurf schrieb:

    Ich bin gerade dabei eine Exportfunktion zu bauen.


    Wenn du damit klar kommst, dass er nicht die Daten selbst, sondern die FormattedValues aus'm DGV nimmt, dann hab ich da was fertig.
    Dem User wird bei mir sogar zur Wahl gestellt:

    - mit oder ohne Header
    - Alle Rows des DGV oder nur selektierte
    - alle oder nur ausgewählte Columns

    Die Methode kann entweder direkt eine Excel-Datei erstellen oder kopiert den Bums in die Zwischenablage.
    Sieht in der GUI dann in etwa so aus:


    Zu deinem Problem: Was spricht dagegen, der Klasse außer dem DGV auch noch das DataSet mitzugeben?
    "Na, wie ist das Wetter bei dir?"
    "Caps Lock."
    "Hä?"
    "Shift ohne Ende!" :thumbsup:
    Naja, mein Problem löst sich ja, wenn ich richtig Caste :o)
    _Dts = DirectCast(DirectCast(_BindingSource.DataSource, BindingSource).DataSource, DtsDaten)
    Ich habe mich entschieden nur das DGV zu übergeben, da ich hier ja die BS einfach auslesen kann.
    Das kann ich sicherlich auch vor dem Aufruf machen, allerdings wird die Exportfunktion von der MainForm gestartet, die DGV liegen auf UserControls innerhalb der TabPages der MainForm.
    Nun brauche ich beim Start des Exports nur prüfen, welche TabPage aktiv ist und habe das DGV:
    Spoiler anzeigen

    VB.NET-Quellcode

    1. Private Sub ExportDGV()
    2. Dim DGVToExport As DataGridView
    3. Select Case True
    4. Case TCHaupt.SelectedTab Is TPAdressen : DGVToExport = UcAdressen.DGVAdresse
    5. Case TCHaupt.SelectedTab Is TPArtikel : DGVToExport = UcArtikel.DGVArtikel
    6. Case TCHaupt.SelectedTab Is TPKundenbestellungen : DGVToExport = UcKundenbestellung.DGVKundenbestellung
    7. Case TCHaupt.SelectedTab Is TPLieferungen : DGVToExport = UcLieferungen.DGVLieferungen
    8. Case TCHaupt.SelectedTab Is TPReklamation : DGVToExport = UcReklamation.DGVReklamation
    9. Case TCHaupt.SelectedTab Is TPSonderpreis : DGVToExport = UcSonderpreise.DGVSonderpreis
    10. Case TCHaupt.SelectedTab Is TPLoginDaten : DGVToExport = UcLoginDaten.DGVLoginDaten
    11. 'TODO Einstellungen exportierbar machen
    12. Case Else
    13. ShowAutoClosingMessageBox("Hier gibt es nichts zu exportieren.")
    14. Return
    15. End Select
    16. Dim ExportClass As New Export(DGVToExport)
    17. ExportClass.CollectUserDataForDGVExport()
    18. End Sub

    Das ist dann mit relativ wenig Code erledigt. Wenn ich nun die Bindingsource mit übergebe (und diese nicht aus den DGV Eigentschaften hole), dann brauche ich dafür recht viel Code, weil teilweise verschiedene BindingSources an einem DGV hägngen können.

    Ich hatte in meinem alten Programm auch eine Exportfunktion, mit der ich die Daten aus dem DGV nehme. Das habe ich dann aber geändert, damit ich alle Spalten einer DataTable exportieren kann, nicht nur die, die im DGV sichtbar sind.
    Hast du dafür eine Lösung? Oder stellt sich das Problem bei dir nicht? Außerdem habe ich festgestellt, dass ein Export aus dem DataSet sehr viel schneller geht als aus dem DGV. (obwohl ich mehr Daten schreiben - weil mehr Spalten - war bei mir der Export von ca. 13.000 Rows, wirklich merklich schneller)
    Edit: Falls du da was testen möchtest, stelle ich dir natürlich gerne meine -soweit fertige - Exportklasse zur Verfügung. Diese sollte ja problemlos auch in anderen Projekten funtkionieren (ui, mein Code wird langsam brauchbar :o)

    VaporiZed schrieb:

    Wenn Du es richtig machen willst, musst Du in der Theo.vbproj angeben, dass die Datei frmHauptDragAndDrop.vb nur ein weiterer Teil von frmHaupt.vb ist. So, wie es jetzt ist, sind es zwei unabhängige Forms mit dem gleichen Klassennamen, aber in unterschiedlich benannten Dateien.

    Ist das im Falle von Partial Classes immer so? Oder liegt es daran, dass ich mit Codedatei einen falschen Container verwendet habe? Hätte ich z.B. ein Modul nehmen sollen?

    EditEdit: letztes Edit gelöscht. wegen Dummheit im Dienst :o)

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

    DerSmurf schrieb:

    Hast du dafür eine Lösung? Oder stellt sich das Problem bei dir nicht?


    Ich hab das "Problem" nicht, denn ich finde eine komplette DataSet-Tabelle exportieren brauchen meine User nicht (höchstens ich als Admin).
    Lässt sich aber soweit ich das beurteilen kann problemlos umbauen, denn ich arbeite mit dem Nuget EPPlus und dem übergebe ich beim Export eine DataTable ;)
    Ich hab meinen Code mal angepasst - allerdings ungetestet:

    VB.NET-Quellcode

    1. Private Sub ExportDtToExcel(dgv As DataGridView, wsn As String, Header As Boolean)
    2. Dim excelFile = saveAs("Excel-Dateien (*.xlsx)|*.xlsx")
    3. If excelFile = "" Then Return
    4. If excelFile.IsInUse Then
    5. msgInformation($"Die Datei {excelFile} ist bereits geöffnet, bitte einen neuen Dateinamen angeben.")
    6. excelFile = saveAs("Excel-Dateien (*.xlsx)|*.xlsx")
    7. Else
    8. If File.Exists(excelFile) Then File.Delete(excelFile) 'vorhandene Datei überschreiben
    9. End If
    10. Dim eFi = New FileInfo(excelFile)
    11. Using pck As New ExcelPackage(eFi)
    12. Dim wks = pck.Workbook.Worksheets.Add(wsn)
    13. wks.Cells("A1").LoadFromDataTable(DirectCast(dgv.DataSource, BindingSource).DataTable, If(Header, True, False))
    14. With wks.Cells(wks.Dimension.Address).Style.Border
    15. .Top.Style = OfficeOpenXml.Style.ExcelBorderStyle.Thin
    16. .Left.Style = OfficeOpenXml.Style.ExcelBorderStyle.Thin
    17. .Right.Style = OfficeOpenXml.Style.ExcelBorderStyle.Thin
    18. .Bottom.Style = OfficeOpenXml.Style.ExcelBorderStyle.Thin
    19. End With
    20. wks.Cells(wks.Dimension.Address).AutoFitColumns()
    21. pck.Save()
    22. End Using
    23. If msgQuestionInfo($"Der Export wurde unter {excelFile} gespeichert.{Environment.NewLine}Soll die Datei geöffnet werden?") = DialogResult.Yes Then
    24. Process.Start(excelFile)
    25. End If
    26. End Sub
    "Na, wie ist das Wetter bei dir?"
    "Caps Lock."
    "Hä?"
    "Shift ohne Ende!" :thumbsup:

    DerSmurf schrieb:

    Ist das im Falle von Partial Classes immer so? Oder liegt es daran, dass ich mit Codedatei einen falschen Container verwendet habe? Hätte ich z.B. ein Modul nehmen sollen?
    Nee, kein Modul. Codedatei passt schon. Aber wenn der Compiler in vermeintlich unabhängigen Dateien Class frmHaupt liest, kommt er offensichtlich durcheinander. Ich mach das mit den Partial Files recht ausführlich. So ziemlich jedes Form bekommt von mir um die 8 zusätzliche Partial Files, in denen jeweils thematisch was anderes passiert. Um dann die vbproj nicht selbst manuell bearbeiten zu müssen, hab ich mir natürlich ein Programm geschrieben, welches das für mich erledigt.
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.
    Gut, Dann läuft jetzt alles - der Export geht auch wie er soll.
    Ich möchte nun, dass der StatusStrip auf meiner Hauptform angezeigt wird, wenn der Export startet und verschwindet, wenn der Export fertig ist. Das habe ich mit Events gelöst:

    VB.NET-Quellcode

    1. 'Aufruf:
    2. Dim ExportClass As New Export(DGVToExport)
    3. AddHandler ExportClass.ExportStartet, AddressOf ShowStatusStrip
    4. AddHandler ExportClass.ExportFinished, AddressOf HideStatusStrip
    5. ExportClass.CollectUserDataForDGVExport()
    6. 'Die entsprechenden Subs:
    7. Private Sub ShowStatusStrip(Text As String, marquee As Boolean)
    8. If marquee Then
    9. StatusStripProgressBar.Style = ProgressBarStyle.Marquee
    10. Else
    11. StatusStripProgressBar.Style = ProgressBarStyle.Continuous
    12. End If
    13. StatusStripTextLabel.Text = Text
    14. StatusStrip1.Visible = True
    15. End Sub
    16. Private Sub HideStatusStrip()
    17. StatusStripTextLabel.Text = ""
    18. StatusStrip1.Visible = False
    19. End Sub
    20. Public Class Export
    21. Public Event ExportStartet(Text As String, marquee As Boolean)
    22. Public Event ExportFinished()
    23. '[...]
    24. Public Async Sub CollectUserDataForDGVExport()
    25. Dim ExportInfos = GetColumnsToExport()
    26. Dim FilePath = GetFilePathFromUser(ExportInfos.Exportcsv)
    27. Dim UserInputCheck = UserSelectionCorrect(ExportInfos, FilePath)
    28. If Not UserInputCheck.Correct Then
    29. ShowAutoClosingMessageBox(UserInputCheck.Errortext)
    30. Return
    31. End If
    32. If ExportInfos.Exportcsv Then
    33. RaiseEvent ExportStartet("csv Export läuft", False)
    34. Await Task.Run(Sub() StartDGVtoCSVExport(ExportInfos, FilePath))
    35. RaiseEvent ExportFinished()
    36. Else
    37. RaiseEvent ExportStartet("xlsx Export läuft", False)
    38. Await Task.Run(Sub() StartDGVtoXLSXExport(ExportInfos, FilePath))
    39. RaiseEvent ExportFinished()
    40. End If
    41. End Sub

    Alles klappt.
    1. Nun möchte ich aber in meiner Progressbar einen Fortschritt angezeigt bekommen. (Diese nutze ich bisher nur im Emailversand, dann mit Style.Marquee - deswegen der boolean im Event).
    Beim Export könnte ich ja den Fortschritt prozentuall ausrechnen: Progress = (CInt((i + 1) * 100 / (ArticleAmount + 1))).
    Aber wie stellte ich das in meiner Progressbar dar?

    2. Ich möchte auf der Progressbar einen Button darstellen, um mein Async Sub abzubrechen. Hier verstehe ich aber die Anleitungen nicht. Könntet ihr mir das hier Beispielhaft erklären?

    Aktuelle Solution hängt dran. Der Code befindet sich in der Datei Export - im Hauptordner. Was zum exportieren findet ihr in den Artiklen (grünes Icon oben).
    Dateien
    • Theo.zip

      (9,19 MB, 14 mal heruntergeladen, zuletzt: )
    1. Mit einem ButtonClick setzt Du eine neu anzulegende Boolean-Abbruch-Variable auf True. In den StartDGVto…Export-Methoden schaust Du in Deinen For-Schleifen bei jedem Schleifendurchgang, ob die Abbruchvariable auf True ist und dann verlässt Du die Methode einfach.
    2. Dafür gibt es ProgressReporter:

    VB.NET-Quellcode

    1. Private ReadOnly ProgressReporter As New Progress(Of Integer)(Sub(x) DeineGuiUpdateMethodeWelcheDieIntWerteEntgegennimmt(x))
    2. Private ReadOnly AbstractProgressReporter As IProgress(Of Integer) = ProgressReporter
    3. '…
    4. Private Sub DeineGuiUpdateMethodeWelcheDieIntWerteEntgegennimmt(aktuellerWert As Integer)
    5. LblFortschrittswert.Text = aktuellerWert.ToString ' oder eben noch ein wenig ausführlicher
    6. End Sub

    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.
    Sorry, kapiere ich nicht.
    Ich habe in der Klasse Export

    VB.NET-Quellcode

    1. Private ReadOnly ProgressReporter As New Progress(Of Integer)(Sub(x) DeineGuiUpdateMethodeWelcheDieIntWerteEntgegennimmt(x))
    2. Private ReadOnly AbstractProgressReporter As IProgress(Of Integer) = ProgressReporter

    eingefügt. Aber die Sub, die die GUI ändert muss sich doch auf der frmMain befinden. In deinem Beispiel müsste doch die Sub DeineGuiUpdateMethodeWelcheDieIntWerteEntgegennimmt innerhalb der gleichen Klasse liegen - oder Public sein.
    Wenn Sie Public ist, bekomme ich doch bestimmt Mecker, weil eine Klasse eine Sub auf der Mainform startet. Das darf doch nur andersrum sein. Oder verstehe ich was falsch?

    Ich habe mir nun folgende Lösung ergooglet, erprobiert. Passt das so, oder isses murks?

    VB.NET-Quellcode

    1. 'ExportKlasse
    2. Public ReadOnly ProgressReporter As New Progress(Of Integer)
    3. Private ReadOnly AbstractProgressReporter As IProgress(Of Integer) = ProgressReporter
    4. 'und innerhalb meiner Exportschleife:
    5. For i = 0 To ArticleAmount
    6. '[...]
    7. AbstractProgressReporter.Report(CInt((i + 1) * 100 / ArticleAmount))
    8. Next
    9. 'und auf der MainForm
    10. Private Sub Aufruf()
    11. '[...]
    12. Dim ExportClass As New Export(DGVToExport)
    13. AddHandler ExportClass.ExportStartet, AddressOf ShowStatusStripForExport
    14. AddHandler ExportClass.ExportFinished, AddressOf HideStatusStripForExport
    15. AddHandler ExportClass.ProgressReporter.ProgressChanged, AddressOf TestSub
    16. ExportClass.CollectUserDataForDGVExport()
    17. End Sub
    18. Private Sub testsub(sender As Object, e As Integer)
    19. StatusStripProgressBar.Value = e
    20. End Sub