594.442 aktive Mitglieder*
4.260 Besucher online*
Kostenfrei registrieren
Einloggen Registrieren

Windows Programmierung in C# /Winforms, Einstieg in C# als praktische Anleitung zur Programmierung

Beitrag 06.04.2014, 14:04 Uhr
sharky2014
Level 7 = Community-Professor
*******

C# ist eine perfekte Sprache für Programmiereinsteiger, auch und weil sie auf der DOTNET Bibliothek aufbaut.

C# wurde völlig neu entwickelt für die .NET Umgebung, und der Einsteiger hat hier die wenigsten Schwierigkeiten und den größten Komfort, verglichen etwa mit C++.

Die .NET Bibliothek weist tausende von Funktionen auf, die man sehr einfach nutzen kann. Keine andere Programmiersprache ist auf .NET so gut abgestimmt wie C# (gilt für VisualBasic mit Abstrichen). Fast alle Codebeispiele im Internet beziehen sich auf C#, für C++ beispielsweise findet sich selten was.

Die Kombination von Windows.Forms mit C++ (CLI) ist höchst problematisch. Man kann Anfängern davon nur abraten.

Ein überaus starkes Werkzeug ist Intellisence, die kontektorientierte Hilfestellung, welche bei der Editierung auf Fehler fast jeder Art hinweist, so daß der Lerneffekt deutlich besser ist als wenn man C++ verwenden würde. Für C++ ist das Intellisense ab der Version Visual Studio 2010 abgeklemmt worden.

C# hat Ähnlichkeiten mit Java, und Java ist derzeit noch viel stärker verbreitet, aus verschiedenen Gründen, z.B. der Plattformunabhängigkeit und da es früher entwickelt wurde. C ist für kleine Umgebungen (Mikrocontroller etc.) nach wie vor nachgefragt, auch die Programmierung von Apps wird vorwiegend in anderen Sprachen durchgeführt, aber wer eine Programmiersprache sucht, die auf dem PC oder Laptop laufen soll, ist mit C# bestens bedient. Man muß hinzufügen, daß die Portabilität von C# auf Entwicklungsumgebungen außerhalb von Windows zwar theoretisch vorhanden ist, praktisch aber nur sehr spärlich umgesetzt wurde. Das kommt noch möglicherweise, aber eine Windows-Umgebung ist für C# die komfortabelste.

Es ist immer vorteilhaft, beim Erlernen einer Programmiersprache, wenn man Kenntnisse in anderen Sprachen hat. Bei C# ist das aber nur bedingt der Fall, denn C# ist extrem typenbasiert sowie objektorientiert (der Anwender wird auch gegen seinen Willen dazu gezwungen), so daß Kenntnisse in prozeduralen Programmiersprachen, wie z. B. C, sogar ein Problem darstellen können, weil man die "Denke" umstellen muß. Zwar ist der Code in C# von C fast kaum zu unterscheiden, C-Notation ist in gewisser Weise eine Untermenge der C# Notation, nur die dahinterstehende Logik ist eine völlig andere.

Visual Studio C# Express ist die kostenlose Entwicklungsumgebung, die jeder downloaden kann. Nach 4 Wochen muß eine Registrierung mit email-Adresse erfolgen.

Wozu der Beitrag?

Ich stelle in einer Folge von Beiträgen praktische Anwenderprogrammierung vor. Die Betonung liegt auf dem praktisch verwertbaren Code, sozusagen ein Kochbuch, nachdem man sich ein nicht banales Anwenderprgramm zusammenstellen kann.

Die Theorie wird soweit behandelt, wie sinnvoll, und nicht en Bloc, sondern in Bezug auf die aktuelle Codierung.

Da man zu C# wie zu keiner anderen Programmiersprache im Internet Fragen nachschlagen kann, beschränke ich mich auf das Wesentliche.

Ich denke, wer die nächsten 20 Beiträge "aushält", ist auch als vollkommener Programmieranfänger anschließend in der Lage, in C# zu programmieren.

Konsole oder WindowsForms?

Es ist allgemein üblich, und das macht auch Sinn, die Grundlagen von der Konsole aus zu erklären. WinForms-Benutzeroberflächen sind sehr schnell zusammengeklickt, es dauert kaum 20 Minuten, um eine "schöne" bzw. funktionelle Eingabemaske auf den Bildschirm zu zaubern. Das hilft aber nicht viel, wenn die erforderliche Funktionalität dahinter fehlt.

Visual Studio WinForms-Anwendungen verleiten dazu, daß man sich die grafische Oberfläche zusammenklickt und anschließend in die von der IDE (=Visual Studio) automatisch erstellten Funktionsrümpfe mal eben etwas Code reinsetzt. Das läuft natürlich, aber nur irgendwie, denn die Programmierung der Funktionalitäten hat eine völllig andere Logik als die der Eingabemasken.

Nach meiner Erfahrung ist es sehr viel besser, zunächst die Funktionalitäten zu programmieren, und anschließend die fertige Funktionalität mit Eingabemasken zu verbinden. Wie gesagt, die Eingabemasken sind schnell zusammengeklickt.

Der Unterschied zwischen der Konsolen-Programmierung und der Programmierung mit WinForms ist nicht sehr groß. Verwendet man Winforms, hat man zu der Programmierung in C# eben noch zusätzliche Elemente von Windows verfügbar, die Buttons, Listboxen, Radiobuttons usf. Formal erkennt man das, wenn man den Einstiegspunkt der Programmierung betrachtet:

//Konsolenanwendung:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Lagerverwaltung_Konsole
{
// Hier können weitere Klassen deklariert werden, aber keine
// Variablen oder Methoden

class Program
{
// Hier ist Raum für weitere Klassen, Variable und Methoden

static void Main(string[] args)
{
// ------------> Hier kommen die Anweisungen rein
} // main


} // class
} // namespace


// Windows.Forms Anwendung:


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace Lagerverwaltung_Winforms
{
// Hier können weitere Klassen definiert werden, aber keine Methoden oder Variablen

public partial class Form1 : Form
{
// Hier ist Raum für weitere Klassen, Methoden, Variable

public Form1()
{
InitializeComponent();
// → Hier kommen die Anweisungen rein

} // das Windows Formular

} // class Form1

} // namespace



Abstrahiert sieht man es deutlicher:

:
//Konsolenanwendung:

namespace Lagerverwaltung_Konsole
{
class Program
{
static void Main(string[] args)
{
} // main
} // class
} // namespace


// Windows.Forms Anwendung:

namespace Lagerverwaltung_Winforms
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
} // Konstruktor für das Windows Formular
} // class Form1
} // namespace


Bei der Konsolenanwendung haben wir den Namespace, in den die Klasse Program eingebettet ist, und innerhalb dieser Klasse findet sich die statische Klasse Main.

Unter Windows Forms haben wir den Namespace mit der Klasse Form1 sowie den gleichnamigen Konstruktor Form1, der Funktionalitäten enthält (als Vorgabe zunächst nur die Basisfunktionalität). Form1 ist nicht statisch, was der Unterschied ist, sehen wir schon gleich im Anfang bald.

Wenn man eine Konsole fertig programmiert hat, das Konsolenprogramm also "läuft", ist es sehr leicht, die Klassen, Methoden und Variablen etc. etc. auf eine Windows-Forms Anwendung zu portieren. Das wird man nicht an einem Stück erledigen, es empfiehlt sich (für Programmieranfänger), bestimmte neue Programmbausteine auf der Konsole zu testen und sie anschließend zu portieren. Sofern die WinForms Programmierung einigermaßen komplex ist,wenn man z. B. die nötigen Anzeigeelemente implementiert hat, kann man die Konsole auch verlassen und direkt in WinForms weiterprogrammieren.

Der Beitrag wurde von sharky2014 bearbeitet: 06.04.2014, 14:13 Uhr


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 06.04.2014, 15:22 Uhr
sharky2014
Level 7 = Community-Professor
*******

Grundaufbau:

Eine Klasse ist in C# ein Container, um Code aufzunehmen. Innerhalb des Namespace darf und kann man keinen Code hinterlegen. Sondern der geeignete Container ist die Klasse. Das ist so festgelegt.

Namespace
{

class irgendwas
{
Variablen und Methoden
}
}

Auf der Konsole können und werden wir die Klassen in den Hauptbaustein hineinschreiben, zur Verdeutlichung. Bei Programmen, die komplexer sind (also eigentlich alle) empfiehlt es sich, die Funktionalität der IDE zu nutzen (IDE = integrierte Entwicklungsumgebung, also Visual Studio Express), indem man im Menü Projekt die Option wählt: hinzufügen, und dann eine Klasse hinzufügt. Die wird dann an anderer Stelle gespeichert. Das empfiehlt sich, weil sonst der Code der Programmierung "auf einem Haufen sitzt" und damit zu lang und zu unübersichtlich wird. Zunächst mal bleiben wir bei alles auf einen Haufen.

Wir fügen jetzt eine Klasse hinzu mit einigen Funktionalitäten:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Lagerverwaltung_Konsole
{
class Program
{
public class kunden
{
private List<string> kundenliste;
public List<string> Kkopie;
public kunden() // Der Konstruktor
{
kundenliste = new List<string>();
kundenliste.Add("Müller ");
kundenliste.Add("Meyer");
Kkopie = new List<string>();
Kkopie = kundenliste;
}
public void zeige_Kundenliste()
{
foreach (string element in kundenliste)
Console.WriteLine(element);
}
public List<string> holeKundenliste()
{
return Kkopie;
}
}

public void zeigeListe(List <string> Liste)
{
foreach (string element in Liste) Console.WriteLine(element);
}
static void Main(string[] args)
{
var hKunden = new kunden();
hKunden.zeige_Kundenliste();
List<string> kopie = hKunden.holeKundenliste();
var hProgram = new Program();
hProgram.zeigeListe(kopie);
Console.ReadKey();
}
}
}

Zum Main: damit die Konsole nicht innerhalb Sekundenbruchteilen verschwindet, wird die Anweisung Warte-auf-Tatstendruck eingefügt. Diese ist Console.ReadKey().

Die neue Klasse Kunden befindet sich innerhalb der Klasse Program. Das muß nicht so sein, sie könnte sich sonstwo befinden, aber erstmal lassen wir sie dort.

Was macht die Klasse?

Wir haben die Definition, um einen Kunden zu erfassen, und zwar anhand des string-Elements, in das wir den Namen reinschreiben. Das ist natürlich nicht realistisch, weil zu dem Kunden nach viele andere Angaben erforderlich wären, aber hier beginnen wir mit dem Namen. Der Name wird auch nicht einfach in einen String abgelegt, sondern wir verwenden das Element List<T>, was soviel heißt wie eine Liste vom Typ <T>. Ist der Datentyp ein string, also eine Zeichenkette, wird die Liste angelegt als List<string>; Die Liste unterscheidet sich von der Vorgängerversion arraylist dadurch, daß sie streng typbezogen ist. Eine List<T> enthält also nur typisierte Elemente, in diesem Falle vom Typ string. Wir könnten auch eine List<T> definieren als List<int> Intliste, da könnten wir aber nicht einen String reinschreiben, nämlich den Kundennamen.

Wenn wir unsere Liste ansehen wollen, benötigen wir eine Funktion für die Ausgabe. Eine Funktion in C# ist eine Methode einer Klasse. In der Klasse Kunden ist sie angelegt, sie heißt: zeige_Kundenliste();

Sie muß in der Klasse selbst angelegt sein, nämlich wegen der Datenkapselung, eines der Haupt-Paradigmen der OOP (objektorientierte Programmierung). Das sagt, außerhalb der Klasse soll möglichst nichts sichtbar sein, was die Klasse nicht selbst verwaltet. Oder anders: es darf nicht möglich sein, daß Variablen einer Klasse von irgendwo im Programmcode unkontrolliert verändert werden. Das ist ein sehr interessanter Gesichtspunkt, sowohl was die Logik betrifft, als auch die praktische Umsetzung, auf den ich recht bald etwas ausführlicher zu sprechen kommen werde. Wer die Datenkapselung nicht verstanden hat bzw. sie nicht praktisch umsetzen kann, hat mit der OOP große Probleme.

Wir rufen also die geschüzten Daten der Liste, denn dieser sind "private", also nur in der Klasse sichtbar, nicht von außerhalb sichtbar, und wenn "private" nicht dabeistehen würde, wären sie dennoch private, es sei denn, man würde sie "public" deklarieren, mit der klasseneigenen Funktion zeige_Kundenliste() auf. Das Ergebnis ist, daß wir die geschützten Daten aus der Klasse auf dem Bildschirm sehen können.

Sehen ja, überschreiben nein. Denn der Datensatz ist private, von einer anderen Klasse oder von Main heraus können wir darauf nicht zugreifen. So soll es sein. So ist es aber nicht wirklich. Die ganze Datenkapselung ist ein sehr problematisches Ding und in sich nicht konsistent bzw. widerspruchsfrei, wie man noch sehen wird. In diesem Falle ist es bis jetzt so, daß die Daten gekapselt sind, bzw. readonly.

Der Datentyp List<T>:

Das ist ein echter Fortschritt gegenüber etwa C. Er liefert uns gratis und franco eine dynamische Liste, was man daran erkennt, daß man die Anzahl der Datensätze nicht vorher festlegen muß. Ein uraltes Problem: wir wissen nicht, wieviele Datensätze in der Laufzeit anfallen (z. B., wenn Datensätze aus einer Datei gelesen werden). Entweder ist die Liste unnötig groß (Speicherverbrauch), oder zu klein (Programmierfehler). List <T> erlöst uns von dem Übel. Wir können zur Laufzeit mit der Methode list<T>.Add beliebig viele Datensätze dranhängen, die Grenze ist nur der Speicher des PC. Einige tausend Datensätze sind überhaupt kein Problem, auch nicht von der performance.

Die Methode foreach()

Sie ersetzt das mühsame Konstrukt (for i=0;i<irgendwas;i++) oder (while ... irgendwas), also eine Auslesung einer Liste anhand fester Indizes oder Abbruchbedingung, wobei der Fehler auftreten kann, daß die Liste == null ist, also nicht initialisiert, daß wir 0 Datensätze haben oder bei irgendwas vergessen, daß es sich um einen nullbasierten Index handelt, bei dem das 100. Element eben nicht den Index 100 hat, sondern 100-1=99, oder die Abbruchbedinung schlicht falsch ist oder am Schleifenanfang oder am Ende stehen müßte. Mit foreach muß man sich um alle diese Dinge nicht kümmern. Das hilft enorm und produziert guten und übersichtlichen Code.

Der Code ist: foreach (Datentyp beliebigerName in List) .... und fast schon umgangssprachlich. Die Methode aus der Klasse Kunden gibt also alle Datensätze aus und wenn die Liste am Ende ist, endet sie, ohne daß man eine extra Fehlerbehandlung machen müßte.

Nun ist es natürlich so, daß ganz unabhängig von der Programmiersprache die Daten in einem Datenspeicher an bestimmten Orten des Datenspeichers gespeichert sind. Ganz unabhängig davon, ob die Programmiersprache die Illusion erweckt, diese Daten wären geschützt oder schreibgeschützt, sind sie es natürlich nicht. Wenn man auf irgendeine Speicheradresse überschreibt, sind die Daten verloren oder verändert. Das kann in C sehr leicht geschehen, indem man in einen Bereich Daten hineinschreibt, die länger sind als der vorgesehene Datenblock. In C benutzt man dafür Zeiger, was gefährlich ist, dafür sind die in C# so nicht vorgesehen. Wie das aber mit der Datensicherheit aussieht, werden wir gleich im Anschluß noch sehen. So gut ist es nämlich damit nicht bestellt, wie der Code uns weismachen will.

Kommen wir zu den weiteren Methoden, die im Code zu sehen sind (ich möchte hier nicht anfangen zu erklären, was ein int ist oder ein double, sondern etwas komplexer kann es schon sein).

Wir sehen in der Klasse Kunden ein sehr praktisches Konstrukt, nämlich den Konstruktor. Den Konstruktor muß man nicht selbst schreiben. Eine Klasse hat einen Standard-Konstruktor, der mit new aufgerufen wird. Nämlich:

var zugriff_auf_die_gewünschte_klasse = new classname();

Mit dem Aufruf wird Speicher reserviert für die Mitglieder der Klasse (ganz allgemein gesagt). Die Variablen der Klasse sind dann auch irgendwie initialisiert, z. B. int Variablen werden auf 0 gesetzt (und nicht auf null), allerdings Verweistypen sind auf null gesetzt, was dann zu einem Laufzeitfehler (=Programmabsturz) führen kann, wenn man sie das erste mal aufruft. Ein Verweistyp ist z. B. unsere List<string>Kundenliste. Obwohl der Typ string nicht mit new instanziiert werden muß, muß es eine List<string> dennoch. Wenn wir sagen: var zugriff = new Kunden(), sind die Werttypen initialisiert, die List<string> allerdings hat den Wert null. Die List<string> müßte zusätzlich noch mit Kundenliste = new List<string> initialisiert werden. Man sieht, das ist alles etwas umständlich und fehleranfällig, insbesondere, weil sich die Datenstruktur bei der Programmentwicklung ja laufend verändert.

Abhilfe von dem Problem, daß die Referenztypen den Laufzeitfehler mit null auslösen können, bzw. gar nicht nutzbar sind, ohne Speicherplatz, schafft hier der selbstgeschriebene Konstruktor. Der ordnet den Eigenschaften bzw. Variablen einen gewünschten Wert zu bzw. sorgt dafür, daß diese initialisiert werden.

Der Konstruktor hat den Namen der Klasse, keine Parameter, und arbeitet so, daß beim Aufruf der Klasse die Werte wie gewünscht initialisiert hat. Formal:

class kunden // die Klasse
{
public kunden() //der gleichnamige Konstruktor, public, weil man ihn sonst nicht aufrufen könnte
{
// tu irgendwas; // der Konstruktor
}
}

Wenn man nun die Klasse mit dem new Operator aufruft, arbeitet NICHT der Standard-Konstruktor, sondern es werden die Mitglieder der Klasse so initialisiert, wie es im SELBSTGESCHRIEBENEN Konstruktor ausgewiesen ist. So kann man dafür sorgen, das Referenztypen "brauchbar" sind und keinen Laufzeitfehler auslösen können.

Die sonstigen Funktionen, die durch kopie angedeutet sind, werden im nächsten Beitrag besprochen.


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 06.04.2014, 16:30 Uhr
sharky2014
Level 7 = Community-Professor
*******

Es gibt zunächst noch eine Erklärung nachzuliefern. Nämlich die, wie man eine Methode einer Klasse aufruft .

In der prozeduralen Programmierung geht es so, daß man mit Funktionsname() die Funktion auslöst. In der OOP bzw. C# geht das so nicht.

Steht die Methode in einer anderen Klasse, benötigt man einen sogenannten Objektverweis. Das Internet ist voll davon, welche Schwierigkeiten die Leute damit haben. Normalerweise wird der Objektverweis folgendermaßen kodiert:

var objektverweis = new Classname();

Was ist ein Objektverweis? Er ist nichts anderes als ein Pointer (Zeiger) auf den Anfang des Speicherbereichs, in dem die neue Intanz der Klasse oder Funktion abgelegt ist. Er ist also NICHTS ANDERES als eine Adresse im Speicher.

Damit wird eine neue Instanz der Klasse Classname angelegt. Die Funktionen der Klasse, also ihre Methoden, werden dann formal wie folgt aufgerufen:

objektverweis.tuwas(); wenn in der Klasse die Methode public void tuwas() abgelegt ist.

Der Punkt stellt das Bindeglied zu den Methoden der Klasse dar.

Drückt man in der IDE den Punkt, zeigt intellisence, was verfügbar ist. Was intellisense nicht zeigt, ist nicht verfügbar. Womöglich deshalb, weil die Methode (eine Methode ist eine Funktion) nicht public deklariert wurde.

Nun ist es die Frage, ob man standardmäßig bzw. immer überhaupt eine neue Instanz der Klasse benötigt. Nach meiner Meinung ist das eigentlich selten der Fall, jedenfalls für meine Programmieraufgaben. Der eigentliche Sinn, immer eine neue Instanz der Klasse zu erstellen, erschließt sich wohl erst dann (und macht dann auch Sinn), wenn sehr große Projekte mit vielen Programmierern erstellt werden. Dann macht auch die Datenkapselung mehr Sinn als wenn man für den eigenen Gebrauch ein Programm schreibt.

Im Beispiel:

