Entwickler-Ecke

IO, XML und Registry - Daten in Datei speichern (serialisieren)


Csharp-programmierer - Fr 01.01.16 14:19
Titel: Daten in Datei speichern (serialisieren)
Hallo ihr Experten.
Ich bin gerade dabei, ein Programm über meine Einnahmen und Ausgaben zu schreiben, also sozusagen eine Finanzverwaltungssoftware für mich selbst. Auf der GUI befindet sich eine ListView. Ich habe selbst herausgefunden, wie ich die darin enthaltenen Daten speichern kann:


C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
try
            {
                string[] files = Directory.GetFiles(Path.Combine(Hauptpfad, "Finanzverwaltung"));
                foreach (string file in files)
                {
                    File.Delete(file);
                }

                foreach (ListViewItem i in this.listView1.Items)
                {
                    nummer++;
                    string s = Path.Combine(Hauptpfad, "Finanzverwaltung""Datei" + nummer.ToString());

                    if (i.SubItems[5].BackColor == Color.Red)
                        Farbe = "red";
                    else if (i.SubItems[5].BackColor == Color.Yellow)
                        Farbe = "yellow";
                    else if (i.SubItems[5].BackColor == Color.Lime)
                        Farbe = "lime";
                    else
                        Farbe = "default";

                    using (StreamWriter w = new StreamWriter(s))
                    {
                        w.WriteLine(i.Text);
                        w.WriteLine(i.SubItems[1].Text);
                        w.WriteLine(i.SubItems[2].Text);
                        w.WriteLine(i.SubItems[3].Text);
                        w.WriteLine(i.SubItems[4].Text);
                        w.WriteLine(Farbe);
                        w.Close();
                    }
                }

                nummer = 0;
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }


Hier wird jedoch immer eine neue Datei erstellt, was mir für uneffizient erscheint. Kennt ihr eine bessere Methode?
Mfg :)


Moderiert von user profile iconTh69: Topic aus Algorithmen, Optimierung und Assembler verschoben am Di 05.01.2016 um 14:53
Moderiert von user profile iconTh69: Titel geändert (war "Algorithmus optimieren").


Palladin007 - Fr 01.01.16 15:24

Du möchtest Du vorhandene Datei erweitern, ohne soe zu löschen?
Lass das bleiben, ist einfacher :D

Du kannst natürlich auch zeilenweise durch gehen und bearbeiten (bei dir müsste das noch gehen), aber sobald die Daten etwas komplexer strukturiert sind (z.B. XML), ist das praktisch unmöglich.
Die einfachste und beste Variante ist, die vorhandenen Daten zu laden, im RAM zu bearbeiten und komplett in eine Datei zu schreiben.
Solange Du nur wenige Daten hast, geht das gut.

Sobald Du so viele Daten hast, dass das im RAM auf fällt, geht das natürlich nicht mehr, aber dann hast Du schon lange die Schwelle übertreten, ab der man sagen kann: Du brauchst eine Datenbank.


Csharp-programmierer - Fr 01.01.16 18:04

Verstehen Sie unter komplexen Daten sehr große Datenmengen aus String und Decimals oder die Datenstrukturen wie string, decimal, datetime usw?

Die Idee mit dem RAM ist nicht schlecht aber damit kenne ich mich nicht aus. Ich hätte vielleicht die Idee, dass man für jedes ListViewItem eine WriteLine im StreamWriter erzeugt. Also das man in der Datei in einer Zeile dann Datum, Betrag, usw hat. Und in der anderen Zeile das selbe nur mit anderen Werten. Mein Problem ist hinsichtlich dieser Sache, dass ich nicht weiß wie man diese Daten trennt und analysiert. Oder gibt es irgendeine Klasse in C# die das Speichern/Laden der Daten einer ListView übernimmt?


Palladin007 - Fr 01.01.16 18:44

Ich rede von Objekten, die sich wieder aus Objekten und irgendwann aus den Grund-Typen zusammen setzen.
Das meine ich mit komplex. Z.B. bei Musik gibt es den Track, der in Alben bzw. Playlists organisiert ist, er hat auch eine Referenz zum Interpreten und je nachdem wie umfangreich das wird, geht noch viel mehr.

Große Datenmengen, da musst Du bei so einfach gehaltenen Daten vermutlich einige Millionen Datensätze anschleppen, damit es problematisch wird.

Und mit "Im RAM arbeiten" meine ich, die Daten einzulesen (die hast Du dann irgendwo in einer Variable), zu bearbeiten und wieder zu speichern.
Am besten wäre, wenn Du dir überlegst, wie deine Daten aussehen und dann eine Klasse dafür schreibst. Wenn Du Adressen speichern willst, würde ich z.B. eine Klasse mit Wohnort, PLZ, Straße und Hausnummer erstellen. Davon ein paar Objekte in einer Liste sind dann meine Daten, die ich direkt bearbeiten kann.
Die Tatsache, dass es irgendwo ein Objekt gibt, bedeutet, dass die Daten in diesem Objekt im RAM liegen. Somit kann ich sie im RAM bearbeiten. Nichts anderes tust Du die ganze Zeit, Du hast also sehr wohl Erfahrung damit ;)



