Autor Beitrag
JohnDyr
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 56
Erhaltene Danke: 1

Win 10
C# (VS 2017)
BeitragVerfasst: Sa 22.12.18 15:56 
Hallo,

aktuell komme ich zu dem Problem, dass meine "Form-Klassen" immer umfangreicher werden. Der auslagerfähige Code, wurde von mir natürlich ausgelagert. Da ich allerdings sehr viele Buttons und Textboxen in einem UserControl habe, und für jedes Element mindestens ein Event, staut sich das alles in meiner Klasse des UserControls wider. Dadurch komme auf ca. 900 Zeilen Code in einem UserControl, welcher eigentlich nichts besonderes macht.

Deshalb wollte ich Fragen welche Ansätze es gibt, um den Code gut zu modularisieren in C# / WinForms / .NET. Einfach eine Subklasse zu erstellen und dann diese von der Main-Klasse erben zu lassen, führt leider dazu dass ich in der Subklasse keinen Zugriff auf die Elemente des User Controls habe.

Hoffentlich wisst ihr was ich meine, ansonsten hakt gerne nochmal nach.

Danke für jede Hilfe,
JohnDyr
Th69
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Moderator
Beiträge: 4118
Erhaltene Danke: 852

Win10
C#, C++ (VS 2015/17)
BeitragVerfasst: Sa 22.12.18 16:49 
Hallo,

so maximal ca. 800-1000 Zeilen je Codedatei halte ich persönlich noch für überschaubar.
Evtl. kannst du aber auch wiederum einzelne Gruppen von Controls innerhalb des UserControls selber wieder als eigenes UserControl erstellen?

Sind denn die Aktionen (Events) je Control komplett verschieden oder kannst du evtl. für mehrere Controls dieselbe Ereignismethode verwenden (und dann z.B. mithilfe der Tag-Eigenschaft im Code auseinanderhalten - Beispiel: Taschenrechner mit den Tasten "0" bis "9")?

Vererbung solltest du nur einsetzen, wenn es auch aus logischer Sicht sinnvoll ist (nicht um Code zu sparen).
Als andere Alternative ginge auch noch das partial-Schlüsselwort (wie es ja auch der Form-Designer macht), so daß du den Code dann auf mehrere Dateien aufteilst, aber trotzdem alles logisch in einer Klasse ist.

Für diesen Beitrag haben gedankt: JohnDyr
JohnDyr Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 56
Erhaltene Danke: 1

Win 10
C# (VS 2017)
BeitragVerfasst: Sa 22.12.18 17:21 
Zitat:
Vererbung solltest du nur einsetzen, wenn es auch aus logischer Sicht sinnvoll ist (nicht um Code zu sparen).
A

Sehe ich genauso wie du.
Zitat:

Sind denn die Aktionen (Events) je Control komplett verschieden oder kannst du evtl. für mehrere Controls dieselbe Ereignismethode verwenden (und dann z.B. mithilfe der Tag-Eigenschaft im Code auseinanderhalten - Beispiel: Taschenrechner mit den Tasten "0" bis "9")?

Tatsächlich sind die Masken identisch. Das einzige was sich unterscheidet ist die DataSource. Auf allen Masken kann ich CRUD Operationen auf der jeweiligen DataSource ausführen. Ist das ein geeigneter Use Case für "Tags"? Müsste mich dann noch in die Materie mal einarbeiten.
Zitat:

Als andere Alternative ginge auch noch das partial-Schlüsselwort (wie es ja auch der Form-Designer macht), so daß du den Code dann auf mehrere Dateien aufteilst, aber trotzdem alles logisch in einer Klasse ist.

Das probiere ich gleich mal aus. Danke!
Edit: Habe es gerade ausprobiert. Eine zweite partial class in der Ersten erstellt. Allerdings muss ich dann die Elemente im Designer auf "public" ändern, weil sonst eine Fehlermeldung erscheint, dass aufgrund des Schutzgrads die Elemente nicht erreichbar seien.
ausblenden C#-Quelltext
1:
2:
  private System.Windows.Forms.Panel PnlBottom;
  private System.Windows.Forms.DataGridView Dgv;


Wenn ich nun in der ersten Klasse (UcCustomer) folgendes ausführe:

ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
        private void BtnNew_Click(object sender, EventArgs e)
        {

            // generate new ID ...
            int newId = CustomerDAO.GetLatestId() + 1;

            // ... check if new id is valid
            if (newId > 0)
            {
                DisableActionButtons(); // <-- DIESE METHODE VERLAGERE ICH, FÜHRT ABER ZUR FEHLERMELDUNG
            }
           
        }


