ComplexConverter: alles in einen String und zurück

    • VB.NET
    • .NET (FX) 1.0–2.0

    Es gibt 26 Antworten in diesem Thema. Der letzte Beitrag () ist von RodFromGermany.

      ComplexConverter: alles in einen String und zurück

      Anbei ein Dingens, in das man verschiedenste Daten hineinschmeißen kann, und es konvertiert die Daten nach String und hängt sie aneinander - also es erzeugt einen Datasstring.
      Und rückwärts kann es das auch: schmeiß ihm einen Datastring hin, dann restauriert es alle Daten, von denen dieser String genommen wurde.

      Sehr praktisch, wenn man WindowState, Bounds, Checkbox-States, Splitter-Distances, Spaltenbreiten und ähnliches speichern und restaurieren will - also den Zustand eines Forms zum Zeitpunkt des Schließens.

      ComplexConverter nutzt dabei 4 Trickse:
      1. es holt sich fürs zu speichernde Objekt einen TypConverter. Also es kann alles persistieren, wofür es einen TypConverter gibt, der nach String konvertieren kann.
      2. Es feuert ein Event, in dem die Konvertierung erfolgt, und zwar in beide Richtungen 8| . Weil dank des ByRef-Features kann eine übergebene Variable sowohl ausgelesen werden als auch neu gesetzt.
      3. Dank des doppel-funktionalen Events muss er nicht merken, welcher Wert wo hingehört, denn der Code der die Werte liest ist der Code, der die Werte schreibt, und damit ist ohne jede Zusatzinformation sichergestellt, dass jeder Wert genau dahin zurück kommt, wo er hergenommen wurde :D .
      4. Rollback: Bevor ComplexConverter die Werte schreibt, feuert er sein Event erstmal im Lese-Modus, und liest den Status Quo in einen Backup-String. Schlägt anschließend das Restaurieren fehl (etwa neue zu persistierende Items wurden in den Event-Code aufgenommen), so werden alle Werte aus dem Backup wieder so gemacht, wies vorher war.

      Die Benutzung ist recht einfach: Zunächstmal Withevents deklarieren und instanzieren, und das Event behandeln.
      Das EventArg stellt v.a. 2 Methoden bereit:
      1. ConvertValue(ByRef item As Object), um einen Wert zu konvertieren
      2. .ConvertList(Of T As New)(liste As T)
        ConvertList muss den Typ kennen und erzeugen können, damit eine Liste restauriert werden kann. Es legt dann beim Restoren zunächst mal unitinialisierte Items an (etwa leere Treenodes in einer TreenodeCollection), und im weiteren können ja mit .ConvertValue() auch die Eigenschaften dieser Nodes restauriert werden.
      Eine sehr einfache Anwendung (wo evtl. das Prinzip klarer rüberkommt) kann man hier angugge:
      My.Settings.save viele viele Textboxen (und folgende)

      Hier ein komplexeres Beispiel (aus beiliegender Sample-Solution):

      VB.NET-Quellcode

      1. Public Class Form1
      2. Private WithEvents _Memory As New ComplexConverter
      3. Private Sub Form1_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
      4. ' My.Settings.Memory ist in den Anwendungseinstellungen eingerichtet
      5. _Memory.ApplyDataString(My.Settings.Memory)
      6. End Sub
      7. Private Sub Form1_FormClosing(ByVal sender As Object, ByVal e As FormClosingEventArgs) Handles Me.FormClosing
      8. My.Settings.Memory = _Memory.CreateDataString()
      9. End Sub
      10. ''' <summary>komplexe Konvertierung für dieses Form</summary>
      11. Private Sub _Memory_Convert(ByVal sender As Object, ByVal e As ComplexConverter.EventArg) Handles _Memory.Convert
      12. ' Form-Properties konvertieren
      13. e.ConvertValue(Me.WindowState)
      14. e.ConvertValue(Me.Bounds)
      15. ' Treeview konvertieren
      16. EnumerateNodes(TreeView1.Nodes, e)
      17. ' ListView-Spaltenbreiten konvertieren
      18. For Each Col As ColumnHeader In Me.ListView1.Columns
      19. e.ConvertValue(Col.Width)
      20. Next
      21. ' ListView-Items konvertieren
      22. e.ConvertList(Of ListViewItem)(ListView1.Items)
      23. For Each LVI As ListViewItem In ListView1.Items
      24. ' ListView-SubItems konvertieren
      25. e.ConvertList(Of ListViewItem.ListViewSubItem)(LVI.SubItems)
      26. For Each LVSI As ListViewItem.ListViewSubItem In LVI.SubItems
      27. e.ConvertValue(LVSI.Text)
      28. Next
      29. Next
      30. End Sub
      31. Private Sub EnumerateNodes(ByVal Nodes As TreeNodeCollection, ByVal e As ComplexConverter.EventArg)
      32. e.ConvertList(Of TreeNode)(Nodes)
      33. For Each Nd As TreeNode In Nodes
      34. e.ConvertValue(Nd.Text)
      35. EnumerateNodes(Nd.Nodes, e)
      36. ' Workaround, da Treenode.IsExpanded readonly, also nicht direkt restaurierbar
      37. If e.IsStoring Then
      38. e.ConvertValue(Nd.IsExpanded)
      39. Else
      40. If e.GetValue(Of Boolean)() Then Nd.Expand()
      41. End If
      42. Next
      43. End Sub

      Sieht viel aus, aber tut auch eine Menge, nämlich persistiert werden:
      • Form.WindowState
      • Form.Bounds
      • rekursiv alle Treenodes eines Treeviews, und zwar mit Treenode.Text und Treenode.IsExpanded
        also der Treeview ist hinterher wieder genauso aufgeklappt wie er verlassen wurde
      • alle Spaltenbreiten einer Listview
      • alle ListviewItem, inklusive ihrer SubItems


      (später Nachtrag:)
      Vergleiche mit Serialisierung
      Zunächst einmal: ComplexConverter befasst sich mit komplexen, nicht serialisierbaren Objekten (hier: ein Form).
      Man müsste also Custom-Serialisierung anwenden, d.h. das Objekt serialisierbar machen.
      Das bedeutet vor allem, man muss Speichern und Laden selbst in die Hand nehmen.
      Hier tritt nun der prinzipielle Unterschied auf, dass bei CustomSerialisierung Speichern und Laden in zwei getrennten Methoden zu behandeln ist:
      1. Speichern: hierfür ist der ISerializable.GetObjectData() - Schnittstellen-Member zu implementieren: Im 1.Spoiler des Hal2000-Beispiels, zeilen#34-#36
      2. Laden: hierfür ist ein besonderer Konstruktor zu implementieren: Im Hal-Beispiel zeilen#42-45
      Also Custom-Serialisierung muss jedes Daten-Item zweimal anfassen, unter Angabe eines String-Schlüssel, Typangabe und DirectCast.
      ComplexConverter muss nur einmal hinlangen, in seinem bidirektionalem Event, und auch nur quasi "mit dem Finger draufzeigen": e.ConvertValue(Me.Bounds) - String-Schlüssel, TypAngabe, Casts sind nicht erforderlich.

      Ein weiterer prinzipieller Unterschied ist, dass Deserialisierung immer ein neues Objekt erzeugt.
      Also Deserialisierung ist ausserstande, ein bestehendes Objekt anhand geladener Daten zu rekonfigurieren (was ComplexConverter eben macht).
      Daher muss Serialisierung, um zu Rekonfigurieren, jedes Daten-Item noch ein drittes Mal anfassen, nämlich vom deserialisierten (neu erzeugten) Objekt sind die Daten hinüber zu kopieren ins zu rekonfigurierende. Anschließend verfällt das deserialisierte Objekt - es wird nicht mehr benötigt.
      In seinem Beispiel umgeht Hal2000 diesen dritten Schritt, indem er das AnwendungsFramework deaktiviert. So kann er das normale Erstellen des MainForms entfallen lassen und das Deserialisat gleich als MainForm verwenden.
      Schlauer Trick, die Re-Konfiguration in diesem Falle zu umgehen: Im Hal2000-Beispiel Zeilen#7-#23

      Ein anderer Serialisierungs-Weg wäre natürlich, besondere serialisierbare Objekte zu schaffen.
      Dabei muss man aber ebenfalls jedes Daten-Item drei mal anfassen:
      1. Die Hilfsklasse muss geschrieben werden, und pro DatenItem einen geeigneten Platz dafür bereitstellen
      2. zum Speichern muss man vom Objekt ins HilfsObjekt übertragen
      3. zum Rekonfigurieren die Werte vom HilfsObjekt ins Ziel-Objekt rück-übertragen
      Kommt vom Aufwand her ungefähr aufs gleiche raus wie Custom-Serialisierung.

      So, und nun distanziere ich mich von der im folgenden aufgeführten Troll-Diskussion, die imo in einem Tutorial nichts verloren hat: Ein Tutorial sollte ein Konzept vorstellen und erläutern. Dazu sind Anmerkungen willkommen, und auch "gute Fragen", die zu weiteren Erläuterungen und größerer Klarheit führen.
      Ein Tutorial sollte sich aber nicht mit eingeworfenen unhaltbaren Behauptungen beschäftigen müssen.
      (my 5 ct)
      Dateien

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

      Den Vorteil würde ich auch gerne Wissen. Jeden primitive Typ kann man serialisieren, und falls man keinen primitiven Typ vor sich hat, kann man ihn in diesen umwandeln.

      Ich würde im groben gerne Wissen, welchen Vorteil dein ComplexConverter gegenüber Serialisierung hat. Das ein String raus kommt sehe ich übrigens nicht als Vorteil.
      Man merkt das du dich sehr wenig mit Serialisierung auskennst .. ;)


      C#-Quellcode

      1. new BinaryFormatter().Serialize(object, new FileStream(path, access, mode);


      C#-Quellcode

      1. var resultType = (TollesObject)new BinaryFormatter().Deserialize(new FileStream(path, access, mode);



      Das ganze kann man noch mit Generics verschönern, sodass der Cast dynamisch wird.
      Ich kann auf den ersten Blick folgenden Vorteil erkennen: Der Serialisierer von EDR kann, im Gegensatz zum BinaryFormatter, Objekte auch nur teilweise serialisieren. Das ist für folgenden (gar nicht so seltenen) Spezialfall nützlich:

      - Es ist eine Klasse vorhanden, die nicht geändert werden kann / soll
      - Die Klasse ist nicht als <Serializable()> markiert, ODER
      - Nicht alle Eigenschaften dieser Klasse sind serialisierbar, woran der BinaryFormatter scheitert.

      ABER: Es geht trotzdem mit dem BinaryFormatter, und zwar gleich auf zwei Arten.

      Möglichkeit 1 (Klasse kann geändert werden, aber Basisklasse ist nicht Serializable und nicht änderbar):
      Spoiler anzeigen

      VB.NET-Quellcode

      1. <Serializable()>
      2. Public Class Form1
      3. Implements ISerializable
      4. Private Shared filename As String = "myfile.ser"
      5. Public Shared Sub Main()
      6. Application.EnableVisualStyles()
      7. Dim f As Form1
      8. If File.Exists(filename) Then
      9. Dim bf As New BinaryFormatter()
      10. Using fs As New FileStream(filename, FileMode.Open)
      11. f = DirectCast(bf.Deserialize(fs), Form1)
      12. End Using
      13. Else
      14. f = New Form1()
      15. End If
      16. Application.Run(f)
      17. End Sub
      18. 'Speichern-Button
      19. Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
      20. Dim bf As New BinaryFormatter()
      21. Using fs As New FileStream(filename, FileMode.Create)
      22. bf.Serialize(fs, Me)
      23. End Using
      24. End Sub
      25. Public Sub GetObjectData(info As SerializationInfo, context As StreamingContext) Implements ISerializable.GetObjectData
      26. info.AddValue("bounds", Me.Bounds)
      27. End Sub
      28. Public Sub New()
      29. InitializeComponent()
      30. End Sub
      31. Public Sub New(info As SerializationInfo, context As StreamingContext)
      32. Me.InitializeComponent()
      33. Me.Bounds = DirectCast(info.GetValue("bounds", GetType(Rectangle)), Rectangle)
      34. End Sub
      35. End Class

      Möglichkeit 2 (Klasse nicht serialisierbar und nicht änderbar):
      Spoiler anzeigen

      VB.NET-Quellcode

      1. Public Class Form1
      2. Private Shared filename As String = "myfile.ser"
      3. Public Shared Sub Main()
      4. Application.EnableVisualStyles()
      5. Dim f As Form1
      6. If File.Exists(filename) Then
      7. Dim bf As New BinaryFormatter(New FormPropertySelector, New StreamingContext(StreamingContextStates.File))
      8. Using fs As New FileStream(filename, FileMode.Open)
      9. f = DirectCast(bf.Deserialize(fs), Form1)
      10. End Using
      11. Else
      12. f = New Form1()
      13. End If
      14. Application.Run(f)
      15. End Sub
      16. Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
      17. Dim bf As New BinaryFormatter(New FormPropertySelector, New StreamingContext(StreamingContextStates.File))
      18. Using fs As New FileStream(filename, FileMode.Create)
      19. bf.Serialize(fs, Me)
      20. End Using
      21. End Sub
      22. End Class
      23. Public Class FormPropertySelector
      24. Implements ISurrogateSelector
      25. Public Sub ChainSelector(selector As ISurrogateSelector) Implements ISurrogateSelector.ChainSelector
      26. Throw New NotImplementedException()
      27. End Sub
      28. Public Function GetNextSelector() As ISurrogateSelector Implements ISurrogateSelector.GetNextSelector
      29. Throw New NotImplementedException()
      30. End Function
      31. Public Function GetSurrogate(type As System.Type, context As StreamingContext, ByRef selector As ISurrogateSelector) As ISerializationSurrogate Implements ISurrogateSelector.GetSurrogate
      32. Debug.Print("GetSurrogate for {0}", type.ToString())
      33. Select Case type.Name
      34. Case "Form1"
      35. Return New FormSerializer
      36. Case "Rectangle"
      37. Return New RectangleSerializer
      38. Case Else
      39. Throw New SerializationException(String.Format("Cannot (de-)serialize type {0}", type))
      40. End Select
      41. End Function
      42. End Class
      43. Public Class RectangleSerializer
      44. Implements ISerializationSurrogate
      45. Public Sub GetObjectData(obj As Object, info As SerializationInfo, context As StreamingContext) Implements ISerializationSurrogate.GetObjectData
      46. Dim r As Rectangle = DirectCast(obj, Rectangle)
      47. info.AddValue("x", r.X)
      48. info.AddValue("y", r.Y)
      49. info.AddValue("width", r.Width)
      50. info.AddValue("height", r.Height)
      51. End Sub
      52. Public Function SetObjectData(obj As Object, info As SerializationInfo, context As StreamingContext, selector As ISurrogateSelector) As Object Implements ISerializationSurrogate.SetObjectData
      53. Dim r As Rectangle = DirectCast(obj, Rectangle)
      54. r.X = info.GetInt32("x")
      55. r.Y = info.GetInt32("y")
      56. r.Width = info.GetInt32("width")
      57. r.Height = info.GetInt32("height")
      58. Return r
      59. End Function
      60. End Class
      61. Public Class FormSerializer
      62. Implements ISerializationSurrogate
      63. Public Sub GetObjectData(obj As Object, info As SerializationInfo, context As StreamingContext) Implements ISerializationSurrogate.GetObjectData
      64. Dim f As Form = DirectCast(obj, Form)
      65. info.AddValue("bounds", f.Bounds)
      66. End Sub
      67. Public Function SetObjectData(obj As Object, info As SerializationInfo, context As StreamingContext, selector As ISurrogateSelector) As Object Implements ISerializationSurrogate.SetObjectData
      68. Dim f As New Form1 'NICHT obj casten, weil der Konstruktor nicht ausgeführt wurde
      69. f.Bounds = DirectCast(info.GetValue("bounds", GetType(Rectangle)), Rectangle)
      70. Return f
      71. End Function
      72. End Class

      Bei Möglichkeit 1 wird die Klasse teilweise serialisiert, auch wenn die Basisklasse nicht serialisierbar ist. Bei Möglichkeit 2 zwingen wir das nicht serialisierbare Objekt einfach durch externe Serialisierung dazu. Hier könnte man noch TypeConverter verwenden um nicht in tausend Surrogate-Klassen zu ersticken. Microsoft hat also wieder mal an alles gedacht.
      Gruß
      hal2000
      Aha! Langsam kommen wir der Sache näher. ThuCommix' behaupteter Einzeiler (lol) ist es zwar nicht, aber immerhin kann hier mit Serialisierung schon die Form.Bounds persistiert werden.

      Nochmal post#1 gucken - da wird in 50 Zeilen folgendes persistiert:

      ErfinderDesRades schrieb:

      • Form.WindowState
      • Form.Bounds
      • rekursiv alle Treenodes eines Treeviews, und zwar mit Treenode.Text und Treenode.IsExpanded
      • alle Spaltenbreiten einer Listview
      • alle ListviewItem, inklusive ihrer SubItems
      Beim Serialisierungs-Ansatz fehlen also noch WindowState (das wird einfach sein), die Listview-Spaltebreiten, die ListviewItem, die ListviewSubItems und alle Treenodes - ginge das überhaupt?

      Edit:
      Der Serialisierer von EDR kann, im Gegensatz zum BinaryFormatter, Objekte auch nur teilweise serialisieren. Das ist für folgenden (gar nicht so seltenen) Spezialfall nützlich:...
      ComplexConverter ist kein Serialisierer.
      Deserialisierung ist die tatsächliche Instanzierung eines neuen Objektes - ComplexConverter kann das nicht, und ist hier auch nicht gewünscht.
      Er ruft nur Werte vom Objekt ab, und packt sie wieder hin - das Objekt selbst muss bei der Deserialisierung Restaurierung schon instanziert sein.

      Der recht häufige Spezialfall, aus dem ComplexConverter entstanden ist, ist die Anforderung, total unterschiedliche, beliebig komplexe Forms beim nächsten Startup wieder so hinzustellen, wie der User sie verlassen hat.
      Und das möglichst robust und flexibel - also dass man auch im Nachhinein das Form umgestalten kann, und nur wenig oder keinen Wartungs-Aufwand an der Persistiererei hat.

      Vermutlich gibts auch weitere Anwendungsmöglichkeiten - etwa tatsächlich als eine Art "Serialisierungs-Engine" - aber hatte ich bislang keinen Bedarf für.

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

      ErfinderDesRades schrieb:

      Aha! Langsam kommen wir der Sache näher.

      Dein Selbstbewusstsein ist ja gewaltig. Spar dir sowas bitte - auch du bist nicht der Oberlehrer, und deine Lösung ist vielleicht gut, aber dennoch nicht die Weltformel. [hal2000 als User, nicht als Moderator, damit keine Missverständnisse aufkommen]

      ErfinderDesRades schrieb:

      Beim Serialisierungs-Ansatz fehlen also noch WindowState (das wird einfach sein), die Listview-Spaltebreiten, die ListviewItem, die ListviewSubItems und alle Treenodes - ginge das überhaupt?

      ThuCommix schrieb:

      ListViewItems und TreeNodes sind als Serialisierbar gekennzeichnet

      --> Also je zwei zusätzliche Zeilen dafür. Die ColumnHeaders gehen über den TypeConverter als String durch den Formatter.
      Gruß
      hal2000

      hal2000 schrieb:

      Dein Selbstbewusstsein ist ja gewaltig. Spar dir sowas bitte
      Sorry - ich meine es wörtlich: Seit Post#3 warte ich darauf, dass ThuCommix seine Behauptungen mal zu belegen versucht, aber er kriegts nicht gebacken, und für ihn freundlicherweise springst du jetzt ein
      - auch du bist nicht der Oberlehrer
      naja - das oberlehrerhafte rührt sicherlich daher, dass ich im Gegensatz zu ThuCommix argumentiere und Aussagen belege - das macht halt viele Worte.
      Es ist einfach so: Wir kommen der Sache näher, zu verstehen, um wieviel komplizierter an Aufwand und Wartungs-Aufwand der Serialisierungs-Ansatz ist - damit haben ja ThuCommix, fichz und SimpleSoft ihre Probleme.
      Das habe ich gesagt, und das stimmt ja auch - Selbstbewustsein hin oder her.

      BTT:
      Mit Serialisierung - so geschickt man es auch anstellt (und dein Ansatz ist wirklich geschickt) - muss man zur Serialisierung eines Wertes eine Zeile schreiben, und zur Deserialisierung an ganz anderer Stelle noch eine zweite, ganz andere.
      Und für die Anforderung "Gui-Restaurierung" muss man gar das Anwendungs-Framework deaktivieren und einen anderen Startup programmieren.
      Also die Code-Komplexität pro Wert ist mehr als doppelt so hoch.
      Wo ich schlicht schreibe:

      VB.NET-Quellcode

      1. e.ConvertValue(Me.Bounds)
      und die Bounds sind abgehandelt, muss Serialisierung in 2 unterschiedlichen Methoden mindestens(!!!) je eine unterschiedliche und nicht sehr intuitive Zeile coden, und die beiden Zeilen müssen exakt aufeinander abgestimmt sein:

      VB.NET-Quellcode

      1. info.AddValue("bounds", Me.Bounds) ' in GetObjectData()
      2. '...
      3. Me.Bounds = DirectCast(info.GetValue("bounds", GetType(Rectangle)), Rectangle) ' in einer Sub New-Überladung
      Und wenn du dich an die Serialisierung von Auflistungen machst, wird die Geschichte noch mal ein gut Stück komplexer, denn du musst für die Werte irgendwie eindeutige String-Schlüssel generieren, damit die Deserialisierungs richtig auspackt, was die Serialisierung eingepackt hat.



      Zum Abschluss noch mal so ein pseudo-schlauer, aber unzweckmäßiger ThuCommix-Einwurf:

      ThuCommix schrieb:

      ListViewItems und TreeNodes sind als Serialisierbar gekennzeichnet also ja, das geht.
      Das soll er bitte zeigen, wie er die Treenode-Serialisierbarkeit nutzen will, um auch den Expandier-Zustand von Treenodes zu restaurieren!

      Er tut klug, aber zu bezweifeln ist, ob er die Anforderung überhaupt irgendwie in den Griff bekäme - was sagst du eigentlich zu ThuCommix' Selbstbewusstsein?

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

      Ich habe Aussagen belegt, außerdem wollte ich am Anfang schlicht wissen, was der Vorteil ist. Du konntest mir keinen nennen, und meine Posts liest du scheinbar auch nicht, was ich höchst ignorant von dir finde. Ich habe niemals gesagt serialisierung ist besser, ich wollte nur wissen, was deine Methode besser macht. "Es funktioniert schlicht weg" ist für mich kein Argument und zeigt mir das du derjenige von uns bist, der nicht versucht zu argumentieren.

      ThuCommix schrieb:

      "Es funktioniert schlicht weg" ist für mich kein Argument
      kein Argument ?
      Ist das kein Vorteil, wenn meine Lösung - belegt! - auch im gezeigten, recht komplexen Beispiel funktioniert?

      Hingegen noch immer nicht belegt ist, ob für dieses Beispiel eine Serialisierungs-Lösung überhaupt möglich ist, und unter welchem Aufwand.

      Und du hast nunmal gar nichts belegt, es war hal2000, der zumindest einen Ansatz gezeigt hat - bei dem allerdings die Ausformung der anspruchsvolleren Restaurierung von List- und Tree-view noch aussteht.
      Aber auch dieser Ansatz, der nur ein einziges Item restauriert, zeigt schon, dass Serialisierung prinzipiell pro Item mindestens doppelt so aufwändig ist. - kein Argument? kein Nachteil?