static void Main(string[] args)
{
var hKunden = new kunden();
hKunden.zeige_Kundenliste();

Wird ein Objektverweis hKunden auf die Klasse Kunden angelegt und die Methode zeige_Kundenliste() aufgerufen. Die Daten sind private, wir haben eine Datenausgabe, aber keinen Schreibzugriff.

Das wollen wir uns einmal genauer ansehen.

Die Daten sind ja gekapselt mit:

private List<string> kundenliste;

Davon machen wir eine Kopie, die nicht gekapselt ist, sondern public:

kundenliste = new List<string>();
kundenliste.Add("Müller ");
kundenliste.Add("Meyer");
public Kkopie = new List<string>();
Kkopie = kundenliste;


Die Zuweisung: Kkopie = Kundenliste; ist eine Zuweisung per reference. Was heißt das? Das heißt, daß wir nichts anderes als einen (von C verpönten) Pointer bzw. Zeiger haben, der nichts anderes markiert als den Adreßbereich des Datensatzes.

Es ist also so, daß des Kaisers neue Kleider gezeigt werden, nämlich. sobald wir die Adresse im Speicher übergeben, hat es sich mit der Datenkapselung bzw. dem Schreibschutz. Wenn wir nämlich nun der Kkopie, der Kopie der Kundenliste, neue Werte zuordnen, z. B. den 2. Datensatz überschreiben mit :

Kkopie[1] = "Dieser Stringwert wird geändert";

und danach die Originalliste wieder aufrufen, die ja private ist, stellt sich heraus, daß die Originalliste ebenfalls geändert wurde. Das heißt nichts anderes, als daß uns mit

Kkopie = kundenliste;

nicht eine Kopie der Werte, sondern schlicht die Speicheradresse übergeben worden ist. Und das ist ja nicht das gewünschte.

Die Frage ist natürlich, was ist gewünscht. Ganz klar: wenn wir private deklarieren, wollen wir auch private haben. Ein solcher Fall wäre z. B. eine Kundenliste, die grundsätzlich schreibgeschützt sein soll, aber natürlich Platz haben muß für neue Kunden. Ist sie schreibgeschützt, gibt es keine neuen Kunden. Ist sie nicht schreibgeschützt, könnte der Bestand überschrieben werden.

Rein programmiertechnisch kann man solche Fälle lösen, indem man zunächst anstelle der Referenz (=Speicheradresse, gefährlich) eine tatsächliche Wertekopie übergibt, so daß ein Rückgriff auf die Originalliste nicht möglich ist. Wenn dann die Kopie verändert wurde, kann mit man einem entsprechenden Algorithmus dafür sorgen, daß die Liste zwar erweitert werden kann, bestehende Datensätze aber nicht überschrieben werden können.

Wie kriegen wir eine Call by Value (=echte Wertekopie) anstelle Call by Reference (=Speicheradresse offenbaren) hin?

Anstelle:

Kkopie=kundenliste; // gefährlich

Sagen wir:

Kkopie = new List<string>

und erzeugen damit eine neue leere Liste in einem andere Speicherbereich.

Die Werte werden dann so kopiert:

foreach (string element in strlist) // strlist ist die private-Originalliste
Kkopie.Add(element);

Die Liste Kkopie ist damit eine exakte Kopie der Originallliste strlist, aber es erfolgt eine ÜBergabe CallbyValue,.

Man erkennt es daran, daß wenn man nun die Listenelemente der Kkopie ändert, und anschließend die Funktion zeigeKundenliste() aufruft, daß die Originalliste unverändert ist.

Anders gesagt, haben wir den Inhalt der Originallliste als (Werte) Kopie in einer anderen Liste, diese ist aber an einer völlig anderen Stelle des Speichers abgelegt und kann so die Werte der Originalliste nicht beeinflussen.

Unabhängig von der Notation der Programmiersprache (private, public, protected etc. etc.) ist es also wichtig, daß man versteht, wie der Speicher organisiert ist.

Man muß wissen, was der Compiler mit dem Speicher macht.

Das kriegt man meiner Meinung nach nicht durch Lesen heraus, sondern man muß es an praktischen Programmbeispielen ausprobieren.

Schließlich bei jeder Art von Programmierung geht es letztendlich nur darum, wie die Daten im Speicher organisiert sind.

Und das ist oft anders, als man bei Betrachtung der Methodenbeschreibungen denkt.

Wir haben bis jetzt ein recht komplexes Datenkonstrukt List<T> kennengelernt, was für jede Art von praktischer Programmierung ein sehr guter Datentyp ist, denn welche Programmierung käme ohne Listen aus? List <T> ist sehr leicht zu handhaben, entspricht ungefähr einer Spalte in einer Excel-Tabelle mit eigener Funktionalität. Man kann sie auch sehr leicht sortieren (eine Aufgabe, die eigentlich auch immer anfällt).

Viele Programme sind, mit Abstand betrachtet, eigentlich nichts anderes als programmierte Excel-Tabellen. Ihre Funktionalität gegenüber der Excel-Tabelle besteht darin, daß die Felder nach strengen Regeln verwaltet und ausgewertet werden und eine freie Eingabe wie bei Excel vermieden werden kann.


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 06.04.2014, 18:00 Uhr
CNCAllgäuer
Level 4 = Community-Meister
****

ER ist wieder da tounge.gif

MfG

Florian
TOP    
Beitrag 06.04.2014, 18:59 Uhr
sharky2014
Level 7 = Community-Professor
*******

Sehen wir uns jetzt einmal eine Variante an, nämlich die Klassen-Member mit einer Methode zu initialisieren, anstelle des Konstruktors. Der Unterschied ist, der Konstruktor wird beim Klassenaufruf automatisch aktiv, haben wir eine Methode, so wird die keinesfalls automatisch aktiv, sondern muß explizit aufgerufen werden.

Das Problem dabei ist: man kann es vergessen. Und die Folge können Laufzeitfehler sein.

OOP: sie sagt ja, sie bildet die Objekte realitätsnah ab. Nun, Objekte im täglichen Leben haben selten etwas mit Zeichenketten zu tun. Wir benötigen mehr Daten. Geburtstag, Gehalt, Frage ob bei der Firma beschäftigt ja nein und so weiter. Näher an der Realität ist der Datensatz mit gemischten Datentypen: Zahlen, Namen, sonstige.

Der ist hier abgebildet als ein Artikel, der aus Name und Nummer besteht. Es handelt sich also um einen Datensatz mit gemischten Datentypen, den man bei der Ausgabe nicht einfach als Zeichenkette abbilden kann. Wenn man das will, müßte man den Zahlenwert für Artikelnr in eine Zeichenkette umformen und mit der Artikelbezeichnung zusammenfassen.

Die String-Funktionen in C# sind phantatisch. Extrem gut gelöst. Nur mal als Beispiel:

Um eine Zahl in einen String umzuformen, benutzt man die Eigenschaft der Zahl, welche zu einer Klasse gehört, beispielsweise Int32. In unserem Beispiel würden wir sagen: string infostring = Artikelnr.ToString() + Artikelbezeichnung; Der Punkt hinter Artikelnr zeigt, daß ToString() eine Methode der Klasse INT32 ist. Und der + Operator verknüpft mit dem nächsten String. Umgekehrt benötigt man die Klasse Convert, z. B. double d = Convert.ToDouble(von irgendwas). Das Problem der Typenumwandlung in String ist in C# perfekt gelöst.

Wenn man es viel mit zeichenketten zu tun hat, sollte man bedenken (auch in C#, Programmierung ist generell):

Umwandlung von Typen birgt immer das Risiko, daß was schief geht.
Will man das vermeiden, muß man viel Code investieren, um mögliche Fehler auszuschließen. Das kostet Performance.
Rechnen, das was der Compiler am besten kann, kann man nur mit den numerischen Datentypen.
Man sollte also die Datensätze IMMER numerisch vorhalten. Die numerische Performance ist um Klassen besser als alles, was mit Strings zu tun hat.
Wenn Zeichenketten ausgegeben werden müssen (eigentlich immer, z. B. in Windows.Forms Textboxen etc.), sollte man den numerischen Datentyp als String darstellen. Die umgekehrte Richtung, aus Strings wieder numerische Datentypen zu erzeugen, ist sehr fehleranfällig und sollte nach Möglichkeit vermieden werden. Da könnnen jede Menge Laufzeitfehler erzeugt werden, die, wenn man die abfangen will, wiederum einen Haufen Code erforderlich macht.

Gehen wir aber weiter mit dem Beispiel:

Wir rufen hier einen Code auf, der scheinbar völlig o.k. ist, aber anders funktioniert als wir denken:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Lagerverwaltung_Konsole
{
class Program
{
public class Ersatzteile // Klase Ersatzteil
{
public class Ersatzteil // Unterklasse 1 Ersatzteil
{
public int artnr; // bestehend aus Artikel-nr
public string artbez; // sowie Text
}
public List<Ersatzteil> elist; // eine Liste, die die Ersatzteile aufnimmt, aber nicht als Text, sondern mit 2 Datenfeldern gemischtem Typs
public void erzeuge_Liste()
{
elist = new List<Ersatzteil>();
var e1 = new Ersatzteil();
e1.artnr = 1;
e1.artbez = "Schraube";
elist.Add(e1); // Daten für den 1. Artikel wurden zugewiesen und in Liste gespeichert
e1.artnr = 2;
e1.artbez = "Mutter";
elist.Add(e1); // und für den 2. Artikel
}
} // class ersatzteile
static void Main(string[] args)
{
var he = new Ersatzteile(); // Wir benötigen einen Objektverweis auf die Funktion (Methode), den wir mit new erzeugen
he.erzeuge_Liste(); // und die Liste wird erstellt

foreach (Ersatzteile.Ersatzteil element in he.elist)
{
Console.WriteLine("nr {0} bez {1} ", element.artnr, element.artbez); // AUsgabe Konsole hat hier Element 0 und 1, von verschidenem Datentyp
} // da WriteLine eine mehrfach überladene Funktion ist, nimmt sie uns dankenswerterweise
// die Umwandlung des INT32 Wertes artnr in String ab, C# ist schon sehr bequem
Console.ReadKey();
}
} // program
} //Ns


Und auf der Konsole sollten wir jetzt unsere Liste sehen, die bislang aus zwei Datensätzen besteht (siehe oben). Was wir aber sehen ist, daß zweimal der 2. Datensatz augegeben wird. Das heißt, würde man die Liste beliebig verlängern, würde endlos der letzte Datensatz ausgegeben werden. Die anderen Datensätze würden wir nicht sehen.

Sind die nun versteckt oder was passiert da?

Ich sag immer nur, wer programmiert, muß verstehen, was der Compiler mit dem Speicher macht. Egal, wie die Befehle und Strukturen sind, am Ende ist das Ergebnis das, was im Speicher abgelegt ist . Und C# gaukelt uns da leicht etwas vor.

Wir würden erwarten, daß die List<T> die Datensätze einpackt und wegspeichert. Und beim Aufruf der Liste die Daten schön ausgespuckt werden.

Wenn wir allerdings immer nur den letzten Datensatz sehen, können wir schon erahnen, was da passiert ist:

Die List<T>speichert nicht die Werte, sondern die Adresse im Speicher. Und zeigt also mit allen Elementen immer auf dieselbe Stelle. Daher sehen wir nur den letzten Datensatz.

Um da Abhilfe zu schaffen, gibt es viele Möglichkeiten. Will man mit neuer Instanziierung arbeiten, muß man dafür sorgen, daß unsere Variable ständig neu erzeugt wird, also ständig einen neuen Speicherort zugewiesen bekommt.

List<T> speichert keine Werte, sondern Adressen!

So würde es funktionieren:

public void erzeuge_Liste()
{
elist = new List<Ersatzteil>();
var e1 = new Ersatzteil();
e1.artnr = 1;
e1.artbez = "Schraube";
elist.Add(e1); // Daten für den 1. Artikel wurden zugewiesen und in Liste gespeichert (denkste, ist nicht so)

e1 = newErsatzteil(); // <===== Mit dem New Operator einen neuen Speicherort erzeugen

e1.artnr = 2;
e1.artbez = "Mutter";
elist.Add(e1); // und für den 2. Artikel (jetzt ist es so, dank new)
}



Andere Möglichkeiten bestehen darin, daß wir Datenstrukturen als static deklarieren. Darüber wird noch zu reden sein. Denn die ständigen Kopien mit new kann man eher eine Mode zuordnen als daß man von praktischer Notwendigkeit sprechen könnte.

Würden wir e1 static erklären, würde es auch ohne new funtkonieren.

Da e1 nicht static ist, funktioniert es hier in dem Beispiel nur mit einer neuen Instanz.

Neue Instanz = neuer Speicherort, um eine Kopie abzulegen.

Die List<T> tut das wie gesagt nicht.


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 06.04.2014, 20:09 Uhr
sharky2014
Level 7 = Community-Professor
*******

Wozu ist der new Operator gut?

Das ist die Frage, über die man in C# Foren niemals etwas liest.

new erzeugt eine neue Instanz einer Klasse.

Will man auf eine Klasse zugreifen, muß man einen Handle = Obektverweis mit new erstellen.

Fragen wir uns aber einmal, wozu das gut sein soll. Von mir kommen da keine Antworten.

Hat man eine Kundenliste, gibt es keinen Grund, eine Kopie dieser Kundenliste anzulegen.

Will man die Kundenliste mit der Artikelliste abgleichen, gibt es ebenfalls keinen Grund, von dieser Liste (oder Datei) eine Kopie zu erstellen.

Bzw. es gibt in den Fällen keinen Grund, mehr als eine Kopie der dahinterstehenden Datei zu erstellen.

Fast alles, was in dem Zusammenhang behandelt wird, ist static. Es gibt nur eine Instanz. Es gibt nicht ein Dutzend Kopien der Artkellliste, wozu sollten diese gut sein? Dann könnten ja mehrere Methoden auf verschiedene Kopien der Artikelliste zugreifen, die gerade online ist, und was wäre die Folge?

Wir haben eine Artikelliste und wünschen nicht, daß da von außerhalb herumgepfuscht wird.

Für viele (fast alle) solche Aufgaben bietet sich der Zugriffsoperator static an.

Static heißt, daß von dieser Klasse nur eine einzige Instanz verfügbar gemacht wird.

Der Statische Konstruktor kann die Variablen der Klasse automatisch auf die gewünschten Werte setzen.

Das geschieht so, daß der statische Konstruktor nach Dateien sucht und die Werte aus der Datei in Listen packt. Die Listen sind entweder C#-Listen oder C# atringarrays, in der Windows.Forms erscheinen sie dann als Listbox oder anderweitig, so daß der Anwender die Liste vor Augen hat und mit Doppelclick oder anders daraus auswählen. kann.

Wichtig ist die Funktionalität der Dateien.

Wir müssen erstmal Dateien haben und lesen und schreiben können.

Das wird der nächste Beitrag.

Es gibt im Grunde zwei verschiedene Dateitypen:

Text, welche Datensätze im String-Format speichert.

Und Binary, welche Datensätze als Byte speichert.

Bei Textdateien gibt es weder beim Speichern noch beim Auslesen irgendwelche Probleme. Der Datensatz wird begrenzt mit dem Trennzeichen EndOfLine.

Es gibt dazu die wirklich phantastischen Methoden WriteAllLines(dateiname) und ReadAllLines(dateiname), die es nicht einmal erforderlich machen, daß man sich um das Öffnen und Schließen der Datei bzw. das Dateiende kümmern muß. Allerdings, um Laufzeitfehler zu vermeiden, muß man schon feststellen, beim Lesen, ob die Datei existiert.

Dafür hat die Klasse File die Funktion File.Exists anzubieten.
Will man mehr über die Datei erfahren, hat die Klasse FileInfo weitere Infors anzubieten. Man braucht einen Objektverweis, z. B. fi=new FileInfo(), und kann die Methoden abrufen.

Das ist in C# sehr gut gelöst.

Wenn wir eine Datei speichern im Binärformat (das ist in der Regel der Fall, wenn wir zusammengesetzte Datentypen haben), ist das Speichern auch kein Problem.

Das Problem tritt auf beim Auslesen. Man stelle sich vor, da ist eine ungeheure Menge von Bytes, also Zeichen, und wie machen die Sinn?

SInn machen die, wenn man sie in eine passende Datenstruktur einliest. Diese Datenstruktur muß bekannt sein, sonst wird man die bytes nicht in ein vernünftiges Format bekommen.

Speichern und Zurücklesen erfolgt mit serialize und deserialize: Wie gesagt muß abe zuvor die Programmlogik passen, damit man weiß, was zurückgelesen wird. Ein Codebeispiel. Ich kommentiere das zeilenweise:



public bool lies_buchungen_binaer(ref List<CLBuchung.Cbuchungssatz> buchungenclasslist)
{
bool erfolg = true //beim ersten Fehler auf false;
var hdatn = new CLDateiFunc.dateinamen(); // class handle mit neuw
string dateiname = hdatn.DN_buchungen_bin;
FileInfo fi = new FileInfo(dateiname); // Handle auf KlasseFileinfo
if (File.Exists(dateiname))
{
if (fi.Length > 0) // deserialize leere Datei = laufzeitfehler Also, größer als 0 bytes dürfte die Datei schon sein
{
try
{
FileStream stream = new FileStream(dateiname, FileMode.Open);
BinaryFormatter bformatter = new BinaryFormatter();
buchungenclasslist = (List<CLBuchung.Cbuchungssatz>)bformatter.Deserialize(stream); // hier wird der Datentyp genannt für die Rückumwandlung
stream.Close();
}
catch (Exception e)
{
MessageBox.Show(e.Message); // wenn was schief geht, zeigt e.Message die passende Fehlermeldung
// try catch ist aber bei vernünftiger Programmierung eigentlich entbehrlich
// ist sowas wie die eierlegende Wollmilchsau und kostet jede Menge performance
erfolg = false;
}
}// if exist
}
else erfolg = false;
return erfolg;
}


Die Funktion leistet, daß die Masse von Bytes dem passenden Datentyp zugeführt wird, nämlich

buchungenclasslist = (List<CLBuchung.Cbuchungssatz>)bformatter.Deserialize(stream);

ein Datentyp, der in der Klasse CLBuchung abgelegt ist, und selbst eine Klasse ist, nämlich Cbuchungsssatz.

Da die Binäre Speicherung die Daten dieses Typs gespeichert hat, wird er auch für die Rückumwandlung benötigt. Würde man die Bytes einem falschen Datentyp zuführen, wäre das Ergebnis Nonsense und Programmabsturz.

Man soll sich hier nur merken:

WIr können mit C#sehr einfach Dateien erzeugen und auslesen

Es gilt zwei Sorten von Dateien zu unterscheiden:

1.) Textdateien

2.) Binärdateien

Warum man nicht alles in Textdateien abspeichern möchte (das wäre das einfachste):

1. Im Prinzip haben wir es immer mit gemischten Datentypen zu tun, die man in Text umwandeln müßte.
2. Umwandlung und Rückumwandlung in Zeichenkette ist sehr fehlersensitiv.
3. Ein Anwender kann eine Textdatei mit einem Editor manipulieren und so das PRogramm abschießen.
4. Deshalb sind Binärdateien, die nicht lesbar sind, ein besserer Ort für die Datenspeicherung als Textdateien.


Wie das genau geht und worauf man achten muß, wurde schon umreissen, Details folgen später.

Grundsätzlich haben wir in C# mit Dateifunktionen wenig Aufwand.


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 07.04.2014, 14:42 Uhr
sharky2014
Level 7 = Community-Professor
*******

Listen, Arrays und Dateien.

Diese Datentypen stellen enthalten die große Masse der Daten einer Programmierung. Sie verwalten Auflistungen von gleichartigen Datentypen, die man sortieren, bearbeiten und abspeichern kann.

C# bietet die Möglichkeit, Auflistungen ohne direkte Indizierung zu handhaben, und wenn immer möglich, sollte man die direkte Indizierung vermeiden, weil diese sehr leicht zu Laufzeitfehlern führt.

Direkte Indizierungen sind dann sinnvoll, wenn die Zahl der Elemente bekannt ist und sich niemals ändert, z. B. Wochentage. Dennoch ist der Zugriff darauf fehlersensitiv, wie man an folgendem Beispiel sieht:

string[] Wochentage = new string[7].
// wir schreiben die Daten rein, "Montag", "Dienstag" ...
// Und versuchen eine Ausgabe:
Console.WriteLine(Wochentag[1]);
Es erscheint "Dienstag". Weil alle arrays nullbasiert sind, ist der erste Datensatz Wochentag[0], und nicht Wochentag[1]. !!!

Am Ende der Auflistung wird es dann kritisch, sozusagen der Generalfehler aller Indizes:
Console.WriteLine(Wochentag[7]); // ------------->>>> Laufzeitfehler, denn das siebte Element ist Wochentag[6], nämlich 0,1,2,3,4,5,6 = 7;

Zunächst ein bißchen Code, um unsere Datenstruktur zu erzeugen (vollständiger Code):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Lagerverwaltung_Konsole
{
class Program
{
class Datum
{
public string[] Wochentag;
public Datum() // Der Konstruktor
{
Wochentag = new string[7] {"Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag","Sonntag"};
}
}
static void Main(string[] args)
{
var hdat = new Datum();
Console.WriteLine(hdat.Wochentag[1]);
Console.ReadKey();
}
} // program
} //Ns


Wir haben eine Klasse class Datum, in welcher wir den Datensatz für die Namen der Wochentage unterbringen.
Wenn die Klasse aufgerufen wird mit var hdat = new Datum(); sind die Werte für die Auflistung direkt verfügbar, weil wir einen

eigenen Konstruktor

definiert haben, nämlich public Datum()

Public muß er sein, da man sonst die Klasse gar nicht aufrufen kann. Der Konstruktor ist ansonsten parameterlos und trägt exakt den Namen der Klasse.
Eine Instanz der Klasse entsteht mit der zuweisung var hdat=new Datum(). Es wird für die Klasse Speicherplatz bereitgestellt, und der Verweistyp hdat enthält die Startadresse der Struktur im Speicher.

Aber Vorsicht: Enthält die Klasse Verweistypen, wird dafür kein Speicherplatz bereitgestellt. Diese haben den Wert null. Um das zu vermeiden, erfolgt die Initialisierung mittels des Konstruktors.
Kommen wir noch einmal auf die Behandlung von Laufzeitfehlern. Es gibt die große Fliegenklatsche dafür, um solche abzufangen:

try
{
// tue irgendwas
}
catch (Exception e)
{
// wenn was schief geht (Laufzeitfehler), mache hier weiter z. B.:
Console.WriteLine(e.Message);
}