Die neue partial Klasse welche nur Form Manipulierungs Methoden enthalten soll...
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
    public partial class UcCustomerFormControls : UcCustomer
    {

        public void DisableActionButtons()
        {
            foreach (Control b in PnlControls.Controls)
                if (b is Button)
                    b.Enabled = false;
            Dgv.Enabled = false;
        }

    }


Das wäre alles nur möglich, wenn ich die Methode DisableActionButtons() als statisch definiere und dann in der ersten Klasse mittels UcCustomerFormControls.DisableActionButtons() darauf zugreife. Unschön finde ich... mache ich einen Denkfehler?

Moderiert von user profile iconChristian S.: Code- durch C#-Tags ersetzt
Th69
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Moderator
Beiträge: 4118
Erhaltene Danke: 852

Win10
C#, C++ (VS 2015/17)
BeitragVerfasst: Sa 22.12.18 18:06 
Ja! ;-)

Ich meinte keine neue Klasse erstellen, sondern einfach den Code auslagern:
UcCustomer.cs:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
partial class UcCustomer
{
    private void BtnNew_Click(object sender, EventArgs e)
    {
         // ...
    }
}

UcCustomer_partial.cs:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
partial class UcCustomer
{
    public void DisableActionButtons()
    {
         // ...
    }
}

Also der Klassenname bleibt gleich!

Aber für solche Hilfsmethoden würde ich eine eigene Klasse mit (statischen) Methoden anlegen (dann kannst du diese auch in anderen Forms oder UserControls verwenden):
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
public class Helper // evtl. besseren Namen finden
{
    public void DisableActionButtons(Control control, Control dgv = null)
    {
        foreach (Control b in control.Controls)
            if (b is Button)
                b.Enabled = false;

        if (dgv != null)
            dgv.Enabled = false;
    }
}

(bzw. noch allgemeiner als generische Methode)
Und dann mittels Helper.DisableActionButtons(PnlControls, Dgv) aufrufen.

Und bzgl. der Ereignismethoden mit nur unterschiedlichen DataSources:
- du könntest dir ein Dictionary (oder List) anlegen, und dann per Tag darauf zugreifen:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
Dictionary<stringobject> dataSources = new Dictionary<stringobject>
{
  { "TABLE_PERSONS", dataSourcePersons },
  { "TABLE_CUSTOMERS", dataSourceCustomers },
  // ...
}

Und den String trägst du dann in die Tag-Eigenschaft der Controls ein und greifst dann in etwa so drauf zu:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
string sTag = control.Tag as string;
if (sTag != null)
{
  var dataSource = dataSources[sTag]; // oder .TryGet(...)
  // ...
}


PS: Schaue jetzt ersteinmal Fußball...
JohnDyr Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 56
Erhaltene Danke: 1

Win 10
C# (VS 2017)
BeitragVerfasst: Sa 22.12.18 19:04 
Zitat:
Also der Klassenname bleibt gleich!


Das erklärt einiges... danke erstmal! Das mit den Generics ist auch ein guter Tipp, möchte ich aber aktuell noch nicht umsetzen.

Viel Spaß beim Fussi.

Edit: habe es mal mit dem partial probiert und folgendes Konstrukt gebaut:

ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
// Allgemeine Logik
public partial class UcCustomer : UserControl
...

// Logik der DataGridView
partial class UcCustomer : UserControl
...

// Logik der Button Elemente
partial class UcCustomer : UserControl
...

// Logik der Textbox Elemente
partial class UcCustomer : UserControl
...


Fehlermeldung:
Der Typ "UcCustomer" besteht aus mehreren partiellen Klassen in derselben Datei.

Was mache ich falsch?

Moderiert von user profile iconChristian S.: Code- durch C#-Tags ersetzt
Ralf Jansen
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 4444
Erhaltene Danke: 914


VS2010 Pro, VS2012 Pro, VS2013 Pro, VS2015 Pro, Delphi 7 Pro
BeitragVerfasst: Sa 22.12.18 20:32 
Zitat:
Der Typ "UcCustomer" besteht aus mehreren partiellen Klassen in derselben Datei.

Was mache ich falsch?

Der Sinn von partiellen Klassen ist eine Klasse auf mehrere Dateien verteilen zu können. Diese Teile einer Klasse dann wieder in eine Datei zu packen ist völlig sinnfrei.

