Autor Beitrag
Th69
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Moderator
Beiträge: 4051
Erhaltene Danke: 839

Win7
C++, C# (VS 2015/17)
BeitragVerfasst: Mi 07.08.19 11:05 
Einleitung

Dieser Beitrag beschreibt die Vorgehensweise beim Datenaustausch zwischen mehreren Formularen (Forms) und/oder Klassen.
Unter WinForms stellt dabei das MVC- bzw. MVP-Pattern der designtechnische Ansatz dar. Für WPF ist dagegen das MVVM-Pattern das bevorzugte Design-Pattern.
Für die Code-Beispiele benutze ich jedoch strikt WinForms (für WPF gelten jedoch analoge Vorgehensweisen).

1. Trennung von Oberfläche (GUI) und Modell (Logik)

1.1 Unabhängigkeit der einzelnen Forms/(User)Controls und Klassen

Bei einem (größeren) Projekt sollten die einzelnen Klassen möglichst unabhängig voneinander programmiert sein, so daß bei Änderung einzelner Klassen andere abhängige Klassen möglichst wenig angepaßt werden müssen.
Da eine Form bzw. ein UserControl (Benutzersteuerelement) oder auch ein selbstentwickeltes Control (Steuerelement) jeweils eine Klasse darstellen, gelten die Überlegungen hier in diesem Beitrag stets für alle.

1.2 Keine Vermischung von GUI-Code und interner Logik

Da die Einstiegspunkte bei WinForms-Projekten aufgrund der ereignisorientierten Programmierung die EventHandler darstellen (z.B. Button_Click oder ListBox_SelectedIndexChanged) ist es ersteinmal sehr bequem, den gesamten Code in diesen Event-Methoden zu schreiben. Man sollte jedoch früh genug erkennen, welcher Anteil davon reiner GUI-Code ist und welcher z.B. interne Berechnungen darstellen. Und letzteres sollte dann in eigene Klassen ausgelagert werden, so daß man eigenständige Methoden (mit einem sinnvollen Namen! ;- ) hat, die man dann auch von anderen Stellen des Projektes aus aufrufen kann. Und falls man dann den Methoden noch passende Parameter mitgibt, vermeidet man auch gleich die Code-Duplizierung (sog. Copy&Paste).

2. Hierarchische Projektstruktur

Bei der Aufteilung der Funktionalität in einzelne Klassen sollte man auch gleich eine entsprechende Projektstruktur wählen. Diese sollte möglichst hierarchisch (d.h. von oben nach unten) aufgebaut sein, so daß untergeordnete Klassen die übergeordneten Klassen nicht kennen.
Beispiel:
ausblenden Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
PersonManagement-Projekt:
- Controls
  - PersonImageControl // Anzeige des Portraits einer Person (2D/3D ;-)
  - PersonUserControl  // Anzeige aller Personendaten + Bild
- Dialogs
  - LoginDialog    // Anmelde-Dialog
  - SettingsDialog // Einstellungen
- Logic
  - Person // Verwaltung aller Personendaten
  - Hobby  // Verwaltung der Hobbies einer Person
MainForm // Hauptformular

Mit diesem Projekt können die Daten zu einer Person (inkl. deren Hobbies) verwaltet werden.
Die Logic-Klassen sind dabei so aufgebaut, daß sie völlig unabhängig (auch in anderen Projekten) verwendet werden können. Die Dialoge (Forms) sollten auch jeweils unabhängig von anderen Klassen programmiert sein, einzig die (User)Controls kennen die Logic-Klassen, damit sie diese entsprechend darstellen können.
Und nur das MainForm hat Zugriff auf alle untergeordneten Klassen, d.h. es ist für die Instanziierung der einzelnen Objekte verantwortlich.

(Als Hinweis: bei größeren professionellen Anwendungen würde man die einzelnen Unterordner sogar als eigenständige Libraries (Assemblies) erzeugen. Die Logic-Library heißt dann häufig BusinessClasses.)

Typische Anfängerfehler

Durch die Vielzahl an Beiträgen in diesem Forum gibt es typische Beispiele, bei denen mehr oder weniger die gleichen Fehler gemacht werden, um eine Kommunikation zwischen mehreren Forms hinzukriegen. Daher beschreibe ich hier die häufigsten, und erkläre, was daran falsch ist.

1. Neues Form-Objekt erstellen

Angenommen man hat ein MainForm und ein davon aufgerufenes SubForm und man möchte dann in diesem SubForm dann eine Eigenschaft oder Methode der MainForm aufrufen:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
class SubForm : Form
{
  // ...

  void button_Click(object sender, EventArgs e)
  {
    // hier soll jetzt die Methode 'UpdateText(string)' von der MainForm aufgerufen werden
    
    MainForm.UpdateText("Test");
  }
}