Dabei sind Exception und die Methode .Message Schlüsselwörter. Die Konsole zeigt dann an, welcher Laufzeitfehler ausgelöst wurde.
Nun wollen wir aber ja bei der Programmierung die Laufzeitfehler so abfangen, daß sie gar nicht erst entstehen. Daher ist das Konstrukt try/catch nicht sehr elegant, welcher Anwender will schon dauernd Fehlermeldungen auf dem Bildschirm sehen. Hinzu kommt, daß das Programm in solchen Fällen oft nicht sinnvoll fortgesetzt werden kann. Das try/catch Konstrukt bietet sich also eher für den Programmierer während der Programmentwicklung an. Man kann den catch-Block dann noch erweitern, um für jede Art von Exception eine spezielle Fehlermeldung zu erhalten. Läuft das Programm, sollte man solche try/catch Blöcke eigentlich nicht mehr benötigen.
Weiter mit dem Array und den Laufzeitfehlern:
Um die direkte Indizierung mit Wochentag[7] zu vermeiden, haben wir andere Möglichkeiten:

1. Die Ausgabe mit foreach: foreach (string element in Wochentag) Console.WriteLine (element);
Um einzelne Elemente anzusprechen, können wir in der foreach-Schleife weitere Bedingungen hineinschreiben.

2. Abfrage der Datenfelder: int anzahl = Wochentag.Count(); // liefert die Anzahl der belegten Fehler
Um das letzte Datenfeld zu sehen, müssen wir aber wieder den Nullbasierten Index beachten, nämlich
Console.WriteLine(Wochentag[anzahl]) führt wieder zu einem Laufzeitfehler, richtig wäre hier Wochentag[anzahl-1]);

3. Wir benutzen einen enum-Typ
Das wäre eine elegante Methode, um die direkte Indizierung vollständig zu vermeiden. Das Problem ist ja, daß man sich unter Wochentag[0] nicht wirklich was vorstellen kann. Beginnt die Woche mit Sonntag oder Montag? Der enum Datentyp wird intern als int32 verwaltet, aber vom Compiler nicht direkt als int32 ausgewiesen, siehe unten.
class Datum
{
public enum Tag { Montag, Dienstag, Mittwoch, Donnerstag, Freitag, Samstag, Sonnntag }
Und schreiben in die Klasse eine Ausgabefunktion:
public void ausgabe()
{
Console.WriteLine(Wochentag[Tag.Montag]); // siehe unten
}
Die wir vom Hauptprogramm aus aufrufen mit
static void Main(string[] args)
{
var hdat = new Datum();
hdat.ausgabe();
Wir müssen nur darauf achten, daß die Reihenfolge für die Enum- und die String-Werte mit demselben Tag beginnen, hier Montag.

Der Code: Wochentag[Tag.Montag] ist sehr fehlersicher, übersichtlich und selbsterklärend. Es gilt nur noch das Format des enum zu korrigieren:
Console.WriteLine(Wochentag[Tag.Montag]); // Compiler meckert, weil er Tag.Wert nicht als Integer akzeptiert.
Abhilfe:
Console.WriteLine(Wochentag[(int)Tag.Montag]);
Mit dem Operator (int) erfolgt eine explizite Typumwandlung in Int32.

Zurück zu den Laufzeitfehlern beim Indizieren. Der Compiler hilft uns schon im Vorfeld, die gröbsten Fehler zu vermeiden. Hier z. B.
int[] intarray;
Console.WriteLine(intarray[0].ToString());
Der Compiler erkennt, daß: Fehler 1 Verwendung der nicht zugewiesenen lokalen Variablen "intarray"

Wenn wir das array instanziieren: int[] intarray = new int[7];
Erhalten wir die Ausgabe 0, weil bei der Instanziierung jeder Wert des arrays auf 0 gesetzt wird (0, nicht null).

Int32 ist allerdings ein Wertetyp. Die Behandlung von Werte- und Verweistypen ist ganz unterschiedlich. Zwischen diesen beiden steht als besonderes Format der string, welcher teils als Verweistyp behandelt wird, aber nicht durchgängig, denn wir müssen den string nicht mit new instanziieren.

Dieser Code löst einen Laufzeitfehler aus:
List<string> StrList=new List<string>();
Console.WriteLine(StrList[0]); // direkte Indizierung

Anders als bei den Wertetypen Int32 wurde hier nichts zugewiesen, nicht einmal null, das erste Element der Liste existiert schlicht gar nicht. Das heißt, die Liste hat zwar eine Startadresse, aber dahinter keinen Speicherbereich für die Elemente (Kunststück, es wurden ja noch keine eingetragen).

Wir versuchen herauszufinden, ob in der Liste was eingetragen ist:
if (StrList[0].Length>0) Console.WriteLine(StrList[0]); // Ergebnis: Laufzeitfehler
Der Laufzeitfehler wird hier ausgelöst, weil auf die Methode eines Elements der Liste zugegriffen wird, dieses Element aber nicht vorhanden ist.

Ergebnis:
Bevor wir auf irgendeinen Datentypen, speziell aber strings und Verweistypen zugreifen, muß die erste Bedingung die Überprüfung auf null sein. Es folgt dann die zweite Bedingung, daß der Datentyp überhaupt Elemente enthält.

Dieser Code:
(1) if (StrList!=null)
(2) if (StrList[0]!=null)
(3) if (StrList[0].Length>0)

führt zum Laufzeitfehler, ohne daß der Compiler vorher was merkt. Der Grund ist, daß StrList != null, aber wenn man versucht, ein Element anzusprechen (direkte Indizierung), dann gerät man außerhalb des Indexes, aus dem Grund, weil die Liste leer ist. Der Absturz erfolgt schon in Zeile 2, Zeile 3 wird gar nicht erst ausgeführt. Um diesen Fehler abzufangen, wird eine neue Zeile 2 eingefügt:

(1) if (StrList != null)
(2) if (StrList.Count()>0) // es ist mindestens 1 Element vorhanden
// was wir aber erst prüfen können, nachdem die Abfrage auf null stattgefunden hat, sonst wieder möglicher Laufzeitfehler
(3) if (StrList[0]!=null)
(4) if (StrList[0].Length>0)

Dieser Code ist auch den folgenden Zuweisungen gewachsen:
StrList.Add (""); // Element[0] wird angehängt
StrList[0]=null; // und genullt
Es passiert nichts, weil die Zeile 3 diesen Fall abfängt.

Man erkennt, daß bei Arrays von Strings und Verweistypen jedes einzelne Element auf null stehen (den Wert null haben) kann, selbst wenn das Array oder die Liste (die übergeordnete Datenstruktur) selbst nicht auf null steht. Und man erkennt auch, daß die direkte Indizierung eine Menge von Fehlermöglichkeiten bietet, und viel Code erforderlich macht, um die abzufangen, weshalb man sie wo es geht vermeiden sollte.

Direkte Indizierung macht vor allem dort keinen Sinn, wo die Anzahl der Elemente während der Laufzeit nicht feststeht, z. B. beim Einlesen aus einer Datei, oder wenn während des Programmbetriebs Datensätze hinzugefügt oder gelöscht werden.

Stringverarbeitung:

Für Strings gibt es verschiedene Datenstrukturen, um sie als Aufzählung zusammenzufassen. Eine davon ist die List<T> mit der Überladung List<string>.
string zeile = "Mein Name ist Hase"; // kein new!

Liste:
List<string> stringlist = new List<string>(); // neue Liste, Organisation ist dynamisch, daher muß die Anzahl der Elemente nicht festgelegt werden
stringlist.Add(zeile); // beliebig viele Elemente anhängen

Stringarray:
string[] strarray = new string[10]; // hier ist es erforderlich, die Anzahl der Elemente festzulegen
string[0]= zeile; // ein Element einfügen, direkte Indizierung

Aus den ganzen oben ausgeführten Gründen ist das Stringarray in dieser Form NICHT empfehlenswert.

Wir kommen aber um die Größenfestlegung herum, wenn wir das stringarray bei der Initialisierung gleich mit Daten beschreiben. Das geschieht, wenn wir z. B. eine Textdatei auslesen:
using System.IO;
string dateiname =@"D:\test.txt"; // Der Operator @ verhindert, daß das Zeichen \ als Escape-Sequenz gedeutet wird, sonst müßte man D:\\test.txt schreiben
string[] strarray = File.ReadAllLines(dateiname);

Man sieht, daß man diesmal bei string[] keine Zahl angeben muß. Wenn in der datei 1000 Datensätze enthalten sind, umfaßt unser Stringarray jetzt alle 1000.

Die Methode File.ReadAll .... macht eine ganze Menge. Sie öffnet die Datei, liest sie komplett aus und schließt sie wieder, alles mit einer Zeile. Um festzustellen, ob die Datei existiert, dient die Methode .Exists():

if (File.Exists(Dateiname)) ….

Der benötigte Datentyp, um die Textdatei auszulesen, ist string[]

Bei der Weiterverarbeitung möchte man vielleicht besser eine Liste verwenden. Die Zuweisung erfolgt dann so
foreach(string element in strarray)Stringlist.Add(element); // wir haben jetzt eine Kopie von string[] in dem Datentyp Liste

Für die Speicherung der Liste existiert analog folgende Methode:
File.WriteAllLines(dateiname,strarray); // Fertig. Einfacher geht´s nicht.

Bisher haben wir die Listen auf der Konsole gesehen. Ich mach jetzt mal einen kleinen Abstecher nach Windows.Forms. Wir wollen unsere Listen im Windows-Format sehen können.
Dazu hätten wir mehrere Container zur Auswahl. Z. B. richTextBox oder listBox. Die Listbox hat den Vorteil, daß wir mit der Maus eine Auswahl aus den Einträgen vornehmen können.

Daten in die Listbox schreiben:

Dafür sollten wir sie nicht vergessen zu leeren, sonst hängt man den Inhalt an den vorhandenen an, was hier nicht gewünscht ist:
this.listBoxKonten.Items.Clear(); // Methode items der Listbox, die wiederum weitere Methoden aufweist, hier .Clear()
„this“ bezieht sich darauf, daß die listBox sich im aktuellen Formular (Fenster) befindet. Man muß das nicht schreiben, aber der Vorteil, wenn man es macht, ist, daß der intellisence anspringt.
Wir tippen ein this. Und sobald der Punkt erscheint, bietet uns intellisence eine Auswahl der verfügbaren Elemente.
foreach (string element in strList) this.listBox1.Items.Add(element); // Damit sind alle Elemente der Liste in die Listbox geschrieben

Gewöhnlich macht man das am Programmanfang, um die Daten verfügbar zu haben, z. B. Artikelliste, Kundenliste etc. etc. , oder im laufenden Programm, um Daten aus einer anderen Datei einzulesen.

Ein kleiner Abstecher, wie man aus der Listbox später den Datensatz selektiert:

Dafür aktivieren wir (zum Beispiel) in den Eigenschaften der Listbox die Funktion double_click, indem wir auf dem Entwurfsformular mit der Maus einen Doppelklick ausführen oder im Eigenschaften-Menü (Aufruf über der Listbox mit der rechten Maustaste) die Funktion unter den Ereignissen (der gelbe Blitz) aussuchen und dort doppeklicken. Das Ergebnis ist dann, daß der Compiler folgenden Funktionsrumpf verfügbar macht:

private void listBoxKonten_DoubleClick(object sender, EventArgs e)
{
}

In den wir nun reinschreiben können, was im Falle Doppelklick geschehen soll. Die listBox stellt eine Methode zur Verfügung, mit der wir das ausgewählte Element erkennen können, nämlich .SelectedItem.

Den Eintrag kann man z. B. zu einem String umwandlen : string zeilstring = listBoxKonten.SelectedItem.ToString();
Sieht dann so aus:

private void listBoxKonten_DoubleClick(object sender, EventArgs e)
{
string zeilstring = listBoxKonten.SelectedItem.ToString();
this.textBox1.Text = zeilstring;
}

Und ihn dann an anderer Stelle ausgeben, z. B. wie oben zu sehen in einer Textbox, oder sonstwas damit anzustellen.

Die Organisation der listBox und richTextBox unter Windows.Forms ist im Kern ebenfalls dynamisch, was man daran erkennt, daß man zur Laufzeit beliebig viele Elemente anhängen kann. Zugleich wird aber ein Index sozusagen „mitgeschleppt“, mit dem man die einzelnen Elemente ansprechen kann. Aus den oben genannten Gründen sollten wir es aber vermeiden, den Index direkt anzusprechen. Es zwingt uns keiner dazu.

Wie geht es weiter?

Ich weiß, die Grundlagen sind langweilig.

Idee ist, daß wir erstmal unser Handwerkszeug kenenlernen, nach praktischen Gesichtspunkten, und wenn der Kenntnisstand reicht, in die Windows Forms Programmierung überwechseln.

Dazu kommt zunächst ein Beitrag über die Sortierung von Listen, weil unsortierte Listen und Dateien keinen praktischen Wert haben.
Im Anschluß ein Beitrag zum Umgang mit Dateien, nicht alle, aber die Dateifunktionen, die man braucht.


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 07.04.2014, 17:09 Uhr
sharky2014
Level 7 = Community-Professor
*******

Kommen wir zu der Sortierung von Listen.

Unsortierte Listen sind im praktischen Gebrauch unpraktisch bzw. wertlos.

Die Windows.Forms bietet in den zugehörigen Datenstrukturen wie z.B. Listbox die Eigenschaft .sort an. Man kann es sogar so einstellen, daß die Sortierung der Liste zur Laufzeit aktuell gehalten wird. Das geht natürlich zu Lasten der Performance, je länger die Liste ist. Und ist auch nicht unbedingt nötig, weil C# dafür bestens ausgestattet ist.

Dazu tritt sehr häufig der Fall auf, daß wir nach individuellen Kriterien sortieren wollen, die die Standardfunktionen nicht anbieten.

Bereits der Typ List<T> enthält eine Methode .Sort.

Bei einfachen Datentypen wie int oder double oder meinetwegen auch string funktioniert das auch. Bei string wäre aber schon zu fragen, wie der .Sort mit den Umlauten und Sonderzeichen umgeht.
Hat man es mit zusammengesetzten Datentypen zu tun, z. B.:

class Teil
{
int bestnr;
string bezeichner;
double preis;
}

Kann die Eigenschaft .Sort nicht mehr weiterhelfen, da die Liste ja wissen muß, nach welchem Feld der Indexer zu indizieren ist. Wir nehmen zur Erklärung genau so einen Datentypen, weil man mit der Methode auch alle anderen Arten von Datentypen indizieren kann.

Wir benötigen dazu eine sogenannte Schnittstelle, die sich von einer anderen Klasse ableitet, welche bereits in der .NET Umgebung verfügbar ist.
Zunächst einmal die “Organisation” unserer Teileliste:

namespace ConsoleApplication1
{
public class Teile // Deklaration der Klasse Teile
{
public class Teil // eingebettete Klasse Teil mit dem Datensatz für Teil
{
public int bestnr;
public string bezeichner;
public double preis;
}
public List<Teil> Teileliste; // die List gehört nicht zur Klasse Teil, sondern zur Klasse Teile
public Teil t; // ebenso die Variable t vom Typ Teil

public Teile() // Achtung: das ist der Konstruktor Ohne den wären diese Daten nichtt verfügbar
{
Teileliste=new List<Teil>();
t = new Teil() {bestnr=375,bezeichner="Schrauben M10 Großpackung", preis=28.14};
Teileliste.Add(t);
t = new Teil() {bestnr=1475,bezeichner="Muttern M8 Großpackung", preis=14.95};
Teileliste.Add(t);
}

public void zeige_Liste(List <Teil> Liste) // würde man zeige_liste nicht in die Klasse Teile schreiben, müßte man Teil ansprechen mit Teile.Teil
{
foreach (Teil element in Liste)
Console.WriteLine(element.bestnr.ToString("0000000")+" " + element.bezeichner.PadRight(35) + element.preis.ToString("#####.##"));

}


}

class Program
{


static void Main(string[] args)
{
var hteil = new Teile();
hteil.zeige_Liste(hteil.Teileliste); // Urzustand: Erst Schrauben, dann Muttern
Console.ReadKey();
}
}
}



Die Symbole in den Klammern nach .ToString(Symbole … ) dienen der Formatierung, damit die Liste bündig am Bildschirm erscheint.

Nachdem die Teileliste erstellt wurde, nun die Schnittstelle für die Sortierung:

public class INameSort : IComparer<Teil>
{
public int Compare(Teil x, Teil y)
{
Teil t1 = x;
Teil t2 = y;
return String.Compare(t1.bezeichner, t2.bezeichner);
}
}

Die Namensgebung ist frei, sollte mit einem I beginnen für Interface (=Schnittstelle), nicht dagegen die Ableitung von der Klasse Icomparer<T>, welche die benötigten Funktionalitäten zur Verfügung stellt. Wie sie das macht, kann uns im Augenblick egal sein. Was sie macht, ist, für die Liste den besten Sortieralgorithmus auszuwählen. Auch bei mehreren tausend Datensätzen ist die Funktion sehr schnell
.
Der Datentyp, der sortiert werden soll, erscheint in < > Klammern. Es ist der Datentyp Teil.
Innerhalb der Schnittstelle deklarieren wir zwei Bezeichner vom Typ Teil, nämlich t1 und t2, und weisen ihnen x bzw. y zu. Warum? Das ist im Schnittstellendesign des Icomparer so festgelegt.

Der eigentliche Vergleich erfolgt hier über eine Methode der Klasse String, nämlich String.Compare. Das muß nicht so sein, wie man gleich noch sehen wird. Innerhalb der Schnittstelle haben wir auch andere Möglichkeiten.

Was ich im Internet hasse, ist, daß man fast immer nur Programmschnippsel findet, die oft nur flüchtig dahingepinnt sind und sich nur sehr sperrig ausprobieren lassen. Richtig weiter beim Programmieren kommt man nur durch funktionierenden Code, mit dem man experimentieren kann, daher ist das in diesem kleinen Tutorial anders.

Das hier ist lauffähiger Code.
Kopieren, in VisualStudio einfügen, ausprobieren und rumprobieren:

namespace ConsoleApplication1
{
public class Teile
{
public class Teil
{
public int bestnr;
public string bezeichner;
public double preis;
}
public List<Teil> Teileliste;
public Teil t;

public Teile()
{
Teileliste=new List<Teil>();
t = new Teil() {bestnr=375,bezeichner="Schrauben M10 Großpackung", preis=28.14};
Teileliste.Add(t);
t = new Teil() {bestnr=1475,bezeichner="Muttern M8 Großpackung", preis=14.95};
Teileliste.Add(t);
}

public void zeige_Liste(List <Teil> Liste)
{
foreach (Teil element in Liste)
Console.WriteLine(element.bestnr.ToString("0000000")+" " + element.bezeichner.PadRight(35) + element.preis.ToString("#####.##"));

}

public class INameSort : IComparer<Teil>
{
public int Compare(Teil x, Teil y)
{
Teil t1 = x;
Teil t2 = y;
return String.Compare(t1.bezeichner, t2.bezeichner);
}
}

}

class Program
{


static void Main(string[] args)
{
var hteil = new Teile();
var hprog = new Program();
hteil.zeige_Liste(hteil.Teileliste); // Urzustand: Erst Schrauben, dann Muttern
hteil.Teileliste.Sort(new Teile.INameSort()); // Aufruf der Sortierfunktion nach Alphabet
hteil.zeige_Liste(hteil.Teileliste); // neuer Zustand: erst Muttern, dann Schrauben
Console.ReadKey();
}
}
}


Im Grunde wird hier die Methode .Sort von List<T> aufgerufen. List<T> erkennt, es ist ein zusammengesetzter Datentyp, der standardmäßig so nicht sortiert werden kann, und benutzt dann die angegebene Schnittstelle, nämlich Teile.INameSort(). Von der wird eine neue Instanz aufgerufen und die Daten sind sortiert. Dabei werden die strings der Bezeichner verglichen, die Rückgabewerte sind größer als 0 (String 1 größer als String 2), kleiner als 0 (der umgekehrte Fall) oder 0, wenn die Strings gleich sein sollten. Die Rückgabewerte sind nicht -1 oder +1, sondern entsprechen der Differenzen im Ascii-Code. Will man nur -1 oder +1, muß man filtern.

Die Methode der Klasse string, nämlich String.Compare(string1, string2) hat den Nachteil, daß damit die Sonderzeichen möglicherweise nicht so behandelt werden, wie wir wünschen.

Wir können die Schnittstelle beim Vergleich auf String auch verfeinern bzw. umschreiben, hier mal nur so zum Spaß bzw. zum Verständnis:

Statt diesem Code:

public class INameSort : IComparer<Teil>
{
public int Compare(Teil x, Teil y)
{
Teil t1 = x;
Teil t2 = y;
return String.Compare(t1.bezeichner, t2.bezeichner);
}
}

Schreiben wir jetzt:

public class INameSort : IComparer<Teil>
{
const char Sonderzeichen ='@';
public int Compare(Teil x, Teil y)
{
Teil t1 = x;
Teil t2 = y;
if (t1.bezeichner[0] == Sonderzeichen) return 1000;
else
return String.Compare(t1.bezeichner, t2.bezeichner);

}
}


Damit dürften alle Teile, die ein @als erste Stelle im Namen führen, am Ende der Sortierung obenauf schwimmen, denn 1000 ist mehr als der größte Ascii-Wert..

Man sieht: direkte Indizierung ohne Prüfung auf null und ohne Prüfung auf Stringlänge. Sowas sollte man natürlich real nicht tun, hier nur als Beispiel.


Kommen wir zu einer anderen Sortierung, nämlich nach Bestell-Nr.

Dazu schreiben wir einen zweiten Icomparer, bzw. copy&paste mit ein paar Änderungen:

public class INumSort : IComparer<Teil>
{
public int Compare(Teil x, Teil y)
{
Teil t1 = x;
Teil t2 = y;
if (t1.bestnr>t2.bestnr) return 1;
else if (t1.bestnr<t2.bestnr) return -1;
else return 0;
}
}


Wir gehen also auf die Bestellnummer. Und fügen den Aufruf der Funktion in Main hinzu.

Ergebnis:
static void Main(string[] args)
{
var hteil = new Teile();
var hprog = new Program();
hteil.zeige_Liste(hteil.Teileliste); // Urzustand: Erst Schrauben, dann Muttern
hteil.Teileliste.Sort(new Teile.INameSort()); // Aufruf der Sortierfunktion nach Alphabet
hteil.zeige_Liste(hteil.Teileliste); // neuer Zustand: erst Muttern, dann Schrauben
hteil.Teileliste.Sort(new Teile.INumSort()); // Numsort nach Bestellnummer
hteil.zeige_Liste(hteil.Teileliste); // neuer Zustand: die Schrauben stehen wieder vorn

Console.ReadKey();
}

Man sieht, wie die Programmierung in C# mit .NET funktioniert:

Anstelle eigener Programmierung gilt es den Umgang mit Methoden der Bibliothek zu lernen und die wie Bausteine in den Code einzufügen.

Da .NET tausende von Klassen und 10.000e Methoden bereitstellt, ist die eigentliche Aufgabe, die Methoden und Klassen zu finden, die man benötigt, und sich damit vertraut zu machen.

Der Beitrag wurde von sharky2014 bearbeitet: 07.04.2014, 17:22 Uhr


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 08.04.2014, 19:03 Uhr
sharky2014
Level 7 = Community-Professor
*******

Nächster angesagter Punkt wäre die Organisation der Dateien, also der Schreib- und Lesezugriffe auf den Datenspeicher.
Aber: am Ende dieses kleinen Tutorials soll ein voll funktionsfähiges Programm für Windows.Forms stehen. Da die Funktionen zahlreicher werden, wird der Code sehr schnell unübersichtlich. Es fehlt, auch in der Demo-Version der Programmentwicklung, die Struktur. Daher vorweg ein paar Worte zur Organisation eines Programms (dieses kleinen Demoprogramms):

C# bietet die sehr komfortable Möglichkeit, Klassen in getrennten Bausteinen abzulegen. Wie genau diese abgelegt werden, muß nicht interessieren, was interessiert, ist die Funktionalität. Die ist so, daß die Klassen uns den Code aufteilen in Textbausteine und wir dennoch innerhalb der Klassen auf den Code der anderen Klassen zugreifen können. Genau das ist ja das gewünschte.

In der Konsolenanwendung haben wir zunächst eine Klasse Program (Default-Name, lassen wir so), die das static Main enthält. Das ist ja der Einstiegspunkt ins Programm. Welche Programmelemente augenblicklich verfügbar sind, sagt uns der Projektmanager der IDE.

Program.cs (ist der C# Code für die Klasse Program)

Würde man nun alles in diese Klasse hineinschreiben, hätte man eine endlose Aneinanderreihung von Code, das wird sehr schnell unleserlich.
Das Anlegen der Klassen wird von der IDE unterstützt. Unter dem Menu Projekt Elemente hinzuzufügen, z. B. Klassen, wird die neue Klasse als Funktionsrumpf angelegt.

Die Frage wäre, nach welchen Gesichtspunkten man die Klassen zusammenstellen und aufteilen soll?

In dem kleinen Demo-Programm haben wir es mit Teilen zu tun, bislang Schraube und Mutter, aus dem Eisenwarenbedarf. Um die Definitionen und Methoden dafür unterzubringen, empfiehlt sich eine Klasse Teile. Nennen wir sie, um gleich zu sehen, daß es eine Klasse ist, CTeile. Dazu sind die Kunden angedacht, die diese Teile kaufen. Sie erhalten eine eigene Klasse, sinnigerweise mit dem Namen CKunden. Teile und Kunden sind die Mehrzahl, innerhalb der Klasse können wir den Datensatz für den einzelnen Kunden bzw. das einzelne Teil CKunde bzw. CTeil benennen. Die Benennung ist immer so eine Sache, man sollte das so präzis und einfach machen, daß man später intuitiv weiß, was gemeint war. Bislang ist das eindeutig und sieht dann so aus:

Program.cs // enthält die Methodenaufrufe der anderen Klassen und sonstigen allgemeinen Code
CTeile.cs // Definition der Datenstruktur Teile sowie Methoden, um diese zu bearbeiten, z. B. Teileliste
CKunden.cs // Dito für Kunden

Wenn wir nun Dateifunktionen schreiben, die die Kunden- und Teileliste auf dem Datenträger speichern, sortieren, auslesen etc. etc., ist die Aufteilung nicht mehr so eindeutig.

Sollen wir die Dateifunktionen für Kunden in die Klasse CKunden schreiben, und dito für Teile in die Klasse CTeile, oder ist es besser, für alle Dateizugriffe eine Klasse CDateifunktionen anzulegen? In dem einen Fall hätten wir eine objektorientierte Strukturierung, in dem anderen Falle eine funktions(bzw. methoden-)gebundene. Wenn man nur 2 Klassen hat, CKunden und CTeile, spielt das erstmal keine Rolle. Die Objekte können sich aber vermehren. Die Methoden können sich zwar auch vermehren, aber bezogen auf die Datenspeicherung in Dateien eigentlich nur sehr unwesentlich. Es wird also beim Anwachsen des Programms immer mehr zusätzliche Objekte geben, aber immer nur wenige Dateifunktionen (lesen, schreiben in verschiedenen Formaten).

Nehmen wir an, wir hätten 200 Klassen, und alle diese 200 Klassen müßten auf irgendeine Art auf dem Datenträger gespeichert und gelesen werden. Dann wird die Sache klarer. Wir müßten, wenn wir die Funktionen an das Objekt binden, dann 200 Methoden für das Speichern dieser Objekte sowie 200 für das Lesen schreiben, und wenn sie sowohl binär als auch als Text behandelt werden können sollen, hätten wir dann 800 Methoden zu schreiben allein für Lesen und Schreiben auf dem Datenträger. Das Problem wäre nicht, diese Methoden zu schreiben, das geht praktisch per copy und paste sehr einfach. Das Problem ist, wenn der Datenträgerzugriff aller Methoden geändert werden soll, muß man 800 Methoden im Code auffinden und bei allen diese Änderungen durchführen. Das wäre eine absehbare Katastrophe. Normal bemüht man sich, Zugriff auf ein Medium mit möglichst nur einer einzigen Klasse zu realisieren, und nicht von überhall her im Code.

Die Lösung wäre als allgemeiner Algorithmus, daß man die Dateifunktionen ausklammert. Man legt also alle Dateifunktionen in einer Klasse zusammen, und die anderen Objektklassen enthalten keine Dateifunktionen.

Die Sache wäre ganz einfach, wenn wir es nur mit Textdateien zu tun hätten. Dazu würden für beliebig viele Objekte zwei Funktionen reichen, nämlich lese(Textdatei) und schreibe(Textdatei),

Erforderliche Parameter für das Speichern beliebiger Textdateien sind der Name und einer dieser beiden Datentypen:

public bool speichere_textdatei(string dateiname, List<string>stringlist) // als List<T>
public bool speichere_textdatei(string dateiname, string[] stringarray) // als Stringarray

Diese Art von Dateizugriffen sollte man daher auf jeden Fall ausklammern, sagen wir in eine Klasse DateifunktionenText.
Text können wir mit zwei Dateifunktionen behandeln.

Bei zusammengesetzten Datentypen geht das so nicht. Jeder zusammengesetzte Typ hat andere Felder mit anderen Datentypen. Gespeichert werden können solche Typen mit dem sogenannten Serialisierungsverfahren. Das Format ist binary, also byte für byte. Beim Auslesen muß genau der Typ benannt sein, von dem die Daten gespeichert wurden, sonst werden die Bytes in falsche Format transponiert und das Programm stürzt ab. Um in diesen Fällen "auszuklammern", hab ich bisher noch keine gute Lösung gefunden. Auch eine Methodenüberladung würde vergleichbare Mengen Code erfordern. Solange man allerdings nur mit einer Handvoll Dateien zu tun hat, ist die Lösung entbehrlich, denn es gibt kein Problem.

Jedenfalls, wie auch immer, die Dateifunktionen wurden schon wegen der Textdateien ausgeklammert, unsere Projektliste sieht nun so aus:

Program.cs // enthält die Methodenaufrufe der anderen Klassen
CTeile.cs // Definition der Datenstruktur Teile sowie Methoden, um diese zu bearbeiten, z. B. Teileliste
CKunden.cs // Dito für Kunden
CDateifunktionen.cs // enthält alle Methoden, um Dateien zu schreiben und zu lesen
CSort.ds // Enthält die Methoden zum Sortieren der Listen

Der Code ändert sich dann natürlich, wenn Elemente anderer Klassen angesprochen werden. Entweder muß man einen Objektverweis setzen mit hAndereKlasse = new AndereKlasse oder bei Variablen oder statischen Klassen dem Bezeichner den Klassennamen vorausstellen: AndereKlasse.Bezeichner ...
Werfen wir erstmal einen Blick auf die neue Klasse CDateifunktionen. Das Problem der Namensgebung wurde hier so gelöst:

class CDateifunktionen
{
public class dateinamen
{
string pfad;
string dn_teileliste_bin;
string dn_teileliste_txt;
public string Pfad { get { return pfad; } set { ;} }
public string DN_teileliste_bin { get { return dn_teileliste_bin; } set { ;} }
public string DN_teileliste_txt { get { return dn_teileliste_txt; } set { ;} }
public dateinamen()
{
pfad = @"D:\";
dn_teileliste_bin = pfad + "TL_LIST.bin";
dn_teileliste_txt = pfad + "TL_LIST.txt";
}
}


Wir sehen hier wieder einen Konstruktor bei der Arbeit. Die set-Anweisung (der Setter, der auf die privaten Dateinamen zugreifen darf) wurde hier nur angedeutet zwecks späterem Ausbau. Normal sollten die Dateinamen nicht vom Anwender ständig gewechselt werden dürfen, weil das gefährlich ist, der Setter behält sich also eine Möglichkeit vor, in bestimmten Fällen doch eine Änderung zuzulassen, was hier noch offen bleibt. Entsprechendes gilt für die Anlage des Verzeichnisses.
Als nächstes folgt die (problemlose) Abwicklung der Textdateien. Dann wird es etwas komplexer, wenn es um die Binärformate geht.
Wir legen eine leere Datei an, die nicht typisiert ist, also 0 Bytes enthält. Das kann Sinn machen, wenn das Programm beim Start nach bestimmten Dateien sucht und zufrieden ist, diese vorzufinden, auch wenn sie leer sind. Dazu werden Methoden der Klasse FileStream verwendet:

public bool leere_datei_anlegen(string dateiname)
{
try
{
FileStream fs = new FileStream(dateiname, FileMode.Create);
fs.Close();
return true;
}
catch (Exception)
{
return false;
}
}


Zwar hab ich oben meine Skepsis geäußert, ob der try/Catch Algorithmus in ein fertiges Programm gehört. Das war so gemeint, wir sollen nicht fehlerhafte Programmierung mit der Fliegenklatsche totschlagen, sondern die Fehler überhaupt vermeiden. Dann braucht es keinen Try/Catch.
Bei allen IO-Methoden allerdings ist try/Catch sehr wünschenswert. Weil die IO (Input-Output)-Operation nämlich auch aus Gründen schief gehen kann, die der Programmierer nicht zu verantworten hat. Dazu gehören z. B. fehlende Zugriffsrechte. Oder ein neues Betriebssystem, oder die Datei ist vorhanden, aber beschädigt. Datentransfer innerhalb eines Netzwerkes kann auch aus vielen Gründen irgendeinen Fehler auslösen.
Einfachster Weg, Text in einer Datei abzulegen:

public bool speichere_textdatei(string dateiname, List<string>stringlist)
{
try
{
File.WriteAllLines(dateiname, stringlist);
return true;
}
catch (Exception)
{
return false;
}
}


Hier wird nicht die Klasse FileInfo verwendet, sondern die Klasse File.
Und um sie wieder auszulesen.
Anschließend wird das Stringarray string[] in eine List<T> kopiert mit foreach()

public bool lies_textdatei(string dateiname,List<string> stringlist)
{
if (!File.Exists(dateiname))return false;
try
{
string[] stringarr =File.ReadAllLines(dateiname);
foreach (string element in stringarr)
{
stringlist.Add(element);
}
return true;
}
catch (Exception)
{
return false; // dateifehler
}
}


Die Datenübergabe erfolgt nicht als Rückgabewert, sondern per Reference. Die Methode gibt den Datenwert boolean true oder false zurück, hat aber vorher den Parameter überschrieben, nämlich die List<T> bzw List<string> stringlist, die dann die neuen Werte enthält.

So sollte es sein.

Es gibt Fälle, wo man die Liste ungewollt explizit als Referenz übergeben muß, der Code wäre:
public bool lies_textdatei(string dateiname, ref List<string> stringlist)

Dann dürfte die Strukturierung des Programms noch nicht optimal sein, den obwohl es funktionert, sollte man Verweistypen nicht als ref übergeben müssen.

Woran solche Schönheitsfehler hängen, ist im Einzelfall oft sehr schwierig herauszufinden. Jedenfalls ist der Code dann noch nicht wirklich optimiert.

Der Beitrag wurde von sharky2014 bearbeitet: 08.04.2014, 19:09 Uhr


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 08.04.2014, 19:37 Uhr
sharky2014
Level 7 = Community-Professor
*******

Kommen wir zu Serialisierung, zum Abspeichern im binären (byte-Format). Dabei sehen wir uns die Fehlerbehandlung bei IO-Methoden nochmal genauer an.

Dieser Code hier:

public bool speichere_teileliste_binaer(List<CTeile.Teil> teileliste)
{
var hdatname = new CDateifunktionen.dateinamen();
string dateiname = hdatname.DN_teileliste_bin;

try
{
FileStream stream = new FileStream(dateiname, FileMode.Create);
BinaryFormatter bformatter = new BinaryFormatter();
bformatter.Serialize(stream, teileliste);
stream.Close();
return true;
}
catch (Exception )
{
return false;
}
}


Speichert unsere Teileliste im Binärformat auf dem Datenspeicher. Dazu stellt die Klasse FileStream die Verbindung zu der Datei her, überschreibt diese, wenn vorhanden, oder legt sie neu an. Es wird eine Instanz der Klasse BinaryFormatter aufgerufen, und der übergibt die gesamten Daten byte für byte an den Stream. Wird der stream geschlossen, ist die Datei gespeichert. Es sei denn, es träte eine Exception auf. Und genau das ist hier der Fall, aber aus Gründen, die im Codeabschnitt nicht zu sehen sind.

Wenn wir abfragen mit if(speichere_Teileliste ... erhalten wir den Rückgabewert false. Aber warum?

Um das herauszufinden, sollten wir den ziemlich lieblos gehaltenen Catch-Block etwas ändern, nämlich statt:

catch (Exception )
{
return false;
}

schreibt man besser:

catch (Exception info)
{
Console.WriteLine(info.Message); // Message ist eine Methode von Exception, Der Bezeichner info oder e oder sonstwas ist wahlfrei
return false;
}

Die Fehlermeldung ist: Serialisierungsfehler, weil die Klasse nicht das Attribut Serializable aufweist.

Um das abzustellen, muß die Klasse wie folgt geändert werden:

namespace ConsoleApplication1
{
class CTeile
{
[Serializable()]
public class Teil
{
public int bestnr;
public string bezeichner;
public double preis;
}

public List<Teil> Teileliste;
public Teil t;



Danach wollen wir die Datei wieder auslesen.

Nachdem die Teileliste auf dem Datenträger gespeichert ist, löschen wir die Liste im Arbeitsspeicher:

Auszug aus Main:


var hteile = new CTeile(); // die Instanz auf die Klasse CTeile
hteile.Teileliste.Clear();


Und lesen die Datei zurück, um zu sehen, ob der Vorgang geklappt hat:

public bool lies_teileliste_binaer(List<CTeile.Teil> teileliste)
{
bool erfolg = true;
var hdatname = new CDateifunktionen.dateinamen();
string dateiname = hdatname.DN_teileliste_bin;
FileInfo fi = new FileInfo(dateiname);
if (File.Exists(dateiname))
{
if (fi.Length > 0) // deserialize leere Datei = laufzeitfehler
{
try
{
FileStream stream = new FileStream(dateiname, FileMode.Open);
BinaryFormatter bformatter = new BinaryFormatter();
teileliste = (List<CTeile.Teil>)bformatter.Deserialize(stream);
stream.Close();
}
catch (Exception )
{
erfolg = false;
}
}// if exist
}
else erfolg = false;
return erfolg;
}


Lesen ist fehlersensitiver als Schreiben. Erstmal wird geprüft, ob die Datei überhaupt existiert. Danach muß man prüfen, ob die Datei Daten enthält, weil es sonst zu einem Laufzeitfehler (Exceptio) kommt und das Programm abbricht. Das Deserilize Verfahren funktioniert nicht bei leeren Dateien. Das wissen wir, und daher müssen wir das nicht in den catch Block setzen, der catch soll uns ja eher unvorhergesene Fehler melden und nicht die schon bekannten. Man sieht, wie die Methode den ganze Block von Daten nicht satzweise, sondern eben blockweise innerhalb einer Zeile einliest.



Der Aufruf aus Main:

if (hdatei.speichere_teileliste_binaer(hteile.Teileliste))
Console.WriteLine("Teileliste binär gespeichert");
else Console.WriteLine("Probleme beim SPeichern");
Console.WriteLine("Und wird gelöscht, neuer Inhalt ist:");
hteile.Teileliste.Clear();
hteile.zeige_Liste(hteile.Teileliste);
if (hdatei.lies_teileliste_binaer(hteile.Teileliste))
Console.WriteLine("Teileliste wurde zurückgelesen. Neuer Inhalt ist");
else Console.WriteLine("Problem beim Zurücklesen");
hteile.zeige_Liste(hteile.Teileliste);
Console.ReadKey();


Das Ergebnis ist, es erfolgt beim Zurücklesen keine Fehlermeldung.
Aber der Inhalt der Teileliste ist trotzdem leer.
Das heißt, der Datenzugriff by reference hat nicht funktioniert. Es wurde aber kein Laufzeitfehler ausgeworfen.

Woran liegt es?

Referenz heißt ja, daß die Daten in einen Speicherbereich geschrieben werden, der mit der Adresse der Referenz anfängt. Referenz heißt also Adresse erhalten und Schreibrechte. Wenn die Adresse da ist, aber nicht geschrieben wird, fehlt es vielleicht an den Schreibrechten.

Wir versuchen jetzt mal, die Referenz explizit zu erzwingen, indem wir den ref Modifizierer verwenden. Dazu müssen wir sowohl die aufrufende Routine als auch die Methode wie folgt ändern:

if (hdatei.lies_teileliste_binaer(ref hteile.Teileliste)) und

public bool lies_teileliste_binaer(ref List<CTeile.Teil> teileliste)


Und jetzt funktioniert es einwandfrei.

Warum das in diesem speziellen Fall so ist, kann ich nicht beantworten. Es kann an den Zugriffsmodifizierern oder irgendeinem anderen Detail liegen, was aber unerkannt bleibt, weil kein Fehler geworfen wird. Ist zwar ärgerlich, wenn man was hat, was man nicht sofort versteht, aber irgendein letztes Rätsel hat die Programmierung immer, sonst macht es ja keinen Spaß, oder?

Der Beitrag wurde von sharky2014 bearbeitet: 08.04.2014, 19:45 Uhr


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 08.04.2014, 20:42 Uhr
V4Aman
Level 7 = Community-Professor
*******

QUOTE (CNCAllgäuer @ 06.04.2014, 18:00 Uhr) *
ER ist wieder da tounge.gif

MfG

Florian


Oh mein Gott..... ja ....


--------------------
Gruß V4Aman


__________________________________________________________________________

Alle sagten: "Das geht nicht." Dann kam einer, der wusste das nicht, und hat's einfach gemacht.
TOP    
Beitrag 09.04.2014, 00:35 Uhr
x90cr
Level 7 = Community-Professor
*******

Ich bevorzuge Visual Basic .NET, das scheint wenn ich deine Beispiele hier so sehe wohl auch die bessere Wahl für Einsteiger.

Ich denke außerdem nicht das deine Beispiele hier irgend jemanden nutzen, da diese ganzen Programmiersprachen zu umfangreich sind um das ganze mit wenigen Beispielen zu vermitteln.


--------------------
!! Mein alter Nickname: canon !!

Beginnt man das System zu hinterfragen, so erkennt man deutlich, dass die „Wahrheit“ zumeist entgegengesetzt des scheinbaren zu finden ist.
  • Wenn wir uns heute keine Zeit für unsere Gesundheit nehmen, werden wir uns später viel Zeit für unsere Krankheiten nehmen müssen.
  • Wenn es klemmt - wende Gewalt an. Wenn es kaputt geht, hätte es sowieso erneuert werden müssen.
TOP    
Beitrag 09.04.2014, 17:59 Uhr
sharky2014
Level 7 = Community-Professor
*******

QUOTE (x90cr @ 09.04.2014, 01:35 Uhr) *
Ich bevorzuge Visual Basic .NET, das scheint wenn ich deine Beispiele hier so sehe wohl auch die bessere Wahl für Einsteiger.

Ich denke außerdem nicht das deine Beispiele hier irgend jemanden nutzen, da diese ganzen Programmiersprachen zu umfangreich sind um das ganze mit wenigen Beispielen zu vermitteln.


Das ist sicherlich unmöglich, daher beschränke ich mich auf das was es braucht, um ein kleines DemoProgamm zu erstellen, was aber voll funktionsfähig ist. Möglicher Vorteil von einem Beitrag wie diesem hier ist es, daß man gezielt funktionierende Programmbausteine nachschlagen kann, wenn man sie mal braucht.

Zu VB: Es gibt viele, die VB benutzen, das hat wohl auch seine Vorteile. Am besten auf .NET abgestimmt ist aber C#, weil es von vornherein für das NET neu entwickelt wurde.


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 09.04.2014, 18:13 Uhr
sharky2014
Level 7 = Community-Professor
*******

Wenn man eine Funktion einsetzt, deren Funktionsweise man nicht versteht, sie aber "augenscheinlich" funktioniert, ist das keine ungefährliche Vorgehensweise. Weil es Seiteneffekte geben kann, die irgendwann das Programm instabil machen.

Der Einsatz des Schlüsselwortes ref an einer Stelle, wo es eigentlich gar nicht nötig sein sollte, weil sowieso ein Verweistyp vorliegt, kann keine Lösung sein, ohne daß man weiß, warum der normale Weg des Verweistypen nicht funktioniert. Bei Microsoft liest man dazu:

"Verwechseln Sie das Übergeben als Verweis nicht mit Verweistypen. Die beiden Konzepte sind nicht identisch. Ein Methodenparameter kann durch ref geändert werden, unabhängig davon, ob es ein Werttyp oder ein Verweistyp ist. Beim Übergeben als Verweis wird kein Werttypboxing durchgeführt."

Wenn man die Werte im Speicher verfolgt, geschieht das:

1. Dateifunktion ruft binary Datei auf zum Auslesen
2. Schreibt Daten in die Liste (Die Liste ist vollständig, die Datenfelder sind innerhalb der Funktion vollständig sichtbar)
3. Rücksprung zum Hauptprogramm
4. Dort sind die Daten verloren.

Besonders Punkt 4 macht mir Kopfschmerzen. Ich wüßte gern, wo die geblieben sind, und man hat die Befürchtung, daß vielleicht an falscher Stelle im Speicher geschrieben wurde.

Eine andere Möglichkeit wäre, das out Schlüsselwort einzusetzen.

Out ist so definiert:

public void rechne_irgenwas(out int1, out double2, out sonstwas3 ... )

kann also beliebig viele Rückgabewerte erzeugen, im Gegensatz zu den normalen Methoden, die immer nur einen Rückgabewert haben. Man beachte, daß die Methode void ist !

Mit out geschieht etwas seltsames, nämlich der Compiler erkennt diese Zuweisung nicht an:

liste = (List<CTeile.Teil>)bformatter.Deserialize(stream);

Fehler 1 Der out-Parameter "stringlist" muss eine Zuweisung erhalten, bevor die Steuerung die aktuelle Methode verlässt.

Setzt man aus formalen Gründen am Ende der Funktion
liste=liste;

Meckert der Compiler zwar wieder, was das soll, ob das wirklich so gemeint ist, aber es funktioniert es tadellos.

Es funktioniert aber eben nur sozusagen "mit der Brechstange".

Um die Methode ohne ref und out funktionstüchtig zu machen, kann man eine lokale Liste liste2 benutzen:

liste2 = (List<CTeile.Teil>)bformatter.Deserialize(stream);
liste = liste2 führt nicht zum Erfolg (das dürfte eigentlich auch nicht sein).

Prüfung auf (referentielle, d.i. die Speicheradresse) Gleichheit ergibt, daß liste==liste2, aber sobald die Funktion verlassen wird, sind die Daten weg.
Mit dieser einen zusätzlichen Zeile ist das Problem aber "sauber" gelöst:

foreach(<Cteile.Teil> element in liste2) liste.Add(element);

Ganz ohne Tricks und ohne ref und out.

Dieses Problem habe ich bisher nur im Zusammenhang mit serialize/deserialize kennengelernt. Möglich, daß der binary-Formatter die Daten in ein anderes Format bringt als gedacht.

Jedenfalls, an dieser Stelle hake ich das ab als vorerst erledigt, "saubere" Lösung ist erfolgt.


Was haben wir bisher an Funktionaliten zur Verfügung?

Teileliste mit unseren Artikeln, die zum Verkauf stehen
Kundenliste mit zwei Kunden
Sortieralgorithmus um diese nach Name oder Nummer zu sortieren
Dateialgorithmus um diese binär oder als Text zu speichern und zu lesen.

Was jetzt noch fehlt, die die verbindende Klasse, denn wenn Kunden und Teile zusammenkommen, ist das Ergebnis eine Rechnung.
Es fehlt also die Klasse CRechnung. Diese muß festlegen, Wieviele Artikel der Kunde gekauft hat und den Rechnungsbetrag ermitteln.
Da das nach dem vorausgegangen ziemlich trivial ist, lasse ich das zunächst weg und gehe in die Windows.Forms Programmierung.

Ziele:
Form1 erzeugen als Startplattform mit einem Button, der Form2 aufruft.

Form1 erledigt folgendes:

Suche nach den Dateien Teileliste und Kunden.

Wir setzen an dieser Stelle voraus, daß die Dateien vorhanden sind. Sonst wird es zu umfangreich, daß wir für die Stammdaten der Kunden und Teile weitere Formulare schreiben müssen etc. Das würde für dieses kleine Tutorial nichts bringen, weil wir immer dasselbe mit anderen Daten durchziehen. Daher belassen wir es bei 2 Dateien mit jeweils 2 Datensätzen. Nach der Devise: have seen one, have seen them all.

Sofern die Startroutine das OK gegeben hat, erledigt sie noch was anderes:

Sie lädt nämlich im Hintergrund die Daten dieser beiden Dateien in zwei Listboxen der Form 2, die noch nicht zu sehen sind, und wenn das erledigt ist, erscheint das zweite Fenster.

Form2 erledigt folgendes:

Es zeigt zwei Listboxen mit den Dateidaten an. Weitere Datenfelder kommen hinzu, Textboxen für die Auswahl, dann Preise und so weiter. Wir sollten auch einen DateTimeChecker dabei haben für das Datum.

Der Anwender soll durch Doppelklick auf die Listbox Kunden einen Kunden auswählen können.

Dann durch Doppelklick auf die Listbox Artikel Teile auswählen, die dem Kunden zugewiesen werden sollen.

Anhand der Listenwerte für die Teile errechnet das Formular den Rechnungsbetrag netto, weist diesem 19% Mehrwersteuer zu und zeigt den Rechnungsbetrag brutto an.

Das ist für eine kleine Einführung genug Funktionalität. Natürlich kann man das in jede Richtung erweitern, was zunächst mal interessiert ist, wie man die Basisfunktionen ans Laufen bringt.

Um in VisualStudio ein Windows-Forms Programm anzulegen, erstellt man ein neues Projekt und wählt Windows Forms aus.

Es erscheint dann ein leeres Fenster.
Hier müssen wir lediglich ein einziges Steuerelement einfügen: Einen Button, mit der Aufschrift "Rechnung schreiben". Um das sauber zu beenden, kommt noch ein zweites Steuerelement mit der Aufschrift "Ende" dazu.

Sowie eine Klasse "CStart" schreiben, welche überprüft, ob die benötigten Dateien vorhanden sind.

Damit man endlich mal was sieht, ein Blick auf das erste Formular.

Dazu wurde folgendes erledigt:

In der Form/Eigenschaften die Eigenschaft Autosize auf true gesetzt. Bewirkt, daß das Fenster immer alle Steuerelement zeigt und diese nicht abschneidet.

Der Button1 wurde umbenannt in btRechnungen mit der Eigenschaft Text = Rechnungen schreiben

Der Button2 wurde umbenannt in btEnde mit der Eigenschaft Text = "Ende"

Wir fügen anschließend einige Basisfunktionalitäten ein und müssen natürlich noch den Job erledigen, eine Klasse "Start" zu implementieren, die überprüft, ob die für den Programmstart erforderlichen Dateien vorhanden sind.
Angehängte Datei(en)
Angehängte Datei  DemoForm1.jpg ( 45.33KB ) Anzahl der Downloads: 14
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 09.04.2014, 18:33 Uhr
sharky2014
Level 7 = Community-Professor
*******

Wir sind jetzt an dem Punkt vom Umstieg von der Konsole auf Windows-Forms (siehe Screenshot).

Die Umgebung der IDE in Win Forms ist praktisch identisch mit der der Konsole.

Um die Klasse CStart dem Projekt hinzuzufügen, wählen wir Projekt-Klasse hinzufügen und benennen sie unten in der Zeile CStart.cs

Die Klasse soll zunächst eine Methode erhalten, um zu überprüfen, ob unsere Dateien da sind.

Das kann sie aber nicht, wenn sie nicht weiß, wie die Dateien heißen.

Sie könnte es, wenn sie mit einem Objektverweis auf die betreffenden Klassen zugreifen könnte. Kann sie aber nicht, wir haben ja nur die Form 1 und die neue Klasse CStart.

Wir müssen hier einmalig unsere Dateien aus dem Konsolenprojekt dem WinFOrmsProjekt hinzufügen.

Im Sceenshot sieht man rechts den ProjektManager, die die Form ausweist und die Klasse.

Aufgeklappt ist das Menü Projekt-Vorhandene Elemente hinzufügen, wo wir unsere bisher erstellten Klassen sehen können.

Die fügen wir nun allesamt dem Forms-Projekt hinzu, bevor wir weitermachen können.

Alle bis auf Program.cs natürlich, denn Program.cs ist eine Konsolenanwendung.

Der Beitrag wurde von sharky2014 bearbeitet: 09.04.2014, 18:35 Uhr
Angehängte Datei(en)
Angehängte Datei  DemoForm2.jpg ( 366.44KB ) Anzahl der Downloads: 16
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 09.04.2014, 19:30 Uhr
sharky2014
Level 7 = Community-Professor
*******

Ich überfliege jetzt nicht mit wenigen Worten komplexe Schritte, wir müssen uns jetzt im Schneckentempo durch die zu bewältigenden Details wühlen, welche rein formaler Art sind und have seen one have seen all. Aber einmal eben muß es sein.

Screenshot:

Die Klassen aus der Konsolenanwendung erscheinen nun im rechten Fenster in der Anzeige des Projektmanagers, allerdings mit einem anderen Logo, es sind jetzt keine Konsolenklassen, sondern WinForms-Klassen.

Das Dumme ist nur, die erste echte Winforms-Klasse CStart kann keine Verknüpfung herstellen, diese Klassen sind für neue WinForms-Klassen unsichtbar.

WARUM?

Weil sie in einem anderen Namespace stehen, nämlich ConsoleApplication1.

Um die alten Konsolen-Klassen sichtbar zu machen, müssen wir, wie im Anhang zu sehen, den Namespace einfügen. Hätten wir das direkt in WinForms programmiert, müßten wir das natürlich nicht. Ich weiß, ich weiß ... hier ist es aber so. Und das Problem wird öfter auftauchen. Fügen wir die using-Direktive hinzu wie im Anhang zu sehen, können wir die Klassen so ansprechen wie auf der Konsole zuvor.

Die Klasse CStart erhält also als erstes eine Methode, welche die Dateien überprüft. Mehr als eine Datei Teile haben wir noch gar nicht programmiert, die Datei Kunden muß noch folgen. Außerdem ist ja auch die Klasse Rechnungen noch gar nicht geschrieben, die wir für das 2. Formular benötigen.

Sofern die benötigten Dateien gefunden werden (hier also erstmal nur eine), soll die Methode der Form1 ein ok zurückgeben, damit wir das zweite Fenster öffnen können, um die Rechnungen zu schreiben.

Dazu gibt es heute nur noch ein paar kurze Hinweise. Ich habe zwar ein bereits funktionsfähiges WinForms Programm entwickelt, allerdings nicht für Lagerverwaltung, sondern eine FIBU, und kann daraus nichts mit copy und paste übernehmen, das wäre dann viel zu komplex und wenig hilfreich.

Die DemoVersion Lagerverwaltung wird wirklich von Grund auf neu programmiert. Alles andere hilft auch nicht wirklich.

Komplexe Programierungsbeispiele gibt es genug.

NUR DIE EINFACHEN HELFEN WIRKLICH WEITER.


Legen wir jetzt eine Form2 an. Das sind mit der IDE ein paar Mausclicks. Projekt - Form hinzufügen. Lassen wir den Namen bei Form2, so wie er ist.

Ich will jetzt nur noch darauf hinweisen, wie man die Forms beendet. Deshalb wird hier überhaupt die Form2 angesprochen, die man eigentlich gar nicht bräuchte.

Jede Form kann man beenden mit Klick auf das rote Feld x oben rechts. Das ist aber keine ordentliche Programmierung, jede Form sollte eine ENDE Taste bereitstellen, also einen Button.

Mit Doppelklick auf den Button im Designer (kommt alles später noch) erhalten wir den Funktionsrumpf, was geschehen soll, wenn der Button geklickt ist.

Hierbei gilt es zu beachten:

Der Button der Form1 schließt mit

this.Close();

Und damit ist das Programm beendet.

Um eine andere Form aufzurufen, benutzt man z. B.:

var f2 = new Form2();
f2.Show()

oder ShowDialog();

Dann erscheint das andere Formular.

Ist man im neuen Formular, und will das Formular beenden, kann man nicht schreiben

this.Close() // falsch,

weil dann das Formular zwar geschlossen wird, aber das aufrufende Formular einen Laufzeitfehler produziert, denn es versucht, auf das bereits geschlossene Formular zuzugreifen.

Daher werden nachgeordnete Formulare geschlossen mit:

this.hide() // verschwinde vom Bildschirm, bleib der Laufzeitumgebung aber noch erhalten.
return;
Angehängte Datei(en)
Angehängte Datei  DemoForm3.jpg ( 468.95KB ) Anzahl der Downloads: 10
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 10.04.2014, 15:37 Uhr
sharky2014
Level 7 = Community-Professor
*******

Anhang: Das Formular Form2, voreingerichtet.

Die Funktionselemente:

listBoxKunden
listBoxTeile

Zu den beiden Listboxes gibt es jeweils zwei Radio-Buttons, welche die Sortierung nach Nummer oder Name steuern,

Alle radiobuttons innerhalb einer Form verhalten sich (dankenswerterweise) so, daß immer nur einer aktiv sein kann.
Hier wollen wir aber zwei voneinander unabhängige Gruppen von jeweils zwei Elementen.

Dazu muß man die Radio Buttons nach Gruppen trennen, sie in ein Steuerelement setzen namens GroupBox. Dann ist innerhalb der Box jeweils ein Button

aktiv, ohne auf die Buttons in anderen Boxen zurückzuwirken.

Eine weitere groupBox ist erforderlich für die radioButtons MWSt 19%, 7%, Differenzbesteuerung und ohne MWST.

Die Werte-Ausgabe erfolgt in den Textboxen. Da wir die Werte durch Clicks bzw. interene Berechnung erzeugen, sind die Textboxen auf readonly gesetzt (Eigenschaftsfeld).

Nur eine einzige ist vorgesehen für eine Eingabe mit der Tastatur: Preisnachlaß. Das dient hier nur, um den Umgang mit solchen Feldern zu demonstrieren.

Die Textboxen mit den Werten benennt man zweckmäßigerweise um, z. B. tbBrutto, tbNetto usf.

Um dem Anwender zu zeigen, was die Textbox darstellt, sind Labels aufgeführt. Das sind die Feldbezeichner. Die Labels braucht man nicht umzubennen,

sondern nur an die passende Stelle vor der Textbox zu platzieren.

Der DateTimePicker wird ausgelesen mit dem Typ value vom Typ DateTime. Das ausgewählte Datum erscheint in einer Textbox.

Vom Umgang mit DateTime siehe unten.

Es gibt 4 Buttons:

Neue Rechnung, Eingabe prüfen, Speichern, Ende.

Die Ansicht ist eine Designer-Ansicht. Starten wir das Programm, verschwindet der Button speichern, warum, weil erst gespeichert werden darf, wenn

die Eingabe fertig und überprüft ist. Man erreicht das mit der Anweisung:

this.btSpeichern.Visible = false;

Und um den zu zeigen, wird die Eigenschaft true gesetzt.


Wenn wir die Vorbereitungen abgeschlossen haben, solange sind wir sozuagen noch prozedural, gelangen wir in die

WINDOWS WELT DER EREIGNISSE

Das heißt, das Programm macht dann nur überhaupt etwas, wenn mit der Maus, Tastur oder sonstirgendetwas irgendein Ereignis ausgelöst wird. Die ganze

übrige Zeit macht das Programm gar nichts. Es sei denn, wir hätten einen Timer aktiviert, der in regelmäßigen Abständen selbst ein Ereignis auslöst.

Das macht bei diesem kaufmännischen Programm keinen Sinn.

Es tut mir leid, ich muß noch mit etwas Theorie weiternerven.

Betrachten wir den Code der Form1:

using System.Windows.Forms;

namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
public Form1() // der Konstruktor
{
InitializeComponent();

/* Was in den Konstruktor an Anweisungen eingefügt wird, wird beim Aufruf der Form1 nur einmalig ausgeführt
Und zu einem Zeitpunkt, zu dem das Formular am Bildschirm noch nicht sichtbar ist
*/

}
}
}


Der Einsprung des Programms ist die Stelle innerhalb der Klasse Program, wo die Funktion form1() steht. Diese beginnt mit der Initialisierung der

benötigten Komponenten. Der Raum für mögliche eigene Anweisungen ist gekennzeichnet.

Um Anweisungen für die Ereignishandler der Element zu erzeugen, können wir mit der IDE die Funktionsrümpfe anlegen, indem wir das Element doppelclicken. Dabei sollten wir eine sinnvolle Reihenfolge beachten, damit nicht alles wild durcheinander steht.

Wir doppelklicken also (z.B.) nacheinander erstmal die Listboxen,

dann die ganzen RadioButtons

danach die Textboxen, damit das nach Gruppen geordnet hintereinander steht.

Das sieht dann ungefähr so aus:

public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

private void radioButton1_CheckedChanged(object sender, EventArgs e)
{

}

private void radioButton2_CheckedChanged(object sender, EventArgs e)
{

}
private void radioButton3_CheckedChanged(object sender, EventArgs e)
{

}
// und so weiter für alle Elemente
}



Die Funktionsrümpfe finden sich innerhalb der Klasse Form1, aber außerhalb des Kontruktors Form1().

Es juckt geradezu in den Fingern, nun in diese Funktionsrümpfe mal eben was reinzuschreiben, z. B. rbMWST19 soll dann in das Feld .MWSt des

Datensatzes Rechnung 19.0 eintragen, der DateTimeChecker trägt das Datum ein, und so weiter. Und fertig ist die Rechnung, oder?

Also: Organisation ist alles.

Daher zu Anfang einige wichtige Überlegungen, wie man so eine Programmierung strukturiert.

Wir können unterscheiden zwischen Anweisungen, die in der Form eine Ausgabe zur Folge haben, z. B. textBox1.Text="Hallo", und solchen, die zur Programmlogik gehören, zur eigentlichen Datenverarbeitung.

Ganz abstrakt steht es damit folgendermaßen:

Wenn wir Programmlogik in die Ereignishandler einfügen, ist die erste und zwangsläufige Folge:

1.) Schlechte und unübersichtliche Strukturierung,

weil der Code wie ein Flickenteppich über das Formular verstreut wird. Die nächste Folge ist

2.) Redundanz,

weil Elemente mit ähnlicher Funktion dieselbe Logik benutzen (müssen). Daher haben wir hier identischen Code an verschiedenen Stellen, was zur Folge

hat

3.) Fehleranfälligkeit

entweder von vornherein oder bei Programmänderungen. Wir müssen dann alle Stellen finden, die von der Änderung betroffen sind, und das ist

4.) Erhöhter Aufwand bei jeder Änderung