So wie ich das sehe, ist für dich XML optimal.
.NET hat eingebaute Parser, die hervorragend und denkbar einfach funktionieren.
Der XMLSerializer [https://msdn.microsoft.com/de-de/library/system.xml.serialization.xmlserializer%28v=vs.110%29.aspx] zum Beispiel kann dir ein Objekt in XML serialisieren und umgekehrt. Der regelt auch das Lesen/Schreiben aus/in Dateien.
Einziges Hindernis ist, dass er nicht mit allen Klassen klar kommt. Das Dictionary zum Beispiel wird von Haus aus nicht unterstützt, das lässt sich aber leicht nach rüsten, gibt endlos Lösungen im Internet.
Dem XMLSerializer-Objekt gibst Du dann eine Liste mit deinen Daten und der schreibt die in eine Datei.
Beim Start des Programms schaust Du nach, ob es die Datei gibst, wenn ja liest Du auf die selbe Weise (Derserialisieren) deine Daten aus der Datei und arbeitest damit weiter. Existiert sie nicht, arbeitest Du mit einer leeren Liste, gibt ja keine Daten.


Csharp-programmierer - Fr 01.01.16 19:04

Hier ist jetzt der Klassencode:

C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
public class IO
    {
        public static IO Laden (string FileName)
        {
            string s = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Finanzverwaltung");
            string path = Path.Combine(s, FileName);
            if(Directory.Exists(path))
            {
                using(FileStream stream = new FileStream(path, FileMode.Open))
                {
                    XmlSerializer xml = new XmlSerializer(typeof(IO));
                    return (IO) xml.Deserialize(stream);
                }
            }
            else
            {
                return (new IO());
            }
        }

        public void Speichern(string FileName)
        {
            string s = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Finanzverwaltung");
            string path = Path.Combine(s, FileName);

            using(FileStream fs = new FileStream(path, FileMode.Create))
            {
                XmlSerializer ser = new XmlSerializer(typeof(IO));
                ser.Serialize(fs, this);
            }
        }

        public string Datum { get; set; }
        public decimal AlterBetrag { get; set; }
        public string Art { get; set; }
        public decimal Betrag { get; set; }
        public string Beschreibung { get; set; }
        public decimal NeuerBetrag { get; set; }
        public List<IO> Objekte = new List<IO>();
    }


Das Problem jetzt jedoch ist, letztendlich habe ich ja den selben Salat wie vorher. Für jeden Eintrag in der ListView eine eigene XML- Datei. Das ist zwar jetzt nicht so im Code, aber das schlussfolgert sich ja daraus. Und ich weiß auch nicht, wie man die Liste Objekte mit Objekten füllt.
Kannst du mir bitte weiterhelfen?


Ralf Jansen - Fr 01.01.16 19:31

Du möchtest eine List<IO> serialisieren nicht IO. Das gleiche Vorgehen das ich dir per PN in einem anderen Context bereits nahe gelegt habe. Eigentlich ist es haargenau die gleiche Problematik ob etwas ein Person ist oder ein Finanzdatensatz ist aus Softwaresicht völlig egal.

Ganz allgemein hängst du hier weil du dein Anwendung ausgehend von den Metaproblemen aus lösen willst. Das macht es nicht nur unnötig schwer sondern fast unmöglich am Ende bei was sinnvollem zu landen.
Du möchtest Datenanzeigen und Daten wegschreiben bringst die beiden Probleme in Beziehung und willst dann noch irgendwie deine Anwendunglogik ("Finanzverwaltung") dazwischen lügen.

Versuche zuerst allgemein das Finanzverwaltung-Problem darzustellen und schreibe dir dazu ein Klassenmodell aus Daten und Logikbestandteilen. Das muss nicht von Anfang funktionieren und kann hier und da aus Platzhaltern bestehen es ist aber letztlich der Kern deiner Anwendung. Woraus die genau besteht kann ich dir nicht sagen. Konten, Buchungen was auch immer Finanzverwaltung ist für mich erstmal nur ein Wort. Danach kannst du dich dann um die anderen Probleme kümmern. Zum Beispiel wie man das speichert oder wie man das in einer Oberfläche anzeigt. Spätestens dann wird dir auffallen das das die beiden Probleme völlig unabhängig voneinander sind und einzeln eher simpel lösbar sind. Nur wenn man die beiden Problem frühzeitig logisch miteinander vermischt sehen die nach einem Hindernis aus.


Csharp-programmierer - Fr 01.01.16 20:11

Gehr das so? Ich habe noch nie mit einer Liste von Objekten gearbeitet.

C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
public static IO Laden (string FileName)
        {
            string s = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Finanzverwaltung");
            string path = Path.Combine(s, FileName);
            if(Directory.Exists(path))
            {
                using(FileStream stream = new FileStream(path, FileMode.Open))
                {
                    XmlSerializer xml = new XmlSerializer(typeof(IO));
                    return (IO) xml.Deserialize(stream);
                }
            }
            else
            {
                return (new IO());
            }
        }


Aber wie weise ich der Liste Werte zu?


Palladin007 - Fr 01.01.16 20:35

Hast Du gelesen und verstanden, was Ralf geschrieben hat?
Das hat einen Sinn und es ist sehr wichtig, dass Du lernst, ein großes Problem in immer kleinere Teil-Probleme zu trennen, sonst übernimmst Du dich mit jedem noch so kleinen Projekt.


Hinzufügen von Elementen zu einer Liste geht mit der Add-Methode.
Das lässt sich auch mit ein wenig Hirnschmalz selber heraus finden. Google und die Intellisense-Hilfe von Visual Studio helfen da hervorragend.
Auch das ist sehr wichtig: Wir haben heute das Internet und viele Hilfsmittel, die gab's früher nicht. Nutze sie, das nimmt sehr viel Arbeit ab.


Csharp-programmierer - Fr 01.01.16 21:32

Naja das mit der Add-Methode wusste ich. Ist ja logisch aber eine Liste beinhaltet ja mehrere Objekte. Ich habe ja aber nur ein Objekt(IO). Aber wenn ihr mir sagt, ich müsste eine Liste anlegen, heißt das ja die Liste muss mehrere Objekte beinhalten. Aber wieso soll ich das Objekt IO mehrmals in die Liste einfügen? Was hat das für ein Sinn?


Palladin007 - Fr 01.01.16 21:36

Den Sinn können wir nur erkennen, wenn wir dein Vorhaben kennen.

Grundsätzlich ist es aber in den meisten Fällen zemlich sinnlos, ein Objekt mehrmals in der selben Liste zu haben.
Das meine ich auch nicht, sondern dass Du die Objekte, die Du speichern willst - alle - in eine Liste legst.
Die speicherst Du dann auf einen Schlag und hast alle Objekte in einer Datei gespeichert.


Csharp-programmierer - Fr 01.01.16 21:43

In meiner ListView sind folgende Columns:
- Datum
- alter Netrag
- Art
- Einzahlung / Auszahlung
- neuer Betrag
- Beschreibung

Wie kann ich diese Daten in eine Liste adden?


Ralf Jansen - Fr 01.01.16 21:50

- Erstell eine Klasse die diese Daten in entsprechenden Properties hält.
- Erstelle eine Liste von dieser Klasse. Eine Liste, wenn dir das noch unklar ist, ist einfach auch eine Klasse. Im Framework gibts dazu zum Beispiel List<T> [https://msdn.microsoft.com/de-de/library/6sh2ey19.aspx] als Listenklasse. Diese Listenklasse hat dann für alle denkbaren Bearbeitungen bezüglich einer Liste entsprechende Methoden (Add, Remove, Find etc.)


Csharp-programmierer - Di 05.01.16 14:47

Hallo. Ich habe es jetzt mit einem Konstruktor versucht:

C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
public class IO
    {
        private string datum, beschreibung ,art;
        private decimal alterBetrag, betrag, Neuerbetrag;
        public List<IO> Liste1 = new List<IO>();

        public IO (string datum, decimal alterbetrag, string art, decimal betrag, decimal neuerbetrag, string beschreibung)
        {
            this.datum = datum;
            this.beschreibung = beschreibung;
            this.alterBetrag = alterbetrag;
            this.betrag = betrag;
            this.Neuerbetrag = neuerbetrag;
        }

        public static IO Laden (string FileName)
        {
            string s = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Finanzverwaltung");
            string path = Path.Combine(s, FileName);
            if(Directory.Exists(path))
            {
                using(FileStream stream = new FileStream(path, FileMode.Open))
                {
                    XmlSerializer xml = new XmlSerializer(typeof(IO));
                    return (IO) xml.Deserialize(stream);
                }
            }
            else
            {
                return null;
            }
        }

        public void Speichern(string FileName)
        {
            string s = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Finanzverwaltung");
            string path = Path.Combine(s, FileName);

            foreach (IO io in Liste1)
            {
                using (FileStream fs = new FileStream(path, FileMode.Create))
                {
                    XmlSerializer ser = new XmlSerializer(typeof(IO));
                    ser.Serialize(fs, io);
                }
            }
        }

        public string Datum { get; set; }
        public decimal AlterBetrag { get; set; }
        public string Art { get; set; }
        public decimal Betrag { get; set; }
        public string Beschreibung { get; set; }
        public decimal NeuerBetrag { get; set; }

        public static string defaultPath
        {
            get
            {
                return (Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Finanzverwaltung"));
            }
        }
    }


Speichern:

C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
private void toolStripButton1_Click(object sender, EventArgs e)
        {
            foreach(ListViewItem i in this.listView1.Items)
            {
                //string datum, decimal alterbetrag, string art, decimal betrag, decimal neuerbetrag, string beschreibung
                decimal alterbetrag;
                if(Decimal.TryParse(i.SubItems[1].Text, out alterbetrag))
                    xab = alterbetrag;

                decimal cbetrag;
                if(Decimal.TryParse(i.SubItems[3].Text, out cbetrag))
                    xbe = cbetrag;

                decimal cneuer;
                if(Decimal.TryParse(i.SubItems[5].Text, out cneuer))
                    xnb = cneuer;
                IO io = new IO(i.Text, alterbetrag, i.SubItems[2].Text, cbetrag, cneuer, i.SubItems[5].Text);
                io.Liste1.Add(new IO(i.Text, alterbetrag, i.SubItems[2].Text, cbetrag, cneuer, i.SubItems[5].Text));
                io.Speichern("Datei 1");
            }


Aber ich bekomme einen Fehler, dass dieses Objekt nicht serialisiert werden kann :(


Th69 - Di 05.01.16 15:51

Hallo,

irgendwie scheinst du das mit der Liste falsch verstanden zu haben. M.E. macht es keinen Sinn, innerhalb der IO-Datenklasse (btw. eigenartiger Name für ein Datenobjekt!) die Liste zu verwalten.
Wenn du eine Liste serialisieren möchtest, dann benötigst du zwei Klassen:
1. eine Klasse, welche die Liste von Datenobjekten hält und die Serialisierungsmethoden (Laden, Speichern) bereitstellt
2. ein Datenklasse

Oder anders ausgedrückt: packe die Daten aus deiner IO-Klasse in eine eigene Klasse.

PS: Ich habe den Beitrag mal ins C#-Forum verschoben und den Titel angepaßt.


C# - Di 05.01.16 16:05

Hallo,

also meines Wissens brauchst du zum serialisieren eine Klasse mit einem Standardkonstruktor, also dem Konstruktor, der keine Parameter hat. Das geht desshalb nur, weil der Serializer ein neues Objekt des Datentyps anlegen muss (in deinem Fall ist der Datentyp deine IO-Klasse). Wenn der Konstruktor aber Parameter hat, woher soll dann der Serializer wissen, was für Argumente er dem Konstruktor übergeben muss? Aber das nur mal so neben bei.


Deine ListView hat ja mehrere Einträge (die Items). Jeder Eintrag hat Details (SubItems). Zum Beispiel: deine ListView beinhaltet alle Schüler aus einer Klasse. Dann ist jedes Item ein Schüler und die Details sind dann wiederum Infos zu den Schülern (Namen, Alter, Wohnort, Noten, ...).

Was du jetzt getan hast, ist eine Klasse zu erstellen, die die ganze ListView beinhaltet (dein IO). In diesem IO willst du alle Daten aus der ListView übernehmen. In meinem Beispiel entspräche das einem Objekt, welches die ganze Klasse (hier meine ich die oben erwähnte Schulklasse, nicht die C# Klasse) beinhaltet. Das brauchst du aber nicht. Denn eine Schulklasse kann ja unterschiedlich viele Schüler haben (also kann deine ListView ja auch unterschiedlich viele Items haben).
Jetzt geht es erst mal darum, den richtigen Datentyp zu definieren (eine C# Klasse). Dabei gehst du vom obersten Layer los und arbeitest dich nach unten durch. Um auf mein Beispiel zurück zu kommen, sähe das ganze so aus:

1. Layer: Schulklasse
Deine Schulklasse ist einfach nur eine Ansammlung von beliebig vielen Schülern. Da die Schulklasse (außer ihren Schülern) keine zusätzlichen Infos besitzt, kannst du schon einen vorhandenen Datentyp benutzen - eine Liste. Also nimmst du für die Schulklasst eine List<Schüler>.
2. Layer: Der Schüler
Dein Schüler besitzt nun mehrere Infos. Jeder Schüler hat einen Namen, ein Alter und ein Wohnort. Also erstellst du dir eine Klasse mit dem Namen Schüler. In dieser Klasse legst du dann Variablen für Name, Alter und Wohnort an. Da der Name ein String ist und das Alter ein Integer sind diese Datentypen klar, also ist die Variable Name ein String und die Variable Alter ein Integer. Doch was ist Wohnort? Der Wohnort ist eine Adresse. Da es keinen Datentyp für sowas gibt, machst du wieder einen.
3. Layer: Die Adresse
Die Adresse besteht bekanntlich aus Ort, Postleitzahl, Straße und Hausnummer. Also erstellst du wieder eine C# Klasse und nennst sie Adresse. In dieser Klasse legst du die Variablen für Ort, Postleitzahl, Straße und Hausnummer an. Deren Datentypen sind wiederum alles Strings, also sind diese vier Variablen alle vom Typ String.

Jetzt hast du deine komplette Schulklasse in C# Code definiert. Nun musst du nur noch den 1. Layer speichern (man nennt das auch Wurzelelement oder Rootelement). Da der 1. Layer den 2. Layer beinhaltet und der 2. Layer wiederum den 3. Layer hat, werden alle Layer gespeichert.
Da der 1. Layer die Schulklasse ist und wir gesagt haben, dass die Schulklasse eine List<Schüler> ist, musst du auch genau das als Typ an den Serializer übergeben. Also wird letztendlich eine List<Schüler> gespeichert.

Ich hab mal schnell was gezeichnet um es verständlicher zu machen:
SerializerSerializer2

Dein Serializer ist dann einfach nur eine Hilfsklasse, die den XmlSerializer verwendet um die Daten zu speichern (wie Th69 gerade geschrieben hat).

Ich hoffe, ich konnte dir dein Problem und die Lösung nun besser verdeutlichen.


Blup - Di 05.01.16 18:27

Diese Spalten "AlterBetrag" und "NeuerBetrag" gehören nicht unbedingt in das Datenobjekt.
Der Wert dieser Spalten dient nur der Anzeige und wird aus der Liste auf Grundlage der vorherigen Buchungen ermittelt.


Csharp-programmierer - Di 05.01.16 20:47

Ich glaube ich hab es jetzt verstanden:

C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
namespace Geldverwaltung
{
    public class IO
    {
        public List<IO> Liste1 = new List<IO>();

        public static IO Laden (string FileName)
        {
            string s = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Finanzverwaltung");
            string path = Path.Combine(s, FileName);
            if(Directory.Exists(path))
            {
                using(FileStream stream = new FileStream(path, FileMode.Open))
                {
                    XmlSerializer xml = new XmlSerializer(typeof(IO));
                    return (IO) xml.Deserialize(stream);
                }
            }
            else
            {
                return null;
            }
        }

        public void Speichern(string FileName)
        {
            string s = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Finanzverwaltung");
            string path = Path.Combine(s, FileName);

            foreach (IO io in Liste1)
            {
                using (FileStream fs = new FileStream(path, FileMode.Create))
                {
                    XmlSerializer ser = new XmlSerializer(typeof(IO));
                    ser.Serialize(fs, io);
                }
            }
        }
        public static string defaultPath
        {
            get
            {
                return (Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Finanzverwaltung"));
            }
        }
    }
}


Datenklasse:


C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
namespace Geldverwaltung
{
    class Datenklasse
    {
        private string datum1, art1, beschreibing1;
        private decimal alterbetrag1, betrag1, neuerbetrag1;

        public Datenklasse(string datum, decimal alterbetrag, string art, decimal betrag, decimal neuerbetrag, string beschreibung)
        {
            this.datum1 = datum;
            this.art1 = art;
            this.beschreibing1 = beschreibung;
            this.alterbetrag1 = alterbetrag;
            this.betrag1 = betrag;
            this.neuerbetrag1 = neuerbetrag;
        }

        public string Datum { get; set; }
        public string Art { get; set; }
        public string Beschreibung { get; set; }
        public decimal AlterBetrag { get; set; }
        public decimal Betrag { get; set; }
        public decimal NeuerBetrag { get; set; }
    }
}


Aber wie verknüpfe ich diese Klassen jetzt?


Th69 - Di 05.01.16 21:16

Hallo,

du möchtest doch eine Liste der Daten haben, also

C#-Quelltext
1:
public List<Datenklasse> Liste1 = new List<Datenklasse>();                    

Und deine Serialisierung mußt du auch noch so anpassen, daß du List<Datenklasse> benutzt (und die foreach-Schleife ist dort auch überflüssig - du willst doch die Liste als Ganzes serialisieren und nicht jedes Element einzeln [und erst recht nicht alle in die selbe Datei (über-)schreiben]!).
Den Code solltest du jetzt selber dafür hinkriegen.


Ralf Jansen - Di 05.01.16 21:24

Die Datenklasse Klasse wird so nicht funktionieren. Der Konstruktor füllt private Felder an die du von außen nicht mehr rankommst. Die haben mit den Properties nix zu tun. Lass die Felder weg udn fülle direkt die Properties (aka Datum = datum; im Konstruktor u.s.w.)

Wenn IO dann die Klammer für die Datenklasse sein soll dann hilft dir die List<IO> nicht. Ein Liste von sich selbst ist meist merkwürdig IO hat eine List evon IO wo jedes Element wieder eine Liste von IO hat etc. . Du willst in IO eine Liste von Datenklasse und zwar auch eine Property und kein public Field. Die meisten Serializer serialisieren nur Properties.

Also eher

C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
public class IO
{
    public List<Datenklasse> Datenklassen { get; set; }

    public IO()
    {
        Datenklassen = new List<Datenklasse>();
    }
}


In Laden()/Speichern() kannst du dann ganz simpel einfach IO serialisieren/deserialisieren lassen. Deine Liste ist ja einfach eine Property der Klasse und der Serialisierer der sich die Properties schnappt weiß was er bei einer Liste zu tun hat. Du musst da nix besonderes tun.


Csharp-programmierer - Mi 06.01.16 13:03

Ich habe es jetzt so gemacht, bekomme aber bei dem Bezeichner der Liste einen Fehler: Inkonsistenter Zugriff: Eigenschaftentyp ist weniger zugriffbar als Eigenschaft Geldverwaltung.IO.Datenklassen


C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
namespace Geldverwaltung
{
    public class IO
    {
        public List<Datenklasse> Datenklassen { get; set; }
        public IO ()
        {
            Datenklassen = new List<Datenklasse>();
        }
        public static IO Laden (string FileName)
        {
            string s = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Finanzverwaltung");
            string path = Path.Combine(s, FileName);
            if(Directory.Exists(path))
            {
                using(FileStream stream = new FileStream(path, FileMode.Open))
                {
                    XmlSerializer xml = new XmlSerializer(typeof(IO));
                    return (IO) xml.Deserialize(stream);
                }
            }
            else
            {
                return null;
            }
        }

        public void Speichern(string FileName)
        {
            string s = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Finanzverwaltung");
            string path = Path.Combine(s, FileName);

            using (FileStream fs = new FileStream(path, FileMode.Create))
            {
                XmlSerializer ser = new XmlSerializer(typeof(IO));
                ser.Serialize(fs, Datenklassen);
            }
        }
        public static string defaultPath
        {
            get
            {
                return (Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Finanzverwaltung"));
            }
        }
    }
}


Datenklasse:

C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
namespace Geldverwaltung
{
    class Datenklasse
    {
        public Datenklasse(string datum, decimal alterbetrag, string art, decimal betrag, decimal neuerbetrag, string beschreibung)
        {
            Datum = datum;
            Art = art;
            Beschreibung = beschreibung;
            AlterBetrag = alterbetrag;
            Betrag = betrag;
            NeuerBetrag = neuerbetrag;
        }

        public string Datum { get; set; }
        public string Art { get; set; }
        public string Beschreibung { get; set; }
        public decimal AlterBetrag { get; set; }
        public decimal Betrag { get; set; }
        public decimal NeuerBetrag { get; set; }
    }
}


Speichernbutton:

C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
private void toolStripButton1_Click(object sender, EventArgs e)
        {
            foreach(ListViewItem i in this.listView1.Items)
            {
                //string datum, decimal alterbetrag, string art, decimal betrag, decimal neuerbetrag, string beschreibung
                decimal alterbetrag;
                if(Decimal.TryParse(i.SubItems[1].Text, out alterbetrag))
                    xab = alterbetrag;

                decimal cbetrag;
                if(Decimal.TryParse(i.SubItems[3].Text, out cbetrag))
                    xbe = cbetrag;

                decimal cneuer;
                if(Decimal.TryParse(i.SubItems[5].Text, out cneuer))
                    xnb = cneuer;
                Datenklasse daten = new Datenklasse(i.Text, alterbetrag, i.SubItems[2].Text, cbetrag, cneuer, i.SubItems[5].Text);
                IO funktionen = new IO();
                funktionen.Datenklassen.Add(daten);
                funktionen.Speichern("Datei 1");
            }
        }


Ralf Jansen - Mi 06.01.16 13:11


C#-Quelltext
1:
class Datenklasse                    


Du hast keine Sichtbarkeit angegeben. Ohne explizite Angabe ist die Sichtbarkeit erstmal internal und somit nur von innerhalb der gleichen Assembly nutzbar. Der Serializer kommt aus dem Framework und gehört nicht zu deiner Assembly sondern halt zu den Framework Assemblies. Für denn ist Datenklasse nicht erreichbar. Datenklasse sollte public sein (nicht nur wegen dem Serializer). Du solltest dir auch angewöhnen immer die Sichtbarkeit anzugeben auch wenn es einen default gibt.


Csharp-programmierer - Mi 06.01.16 13:29

Dankeschön :) der Fehler ist nun behoben.
Aber leider Gottes kommt jetzt ein Fehler beim Speichern :autsch: :flehan:

Bei dem Speicherncode bei typeof(IO) kommt nun der Fehler: Fehler beim Reflektieren des Typs 'Geldverwaltung.IO'.

Ich habe es auch schon statt IO mir Datenklassen und Datenklasse probiert. Nichts funktioniert :(


Ralf Jansen - Mi 06.01.16 13:36

Zitat:
XmlSerializer ser = new XmlSerializer(typeof(IO));
ser.Serialize(fs, Datenklassen);


Was für ein Typ ist Datenklassen? List<Datenklasse>! und was hast du gesagt möchtest du serialisieren IO! Wer ist IO wenn du in einer IO Klasse bist. this!


Th69 - Mi 06.01.16 13:57

Ich will ja nicht verwirren, aber ich fände es besser (wie schon geschrieben) List<Datenklasse> zu serialisieren und nicht die Hilfsklasse IO (ich finde die beiden Namen immer noch sehr unglücklich gewählt!).

Edit: Mit wenig Änderung an der Klasse IO könnte man daraus dann nämlich eine generische Klasse machen, welche jeden beliebigen Typ serialisieren kann ;-)


Csharp-programmierer - Mi 06.01.16 13:58

Entschuldigung. Ich stelle mich gerade echt dumm an aber ich habe jetzt folgendes gemacht:

C#-Quelltext
1:
2:
XmlSerializer ser = new XmlSerializer(typeof(IO));
ser.Serialize(fs, this);

Jetzt kann das Objekt nicht reflektiert werden :motz: :angel:


Ralf Jansen - Mi 06.01.16 14:37

Das ist jetzt so richtig.

Was jetzt noch fehlt ist tricky und kann man fast nicht wissen sondern muß es schmerzlich lernen ;) Du hast der Klasse einen Konstruktor gegeben. Damit hat die keinen automatischen Standardkonstruktor mehr denn bekommt man nur wenn man sonst keinen Konstruktor hat. Ein new Datenklassen() geht also nicht mehr. Wie erzeugt der XmlSerializer die Klasse er erzeugt die über den Standardkonstruktor und befüllt danach dessen Properties. Datenklasse hat aber keinen ;) Gib der Datenklasse also einen Standardkonstruktor (einen ohne Parameter) oder entferne den mit Parameterm um den automatischen Standardkonstruktor zu bekommen.

Typischer Fall von ist alles ganz simpel .... wenn man es schon weiß ;)


Zitat:
Edit: Mit wenig Änderung an der Klasse IO könnte man daraus dann nämlich eine generische Klasse machen, welche jeden beliebigen Typ serialisieren kann ;-)


Darum hab ich ihm geraten eine Klasse zu machen und die nicht von der Liste abzuleiten sondern diese als Property hinzuzufügen damit er die später um weitere Properties erweitern kann. Das in seiner Anwendung nur die Datenklasse(n) zu speichern sind ist ja eher unwahrscheinlich ;) Da jetzt noch um IO eine generischen Serializer-kapsel drumzulegen um laden/speichern rauszuziehen wäre ein Boni für später. Und ja die Namen sind nicht nur unglücklich sondern scheiXXXe. Aber so wie er entwickelt hat "ich packe das was ich auf dem Bildschirm sehe in eine Klasse" verhindert die gute Benamsung der Klassen. Wenn wir irgendwas übliches aus der Problemwelt nehmen würden um sie als Bezeichner der Klassen zu nehmen (Einnahmen/Ausnahmen/Saldo/Buchungen oder sowas) würde das nicht passen und wäre noch problematischer als schlechte generische Namen. Seine Klassen bilden einfach noch kein benennbares Problem ab. Für später sollte man das redesignen zum Beispiel die Datenklasse durch ein Buchungsklasse ersetzen. Dazu müßte man aber dann auch z.B. bemerken das AlterBetrag/NeuerBetrag keine tatsächliche Eigenschaften der Klasse(n) sind sondern berechnete Größen aus der Menge der Buchungen.


Csharp-programmierer - Mi 06.01.16 14:50

Ich habe jetzt den Konstruktor der Klasse entfernt. Aber new List<Datenklasse>(); fällt ja jetzt weg. Daher kann ich dieser Liste nichts mehr zufügen. Aber jetzt kommt der Fehler: der Objektverweis wurde nicht auf eine Objektinstanz festegelegt.

Und wenn ich trotzdem new List<Datenklasse>(); schreibe, kommt der Fehler mit dem Reflektieren auf :crying: :crying: :crying: :crying: :crying:


Palladin007 - Mi 06.01.16 15:02

Nicht der Konstruktor von IO muss entfernt werden, der hat keine Parameter, an denen sich der Serializer stören würde.
Es geht um Datenklasse, woher soll der Serializer denn wissen, welche Parameter der Konstruktor bekommt?

Das erklärt auch das Verhalten. Die Liste, die nie zugewiesen wurde, wird vorher verwendet, er kommt also nie zum speichern.
Schreibst Du den Konstruktor, wo die Liste gesetzt wird, wieder rein, dann kommt dein Programm dort weiter und schafft es bis zum Speichern, wo der alte Fehler noch wartet.


Ralf Jansen - Mi 06.01.16 15:09

keine Ahnung was dein problem ist.

Ich habe deinen Code jetzt mal genommen (+ die besprochenen Änderungen) und das geht.
Laden war noch Buggy, du hast geprüfst mit Directory.Exists ob ein File existiert und ich habe zusätzlich in speichern noch ein Directory.CreateDirectory(s) ergänzt.



C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Serialization;

namespace ConsoleApplication35
{
    class Program
    {
        static void Main(string[] args)
        {
            Datenklasse daten = new Datenklasse() { Datum = DateTime.Now.ToString(), Art = "Hallo", Beschreibung = "Welt", AlterBetrag = 0.0m, Betrag = 5.5m, NeuerBetrag = 5.5m };
            IO funktionen = new IO();
            funktionen.Datenklassen.Add(daten);
            funktionen.Speichern("Datei.xml");

            IO funktionen2 = IO.Laden("Datei.xml");
        }
    }

    public class Datenklasse
    {        
        public string Datum { get; set; }
        public string Art { get; set; }
        public string Beschreibung { get; set; }
        public decimal AlterBetrag { get; set; }
        public decimal Betrag { get; set; }
        public decimal NeuerBetrag { get; set; }
    }

    public class IO
    {
        public List<Datenklasse> Datenklassen { get; set; }
        public IO()
        {
            Datenklassen = new List<Datenklasse>();
        }
        public static IO Laden(string FileName)
        {
            string s = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Finanzverwaltung");
            string path = Path.Combine(s, FileName);
            if (File.Exists(path))
            {
                using (FileStream stream = new FileStream(path, FileMode.Open))
                {
                    XmlSerializer xml = new XmlSerializer(typeof(IO));
                    return (IO)xml.Deserialize(stream);
                }
            }
            else
            {
                return null;
            }
        }

        public void Speichern(string FileName)
        {
            string s = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Finanzverwaltung");
            Directory.CreateDirectory(s);
            string path = Path.Combine(s, FileName);

            using (FileStream fs = new FileStream(path, FileMode.Create))
            {
                XmlSerializer ser = new XmlSerializer(typeof(IO));
                ser.Serialize(fs, this);
            }
        }

        public static string defaultPath
        {
            get
            {
                return (Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Finanzverwaltung"));
            }
        }
    }
}


Csharp-programmierer - Mi 06.01.16 15:25

Ja. Jetzt haut es hin. Ich wusste nicht, dass man den Feldern (strings) auch ohne Konstruktor sofort einen Wert zuweisen muss. Jetzt tüftle ich mir nur noch einen Algorithmus zusammen, der den Datein noch entsprechende Nummern gibt und dann sollte alles hin hauen. Aber es gibt jetzt nicht wirklich einen großen Unterschiedn zwischen dem von mir vorher benutzten StreamWriter, oder doch?


Th69 - Mi 06.01.16 15:53

Csharp-programmierer hat folgendes geschrieben:
Ja. Jetzt haut es hin. Ich wusste nicht, dass man den Feldern (strings) auch ohne Konstruktor sofort einen Wert zuweisen muss.

Nicht "muss", sondern "kann"!

Edit: was mir vorhin schon aufgefallen ist: der dreifache exakt gleiche Code zum Pfadzusammensetzen
Besser:

C#-Quelltext
1:
string s = DefaultPath; // und Eigenschaftennamen mit großem Anfangsbuchstaben                    


Ralf Jansen - Mi 06.01.16 16:22

Definiere Unterschied. Wenn du die Software nie nie wieder änderst weil du dir nur jetzt verwendest und in Zukunft nie wieder ist es immer egal wie du es tust. Wenn es jetzt funzt egal wie ist das Problem gelöst. Software lebt aber üblicherweise. Jemand muss die erweitern ändern etc. Das passiert auch üblicherweise nicht zeitnah.

Stell dir vor in 2 Jahren mußt du Datenklasse um eine Property erweitern. Das du die UI passend ändern mußt wird offensichtlich sein. Das du in Laden/Speichern was ändern mußt nicht. Ein simpler Streamwriter Code wird nicht beim kompilieren meckern aber die zusätzliche Property auch nicht speichern. Die Wahrscheinlichkeit ist hoch das du das einfach vergisst. Die jetzige Lösung wird das einfach automatisch mit erledigen. Der Code ist also deutlich Wartungssicherer. Das ist eigentlich immer der primäre Grund für einen bestimmten Codestil. Ist das pflegbar auch von jemanden anderen als ich selbst. Kann ich selbst das noch pflegen auch wenn ich mittlerweile vergessen habe wie es funktioniert.

Darum reiten wir ja hier auch z.B. so auf dem Thema Naming rum gutes Naming macht Software wartbar. Und richtige Namen lassen einen auch richtig über das Problem nachdenken. Sowohl beim Programmieren als auch beim warten der Software.


C# - Mi 06.01.16 16:30

Hallo,

ich habe jetzt mal ein komplettes Beispielprojekt angehängt (mit meinem Schüler und Schulklasse Beispiel). So würde ich sowas angehen.

In der Anwendung kannst du Schüler in einer LisstView hinzufügen oder entfernen. Mit "Save" wird das ganze in ner XML Datei gespeichert (die liegt im Debug Ordner des Projekts) und mit Load entsprechend wieder geladen.

@Ralf
Apropos Naming: "ConsoleApplication35" ;)


Ralf Jansen - Mi 06.01.16 16:37

Schönes Beispiel den XmlWriter in der Serializer Klasse solltest du noch einen using verpassen damit auch Dispose aufgerufen wird. Sonst bleibt das File dahinter eine unbestimmte Zeit gesperrt.
Der AddForm würde ich die Student Klasse übergeben und an die Controls binden anstatt die Einzelproperties.


Csharp-programmierer - Mi 06.01.16 19:02

Also das Speichern klappt jetzt. Aber jetzt fehlt noch das Öffnen. Ich deserialisier ja IO aber wie kommt die Datenklasse dort mit rein?


Ralf Jansen - Mi 06.01.16 19:10

Zitat:
Ich deserialisier ja IO aber wie kommt die Datenklasse dort mit rein?


IO ist die Summe seiner Properties also wird die List<Datenklasse> auch mit serialisiert sie ist Teil von IO. Du mußt dich nicht fragen wie du noch die Datenklasse serialiseren/deserialisieren mußt. Es passiert bereits.


Csharp-programmierer - Mi 06.01.16 19:54

Achso ja stimmt. Nur mal eine Frage: alle Felder mit { get; set; } werden serialisiert / deserialisiert, oder? Also ohne getter und setter nicht?


C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
if (sender != null)
            {
                ToolStripDropDownItem d = (sender as ToolStripDropDownItem);
                string s = Path.Combine(IO.defaultPath, d.Text, "Finanzen");
                HauptPfad = Path.Combine(IO.defaultPath, d.Text);
                string[] files = Directory.GetFiles(s);

                foreach (string file in files)
                {
                    IO io = IO.Laden(file);
                    foreach (var objekt in io.Datenklassen)
                    {
                        Datenklasse daten = objekt;
                        ListViewItem i = new ListViewItem(daten.Datum);
                        i.SubItems.Add(daten.AlterBetrag.ToString());
                        i.SubItems.Add(daten.Art);
                        i.SubItems.Add(daten.Betrag.ToString());
                        i.SubItems.Add(daten.NeuerBetrag.ToString());
                        i.SubItems.Add(daten.Beschreibung);
                        this.listView1.Items.Add(i);
                    }
                }
            }


Ich habe in meinem Toolstrip ein Item, über welches man die einzelnen Geldanlagen öffnen kann. Die Items dafür werden dynamisch erzeugt. Jetzt tritt in der foreach- Schleife bei io.Datenklassen der Fehler aus: Der Objektverweis wurde nicht auf eine Objektinstanz festgelegt.

Aber ich habe ja darauf hingewiesen? Woran liegt das jetzt schon wieder? :think:


Ralf Jansen - Mi 06.01.16 20:55

Ich habe so langsam das Gefühl wir/ich helfen völlig an dir vorbei. Möglicherweise solltest du noch mal ein Schritt zurückmachen und kleiner Brötchen backen :cry:

Was soll in dem Code das iterieren über files? Es sollte mittlerweile klar sein das alle Daten in genau einer IO Instanz stecken. Es gibt nur ein einziges File das du lesen solltest. Ich kann dir nicht sagen was da jetzt dieses konkrete Problem auslöst. Spekulativ liest du alle möglichen Files in dem Ordner die da noch in irgendeinem Zustand zwischenzeitlicher Versuche rumliegen und irgendwas bei dir auslösen. Das wäre hier die richtige Stelle um mal deine Debugging Fähigkeiten zu trainieren. Also Debug den Code, gehe ihn per Debugger Schritt für Schritt durch. Schau dir dabei an ob in den einzelnen Variablen das steht was du erwartest und mach dir dann Gedanken wenn irgendwas nicht deinen Erwartungen entspricht.


Csharp-programmierer - Do 07.01.16 21:10

Hallo Ralf.

Ja. Ich habe gestern nur sehr wenig Zeit gehabt aber wollte versuchen das Problem mit dem Serialisieren schnell zu lösen. Ich habe nun den von Ihnen geschriebenen Code abkopiert und es haut hin. Ich weiß zwas im Moment noch nicht den Unterschied zwischen meinem :gruebel:

Ich versuche in den nächsten Tagen den Code zu analysieren und zu verstehen. Ich melde mich dann nochmal.
Und noch mal einen großen Dank für Ihre Motivation und Ausdauer mir bei all meinen Problemen geholfen zu haben. Echt tolles Forum :zustimm:


Ralf Jansen - Do 07.01.16 21:26

Zitat:
Und noch mal einen großen Dank für Ihre Motivation und Ausdauer mir bei all meinen Problemen geholfen zu haben. Echt tolles Forum :zustimm:


Gern geschehen. Falls ich hier und da mal genervt geklungen haben sollte. Sorry.


Csharp-programmierer - Di 12.01.16 16:38

Vielen Dank für eure Mitarbeit. Jetzt haut alles einwand frei hin :think: :dance: