Follow my new blog

Montag, 4. Januar 2010

Aspekttrennung im GUI

Mich wundert, dass es bisher noch niemand bemerkt zu haben scheint: Der WinForms-Designer (und auch der WPF-Designer) vermischt zwei Aspekte bei der GUI-Programmierung. Das sind die Aspekte Gestaltung und Funktionalität. Dass er das tut, finden wir natürlich alle irgendwie bequem. Doch mich beschleicht das Gefühl, dass hier zuviel des Guten getan wird.

Ein Beispiel ein kleines Formular zum Addieren zweier Zahlen:

image

Da steckt etwas Gestaltung drin –  Steuerelemente wollten ausgewählt und angeordnet werden – und Funktionalität – Ereignisbehandlungsroutinen wollten hinter die Steuerelemente gestellt werden.

Die Programmierung war ganz einfach: Steuerelemente auf das Formular ziehen (Gestaltung), Doppelklick auf ein Steuerelement, um einen Eventhandler zu schreiben (Funktionalität). So weit, so gut. Das möchte ich kaum anders haben.

Wenn ich genau hinschaue, stört mich aber das Ergebnis dieser einfachen Programmierung im Code. Das sieht nämlich so aus:

public partial class WinDoTheMath : Form

{

    public WinDoTheMath()

    {

        InitializeComponent();

    }

 

    private void button1_Click(object sender, EventArgs e)

    {…}

 

    private void textBox1_Validating(object sender, CancelEventArgs e)

    {…}

}

Ganz normal – aber subtil verstörend. Denn was mir hier fehlt, das ist eine Zuordnung der Ereignisbehandlungsroutinen. Die “stehen im Code nur so rum”. Ich muss mir anhand der im Methodennamen steckenden Hinweise zusammenreimen, wie/wann sie zum Einsatz kommen.

Das finde ich inzwischen irgendwie umständlich – oder anders ausgedrückt: Der Zwang zum Zusammenreimen erhöht für mich die Komplexität des Codes. Ich sehe einfach nicht die Abhängigkeiten bzw. Zusammenhänge. Die hat der Designer nämlich im code behind versteckt:

private void InitializeComponent()

{

    …

    //

    // textBox1

    //

    this.textBox1.Location = new System.Drawing.Point(12, 12);

    this.textBox1.Name = "textBox1";

    this.textBox1.Size = new System.Drawing.Size(55, 20);

    this.textBox1.TabIndex = 0;

    this.textBox1.Validating +=

           new System.ComponentModel.CancelEventHandler(this.textBox1_Validating);

    //

    // textBox2

    //

    this.textBox2.Location = new System.Drawing.Point(121, 12);

    this.textBox2.Name = "textBox2";

    this.textBox2.Size = new System.Drawing.Size(55, 20);

    this.textBox2.TabIndex = 1;

    this.textBox2.Validating +=

           new System.ComponentModel.CancelEventHandler(this.textBox1_Validating);

    //

    // button1

    //

    this.button1.Location = new System.Drawing.Point(202, 9);

    this.button1.Name = "button1";

    this.button1.Size = new System.Drawing.Size(75, 23);

    this.button1.TabIndex = 2;

    this.button1.Text = "Add";

    this.button1.UseVisualStyleBackColor = true;

    this.button1.Click += new System.EventHandler(this.button1_Click);

    …

Sehen Sie die Zusammenhänge? Die stecken in der jeweils letzten Zeile der Codeblöcke für die Steuerelemente. Dort wird der Eventhandler dem Control zugewiesen. Da geht es um Funktionalität. Und was steht davor? Das geht es um die Gestaltung.

Gestaltung und Funktionalität sind im code behind vermischt. Das ist gut gemeint vom Designer – aber ich finde das nicht mehr verständnisfördernd. Früher war das genial; heute fühle ich mich dadurch verwirrt. Liegt das am Alter? ;-) Nein, ich glaube, das liegt am durch Clean Code Developer geschärften Blick für Verständlichkeit und Abhängigkeiten.

Wo Zusammenhänge nicht sofort klar werden, da regt sich ein ungutes Gefühl bei mir. Und unklar sind sie, wenn ich nicht dort, wo ich Code lese – also im “foreground code” –, mich leicht darüber informieren kann, wer die beteiligten Parteien sind und wie sie zusammen hängen. Weder weiß ich dort, welche Steuerelemente es gibt, noch weiß ich, wie daran Ereignisbehandlungsroutinen geknüpft sind. Das finde ich unschön.

Ich werde daher in Zukunft nicht mehr einfach auf Steuerelementen doppelklicken, um mir Eventhandler generieren zu lassen! Stattdessen schreibe ich die von Hand. Dabei kann ich dann auch wählen, wie ich sie implementiere: ob als eigenständige Methode oder doch nur als Lambda Funktion:

public partial class WinDoTheMath : Form

{

    public WinDoTheMath()

    {

        InitializeComponent();

 

        this.textBox1.Validating += ValidateNumberEntered;

        this.textBox2.Validating += ValidateNumberEntered;

 

        this.button1.Click += (s, e) =>

              {

                  var sum = int.Parse(textBox1.Text) + int.Parse(textBox2.Text);

                  MessageBox.Show(string.Format("Sum: {0}", sum));

              };

    }

 

    private void ValidateNumberEntered(object sender, CancelEventArgs e)

    {

        int i;

        e.Cancel = !int.TryParse(((TextBox) sender).Text, out i);

 

        this.errorProvider1.SetError((Control)sender,

                                     e.Cancel ? "Input is not an integer!" : "");

    }

}

Jetzt habe ich alles auf einen Blick in der Codeansicht: die wirklich relevanten Steuerelemente mit ihrer Funktionalität.

Und ich habe die beiden Aspekte Gestaltung und Funktionalität sauber getrennt. Der Designer ist jetzt wirklich nur noch für die Gestaltung zuständig. Die schaue ich mir im Design View an – und der Code dafür liegt irgendwo verborgen, weil er mich für die Funktionalität, mit der ich sonst beschäftigt bin, nicht interessiert.

Das finde ich sauber. Wer noch?

8 Kommentare:

Andreas Adler hat gesagt…

Hallo Ralf,

mich hat diese Eigenheit von C# und dem Winforms-Designer auch immer gestört. Deine Lösung trägt zur Verständlichkeit natürlich bei, aber der Bequemlichkeit ist ja leider nicht mehr gedient. ;)

Finde daher die Lösung in VB sehr elegant, einer Methode per Handles-Klausel direkt verschiedene Ereignisse zuordnen zu können. :)

Grüße,

Andreas

Rico hat gesagt…

Interessant finde ich hier wie VB.Net das handhabt. Hier gibt es das "Handles" Schlüsselwort.

Mit Hilfe von Handles bleibt die Definition des Events auch auf Logischer Ebene.


Public Class Form1
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
MessageBox.Show("Hallo")
End Sub
End Class


Empfehlenswert ist hier sich den daraus entstandenen Code im Reflector mal anzusehen...

mohk hat gesagt…

Da hat aber jmd das Refactoring noch nicht so ganz abgeschlossen:
Grober Verstoss gegen Single Level of Abstraction.
Sprich den ganzen Code in InitializeComponentsWithOwnCode() und ich nicke den Artikel durch, so aber nicht!

Christian

Ralf Westphal - One Man Think Tank hat gesagt…

@Christian: Ich reagiere auf deinen knappen Code jetzt mal auf drei Weisen. Such dir daraus eine raus, die dir passt:

Antwort 1: Ja, du hast recht. Das ist aus CCD-Sicht noch nicht ganz sauber. Ich sollte die Bindung von Controls und Eventhandlern nicht einfach so im Ctor stehen lassen. Dort sollte vielmehr ein BindEventhandlersToControls() aufgerufen werden, das die Eventhandlerzuweisung enthält.

Deinen Methodennamen finde ich nicht passend, weil er nicht den eigentlichen Zweck des Codes beschreibt. (In dem Sinne ist auch InitializeComponents() natürlich suboptimal. Die Methode hieße bei konsequenter Aspekttrennung besser z.B. InitializeLayout().)

Antwort 2: Ja, du hast recht. Aber ich finde die Auslagerung für mein Beispiel unnötig. Sie würde das Wesentliche ein Stück in den Hintergrund rücken. Denn so wie von mir codiert sieht man ja ganz deutlich, welcher Code wo stehen sollte.