Du machst offensichtlich Winforms. Jede Form ist dort eine partielle Klasse. Schau dir doch irgendeine deiner Formen mal genauer im Sourcecode an du wirst dazu eine MEINELIEBEFORM.Designer.cs finden mit den generierten Codeteilen der Form. Dann kannst du dir überlegen ob dir partielle Klassen helfen etwas mehr Übersicht in einer Klasse zu schaffen.

Zitat:
Deshalb wollte ich Fragen welche Ansätze es gibt, um den Code gut zu modularisieren in C# / WinForms / .NET. Einfach eine Subklasse zu erstellen und dann diese von der Main-Klasse erben zu lassen, führt leider dazu dass ich in der Subklasse keinen Zugriff auf die Elemente des User Controls habe.


Und das ist gut so. Die Controls in einem UserControl sollten nicht von außerhalb des UserControls erreichbar sein und man sollte nicht versuchen auf diese von außerhalb zuzugreifen. Ein wichtiges Prinzip der objektorientierten Programmierung ist das Geheimnisprinzip. Wenn man jedes Ding von überall aufrufen könnte hast du ein exponenzielle Steigerung von Komplexität. Da ist ein wesentlich größeres Problem als ein etwas zu umfangreiche Klasse zu haben. Wenn diese umfangreiche Klasse ein kleine ansprechbare Oberfläche hat durchzieht dieses innere Komplexitätsproblem nicht gleich die ganze Anwendung und du hast nur das Problem einer komplexen Klasse und nicht gleich das Problem einer vielfach komplexeren Anwendung.
JohnDyr Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 56
Erhaltene Danke: 1

Win 10
C# (VS 2017)
BeitragVerfasst: So 23.12.18 12:17 
Zitat:
Der Sinn von partiellen Klassen ist eine Klasse auf mehrere Dateien verteilen zu können


Also muss man eine neue Klasse erstellen die genauso heißt? Was ist mit der .Designer.cs und der .resx? Die Fehlen ja dort dann und ich kann z.B. nicht alle Button Events dorthin verlagern, weil die Events eben nicht in der .Designer.cs gefunden bspw.

Stehe auf dem Schlauch...
Ralf Jansen
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 4444
Erhaltene Danke: 914


VS2010 Pro, VS2012 Pro, VS2013 Pro, VS2015 Pro, Delphi 7 Pro
BeitragVerfasst: So 23.12.18 13:16 
Zitat:
Also muss man eine neue Klasse erstellen die genauso heißt?


Nein. Erstelle eine neue Datei in der du bestimmte Teile der Klasse auslagerst. So wie das mit der Designer.cs auch passiert ist. Da ist keine andere Klasse drin es ist die gleiche Klasse nur mit Teilen in einer anderen Datei.

Zitat:
Was ist mit der .Designer.cs und der .resx?


Die gehören weiterhin zu der einen Klasse die du schon hast. Die Designer.cs enthält einfach nur einen partiellen Teil der einen Klasse. Aus wie vielen Dateien deine Klasse besteht ist egal.

Zitat:
Die Fehlen ja dort dann und ich kann z.B. nicht alle Button Events dorthin verlagern, weil die Events eben nicht in der .Designer.cs gefunden bspw.


Doch werden sie wenn du es richtig machst. Schau dir den Aufbau der Designer.cs an. Wie da die Klassendefinition aussieht. Du musst in einer weiteren Datei einfach nur das gleiche anstellen.

Zitat:
Stehe auf dem Schlauch...


Das mit der partiellen Klasse ist auch eher keine Lösung von Problemen sondern nur eine kleine Schmerzlinderung. Vielleicht lässt du das einfach wenn du damit nicht weiter kommst.
JohnDyr Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 56
Erhaltene Danke: 1

Win 10
C# (VS 2017)
BeitragVerfasst: So 23.12.18 14:09 
Zitat:
Das mit der partiellen Klasse ist auch eher keine Lösung von Problemen sondern nur eine kleine Schmerzlinderung. Vielleicht lässt du das einfach wenn du damit nicht weiter kommst.

Stimmt. Gerade möchte ich aber auch einfach nur das Prinzip verstehen. Leider funktioniert es immer noch nicht. Folgendes habe ich gemacht:

Ziel: Ich möchte z.B. das KeyPress Event auslagern in eine partial class.