Zudem wird unser Formular durch die Logik und die Redundanz ungehemmt aufgebläht, so daß wir einen

5.) Berg von unnötigem Code mit vielen Wiederholungen

anhäufen.

Ein Beispiel, der Mehrwertsteuersatz:

rbuttonMWSt19 setzt den gewählten Satz auf 19 Prozent. Will der radioButtonEreignishandler dies in den Datensatz Rechnung eintragen, müßte er erstmal

überprüfen, ob die Voraussetzungen gegeben sind, z. B. innergemeinschaftlicher Verkehr in der EU, jedoch nicht Inlandsverkehr, oder ob der Kunde aus

irgendeinem Grund nicht mit 19%, sondern differenzbesteuert wird. Womöglich müßte er auch prüfen, ob dieser Kunde bis zu einem gewissen Stichtag so,

und danach anders besteuert wird, oder das nur für bestimmte Warengruppen gilt, und woher die Daten kommen sollen, wäre auch noch eine Frage.

Irgendeine Datenbank käme dafür infrage.

Ein bißchen viel Holz für einen kleinen Radiobutton, oder?

Nicht nur das. Derselbe Code müßte von den 3 anderen Radiobuttons ebenfalls aufgerufen werden, und zum Schluß müßte die Überprüfung der Eingabe das
zum 5. Male aufrufen.

Man sieht, was für diese Art von Programmierung spricht: gar nichts.

Und was dagegen spricht: Alles.

Man sollte bei der Programmierung (meiner Meinung nach) folgendes anstreben, ob OOP oder nicht OOP:


ÄNDERUNGEN AN DATENFELDERN EINER KLASSE SOLLEN ZENTRAL VON EINER EINZIGEN METHODE DURCHGEFÜHRT WERDEN