Nicht nur Code sollte dem SRP folgen, sondern auch ein Blogposting. Dessen Zweck ist nun nicht "perfektes CCD", sondern Demonstration der Aspekttrennung. So habe ich mich gegen eine Refaktorisierung entschieden, um diesen Zweck deutlich zu machen. Angesichts der Übersichtlichkeit des Code, halte ich das für passend. (Bei längerem Code z.B. zu einem dotnetpro Artikel sehe ich das allerdings anders. Der soll zwar meistens nur einen inhaltlichen Zweck erfüllen, braucht dafür aber eher auch eine CCD-gemäße Form. Sonst ist er unverständlich - und gibt denn tatsächlich qua Größe ein falsches Bild davon, wie man Software schreiben sollte.)

3. Ich stimme dir nicht zu. Refaktorisierung ist kein Selbstzweck. Sie sollte nicht per se vorgenommen werden, sondern bei Bedarf. Bedarf ergibt sich aber erst, wenn ein nächstes Feature realisiert werden soll. Ich bin mit dem Code jedoch am Ende und er ist so übersichtlich, dass eine Herauslösung keinen Gewinn für den Zweck des Artikels brächte.

Das TDD-Mantra red-green-refactor ist für mich aus dem Zusammenhang gegriffen. Erst wenn es nach refactor noch weiter geht, macht es Sinn. Also red-green-refactor-red-green-.... Und dann kannst du dir eine Klammer so denken: red-green-(refactor-red-green-(...)).

Wer refactoring "einfach so am Ende" betreibt, der folgt zwar einem löblichen Gedanken (und sicherlich soll man auch "seinen Arbeitsplatz" aufräumen), läuft jedoch Gefahr, YAGNI und KISS zu widersprechen.

Hoffe, bei den Antworten ist was dabei für dich - und die anderen Leser.

-Ralf

Golo Roden hat gesagt…

FULLACK. Mehr kann ich dazu eigentlich nicht sagen.

Interessant ist: Wer an der von Dir vorgeschlagenen Trennung zweifelt, soll sich nur einmal an seine ersten Tage mit Windows Forms zurückerinnern, und sich fragen, wie lange er damals gebraucht hat, um einen Eventhandler sauber zu entfernen, der an einem nicht mehr existenten Control hing ...

Boas hat gesagt…

Der Artikel trifft in der tat einen blinden Fleck den wohl viele haben.

Der generierte Eventhandler code ist einfach grausam. Genauso aber auch Zustandsinformationen welche z.bps. den Standardzustand ausdrücken.

Wer schonmal an einem Projekt gearbeitet hat, welches schon länger als 1,2 Jahre existiert, sieht in welche Wucherungen das ganze enden kann...

Der nächste logische Schritt finde ich aber mindestens genauso wichtig:

Nämlich dass jeder Button egal was er macht, ein externes Behavior oder wie bei WPF ein ICommand antriggert (oder bindet) und gar nichts mehr selbst (vorallem auch nichtdurch seine Container (Forms)oder ähnliches) _selbst tut_

Also:
this.button1.Click += (s, e) =>
{ ShowSumCommand.Execute()
};

oder eben ein CommandBinding.

So erreicht man eine wesentlich größere Testbarkeit.

Ausserdem interessiert es jemanden in seiner Rolle als Entwickler wirklich, ob ein Button oder ein ContextMenü eine Funktion auslöst ?

Ich denke nicht. Es geht ja mehr darum dass wenn eine Funtion X eine MessageBox mit der Summe zur Folge haben soll.

Anonym hat gesagt…

Hallo,

ich finde diesen Ansatz auch sehr interessant.
Daher möchte ich alle Interessierten hier auf die "Presentation Patterns" von Jeremy D. Miller, die er auch in seinem Storyteller anwendet, aufmerksam machen (http://codebetter.com/blogs/jeremy.miller/archive/2009/08/08/the-presentation-patterns-wiki-is-live.aspx)

Gruss Jan

Anonym hat gesagt…

Zitat: ...läuft jedoch Gefahr, YAGNI und KISS zu widersprechen...
Hm, aber wenn er es nicht tut, läuft er Gefahr gegen die Pfadfinderregel zu verstoßen... ;-)