Partial Class Aufbau:
ausblenden Quelltext
1:
Dateiname: Test.cs					

ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
namespace MyProject
{
    partial class UcCarrier
    {

        private System.Windows.Forms.TextBox TbId;

        /// <summary>
        /// Only allow numeric values in carrier id.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void TbId_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (!char.IsControl(e.KeyChar) && !char.IsDigit(e.KeyChar))
            {
                e.Handled = true;
            }
        }
    }
}


Führt zu lauter Fehlermeldungen in der Designer.cs.
ausblenden Quelltext
1:
2:
3:
4:
5:
Schweregrad  Code  Beschreibung  Projekt  Datei  Zeile  Unterdrückungszustand
Fehler  CS0229  Mehrdeutigkeit zwischen "UcCarrier.TbId" und "UcCarrier.TbId"  ...\UcCarrier.Designer.cs  98  Aktiv

Schweregrad  Code  Beschreibung  Projekt  Datei  Zeile  Unterdrückungszustand
Fehler  CS0246  Der Typ- oder Namespacename "KeyPressEventArgs" wurde nicht gefunden (möglicherweise fehlt eine using-Direktive oder ein Assemblyverweis).


Muss ich sämtlichen Code aus der Designer.cs, welcher für Formelement "TbId" verantwortlich ist, jetzt in der Test.cs übertragen? Das scheint mir nicht im Sinne des Erfinders zu sein...
Th69
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Moderator
Beiträge: 4118
Erhaltene Danke: 852

Win10
C#, C++ (VS 2015/17)
BeitragVerfasst: So 23.12.18 14:38 
Den Code aus der ".designer"-Datei solltest du nicht ändern (und auch nicht in die neue Datei übernehmen), also z.B. nicht die Membervariablen!

Und die 2. Fehlermeldung ist doch eindeutig: du benötigst auch hier die entsprechenden using-Direktiven.

Zusammengefasst:
ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
using System;
using System.Windows.Forms;

namespace MyProject
{
    partial class UcCarrier
    {
        /// <summary>
        /// Only allow numeric values in carrier id.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void TbId_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (!char.IsControl(e.KeyChar) && !char.IsDigit(e.KeyChar))
            {
                e.Handled = true;
            }
        }
    }
}

Ich würde jedoch die ganzen Event-Handler in der Originaldatei belassen und nur private Hilfsmethoden (o.ä.) auslagern (evtl. reagiert der Designer darauf nicht, weil er nur in der zu der ".designer" passenden Datei sucht).
JohnDyr Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 56
Erhaltene Danke: 1

Win 10
C# (VS 2017)
BeitragVerfasst: So 23.12.18 14:51 
Wow... die using Direktiven hatte ich zuerst auch drin gehabt, hatte aber weiterhin andere Fehlermeldungen... dann dachte ich mir, der zieht sich die using Verweise aus einer anderen partial class. Jedenfalls funktioniert es jetzt. :o :roll: :)

Danke für die gute Unterstützung (auch für so einen Idioten wie mich :roll: )

Edit:
Zitat:
(bzw. noch allgemeiner als generische Methode)
Und dann mittels Helper.DisableActionButtons(PnlControls, Dgv) aufrufen.

Das setze ich jetzt doch gleich mit um. Ist doch ganz cool eigentlich.

Durch diese beiden Aktionen wird alles schon etwas übersichtlicher.

Moderiert von user profile iconTh69: C#-Tags hinzugefügt
Chiyoko
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 298
Erhaltene Danke: 8

Win 98, Win Xp, Win 10
C# (VS 2017)
BeitragVerfasst: Mi 26.12.18 21:22 
Partielle Klassen sind zwar hilfreich aber irgendwann auch irritierend. Das kann mit statischen Klassen genauso sein, wenn sie Zuviel werden.
Auch wenn der Hinweis schon gegeben wurde:

Du kannst Events mehreren Controls (dessen EventHandlern) das selbe Event zuordnen.

ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
btn1.Click += Btn_Click;
btn2.Click += Btn_Click;

public void Btn_Click(object sender, EventArgs e)
{
    if(sender == btn1)
    {
        Provider.Search();
    }

    if(sender == btn2)
    {
        ...
    }
}


Damit reduziert sich die Zahl der Methoden erheblich.
(Fast) alles, was der Button tun soll, lagerst du als Service (extra Klasse) oder Funktion in der Forms Klasse aus.
Struktur ist alles.

Für diesen Beitrag haben gedankt: JohnDyr