PROGRAMMLOGIK UND BILDSCHIRMSTEUERUNG (Anwenderdialog) SOLLTEN STRENG VONEINANDER GETRENNT WERDEN


Was heißt das praktisch für das kleine Demo-Programm?

1. Die Eventhandler der Eingabesteuerung dürfen nur Anzeigeanweisungen enthalten. Sie dürfen keine Programmlogik enthalten.

2. Die gesamte Programmlogik wird in einer (oder je nachdem auch mehreren) Klassen zusammengefaßt.

3. Die Klasse befindet sich nicht in dem Form1-Formular, sondern wird ausgelagert, so daß das Formular schön "schlank" bleibt.


„OOP bedeutet für mich nur Messaging, lokales Beibehalten, Schützen und Verbergen des Prozesszustands und spätestmögliche Bindung aller Dinge.“

– Alan Kay-

Wenn wir die späte Bindung mal zum Extrem treiben, könnte das bedeuten, daß wir in ALLE EREIGNISHANDLER ÜBERHAUPT KEINEN CODE schreiben.

Wir lassen den Anwender frei herumclicken, und die WinForms Laufzeitumgebung switcht die Buttons oder die Listenfelder, scrollen kann man ja auch

nach Belieben.

BIS AUF EINEN BUTTON: Prüfe Eingabe.

Jetzt wird die Programmlogik aktiv und überprüft das gesamte Formular auf Zulässigkeit, Fehlern bei der Eingabe etc. etc. und kann dann darauf reagieren, etwa mit Fehlerhinweisen und Anweisungen.

Der Nachteil dabei ist natürlich, daß der Anwender erst dann etwas bemerkt, wenn er diesen einen Button drückt. Dann sieht er seine gemachten Fehler und ärgert sich natürlich, daß das Eingabefeld nicht sofort die Fehlermeldung gebracht hat. Bei größeren Formularen mit voneinander abhängigen Feldern ärgert er sich zu Recht.

Das heißt, die Programmlogik soll bei jedem Click- oder sonstigem Ereignis sofort reagieren, dafür muß sie im Hintergrund ständig aktiviert werden.

Sowas kann man durchaus mit einem Timer machen. Man stellt den TimeSpan auf sagen wir 350ms, und das Ereignis ist dann der Aufruf der methode pruefeEingabe(). Wenn der Anwender allerdings das Formular in der Mittagspause geöffnet läßt, und der Timer alle 50ms seine Überprüfung durchführt, haben wir eine Menge sinnlosen Betrieb im Speicher.

Meine Empfehlung, um sowas sauber zu programmieren, wäre die folgende:

Alle Ereingishandler der sie auslösenden Ereignisse enthalten nur eine einzige Anweisung, und zwar alle dieselbe:

private void radioButton1_CheckedChanged(object sender, EventArgs e)
{
pruefeEingabe();
}

private void radioButton2_CheckedChanged(object sender, EventArgs e)
{
pruefeEingabe();
}

Egal was der Anwender macht, aber IMMER wenn er etwas macht, wird EINE EINZIGE METHODE aufgerufen, die das gesamte Formular auf formale und logischwe

Fehler überprüft, und zwar nicht erst am Ende, wenn die ganzen Eingabefehler schon drin sind, sondern ständig.

Den Zustand der Buttons und Felder kann die Methode sehr leicht abfragen. Daher ist es völlig egal, ob der Button MWST19 oder der Button

Warengutschrift aktiviert wurde, also von welchem Sender das Ereignis ausgelöst wurde.

Die Zustandsabfrage sieht dann etwa so aus:

if (rbMWSt19.Checked) ... tu was
else if (rbMWSt7.Checked) ... mach was anderes
else if ...

if (ListBox1.SelectedItem == irgendwas) mach irgendwas
else if (Listbox1.SelectedItem == was anderes) mach was anderes

Und so weiter.

Stehen die Parameter insgesamt fest, können auch komplexere Anweisungen folgen, z. B. könnte man einen Lagerbestand abfragen, ob noch genügend von den angeforderten Artikeln vorhanden sind oder ob ein anderer Lieferant mit dieser Schraubengruppe aushelfen kann. Diese Abfragen erfolgen vielleicht über ein Netzwerk, und wenn sich das ändert, muß die Abfragekommunikation vielleicht geändert werden etc. etc.

Man erkennt, all dieses hat innerhalb der Ereignishandler der Click- oder sonstigen Ereignisse wirklich NICHTS ZU SUCHEN.

Ich weiß, es juckt in den Fingern, sofort was in die Ereignishandler einzutippen.

Es kostet einige Überwindung, darauf zu verzichten.


Behandeln wir im Anschluß noch einige Besonderheiten von Eingabefeldern.

So ein Kandidat, der mit Systemfunktionen schlecht zu behandeln ist, ist ein Textfeld, das bestimmte Zeichen erwartet. Es wurde schon gesagt, wir lassen den Anwender freie Eingabe, und prüfen anschließend. Nur daß wir dafür keinen Ereignisauslöser haben. Wenn wir nämlich:

private void textBox1_TextChanged(object sender, EventArgs e)
{
// ueberpruefeNumerischeEingabe() // so nicht
CRegister.textBox1 = true; // besser
}

formulieren, dann wartet das Ereignis TextChanged nicht, bis der Anwender seine Eingabe fertig eingegeben hat, sondern wird schon mit dem ersten Zeichen aktiv, so daß der Anwender seine Eingabe nicht beenden kann.

Wir könnten das Problem mit einem Flag lösen, also einem Schalter in einem Register, der auf true gesetzt wird, wenn hier eine Eingabe erfolgt ist.

Die Methode pruefeEingabe() stellt dann fest, ok, hier wurde was geändert, und behandelt dieses Textfeld. Aber eben nicht sofort, sondern erst, wenn sie von anderer Stelle aufgerufen wurde. Sind alle Eingaben schon ausgeführt, und dieses Textfeld an letzter Stelle, dann passiert nichts, bis "prüfe Eingabe" gedrückt wird. Dazu muß man sich dann was passendes überlegen.

DateTimePicker

Man kann, was am ehesten gewünscht ist, das Datumsformat in den Eigenschaften auf "short" setzen. Dann ist das Format nach der Stringumwandlung

"tt.mm.yyy"; Leider hängt da aber noch die Uhrzeit dran, die in der Regel nicht gewünscht wird.

Um den string zeitstring z1 "26.04.2011 ... Uhrzeit) abzuschneiden, kann man den Substring bilden:

z1 = z1.Substring(0,10), was dann Umwandlung in "26.04.2011" zur Folge hat.


Umwandlung von Fließkommazahlen in string und umgekehrt

Erwartet das Textfeld eine Zahlenangabe, könnte man die masked.TextBox benutzen. Die mag ich aber gar nicht, weil die keine Fließkommazahlen kann.

Die Eingabe ist daher stets sehr hakelig. Wir lassen dem Anwender freie Wahl.

Und den String wandeln wir um mit double d=Convert.ToDouble(zahlstring). Das setzen wir in einen try/catch Block. Bei Umwandlungsfehler wird der

Betrag 0.00 zurückgegeben.

Es gibt ein Problem mit dem Dezimalkennzeichen. Convert.ToDouble erwartet in dem String ein Komma als Trennzeichen. Steht dort ein Punkt, ignoriert die Methode dies, und aus 47.11 wird 4711.

Abhilfe: Wir überprüfen vor der Umwandlung den String auf PUNKT, und wenn wir PUNKT finden, ersetzen wir mit KOMMA.

const char Punkt='.';
const char Komma=',';
if (zahlstring != null ...
if (zahlstring.Length> 0 ... usf.
for (int i=0;i<zahlstring.Length;i++)if(zahlstring[i]==PUNKT) zahlstring[i]=KOMMA;
Danach mit try/Catch die Convert Funktion.

Insgesamt sind alle diese Umwandlungen nicht sicher. In einem anderen Kulturkreis (auf den der Compiler versehentlich eingestellt wurde oder aus Spaß), kann die ganze Programmlogik, die sich auf Strings aufbaut, zerschossen werden. Es würde auch schon ausreichen, wenn das DateTime Format nicht auf short gestellt wurde.

Man sollte daher

INTERN

nur mit "sicheren" Datentypen arbeiten, DateTime, int, double etc., und die Strings nur bilden zu reinen Ausgabefunktionen, also

1.) Sicherer Typ in String umwandeln zu Ausgabezwecken = OK

2.) String in Zahlentypen zurückverwandeln = NOTOK, davon ist dringend abzuraten.

Bei 1.) geschieht im schlechtesten Falle, daß die Ausgabe Unsinn anzeigt, intern jedoch läuft das Progamm stabil.

bei 2.) wird das ganze Programm zerschossen, sobald der kleinste Formatierungsfehler auftritt oder die Formatierung geändert wurde.

Bündig formatierte Strings aus zusammengesetzten Datentypen:

Wir müssen zur Ausgabe von Datenfeldern strings benutzen. Mit der Umwandlung string s = double d.ToString() erzeugt man zwar den gewünschten Betrag, aber abhängig von der Zahlengröße ist der String unterschiedlich lang. Fügt man das nun so zusammen:

string zeile = data.Name + data.Nr.ToString() + data.betrag.ToString() und gibt das in einer Liste aus, habes wir ein ziemlich wüstes Schriftbild, so

etwa:

"Müller AG Remscheid183411,36"
"Schnell GmbH58,10"
"Schmid Rainer Privat6011,00"

Um da saubere Verhältnisse zu schaffen, brauchen wir für jedes Feld eine bestimmte Länge, die auch am Rand noch Leerzeichen stehen läßt, damit die Daten nicht zusammenklumpen. Hierfür bietet die Methode ToString() die Möglichkeit, in den runden Klammern Formatierungszeichen einzustellen, die man nachschlagen kann, führe ich hier nicht weiter aus. Diese müssen dafür sorgen, daß immer zwei Nachkommastellen erzeugt werden, sowie eine führende Null bei Double wäre gewünscht. Ist das erfolgt, kann man die Strings folgendermaßen auf eine bestimmte Länge rechts- oder linksbündig setzen:

data.Name = data.Name.Trim() (lösche alle Leerzeichen vorn und hinten). TrimStart löscht die Leerzeichen nur vorn, TrimEnd nur hinten.

Rechtsbündig machen:

data.Name=data.Name.Padleft(50). Das erzeugt eine Feldlänge von 50, in der der Text rechtsbündig steht. Bei Text ist das natürlich Quatsch, der soll

linksbündig stehen. Also:

data.Name = data.Name.PadRight(50)

Dagegen sollen alle Fließkommazahlen gewöhnlich rechtsbündig stehen:

data.betrag=data.betrag.ToString(gewünschte Formatierungsmerkmale);

data.betrag= data.betrag.PadLeft(15);

Und als Lohn der Mühe? Wir sehen, wir haben immer noch ein wüstes Schriftbild. Nichts von dem, daß die Spalten links- oder rechtsbündig fluchten.

Wieso?

Wir müssen eine Schriftart wählen, die nichtproportional ist. 99 Prozent der Schriftarten sind proportional, und so helfen die Pad-Anweisungen wenig.

Eine gut geeignete NON-Porportionalschrift ist Courier bzw. Courier New. In den Ausgabeelementen listBox stellen wir diese Schriftart unter font ein.

Dann haben wir alles schön bündig.

Das soll jetzt erstmal reichen als Bemerkungen zur Einrichtung unseres Formulars form2, also Rechnungen.

Was ist als nächstes zu erledigen?

Die Klasse CRechnungen ist zu erstellen.
Die Klasse CStart muß komplettiert werden
Die Klasse CControl muß erstellt werden (Control: überprüft die Eingabe unserer Rechnung formal und logisch)

In der Klasse CRechnungen brauchen wir auch Methoden, welche die Daten zu einer Rechnung der gewünschten Form zusammenstellen.
In den Klassen CKunden und CTeile fehlen noch Methoden, um aus der Struktur einen bündigen String zu erzeugen
In der Klasse CSort fehlen noch Methoden für die neuen Klassen

Also eine ganze Menge Kleinkram.

Ohne daß das erledigt ist, kann man die Form2 aber nicht wie gewünscht in Betrieb nehmen.

Der Beitrag wurde von sharky2014 bearbeitet: 10.04.2014, 15:46 Uhr
Angehängte Datei(en)
Angehängte Datei  DemoForm4.jpg ( 324.37KB ) Anzahl der Downloads: 9
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 11.04.2014, 11:42 Uhr
sharky2014
Level 7 = Community-Professor
*******

Der aktuelle Stand des Demo-Projekts.

Abbildungen:

DemoForm5: Zeigt ein Fehlerbehandlungsfenster, welches fehlende Dateien entdeckt hat. Am Beispiel eines selbstdefinierten Rückgabetypen.

DemoForm7: Zeigt das Rechnungsformular mit der Funktionalität Teileliste sortiert nach Art-Nr.

DemoForm8: Zeigt dasselbe, nur daß diesmal die Teile alphabetisch sortiert sind.

Um die Scroll-Funktion der Listbox für die Teileliste zu erzwingen, wurde die Teileliste mit einem Random-Verfahren auf einige hundert Datensätze erweitert. Mal erkennt, daß der Scroll-Balken erscheint, das muß man nicht programmieren, das macht WinForms automatisch.

Soviel zu dem was man sehen kann. Das war die gute Nachricht.

Die schlechte: ohne einiges an Theorie und Codierung ist das nicht zu haben. Die folgt jetzt auf dem Fuße.
Angehängte Datei(en)
Angehängte Datei  DemoForm5.jpg ( 68.67KB ) Anzahl der Downloads: 4
Angehängte Datei  DemoForm7.jpg ( 155.39KB ) Anzahl der Downloads: 7
Angehängte Datei  DemoForm8.jpg ( 160.67KB ) Anzahl der Downloads: 5
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 11.04.2014, 12:08 Uhr
sharky2014
Level 7 = Community-Professor
*******

Fahren wir mal fort mit dem selbstdefinierten Rückgabetypen. Siehe Abbildung demoform5.jpg aus dem obigen Beitrag.

Es ist relativ häufig der Fall, daß man nach mehreren Bedingungen sucht, die allesamt erfüllt sein müssen. Z. B. können dies Dateien sein, es kann auch sein, daß die Dateien nicht leer sein dürfen, oder was auch immer. Wenn man das boolean codiert:

bool pruefe()
{
bool erfolg=true;
if (!bedingung1) erfolg=false;
else if (!bedingung2) erfolg = false;

.....

return erfolg;
}


dann weiß man zwar, daß da was nicht geht, aber man weiß nicht, welche Bedingungen erfüllt sind und welche nicht. Warum man das nicht wissen kann, ist der Umstand, daß die Methode nur einen Rückgabewert hat. Natürlich könnte man den auf int setzen, also einen Error-Code zurückgeben, nach dem man dann aus einer Error-Liste den Zustand heraussucht. Oder man könnte den Parameter out verwenden, der beliebig viele Rückgabewerte zuläßt. Das ist aber alles sehr umständlich bzw. fehlersensitiv.

Statt dessen definieren wir eine neue Klasse:

public class CBas // CBas = Basisdefinitionen allgemein
{

public class Fehlermeldung
{
public bool erfolg;
public bool hinweis;
public string fehlerstring;
public string hinweisstring;
public Fehlermeldung()
{
erfolg = false;
hinweis = false;
fehlerstring = "";
hinweisstring = "";
}

}

public const char EOL = '\n';
public const char BLANK = ' ';
public const char PUNKT = '.';
public const char KOMMA = ',';

}


Die erledigt auch noch vielbenutzte Bezeichner, z.B. EOL = End of Line, Zeilenumbruch, oder Punkt, Komma, Leerzeichen. Das empfiehlt sich erstens, weil damit Tippfehler sicher vermieden werden, und zweitens, weil die Übersicht verbessert wird.

Der Rückgabetyp ist eine eigene Klasse Fehlermeldung. Die besagt, ob die Abfrage erfolgreich war, ob das Programm Gründe für Hinweise gefunden hat, und führt zwei Strings, in welche die aufgerufene Methode nun detailliert hineinschreiben kann, was wo wie nicht in Ordnung ist. Der Konstruktor hat hier die Aufgabe zu verhindern, daß fehlerstring und hinweisstring (wie es mit dem Standard-Konstruktor der Fall wäre) den Wert null zugewiesen erhalten. Dann wäre die spätere Handhabung umständlich, weil man dies mit einer Zuweisung abstellen müßte. Hier können wir sicher sein, die Stringlänge ist 0, aber die Adresse nicht null.

Die aufgerufene Methode muß diesen Datentyp in der Kopfzeile führen, nämlich:

CBas.Fehlermeldung pruefe()
{
}



Sieht in dem Demo Programm derzeit so aus:

public CBas.Fehlermeldung pruefeDateien()
{
var fm = new CBas.Fehlermeldung();
var hdn = new CDateifunktionen.dateinamen();
string datei1 = hdn.DN_teile_bin;
string datei2 = hdn.DN_kunden_bin;

if (!File.Exists(datei1)) fm.fehlerstring += "Fehlende Datei " + datei1 + CBas.EOL;
if (!File.Exists(datei2)) fm.fehlerstring += "Fehlende Datei " + datei2 + CBas.EOL;

if (fm.fehlerstring.Length > 0) fm.erfolg = false;
else fm.erfolg = true;
return fm;
}


Wenn ein Fehler gefunden wird, muß die Stringlänge von fehlerstring größer als Null sein, also erfolg=false;

Wie ruft man die Methode auf?

Das könnte man so machen:

var CBas.Fehlermeldung fm = new CBas.Fehlermeldung();
fm = pruefe .....

ist aber umständlich.

Wesentlich besser ist es so:



var pruef = hst.pruefeDateien();
if (pruef.erfolg) f2.ShowDialog();
else this.richTextBox1.Text = pruef.fehlerstring;


Der Datentyp wird bei der Initialisierung implizit zugewiesen durch den Rückgabetypen der Funktion.

Der Aufruf erfolgt hier aus Form1, und wenn die Sache erfolg hatte, wird das zweite Fenster form2 aufgerufen.

Wenn nicht, übergeben wir die Summe der Fehlermeldungen (die kann ja beliebig lang sein), einem Ausgabelement, und zwar mit einer einzigen Zuweisung:

this.richTextBox1.Text = pruef.fehlerstring;

Da ist schöne übersichtliche kurze Codierung mit viel Information. Da die MessageBoxen sehr viel Unruhe am Bildschirm schaffen (finde ich), erfolgt der Dialog hier über eine Textbox innerhalb des Formulars form1. Natürlich könnte man auch eine MessageBox verwenden.

Die aufgerufene Routine muß nur darauf achten, daß sie bei jeder Erweiterung des Fehlerstrings den Zeilenumbruch dazusetzt, nämlich:

fehlerstring += "da stimmt was nicht " +CBas.EOL; // End of Line

denn sonst wird das Schriftbild in der Box sehr unruhig.

Dieser Rückgabetyp Fehlermeldung eignet sich auch zum Schachteln. Die Prüfmethode könnte eine andere aufrufen, welche eine IO-Funktion prüft, und die Nachricht

catch (Exception info)
{

fehlerstring+=info;

}

aus dem Catchblock, also die Systemmeldung über die Art des Fehlers, in den Infostring packt. Die aufrufende Methode hängt diesen zweiten Infostring an den eigenen Infostring an und so entsteht eine komplette Liste aller Fehlermeldungen aus verschiedenen Methodenaufrufen.

Der Beitrag wurde von sharky2014 bearbeitet: 11.04.2014, 12:14 Uhr


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 11.04.2014, 12:32 Uhr
sharky2014
Level 7 = Community-Professor
*******

Um zu verstehen, wie die Daten auf den Bildschirm gelangen, z. B. die Kunden- und die Teileliste, etwas Programmierung.

Wir sehen uns den aktuellen Stand der Klasse CTeile an:

class CTeile
{
[Serializable()] // ohne das läßt sich die Klasse nicht serialiseren, bzw. auch noch Laufzeitfehler
public class Teil
{
public int bestnr;
public string bezeichner;
public double preis;
}

public List<Teil> Teileliste; // ob man die unbedingt public machen muß, ist nicht gesagt, hier ist es so

public CTeile()

{
fuelle_teileliste_random(); // Der Konstruktor ruft eine Random-Funktion auf, um dieTeileliste zu füllen
}


// Umformung des gemischten Datentyps Teil in einen String
// brauchen wir für die Ausgabe am Bildschirm, z. B. Listbox
// Damit das dann im Fenster auch bündig ist, wird eine nicht-proportionale Schrift benötigt:

public List<string> structlist_to_stringlist(List<Teil> structlist)
{
List<string> stringlist = new List<string>();
string intstring;
string dezstring;
foreach (Teil element in structlist)
{
intstring = (element.bestnr.ToString()).PadLeft(6); // rechtsbündig Länge 6
dezstring = (element.preis.ToString(CFormat.Dezimalformat)).PadLeft(8);
stringlist.Add(intstring+" "+element.bezeichner.PadRight(30)+"Preis €"+dezstring);
}
return stringlist; // es wird eine List<string> zurückgegeben
}

public void sortiereTeilelisteAlphabetisch(List<Teil> tlist) // greift auf die Klasse CSort zu, wo die IComparer Sortierung abgelegt ist
{
var hsort = new CSort();
tlist.Sort(new CSort.INameSort());
}

public void sortiereTeilelisteNumerisch(List<Teil> tlist)
{
var hsort = new CSort();
tlist.Sort(new CSort.INumSort());
}



void fuelle_teileliste_random()
{
Teileliste = new List<Teil>();
var rand1= new Random();
int zahl;
Teil t;
string str = "";
for (int i=0;i<100;i++)

{
t= new Teil();
str="";
zahl = rand1.Next(0, 3); // Bereich 0-2, nicht 0-3!!!
switch (zahl)
{
case 0: str+="Inbus M "; t.bestnr=3000; break; // Inbus im Alphabet vorn kriegt eine hohe Best-Nr, damit man den Numsort/Namesort überprüfen
// kann Sonst würde immer Inbus vorn stehen und man es nicht so gut sehen können
case 1: str+="Sechskant M ";t.bestnr=1000;break;
case 2: str+="Mutter M ";t.bestnr=2000;break;
}
zahl = rand1.Next(4,48);
str+= (zahl/2).ToString(); // Gewindegröße
zahl = rand1.Next(1, 999);
t.bestnr +=zahl;
t.preis = 1.0+zahl/80.0;
t.bezeichner=str;
Teileliste.Add(t);
}
}

} //cl



