Swing und MVC


Swing-Komponenten haben nicht nur 1 gegenüber AWT veränderte Oberflächengestaltung. Das Entscheidende ist die konsequente Trennung von Application und Behandlung der Daten.

In diesem Beitrag wird dieses Prinzip anhand einer einfachen Zaehler-Application demonstriert.

Das Model-View-Controller-Prinzip

Anstatt den gesamten Code in eine einzelne Klasse zu packen, werden beim MVC-Konzept drei unterschiedliche Bestandteile eines grafischen Elements nach dem Observer-Pattern unterschieden:

Model-Delegate-Prinzip

vereinfachte Variante von MVC: es wird die Funktionalität von View und Controller in einem UI Delegate zusammengefaßt. Dadurch wird das Programmieren etwas übersichtlicher.

Swing realisiert also kein reines MVC, sondern eine sog. Separable model architecture:

Trennung von Application und inhaltlicher Behandlung der Daten der Applikation. Der Teil für die Behandlung der Daten heisst Model (application-data model).

So funktioniert es

Die Application enthält in ihrem User-Interface Bedienelemente zur Eingabe von Benutzer-Befehlen. Diese melden dem Model: Der Benutzer wünscht die Daten zu manipulieren. Das Model manipuliert also die Daten und meldet der Application: Daten manipuliert. Die Application ihrerseits kann jetzt die frischen Daten im Model abrufen, um diese z.B. an der Benutzer-Oberfläche anzuzeigen.

Diese Tatsache, dass die Application nochmal nachfragt, heisst Lightweight-Notification. Die Alternative wäre, dass das Model bei seiner Rückmeldung gleich die frischen Daten mitbringt (stateful notification).

Die Swing-API tut es sowieso

Dieses Konzept ist in Komponenten der Swing-API bereits fest eingebaut, z.B. hat jeder JSlider ein BoundedRangeModel, jede JList ein ListModel oder allg. gesagt: jede XYZ-Komponente hat ihr XYZ-Model.

Die Trennung von Model und Komponente wird dadurch unterstrichen, dass Modelle angepasst und ausgetauscht werden können. Bei Bedarf können sogar mehrere Komponenten auf 1 gemeinsames Model zugreifen und so gemeinsam dieselben Daten benutzen. Demzufolge besitzt jede Komponente folgende Methoden:

   public void setModel( XYZModel model )
   public XYZModel getModel()
Umgekehrt besitzt jedes Model eine Methode, mittels derer es sich merkt, welche Komponente Interesse an den Daten hat:
public void addChangeListener( ChangeListener komponente )
Diese Ausdrucksweise deutet an, dass die Komponente aus Sicht des Models die Rolle desjenigen spielt, welcher darauf lauscht ( engl. to listen ), ob im Model Änderungen eingetreten sind.

Für selbstprogrammierte Komponenten gilt, dass man selbst das passende Model bauen muss. Im folgenden Bsp. wird ein Zaehler vorgestellt, für den ein entsprechendes ZaehlerModel implementiert wird.

MVC mit Interface

Aus der Trennung von Application und Model folgt: beide müssen inhaltlich unabhängig voneinander funktionieren und dürfen voneinander keine Implementierungs-Details kennen. Das einzige, was sie voneinander wissen: Mit anderen Worten: Application und Model verkehren nicht direkt miteinander, sondern über Interfaces.

Exkurs: Interface

Bsp.: Zaehler 1te Version

Die erste Version des Zaehlers hat 1 Button zum Weiterzählen, 1 TextFeld zum Anzeigen und 1 Model, das Zahlen von 0 bis 24 verwalten kann. Nach 14-maligem Klicken des Buttons sieht die Application folgendermassen aus:

Abb.: Zaehler1

Zaehler in einfacher Version

Die Application besteht aus 2 Interfaces und 3 Klassen.

Der Zusammenhang der Klassen und Interfaces ist wie folgt:

Abb.: UML-Klassendiagramm