Nun kommt beim Compilieren die Fehlermeldung
Zitat:
Für das nicht statische Feld, die Methode oder die Eigenschaft "MainForm.UpdateText(string)" ist ein Objektverweis erforderlich.

Nun ist klar, daß man ja ein Objekt der Klasse "MainForm" benötigt (keinesfalls sollte man nun die UpdateText-Methode als 'static' deklarieren, da man ja dann innerhalb dieser Methode nicht mehr auf Member der Klasse "MainForm" zugreifen kann!!!).
Es entsteht demnach dann folgender Code:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
class SubForm : Form
{
  // ...

  void button_Click(object sender, EventArgs e)
  {
    // hier soll jetzt die Methode 'UpdateText(string)' von der MainForm aufgerufen werden

    MainForm mainForm = new MainForm();    
    mainForm.UpdateText("Test");
  }
}

Ohne das "new MainForm()" würde man ja eine "NullReferenceException" erhalten, also benötigt man ja ein Objekt und erstellt es mittels "new".
Leider ist dann aber zur Laufzeit nichts im MainForm zu sehen, weil nämlich mittels "new MainForm()" ein neues (aber unsichtbares) Objekt dieser Klasse erzeugt und dessen Methode aufgerufen wurde.

Die korrekte Lösung wäre die Übergabe einer Referenz an dieses SubForm:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
class SubForm : Form
{
  SubForm(MainForm mainForm)
  {
    this.mainForm = mainForm;
  }

  private MainForm mainForm; // hält die Referenz des übergebenene MainForms

  void button_Click(object sender, EventArgs e)
  {
    // hier soll jetzt die Methode 'UpdateText(string)' von der MainForm aufgerufen werden

    mainForm.UpdateText("Test");
  }
}

Jedoch ist dies auch nicht die beste Lösung, da ein untergeordnetes Form (SubForm) niemals direkten Zugriff auf übergeordnete Forms (hier MainForm) tätigen sollte!

2. Zugriff auf private Controls einer anderen Form

Anstatt eine UpdateText()-Methode zu benutzen, wird häufig auch versucht direkt auf Unterelemente einer anderen Form zuzugreifen:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
class SubForm : Form
{
  // ...

  void button_Click(object sender, EventArgs e)
  {
    mainForm.labelText.Text = "Test";
  }
}

Und weil die Fehlermeldung
Zitat:
Der Zugriff auf "MainForm.labelText" ist aufgrund der Sicherheitsebene nicht möglich.

erscheint, wird dann einfach der Member "labelText" in MainForm als "public" deklariert.

Ebenso falsch ist aber auch der umgekehrte Zugriff, also vom MainForm aus auf die internen Controls einer SubForm zuzugreifen:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
class MainForm : Form
{
  // ...
  void button_Click(object sender, EventArgs e)
  {
    SubForm subForm = new SubForm();
    subForm.labelDescription.Text = "Test Description";
  }
}


Durch das Kapselungsprinzip von OOP sollte jedoch eine Klasse nur auf die öffentliche Schnittstelle einer anderen Klasse zugreifen (und Controls stellen nur interne Implementierungen einer Klasse dar!).

Und im nächsten Abschnitt wird erklärt, wie die professionelle, gute Lösung aussieht. :- )

Lösung: Verwendung von Eigenschaften (Properties) und Ereignissen (Events)

1. Eigenschaften

Anstatt auf interne Controls von außen zuzugreifen, sollte die Form-Klasse selber eine entsprechende Eigenschaft zur Verfügung stellen (diese stellt dann einen Teil der öffentlichen Schnittstelle dar):
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
class SubForm : Form
{
  // ...

  public string Description
  {
    get { return labelDescription.Text; }
    set { labelDescription.Text = value; }
  }
}

So wird der Beschreibungstext einfach weitergeleitet an das interne Control. Und wenn statt des Labels später einmal ein anderes Steuerelement (TextBox, WebBrowser, ...) dafür verwendet werden soll, so braucht nur die SubForm-Klasse geändert werden und nicht der Code, der diese Klasse und die Eigenschaft "Description" aufruft.

So kann man dann mehrere Eigenschaften zur Verfügung stellen (so wie es die Standard-Controls ja auch machen).

Zusätzlich zu den Eigenschaften können selbstverständlich auch öffentliche Methoden definiert werden, um mehrere Eigenschaften auf einmal zu übergeben oder aber falls noch zusätzliche Parameter benötigt werden, z.B.:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
class SubForm : Form
{
  // ...

  public void SetColoredDescription(string description, Color foreColor, Color backColor)
  {
    labelDescription.Text = description;
    labelDescription.ForeColor = foreColor;
    labelDescription.BackColor = backColor;
  }
}


2. Ereignisse

Möchte man jedoch von einer SubForm auf eine andere (übergeordnete) Form zugreifen, so sollte man zwingend Ereignisse (Events) verwenden. Einen FAQ-Beitrag zum Erstellen eigener Ereignisse gibt es z.B. unter myCSharp.de: [FAQ] Eigenen Event definieren / Information zu Events (Ereignis/Ereignisse).

Ereignisse lösen das Dilemma, daß man auf eine andere Klasse (Form) indirekt zugreifen kann, ohne sie zu kennen!

Für das Beispiel, daß eine SubForm den Label-Text der MainForm aktualisieren soll (z.B. um einen Status anzuzeigen) sieht der Code dann so aus:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
class SubForm : Form
{
  // ...

  public class TextEventArgs : EventArgs
  {
    public TextEventArgs(string text)
    {
      Text = text; // Eigenschaft setzen
    }

    public string Text { get; set; } // Text als Eigenschaft definieren
  }

  public event EventHandler<TextEventArgs> UpdateText; // Ereignis deklarieren

  void button_Click(object sender, EventArgs e)
  {
    // ...
    
    OnUpdateText(new TextEventArgs("Test")); // Ereignis-Methode aufrufen
  }

  protected virtual void OnUpdateText(TextEventArgs e)
  {
    EventHandler<TextEventArgs> ev = UpdateText;
    if (ev != null)
      ev(this, e); // abonnierte Ereignismethode(n) aufrufen
  }
}

Und nun muß im MainForm dann noch dieses Ereignis abonniert (zugewiesen) werden:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
class MainForm : Form
{
  void button_Click(object sender, EventArgs e)
  {
    SubForm subForm = new SubForm();
    subForm.UpdateText += UpdateLabelText; // Ereignis abonnieren
    subForm.ShowDialog(this);
  }

  void UpdateLabelText(object sender, SubForm.TextEventArgs e)
  {
    labelText.Text = e.Text; // Zugriff auf Eigenschaft des Ereignisses
  }
}


So hat man nun eine saubere Entkopplung der einzelnen Form-Klassen. Auch wenn der Aufwand einmalig etwas höher ist (Ereignis und Argumente sowie Ereignis-Methoden definieren), so lohnt es sich langfristig, denn so kann man die Klassen (bzw. Forms) nachträglich bequemer erweitern und sogar einfach in andere Projekte übernehmen.

Verschiedene Möglichkeiten und deren Lösungen

Da es verschiedene Arten der Kommunikation von 2 Formularen gibt, zeige ich im folgenden exemplarisch die allgemein beste Lösung dafür.

Hinweis: Zur besseren Lesbarkeit verwende ich hier in den folgenden Code-Beispielen keine Namensbereiche (namespaces), d.h. diese müssen dann in eigenen Projekten entweder explizit mittels des Bereichsoperators "." hinzugefügt oder aber mittels der "using"-Direktive verfügbar gemacht werden.

1. Hauptform ruft Unterform mittels ShowDialog() auf

Da mittels ShowDialog() ein modaler Dialog angezeigt wird, kehrt diese Methode erst zurück (und liefert ein DialogResult), wenn dieser geschlossen wird (vom Anwender oder per Close()-Methode bzw. Setzen der Eigenschaft DialogResult). Somit können die im Dialog gesetzten Eigenschaften direkt nach Aufruf ausgewertet werden.

ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
class MainForm : Form
{
  void button_Click(object sender, EventArgs e)
  {
    SubForm subForm = new SubForm();
    subForm.Text = "Unterformular";
    subForm.SetColoredDescription("Beschreibung...", Color.Blue, Color.White);

    if (subForm.ShowDialog(this) == DialogResult.OK)
    {
        // SelectedValue und SelectedText sind Eigenschaften der SubForm-Klasse
        int nValue = subForm.SelectedValue;
        string sText = subForm.SelectedText;
        // ...
    }
  }
}

Nur, wenn während der Dialog geöffnet ist, Daten an das Hauptformular übermittelt werden sollen, ist es notwendig, daß auch hier mit Ereignissen gearbeitet wird (wie beim Beispiel "UpdateText" unter "Ereignisse" beschrieben).

2. Hauptform ruft Unterform mittels Show() auf

Wird ein nicht-modales Formular mittels Show() aufgerufen, so kehrt diese Methode sofort nach Anzeige des Formulars zurück, d.h. man sollte danach nicht sofort auf Eigenschaften dieser Form zugreifen, da diese noch den initialen Wert besitzen. Hier muß man dann zwingend Ereignisse zur Kommunikation einsetzen.
Möchte man z.B. explizit auf das Schließen dieser Form warten, so kann man dafür dann FormClosed abonnieren:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
class MainForm : Form
{
  void button_Click(object sender, EventArgs e)
  {
    SubForm subForm = new SubForm();
    subForm.FormClosed += subForm_FormClosed;

    subForm.Show(this);
  }

  void subForm_FormClosed(object sender, FormClosedEventArgs e)
  {
     MessageBox.Show("Das Unterformular wurde geschlossen!" + Environment.NewLine +
                     "Grund: " + e.CloseReason.ToString());
  }
}

Insbesondere für ToolWindows (d.h. FormBorderStyle = FixedToolWindow oder SizedToolWindow) macht es keinen anderen Sinn als Ereignisse zu benutzen, z.B. Farbauswahl bei einer Palettenanzeige:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
class MainForm : Form
{
  MainForm()
  {
    ToolWindow toolWindow = new ToolWindow();
    toolWindow.SelectedColorChanged += toolWindow_SelectedColorChanged;

    toolWindow.Show(this);
  }

  void toolWindow_SelectedColorChanged(object sender, ToolWindow.ColorEventArgs e)
  {
     Color selectedColor = e.Color;

     // z.B. Hintergrundfarbe setzen
     this.BackColor = selectedColor;
  }
}

Das ToolWindow hat dann entsprechend dieses Ereignis definiert:
ausblenden volle Höhe C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
class ToolWindow : Form
{
  public class ColorEventArgs : EventArgs
  {
    public ColorEventArgs(Color color)
    {
      Color = color; // Eigenschaft setzen
    }

    public Color Color { get; set; } // Color als Eigenschaft definieren
  }

  public event EventHandler<ColorEventArgs> SelectedColorChanged; // Ereignis deklarieren

  static readonly Color[] colors =
    { Color.White, Color.Black, Color.Red, Color.Green, Color.Blue };

  void button_Click(object sender, EventArgs e)
  {
    Button button = sender as Button;
    if (sender != null)
    {
      Color color = colors[(int)button.Tag]; // Tag gibt den Index der Farbe an

      OnSelectedColorChanged(new ColorEventArgs(color));
    }
  }

  protected virtual void OnSelectedColorChanged(ColorEventArgs e)
  {
    EventHandler<ColorEventArgs> ev = SelectedColorChanged;
    if (ev != null)
      ev(this, e); // abonnierte Ereignismethode(n) aufrufen
  }
}

Dabei hat dann jeder Farbbutton die Methode "button_Click" abonniert und für die Eigenschaft "Tag" den entsprechenden Index der obigen Farbtabelle "colors" gesetzt.

3. Werte zwischen 2 Unterforms austauschen

Möchte man Werte zwischen 2 (oder mehreren) Unterforms austauschen, so sollte die Kommunikation stets über das gemeinsame übergeordnete Formular (Parent) erfolgen, da sich die Unterformulare gegenseitig nicht kennen sollten.
Dies gilt nicht nur für Formulare, sondern auch für andere Elemente (z.B. Benutzersteuerelemente oder auch beim Datenaustausch zwischen verschiedenen Klassenobjekten).
Ein weiteres häufiges Szenario ist auch der Einsatz eines TabControls, wobei dann jede TabPage aus einem separatem Benutzersteuerelement (UserControl) besteht.

Und auch hier werden dann zum Senden der Daten Ereignisse in den untergeordneten Elementen verwendet und die übergeordnete Form (bzw. Klasse) delegiert diese Daten dann mittels Eigenschaften oder Methoden an das andere Unterelement weiter.

Als ein einfaches Beispiel das Ändern eines Textes und einer Farbe bei Auslösung eines Ereignisses im anderen Formular:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
class MainForm : Form
{
  MainForm()
  {
     subForm = new SubForm();
     subForm.UpdateText += subForm_UpdateText;
     subForm.Show(this);

     toolWindow = new ToolWindow();
     toolWindow.SelectedColorChanged += toolWindow_SelectedColorChanged;
     toolWindow.Show(this);
  }

  private SubForm subForm;
  private ToolWindow toolWindow;

  void subForm_UpdateText(object sender, SubForm.TextEventArgs e)
  {
    toolWindow.Text = e.Text; // Überschrift des ToolWindow ändern
  }

  void toolWindow_SelectedColorChanged(object sender, ToolWindow.ColorEventArgs e)
  {
    subForm.BackColor = e.Color; // Hintergrundfarbe der SubForm setzen
  }
}

Selbst wenn "subForm" und "toolWindow" zwei Instanzen derselben Formklasse wären, so benutzt man auch hier die Ereignisse zur Kommunikation zwischen diesen beiden Forms.

Weiterführende Links

Model View Controller (MVC-Pattern)
Model View Presenter (MVP-Pattern)

Für diesen Beitrag haben gedankt: Christian S., Nersgatt