Die Teileliste wird hier (natürlich nur zu DEMO-Zwecken) im Konstruktor erstellt, und anschließend wird sie abgespeichert in einer Datei, und zwar, da es sich um einen zusammengesetzten Datentyp handelt, binär, mit dem serialize-Verfahren. Die passende Methode findet sich in der Klasse CDateifunktionen:


class CDateifunktionen
{
public class dateinamen
{
string pfad;
string dn_teileliste_bin;
string dn_teileliste_txt;
string dn_kundenliste_bin;
string dn_kundenliste_txt;
public string Pfad { get { return pfad; } set { ;} }
public string DN_teile_bin { get { return dn_teileliste_bin; } set { ;} }
public string DN_teile_txt { get { return dn_teileliste_txt; } set { ;} }
public string DN_kunden_bin { get { return dn_kundenliste_bin; } set { ;} }
public string DN_kunden_txt { get { return dn_kundenliste_txt; } set { ;} }
public dateinamen()
{
pfad = @"D:\";
dn_teileliste_bin = pfad + "Teile.bin";
dn_teileliste_txt = pfad + "Teile.txt";
dn_kundenliste_bin = pfad + "Kunden.bin";
dn_kundenliste_txt = pfad + "Kunden.txt";
}


}


public bool leere_datei_anlegen(string dateiname)
{
try
{
FileStream fs = new FileStream(dateiname, FileMode.Create);
fs.Close();
return true;
}
catch (Exception)
{
return false;
}
}


public bool lies_textdatei(string dateiname,List<string> stringlist) // Übergabe als Verweistyp funktioniert hier tadellos
{
if (!File.Exists(dateiname))return false;
try
{
string[] stringarr =File.ReadAllLines(dateiname);
foreach (string element in stringarr)
{
stringlist.Add(element);
}
return true;
}
catch (Exception)
{
return false; // dateifehler
}

}

public bool speichere_textdatei(string dateiname, List<string>stringlist)
{
try
{
File.WriteAllLines(dateiname, stringlist);
return true;
}
catch (Exception)
{
return false;
}
}



public bool speichere_teileliste_binaer(List<CTeile.Teil> teileliste)// Ohne Serializable Attribut der Klasse nicht möglich
{
var hdatname = new CDateifunktionen.dateinamen();
string dateiname = hdatname.DN_teile_bin;
var erfolg = true;
try
{
FileStream stream = new FileStream(dateiname, FileMode.Create);
BinaryFormatter bformatter = new BinaryFormatter();
bformatter.Serialize(stream, teileliste);
stream.Close();
}
catch (Exception info )
{
erfolg = false;
MessageBox.Show(info.Message);
}
return erfolg;
}

public bool speichere_kundenliste_binaer(List<CKunden.Kunde> kundenliste)
{
var hdatname = new CDateifunktionen.dateinamen();
string dateiname = hdatname.DN_kunden_bin;
var erfolg = true;
try
{
FileStream stream = new FileStream(dateiname, FileMode.Create);
BinaryFormatter bformatter = new BinaryFormatter();
bformatter.Serialize(stream, kundenliste);
stream.Close();
}
catch (Exception info)
{
erfolg = false;
MessageBox.Show(info.Message);
}
return erfolg;
}


// Bei den binären Lesefunktionen wurde wie oben schon besprochen festgestellt, daß die Übergabe als Verweistyp in der Kombination

deserialize/List<T> hier nicht funktioniert. Kein Laufzeitfehler, aber die Daten sind weg. Daher wurde hier die Codierung etwas geändert, das ist im Code orange markiert. So läuft es tadellos


public bool lies_teileliste_binaer(List<CTeile.Teil> teileliste)
{
var erfolg = true;
var liste2 = new List<CTeile.Teil>(); // wir nehmen zum Einlesen eine lokale Liste
var hdn = new CDateifunktionen.dateinamen();
string dateiname = hdn.DN_teile_bin;

FileInfo fi = new FileInfo(dateiname);
if (File.Exists(dateiname))
{
if (fi.Length > 0) // deserialize leere Datei = Laufzeitfehler
{
try
{
FileStream stream = new FileStream(dateiname, FileMode.Open);
BinaryFormatter bformatter = new BinaryFormatter();
liste2 = (List<CTeile.Teil>)bformatter.Deserialize(stream);
stream.Close();
foreach (CTeile.Teil element in liste2) teileliste.Add(element); // und weisen so dem Rückgabetypen teileliste die ausgelesen WErte zu
}
catch (Exception)
{
erfolg=false ;
}
}// if exist
}
return erfolg;

}


public bool lies_kundenliste_binaer(List<CKunden.Kunde> kundenliste)
{
var erfolg = true;
var liste2 = new List<CKunden.Kunde>();
var hdn = new CDateifunktionen.dateinamen();
string dateiname = hdn.DN_kunden_bin;

FileInfo fi = new FileInfo(dateiname);
if (File.Exists(dateiname))
{
if (fi.Length > 0) // deserialize leere Datei = Laufzeitfehler
{
try
{
FileStream stream = new FileStream(dateiname, FileMode.Open);
BinaryFormatter bformatter = new BinaryFormatter();
liste2 = (List<CKunden.Kunde>)bformatter.Deserialize(stream);
stream.Close();
foreach (CKunden.Kunde element in liste2) kundenliste.Add(element);
}
catch (Exception )
{
erfolg=false ;
}
}// if exist
}
return erfolg;

}


} // class dateifunc



Dazu gibt es noch eine neue Klasse, um den hakeligen Umgang mit den C#-Formatierungssymbolen zu erleichtern:


{
public static class CFormat
{
public const string Prozentformat = "0.0 %";
public const string Dezimalformat = "#,##0.00";
}
}


Prozentformat erzwingt führende 0 und einen Blank hinter der Zahl, damit das Prozentzeichen nicht "dranpappt"

Dezimalformat erzwingt führende Null, zwei Nachkommastellen und führt ein 1000er Trennzeichen.