Interface ZaehlerChangeListener

Dieses Interface legt in allg. Form fest, dass Klassen, welche sich für Daten in 1 Model interessieren, vom Model benachrichtigt werden: das Model ruft im Fall von Datenänderung die Methode
valueChanged( ZaehlerChangeEvent )
der interessierten Klasse auf: sog. Notification.

package mvc.einfach;

import java.util.EventListener;

public interface ZaehlerChangeListener extends EventListener
{
    public void valueChanged( ZaehlerChangeEvent e );
}

Klasse ButtonZaehler

Diese Klasse ist die eigentliche Application: Hier wird die GUI implementiert, und hier wird festgelegt, welches Model die Daten verwalten darf.

In Bezug auf die GUI gibt es hier die üblichen
JButton, JTextField, ActionListener, actionPerformed(ActionEvent).

In Bezug auf das Model gibt es
setModel( ZaehlerModel ), getModel()
und vor allem
valueChanged( ZaehlerChangeEvent )
siehe Interface. Diese Methode fragt beim Model nach:
...getValue()...
also Lightweight-notification, und verarbeitet den Wert weiter.


package mvc.einfach;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;


public class ButtonZaehler extends JPanel implements ActionListener, ZaehlerChangeListener
{
   private JButton jButton1;
   private JTextField jTextField;
   private ZaehlerModel zaehlerModel;

   public ButtonZaehler( ZaehlerModel m )
    {
        super();

        jButton1 = new JButton( "+" );
        jTextField = new JTextField( 13 );

        setLayout( new FlowLayout());
        add( jTextField );

        add( jButton1 );                   // plus-Button
        jButton1.addActionListener( this );

        setModel( m );
    }

    public void actionPerformed( ActionEvent e ){
       if( e.getActionCommand().equals( "+" )){
           getModel().weiter();
       }
    }

    public void valueChanged( ZaehlerChangeEvent e ){
          jTextField.setText( getModel().getValue().toString());
    }

    protected void setModel( ZaehlerModel newModel ){
        zaehlerModel = newModel;
        zaehlerModel.addChangeListener( this );
    }
    protected ZaehlerModel getModel()
     {
         return zaehlerModel;
     }

     static public void main( String [] args ){

         JFrame jFrame = new JFrame( "Zaehler" );
         jFrame.getContentPane().add( new ButtonZaehler( new ZahlModel()));
         jFrame.setSize( 234, 123 );
         jFrame.setVisible( true );
     }
}
Die Komponente spielt also in Bezug auf das Model eine Doppelrolle: Einerseits durch den JButton das Model zur Datenänderung veranlassen:
getModel().weiter()
andererseits auf die dann erfolgende Benachrichtigung durch das Model warten mit anschliessender Nachfrage:
getModel().getValue()

Diese Klasse hat auch den main der ganzen Application: Es wird 1 JFrame erzeugt und eine Instanz von ButtonZaehler, deren Konstruktor 1 passendes Model-Objekt übergeben wird.

Klasse ZaehlerChangeEvent

Das Model ruft nach jeder Änderung der Daten die Methode
fireStateChanged()
siehe unten. Dadurch wird jedem ZaehlerChangeListener die Methode
valueChanged( ZaehlerChangeEvent )
geschickt. ( In unserem Bsp. gibt es nur einen ZaehlerChangeListener, nämlich den ButtonZaehler ).

Das Model erzeugt bei dieser Benachrichtigung ein Objekt der Klasse ZaehlerChangeEvent und schickt es mit der Nachricht an die Komponente. Die Komponente kann daraus weitere Informationen über das Model entnehmen, das diese valueChanged( ZaehlerChangeEvent )-Nachricht geschickt hat. Sie darf dieses aber auch ignorieren, was sie in unserem Bsp. auch tut.


package mvc.einfach;

import java.util.EventObject;

public class ZaehlerChangeEvent extends EventObject
{

    public ZaehlerChangeEvent( Object source  ){
        super( source );
    }

    public Object getSource(){
        return source;
    }
}

Interface ZaehlerModel

Das Interface des Models legt fest, dass die Daten manipuliert werden können:
weiter()
und dass man die Daten abfragen kann:
getValue()

Das Interface definiert 1 Methode, mittels derer das Model sich merkt, wer von Datenänderungen benachrichtigt werden soll:
addChangeListerner( ChangeListener )
Da die Komponente das Interface ChangeListener implementiert, kann sie vom Model benachrichtigt werden. Diese Benachrichtigung wird realisiert durch die Methode
fireStateChanged()
Die konkrete Ausgestaltung dieser Methode überlässt das Model-Interface einem konkreten Model.


package mvc.einfach;

public interface ZaehlerModel
{
    public Object getValue();

    public void weiter();

    public void addChangeListener( ZaehlerChangeListener l );

    public void fireStateChanged();
}

Klasse ZahlModel

Jede beliebige Klasse, die das Interface ZaehlerModel implementiert, kann ein Model sein. Hier ist als Bsp. eine Klasse, welche die Zahlen von 0 bis 24 verwalten kann.

Die Klasse hat für die Verwaltung des Zahlenwerts Variablen
min, max, val
zum Verändern eine Methode
weiter()

Zum Benachrichtigen der interessierten Klassen dient die Methode
fireStateChange()
Allen ZaehlerChangeListenern wird dadurch in der for-Schleife die Nachricht geschickt:
valueChanged( new ZaehlerChangeEvent( this ))
Das this heisst hier: das Model schickt eine Referenz auf sich selbst mit an den ZaehlerChangeListener, zum Zweck der Auswertung durch diesen.

In unserem Bsp. gibt es nur 1 ZaehlerChangeListener, das ist die Komponente.

Damit das Model überhaupt weiss, wer ZaehlerChangeListener ist, werden diese in einem Array listenerArray verwaltet ( Vektor wäre besser ), und es gibt 1 Methode, um die ZaehlerChangeListener in diesen Array einzutragen:
addChangeListener( ZaehlerChangeListener )


package mvc.einfach;

public class ZahlModel implements ZaehlerModel{

    private int min, max, val;

    protected ZaehlerChangeListener listenerArray[] = new ZaehlerChangeListener[7];
    private int count = 0;

    public ZahlModel(){

        val = 0;
        min = 0;
        max = 24;
    }

    public Object getValue() {
        return new Integer( val );
    }

     public void addChangeListener( ZaehlerChangeListener l ){
         listenerArray[ count++ ] = l;
         l.valueChanged( new ZaehlerChangeEvent( this ));
     }

      public void weiter() {
          if( val < max ){
              val++;
          }
          fireStateChanged();
      }

      public void fireStateChanged(){
          for( int i = 0; i < count; i++ ){
              listenerArray[ i ].valueChanged( new ZaehlerChangeEvent( this ));
          }
      }
}

Soweit die kleinst mögliche Version eines einfachen Programms mit GUI und MVC.

Warum?

Der Nutzen eines solchen Programm-Designs wird deutlich, wenn man es ausnutzt: Ein Zaehler soll natürlich nicht immer nur bis 24 zählen, er soll alles zählen, z.B. die Wochentage von Montag bis Sonntag; also definiert man dafür 1 anderes Model.

Und nicht jeder Zaehler hat 1 plus-Button, manche haben einen Schieberegler usw, also anderes UserInterface. Diese Austauschbarkeit ist ein Vorteil von MVC.

Der andere Aspekt: Weil die ganze Swing-API selbst nach diesem Schema implementiert ist, muss man sich da eh auskennen.

Und: Moderne Entwurfsmethoden, UML, Patterns usw sind zu ihrer Implementierung auf Programmiersprachen (und Programmierer) angewiesen, die so etwas umsetzen können.


Weiter mit ausführlichem Zaehler-Bsp.
Weiter mit nützlichem JRechner
Weiter mit lustigem Sinus-Demo