Bei dem Prozentformat muß man aufpassen: Weist man z. B. 19.0 der Mehrwertsteuer zu, erscheint 1950, weil die Funktion dummerweise den Betrag mit 100 multipliziert. Man muß den Betrag vor der Umwandlung durch 100.0 dividieren (MWSt/100)ToString( ......Prozentformat

Der Beitrag wurde von sharky2014 bearbeitet: 11.04.2014, 12:43 Uhr


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 11.04.2014, 13:02 Uhr
sharky2014
Level 7 = Community-Professor
*******

Nachzutragen wäre noch der gegenwärtige Stand der Klasse Sort, welche uns die Teileliste nach Nr. oder Name sortiert.

namespace DemoLagerverwaltung
{
class CSort
{
public class INameSort : IComparer<CTeile.Teil>
{
public int Compare(CTeile.Teil x, CTeile.Teil y)
{
CTeile.Teil t1 = x;
CTeile.Teil t2 = y;
return String.Compare(t1.bezeichner, t2.bezeichner);
}
}

public class INumSort : IComparer<CTeile.Teil>
{
public int Compare(CTeile.Teil x, CTeile.Teil y)
{
CTeile.Teil t1 = x;
CTeile.Teil t2 = y;
if (t1.bestnr > t2.bestnr) return 1;
else if (t1.bestnr < t2.bestnr) return -1;
else return 0;
}
}


} // cl
} //ns



Die Klasse CKunden ist praktisch eine Copy&Paste Version von CTeile, die Namensfelder wurden umbenannt, sonst ist aber beinahe alles identisch.

Auf eine Sortierfunktion für Kunden wird daher verzichtet, wäre nur eine Wiederholung der Sortierung für die Teile (alles copy und paste).


Nachdem die deskriptiven Klassen nunmehr vorgestellt wurden, kommen wir mal zur Funktionalität.

Wir haben, wenn wir

var ht = new CTeile();

aufrufen, dank des eigenen Konstrutors die Teileliste verfügbar.

Die soll nun abgespeichert werden. Damit simulieren wir einen Zustand, daß ein Programm beim Start auf bestimmte Dateien zugreifen kann, um sinnvoll betrieben werden zu können. Woher die Teileliste kommt, aus dem Random-Generator oder einer Datenbank, ist im Prinzip egal.

Die Abspeicherung der vom Konstruktor erzeugten Teileliste übernimmt Form1.

Und beim Speichern kann natürlich was schief gehen (bei I/O Methoden kann immer was schief gehen).

Daher muß form1 feststellen, ob der Vorgang erfolgreich war, und wenn der Vorgang nicht erfolgreich war, den Programmbetrieb sperren.

Das sieht dann bis jetzt so aus:

namespace DemoLagerverwaltung
{
public partial class Form1 : Form
{
bool SPERRE = false;

public Form1()

{
InitializeComponent();
var ht = new CTeile();
var hk = new CKunden();
var hdat=new CDateifunktionen();
var machwas= hdat.speichere_teileliste_binaer(ht.Teileliste);
var machnochwas = hdat.speichere_kundenliste_binaer(hk.Kundenliste);

if (machwas == false || machnochwas == false) SPERRE = true;
if (SPERRE)
{
this.richTextBox1.Text="Dateifehler\n\nProgrammausführung nicht möglich";

// ist die Sperre aktiviert, bleiben wir im ersten Fenster "hängen", was ja auch richtig ist.
// ein möglicher Fehler beim Versuch, die Listen zu speichern, wäre das fehlende Attribut serializable der zugrundeliegenden Klasse
//wie auch immer, keine Dateien, kein Programmbetrieb

}
}

private void btEnde_Click(object sender, EventArgs e)
{
this.Close();
}

private void btRechnungen_Click(object sender, EventArgs e)
{
if (!SPERRE)
{
var f2 = new Form2(); // Aufruf unseres Formulars Rechnungen wie in den Abbildungen weiter oben schon zu sehen
var hst = new CStart();
var pruef = hst.pruefeDateien(); // Pruefmethode aus der Klasse CStart
if (pruef.erfolg) f2.ShowDialog();
else this.richTextBox1.Text = pruef.fehlerstring; //Aufruf mit dem selbstgeschriebenen Rückgabetypen Fehlermeldung

}
}
}
}


Die hier aufgerufene Funktion der Klasse CStart sieht bis jetzt so aus:

class CStart
{
public CBas.Fehlermeldung pruefeDateien()
{
var fm = new CBas.Fehlermeldung();
var hdn = new CDateifunktionen.dateinamen();
string datei1 = hdn.DN_teile_bin;
string datei2 = hdn.DN_kunden_bin;

if (!File.Exists(datei1)) fm.fehlerstring += "Fehlende Datei " + datei1 + CBas.EOL;
if (!File.Exists(datei2)) fm.fehlerstring += "Fehlende Datei " + datei2 + CBas.EOL;

if (fm.fehlerstring.Length > 0) fm.erfolg = false;
else fm.erfolg = true;
return fm;
}


}


Das läßt sich natürlich noch beliebig erweitern, mehr brauchen wir erstmal nicht.


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 11.04.2014, 13:27 Uhr
sharky2014
Level 7 = Community-Professor
*******

Weiter geht es mit den Funktionalitäten von Form2, also unserem Rechnungsformular.

Das steht noch nicht sehr viel drin, aber immerhin können wir die Kundenliste und die Teileliste ja schon sehen und darin scrollen. Also müssen die Daten ja irgendwie dahin gekommen sein. Hier also der Code für das zweite Fenster:

public partial class Form2 : Form
{
public Form2() // Initialiserung und Methoden des Konstruktors, Ausführung nur einmalig
{
InitializeComponent();
this.btSpeichern.Visible = false; // wir verstecken den Button zum Speichern
var hk = new CKunden();
var ht = new CTeile();
List<string> klist = hk.structlist_to_stringlist(hk.Kundenliste); // Die strukturierte Liste wird in eine Stringliste umgewandelt
List<string> tlist = ht.structlist_to_stringlist(ht.Teileliste); // Dito
foreach (string element in klist) this.listBoxKunden.Items.Add(element); // Daten in die Listbox Kunden schreiben
foreach (string element in tlist) this.listBoxTeile.Items.Add(element); // und in die Listbox Teile
}

private void rbKundenNum_CheckedChanged(object sender, EventArgs e)
{

}

private void rbKundenAlphabetisch_CheckedChanged(object sender, EventArgs e)
{

}

private void rbTeilNr_CheckedChanged(object sender, EventArgs e)
{
var ht = new CTeile();
ht.sortiereTeilelisteNumerisch(ht.Teileliste);
List<string>strlist =ht.structlist_to_stringlist(ht.Teileliste);
this.listBoxTeile.Items.Clear();
foreach (string element in strlist) this.listBoxTeile.Items.Add(element);
}

private void rbTeileAlphabetisch_CheckedChanged(object sender, EventArgs e)
{
var ht = new CTeile();
ht.sortiereTeilelisteAlphabetisch(ht.Teileliste);
List<string> strlist = ht.structlist_to_stringlist(ht.Teileliste);
this.listBoxTeile.Items.Clear();
foreach (string element in strlist) this.listBoxTeile.Items.Add(element);
}
private void btEnde_Click(object sender, EventArgs e)
{
this.Hide();
return;
}
}



Ich hatte ja einige Beiträge zuvor des Langen und des Breiten ausgeführt, warum Programmlogik in Eventhandlern nichts zu suchen hat. Was wir bei den Radiobuttons hier sehen, ist keine Ausnahme von dieser Regel, sondern das ist die Regel. Es handelt sich um reine Ausgabefunktionen. Und wir können nicht bei irgendeinem Click auf irgendeinen Button jedesmal die Sortierung von Listen ändern und neu ausgeben. Sowas geht nicht, schon allein, weil die Liste dann verrutscht.

Fangen wir an mit dem Konstruktor.

Er versteckt den Button speichern, weil ja der Datensatz noch gar nicht bearbeitet wurde. Wenn die Eingabe geprüft ist, dann wird dieser Button sichtbar, und der Anwender hat die Möglichkeit, den Datensatz zu speichern.

Die strukturierte Liste, bestehend aus Feldern des Datentyps int, double und string, kann so nicht als String ausgegeben werden. Daher erfolgt die Umwandlung structlist_to_stringlist, welche namensgleich, aber nicht parametergleich, in der jeweiligen Klasse hinterlegt ist. Der Parameter muß natürlich zu dem behandelten Datentyp passen.

Die Daten werden in die listBoxen geschaufelt mit der Methode listBox.Items.Add. Ich glaube, es gibt auch eine AddRange methode, da braucht man keine Schleife. Insgesamt ist die foreach-Methode aber so hervorragend, daß man nichts vermißt.

Nun zur Funktionalität der Radio-Buttons:

Drücken wir den RadioButton mit dem Namen rbTeilNr , was schon andeutet, was gewünscht ist, geschieht folgendes:

Aus der Klasse CTeile wird die numerische Sortierung aufgerufen.
Dann haben wir die Liste numerisch sortiert.
Sortiert wird aber keine List<string>, sondern eine List<CTeile.Teil>, also der strukturierte Datentyp, der sich als String so nicht ausgeben läßt.

Daher wird im folgenden die Methode, die ebenfalls in der Klasse CTeile hinterlegt ist, structlist_to_stringlist aufgerufen. Jetzt haben wir die Strukturierte Liste umgeformt als STringliste mit bündig abschließenden Datenfeldern.

Würde man die jetzt so in die Listbox packen, hätte man den Inhalt der Listbox verdoppelt, weil die alten Daten ja noch drin sind.

Daher muß der Inhalt erstmal mit Clear() gelöscht werden.

Und wir packen mit foreach nun die Strings da hinein. Fertig.

Der Beitrag wurde von sharky2014 bearbeitet: 11.04.2014, 13:34 Uhr


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 12.04.2014, 14:58 Uhr
sharky2014
Level 7 = Community-Professor
*******

Wir machen weiter mit Programmdesign.

Ein schlecht strukturiertes Programm ist erstens nicht stabil, zweitens spätestens bei der ersten ernsthaften Überarbeitung reif für die Tonne. Denn aus schlechtem Code ein vernünftiges Programm zu machen ist wie Altbau: Abreißen und neu ist billiger.

Zunächst mal die Aufgabenstellung:

Wir haben zwei Datenquellen, Artikel (Teile) und Kunden. Die Aufgabe des Programms ist es, diese Daten so zu verknüpfen, daß der Kunde für gelieferte Ware eine Rechnung erhält. Das ist die Kernfunktion. Darüber hinaus sind jede Menge Erweiterungen denkbar, aber bleiben wir bei der Kernaufgabe.

Um die Daten zusammenzuführen, brauchen wir eine Klasse CRechnung. Diese Rechnung enthält die Kundendaten, soweit erforderlich, sowie eine Auflistung der Artikel, die der Kunde gekauft hat, und die Beträge Netto, Steuer, Steuersatz, Brutto sowie das Rechnungsdatum.

Wir haben es hierbei mit einer Struktur zu tun, die so ähnlich aufgebaut ist wie ein jagged Array.

Jagged Arrays sind Arrays, die wiederum Arrays als Elemente enthalten.

Für den einzelnen Datensatz Rechnung sieht es so aus:

Die Vertikalkomponente, nennen wir das mal Spalte, sind die Felder, die EINMALIG in jeder Rechnung auftauchen. Die Horizontalkomponente, analog Spalte genannt, ganz wie wir das von der Tabellenkalkulation gewöhnt sind, ist Datenplatz für die einzelnen Artikel, die dem Kunden in Rechnung gestellt werden. Grafisch sieht das ungefähr so aus:

R
R
R
R
R[] AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA // Array

R
R
R
R
R[] AAAAAAAAAAA // Array


Wobei R die Datenfelder der Rechnung darstellen sollen (Für jede Rechnung nur jeweils nur ein Datenfeld), und A die Artikelliste, für jede Rechnung mindestens 1 Artikel, wahrscheinlich aber mehr, nur wieviele, wissen wir zum Zeitpunkt der Programmierung nicht. Das entscheidet sich erst zur Laufzeit.

Der Aufbau könnte auch so sein:

RDatum
R[] netto1 Rabatt netto2 MWSt brutto // Array
RKunde
R[]AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA // Array

Wenn wir alle Rechnungen (später mal) selbst wiederum in einer Liste zusammenfassen, haben wir eine 3dimensionale Liste, die wiederum Arrays enthält, und somit ein echtes Jagged Array.

Die Klasse CRechnungen sollte für die Rechnung einen einzelnen Datensatz CRechnung bereitstellen, also die zweidimensionale Struktur. Das Problem dabei ist, wie wir den Speicherplatz für die Artikel bereitstellen sollen, wenn wir gar nicht wissen, wie groß dieser sein wird. Ganz gewiß machen wir das nicht so, daß wir maximal n Artikel vorsehen, also ein fest indiziertes Arrray, was im Einzelfall vielleicht nicht reicht, in der Masse aber Verschwendung von Speicherplatz darstellt.

In Ansi-C müßten wir jetzt richtig viel programmieren, nämlich eine dynamische Liste. In C# brauchen wir das nicht, wir nehmen einfach den Datentyp List<T>, der eine beliebige Menge Datensätze aufnimmt.

Bei der Frage, wie man Datensätze abspeichert mit unterschiedlicher Länge der Felder, wie es ja ganz gewiß der Fall ist, bzw., Abspeichern ist das eine, aber wie zurücklesen????, brauchen wir uns auch keinen Kopf zu machen. Das machen wir mit serialize/deserialize wie oben schon demonstriert.

C# ist schon richtig komfortabel! Ich beschäftige mich zwar erst seit einigen Monaten damit, aber allmählich komme ich auf den Geschmack. Was man früher zu Fuß programmieren mußte, holt man hier einfach aus dem Regal. Gut, daß es bei MS so schlaue Leute gibt.

Somit ist das Design der Klasse CRechnungen quasi durch die Problemstellung schon vorgegeben:

public class CRechnungen
{

public class Posten
{
public int artnr;
public string artbez;
public double einzelpreis;
public double anzahl;
}

public string struct_to_string(Posten posten)
{
string kurzbez =posten.artbez+" ";
kurzbez=kurzbez.Substring(0,15);
string strresult = "";
strresult += posten.artnr.ToString().PadLeft(6);
strresult +=" "+ posten.artbez.PadRight(15);
strresult += posten.einzelpreis.ToString(CFormat.Dezimalformat).PadLeft(10);
strresult += posten.anzahl.ToString().PadLeft(8);
strresult += (posten.anzahl*posten.einzelpreis).ToString(CFormat.Dezimalformat).PadLeft(12);
return strresult;
}

public class Rechnung
{
public DateTime datum;
public int kdnr;
public string kunde;
public double netto1;
public double rabatt;
public double netto2;
public double mwstproz;
public double mwst;
public double brutto;
public List<Posten> Einkaufsliste; // diese EInkaufsliste listet beliebig viele Artikel, da dynamisch

}
}


Gemäß dem Grundsatz, daß wir zwar "sichere" Datentypen in Strings umwandeln dürfen, aus Gründen der Ausgabe, aber niemals aus Strings rechnerische Werte zurückumwandeln "dürfen" (bzw. "nicht wollen", selbstauferlegte Askeseregel), weil wir dann in völliger Abhängigkeit von Formatierungsmerkmalen stehen und jeder Tippfehler oder eine Änderung der Formatierung das Programm abschießt, haben wir hier eine Methode, um den strukturierten Datensatz in eine Textzeile zu formatieren,

aber wir haben nicht die umgekehrte Methode. Extra nicht (siehe oben).

class Rechnung ist also unserer einzelner 2dimensionaler Datensatz für eine einzelne Rechnung. Die zweite Dimension kommt rein mit der List<Posten>, wo beliebig viele Artikel drin stehen können.

Achtung bei der Initialisierung:

var Rechnung = new CRechnungen.Rechnung();

macht nicht das, was man denken mag. Weil nämlich die List<Posten> damit keinesfalls initialisiert ist, sondern den Wert null aufweist.

List<T> wie übrigens auch string[] und sonstige sind VERWEISTYPEN!

Verweistypen werden bei der Instanziierung der Klasse NICHT INSTANZIIERT, sondern weisen den Wert null auf.

Der Laufzeitfehler läßt hier grüßen, wenn man versucht, darauf zuzugreifen.

Daher benötigt man hier zwei Zuweisungen:

var Rechnung = new CRechnungen.Rechnung();
Rechnung.Einkaufsliste = new List<CRechnungen.Posten>();



Werfen wir jetzt mal einen Blick auf die Funktionalität von form2.

Ich hab da extra noch nicht soviel reingepinnt, damit man das Prinzip besser sehen kann:

namespace DemoLagerverwaltung
{
public partial class Form2 : Form
{

CKunden hkunden = new CKunden();
CDateifunktionen hdatei = new CDateifunktionen();
CRechnungen hrechg = new CRechnungen();
CSort hsort = new CSort();
CStart hstart = new CStart();
CTeile hteile = new CTeile();

public Form2()
{
InitializeComponent();
Init();
}

private void Init()
{
var Rechnung = new CRechnungen.Rechnung();
Rechnung.Einkaufsliste = new List<CRechnungen.Posten>();

}

private void pruefeEingabe(object sender)
{

}


} // form2
} //NS


Als erstens stehen die Objektverweise auf die anderen Klassen. Public. Ob man das public machen muß oder will, ist Geschmackssache.

Wir müssen uns bei den Objektverweisen aber über eines klar sein:

Jeder Objektverweis legt mit dem Operator "new" eine Kopie der verknüpften Klasse an. Je mehr Objektverweise, umso mehr Kopien sind im Umlauf. Die Gefahr ist, wenn man den Feldern der Klasse Werte zuweist, daß dann die Kopie Nr. 5 einen Wert hat, aber die aufrufende Methode die Kopie Nr. 2 auf den Bildschirm pinnt und man sich wundert, warum da nichts zu sehen ist.

Insbesondere besteht diese Gefahr in der Ereigniswelt der WindowsForms, wenn jedes Click-Ereignis vor sich hinwurstelt.

Wie schon ganz im Anfang gesagt, man könnte viele Klassen einfach static machen, da hätte man das Problem nicht. Aber static hat einiges an Einschränkungen, man spürt, das ist ein Zugeständnis, wird aber von C# nicht wirklich gewollt. Wenn man also static geht, dann sollte es gute Gründe dafür geben, weil man dann die Programmlogik anders aufbauen muß. Im hiesigen Demo-Programm wurden zwei Klassen static gemacht, dazu später.

Im Konstruktur der Form2 findet sich bis jetzt nur die einzige Anweisung:

init();

Init macht bis jetzt nur folgendes, eine neue Instanz eines Rechnungsformulars aufzurufen, sowie eine neue Instanz der im Rechnungsformular enthaltenen Datenstruktur List<T> Einkaufsliste. Diese ist ein Verweistyp und wird bei der Initialisierung der Basisklasse nicht initialisiert.

Dann folgt die eierlegende Wollmilchsau pruefeEingabe(objekt sender).

Ich habe mich (das ist hier noch nicht zu sehen) aus formalen und logischen Gründen für eine streng puritanische Lösung entschieden:

Die ganzen CLick- und sonstigen Ereignisse erhalten nur eine einzige FUnktionalität, sie sollen die Methode pruefeEingabe(object sender) aufrufen und sonst nichts.

Um das mal anschaulich zu machen:

Wir erhalten die Funktionsrümpfe der meisten Elemente, indem wir auf dem Entwurfsformular einen Doppeklick draufsetzen. Bei den Buttons erscheint dann der Funktionsrumpf:

private void btHinzufuegen_Click(object sender, EventArgs e)
{

}


Ich benenne meine Buttons bt...Name, damit ich weiß, es ist ein button.

Wenn wir mit Listbox einen Doppelklick verwenden wollen, damit der Anwender das Element auswählt, muß man anders vorgehen. Der Doppelklick im Entwurfsformular auf die Listbox würde den folgenden Code auswerfen:

private void listBoxTeile_SelectedIndexChanged(object sender, EventArgs e)
{

}


Das ist nicht gewünscht, gewünscht ist das Ereignis double_click. Dazu müssen wir die Eigenschaften anwählen, den gelben Blitz anklicken, um die Ereignisse zu sehen, und unter den Ereignissen double_click aktivieren, indem wir in der rechten Spalte darauf doppelklicken. Es erscheint nun der gewünschte Funktionsrumpf:

private void listBoxTeile_DoubleClick(object sender, EventArgs e)
{

}


ACHTUNG: Wenn man das mit der Hand eintippt, hat es keine Funktionalität. Weil der Designer, wenn die Eigenschaft nicht aktiviert ist, von dem Ereignis nichts weiß. Man macht es wirklich komfortabler, indem man die Funktionen der IDE auch benutzt.

Was soll nun rein in die ausgwählten Ereignishandler?

Folgendes:


private void btHinzufuegen_Click(object sender, EventArgs e)
{
pruefeEingabe(this.btHinzufuegen);
}


private void listBoxTeile_DoubleClick(object sender, EventArgs e)
{
pruefeEingabe(this.listBoxTeile);
}



Sonst gar nichts.

Für alle Ereignishandler nur diese einzige Zeile.

Wenn wir die Funktionalitäten für die eierlegende Wollmilchsau pruefeEingabe(object sender) schon mal andeuten, wird es schnell klar, wie das gedacht ist:


private void pruefeEingabe(object sender)
{

if (sender == this.btHinzufuegen)
{

}
if (sender == this.btRechnungPruefen)
{

}
if (sender == this.btRechnungSpeichern)
{

}
if (sender == this.btEnde)
{

}

if (sender == this.listBoxTeile)
{

}
if (sender == this.listBoxKunden)
{

}
if (sender == this.listBoxEinkaufsliste)
{

}
if (sender == this.comboBoxStueckzahl)
{

}
if (sender == this.tbPreisnachlass)
{

}

}

Wir müssen da gar nicht mit if else Verzweigungen arbeiten, alles schön mit if hintereinander, das ist eh innerhalb einer Millisekunde durch.

Ob die Methode pruefeEingabe(object sender) die Information über den sender überhaupt auswertet, weiß der sender nicht. Der funkt brav folgendes durch:


Hallo, hier ist der Button Programmende. Ich bin gerade geklickt worden.

Würde man dem Button das überlassen, das Programm zu beenden, wäre die Programmierung schlecht, weil der Button über den Programmzustand nichts weiß. Er weiß z. B. nicht, ob noch irgendwelche Aktionen zu erledigen sind, bevor die Form schließt.

Kurz zusammengefaßt:

Was geklickt wird, oder sonstwie ein Ereignis zu melden hat, sagt der Funktion

pruefeEingabe(objekt sender)

"Hier meldet sich Außenposten Button oder Combobox oder sonstwer. Ich bin gerade geklickt worden."

Somit verwaltet diese einzige Methode pruefeEingabe(... das gesamte Formular, und alle Aktion geht von hier aus.

Sämtlich Ereignishandler sind zu reinen Meldeposten degradiert und dürfen gar nichts.

Der Beitrag wurde von sharky2014 bearbeitet: 12.04.2014, 15:09 Uhr


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 12.04.2014, 19:04 Uhr
sharky2014
Level 7 = Community-Professor
*******

Wir gehen nochmal zurück auf Start.

Der gesamte Code der form2 wurde gelöscht.

Zugunsten einer extrem formalisierten Programmierung.

In dem angehängten Screenshot sehen wir das grafische Ergebnis der Neuprogrammierung. Das Formular wurde etwas umgestaltet, und beim Aufruf des Formulars erscheint die Kundenliste (unsortiert, sind ja nur 2 Kunden) sowie die Teileliste, sortiert nach Bestellnummer.

Das hatten wir vorher schon mal, nur der Weg, wie wir dahinkommen, ist jetzt ein anderer. Der Code ist stärker formalisiert.

Wenn die Form2 aufgerufen wird, wird der Anfangszustand des Formulars mit der Funktion init() hergestellt. Die Funktion init() wird also nur einmal, und zwar beim Aufruf des Formulars, ausgeführt.

public Form2()
{
InitializeComponent();
Init();

}

private void Init()
{
var Rechnung = new CRechnungen.Rechnung();
Rechnung.Einkaufsliste = new List<CRechnungen.Posten>();
Kundenliste=new List<CKunden.Kunde>();
Teileliste=new List<CTeile.Teil> ();
if (hdatei.lies_kundenliste_binaer(Kundenliste))
if (hdatei.lies_teileliste_binaer(Teileliste))
{
// Teileliste Vorgabe numerische Sortierung
this.rbTeilNumSort.Checked = true;
hteile.sortiereTeilelisteNumerisch(Teileliste);
listBoxTeileFill(Teileliste);
listBoxKundenFill(Kundenliste);
SPERRE = false;
}


}


Wir rufen eine Instanz der Klasse Rechnung auf. Und des darin befindlichen Arrays Einkaufliste.
Dann wird eine Instanz der Kundenliste aufgerufen.
Sowie der Teileliste.

Jetzt müssen wir die Listen noch mit Daten füllen und in die Listboxen hineinschreiben.
Die Daten erhalten wir aus vorhandenen Dateien (oder auch nicht, wenn diese nicht vorhanden sind).

Wir überprüfen zwei einschließende Bedingungen, beide Dateien müssen vorhanden sein. Und heben dann, wenn das zutrifft, die vorsorglich festgelegte SPERRE auf.

Da wir die RadioButtons numerisch oder Name in dem Formular vorgesehen haben, müssen wir uns für eines davon entscheiden. Wir entscheiden uns für den numsort. Dazu muß aber dann der radioButton passend gesetzt werden:

this.rbTeilNumSort.Checked = true;

Anschließend wird die numerische Sortierung durchgeführt.

Das Auffüllen der Listen geschieht folgendermaßen:

Die Listen können ja nur Strings aufnehmen, wir haben es aber mit strukturierten Datensätzen zu tun. Dazu benutzen wir also die Umwandlungsmethoden der betreffenden Klassen und schreiben die ganze Liste als umgewandelten String in die listBoxen rein. Um die Übersicht zu verbessern (wir wollen in init möglichst wenig Code haben, damit wir besser sehen, was passiert), wurden für diese Funktion zwei eigene Methoden geschrieben, die in Form1 abgelegt sind, und von init() aufgerufen werden:

private void listBoxTeileFill(List<CTeile.Teil> structlist)
{
this.listBoxTeile.Items.Clear();
foreach (CTeile.Teil element in structlist)
listBoxTeile.Items.Add(hteile.struct_to_string(element));
}
private void listBoxKundenFill(List<CKunden.Kunde> structlist)
{
this.listBoxKunden.Items.Clear();
foreach (CKunden.Kunde element in structlist)
listBoxKunden.Items.Add(hkunden.struct_to_string(element));
}




Die entsprechenden Umwandlungsfunktionen wandeln jeweils einen einzigen Datensatz um (das wurde gegenüber vorher geändert), z. B. in der Klasse Teile die Umwandlung des strukturierten Datensatzes Teile in String:

public string struct_to_string(Teil element)
{
string formatstring = "";
string intstring;
string dezstring;
intstring = (element.bestnr.ToString()).PadLeft(6); // 6 Stellen BestellNr.
dezstring = (element.preis.ToString(CFormat.Dezimalformat)).PadLeft(8); // 8 STellen Stückpreis
formatstring =intstring + " " + element.bezeichner.PadRight(30) + "Preis €" + dezstring; // Reihenfolge BestNr. 30 Stellen Name Preis in Euro
return formatstring; // RÜckgabewert = formatierter String
}


Die Ereignishandler in der Form2 werden nun nach und nach in Betrieb genommen. Als erstes nehmen wir z. B. die Sortierung der Teileliste:

private void rbTeilNumSort_CheckedChanged(object sender, EventArgs e)
{
pruefeEingabe(this.rbTeilNumSort);
}

private void rbTeilNameSort_CheckedChanged(object sender, EventArgs e)
{
pruefeEingabe(this.rbTeilNameSort);
}


Die RadioButtons melden also der Generalfunktion, daß sie geklickt worden sind.

In der Methode pruefeEingabe werden sie folgendermaßen erfaßt (schematisch):

if (sender == this.rbTeilNameSort||sender==this.rbTeilNumSort)
{

// ToDo:
// LIstboxTeile löschen
// Liste neu sortieren -> dafür gibt es fertige Methoden
// Listbox mit neuer Sortierung füllen -> auch dafür stehen fertige Methoden bereit


}


Da in beiden Fällen, ob Num- oder Namesort gleiche Aufgaben anfallen, nämlich sortieren, listBox löschen, Daten neu reinschreiben, werden die beiden RadioButtons zusammengefaßt. Die gemeinsamen Aufgaben werden also ausgeklammert, der Sort wird dann mit if( nochmal unterschieden, welcher es denn sein soll.

Zur Verdeutlichung das Gerüst der Codierung der Form2, völlig unfertig, aber man sieht den Rahmen besser, als wenn alles fertig ist:

public Form2()
{
InitializeComponent();
Init();

}

private void Init()
{
var Rechnung = new CRechnungen.Rechnung();
Rechnung.Einkaufsliste = new List<CRechnungen.Posten>();
Kundenliste=new List<CKunden.Kunde>();
Teileliste=new List<CTeile.Teil> ();
if (hdatei.lies_kundenliste_binaer(Kundenliste))
if (hdatei.lies_teileliste_binaer(Teileliste))
{
// Teileliste Vorgabe numerische Sortierung
this.rbTeilNumSort.Checked = true;
hteile.sortiereTeilelisteNumerisch(Teileliste);
listBoxTeileFill(Teileliste);
listBoxKundenFill(Kundenliste);
SPERRE = false;
}


}

private void pruefeEingabe(object sender)
{
if (sender == this.btHinzufuegen)
{

}
if (sender == this.btRechnungPruefen)
{

}
if (sender == this.btRechnungSpeichern)
{

}
if (sender == this.btEnde)
{

}

if (sender == this.listBoxTeile)
{

}
if (sender == this.listBoxKunden)
{

}
if (sender == this.listBoxEinkaufsliste)
{

}
if (sender == this.comboBoxStueckzahl)
{

}
if (sender == this.tbPreisnachlass)
{

}
if (sender == this.rbTeilNameSort||sender==this.rbTeilNumSort)
{
// ToDo:
// LIstboxTeile löschen
// Liste neu sortieren -> dafür gibt es fertige Methoden
// Listbox mit neuer Sortierung füllen -> auch dafür stehen fertige Methoden bereit
}

}

private void btHinzufuegen_Click(object sender, EventArgs e)
{

}

private void listBoxTeile_SelectedIndexChanged(object sender, EventArgs e)
{

}

private void listBoxTeile_DoubleClick(object sender, EventArgs e)
{

}

private void rbTeilNumSort_CheckedChanged(object sender, EventArgs e)
{
pruefeEingabe(this.rbTeilNumSort);
}

private void rbTeilNameSort_CheckedChanged(object sender, EventArgs e)
{
pruefeEingabe(this.rbTeilNameSort);

}
private void listBoxTeileFill(List<CTeile.Teil> structlist)
{
this.listBoxTeile.Items.Clear();
foreach (CTeile.Teil element in structlist)
listBoxTeile.Items.Add(hteile.struct_to_string(element));
}
private void listBoxKundenFill(List<CKunden.Kunde> structlist)
{
this.listBoxKunden.Items.Clear();
foreach (CKunden.Kunde element in structlist)
listBoxKunden.Items.Add(hkunden.struct_to_string(element));
}


} // form2
}

Der Beitrag wurde von sharky2014 bearbeitet: 12.04.2014, 19:15 Uhr
Angehängte Datei(en)
Angehängte Datei  DemoForm9.jpg ( 167.07KB ) Anzahl der Downloads: 8
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 12.04.2014, 19:54 Uhr
sharky2014
Level 7 = Community-Professor
*******

Nochmal zusammengefaßt, umso mehr Code, umso mehr geht die Übersichtlichkeit verloren.
Ich halte bei der Programmierung für das wichtigste, daß man eine klare und gute Strukturierung reinbringt.
Das schlimmste ist hier und dort anflicken. Flickenteppich, Spaghetticode. Wenn das so in die Richtung geht, das kann sein, weil man erst später merkt, daß man die Aufgabenstellung im Anfang falsch eingeschätzt hat, dann ALLES LÖSCHEN UND NEU.


Wir haben in der form2 folgende Struktur:


public Form2()
{
InitializeComponent();
Init();

}


Das ist der Konstruktor. Dje Klasse ist Form2, der Konstruktor ist Form2(). Die erste Zeile ist von WinForms vortegeben und darf nicht verändert werden.
Danach kommt der Aufruf der eigenen Methode Init();

Wir müssen sehr vorsichtig sein mit dem Schlüsselwort new, mit dem eine neue Instanz aufgerufen wird. Innerhalb der Unterfunktionen sollten wir new unbedingt vermeiden, weil sonst von einer Klasse eine inflationäre Schar von Kopien im Programm herumgeistern. Daher ist es auch wichtig, daß init() nur zu Beginn, also nur einmal aufgerufen wird.

Die nächste Gelegenheit, einen Datensatz für Rechnungen mit new() aufzurufen, wäre, wenn die nächste Rechnung geschrieben wird. Aber bitte nicht im laufenden Prozeß der gegenwärtigen Rechnung. Das kann leicht passieren, wenn man irgendwo eine untergeordnete Methode hat, die sich vom Rechnungsformular einen Objektverweis besorgt. Dann haben wir eine neue Kopie.

Grundsätzlich sollte man bei dem Schlüsselwort new also eigentlich zusammenzucken. !!!


Also, init, alles nur einmal beim ersten Aufruf der Form:

private void Init()
{
var Rechnung = new CRechnungen.Rechnung();
Rechnung.Einkaufsliste = new List<CRechnungen.Posten>();
Kundenliste=new List<CKunden.Kunde>();
Teileliste=new List<CTeile.Teil> ();
if (hdatei.lies_kundenliste_binaer(Kundenliste))
if (hdatei.lies_teileliste_binaer(Teileliste))
{
// Teileliste Vorgabe numerische Sortierung
this.rbTeilNumSort.Checked = true;
hteile.sortiereTeilelisteNumerisch(Teileliste);
listBoxTeileFill(Teileliste);
listBoxKundenFill(Kundenliste);
SPERRE = false;
}
}


Der nachfolgende Code gliedert sich in drei Gruppen von Elementen, nämlich die Eventhandler, die Methode pruefeEingabe(object sender) und die Hilfsmethoden.:

Eventhandler aller Steuerelemente, die ein Ereignis auslösen können:

private void rbTeilNumSort_CheckedChanged(object sender, EventArgs e)
{
pruefeEingabe(this.rbTeilNumSort); // Wenn da ein Click erfolgt, meldet dieser Ereignishandler, daß er geklickt wurde
}

private void rbTeilNameSort_CheckedChanged(object sender, EventArgs e)
{
pruefeEingabe(this.rbTeilNameSort);

}


Alle diese meldungen laufen in der Generalfunktion zusammen, die anhand des senders erkennt, von welchem Element das Ereignis ausgelöst wurde:

private void pruefeEingabe(object sender)
{
if (sender == this.btHinzufuegen)
{
// der Anwender will einen neuen Datensatz hinzufügen
}
if (sender == this.btRechnungPruefen)
{
// Eingabe ist fertig, Rechnung muß auf Richtigkeit überprüft werden
// wenn da noch Fehler sind, muß eine Fehlerbehandlung erfolgen
}


Die Hilfsfunktionen, um den Code der aufrufenden Funktion übersichtlich zu halten, z. B. diese:

private void listBoxKundenFill(List<CKunden.Kunde> structlist)
{
this.listBoxKunden.Items.Clear();
foreach (CKunden.Kunde element in structlist)
listBoxKunden.Items.Add(hkunden.struct_to_string(element));
}



Noch abstrakter:

Form2

Konstruktor

Generalmethode pruefeEingabe(object sender)

Eventhandler: alle Eventhandler rufen immer nur die eine Methode auf, nämlich pruefeEingabe(object Absender, also der Name des Elements)

Hilfsfunktionen



Das ist die ganze Form2.

Vorgabe ist klare Strukturierung, gute Übersicht, stabile Codierung.
.


--------------------
A programmer is just a tool which converts caffeine into code
TOP    



2 Besucher lesen dieses Thema (Gäste: 2)
0 Mitglieder: