Weitere aktuelle Java-Titel finden Sie bei dpunkt.
 Inhaltsverzeichnis   Auf Ebene Zurück   Seite Zurück   Seite Vor   Auf Ebene Vor   Eine Ebene höher   Index


8.6.1

Model-View-Controller



Das Model-View-Controller-Konzept wird in vielen Bereichen moderner Softwareentwicklung eingesetzt und bedeutet die strikte Aufgabenverteilung bei einer Anwendung. So wird als Model die Datenquelle bezeichnet, die Daten unabhängig vom Erscheinungsbild liefert (also beispielsweise aus einer relationalen Datenbank). Die View zeigt diese Daten dann in passender Art und Weise an (z.B. als Tabelle in einer Java-Applikation) - bestimmt durch den »Look«. Wie diese View die Daten anzeigt wird nicht vom Model beeinflusst.

Abbildung 8.32: Das Model-View-Controller-Prinzip
Abbildung 8.32

Der Controller kümmert sich um die Interaktion mit dem Benutzer (das Verhalten der Komponente). Wird beispielsweise in der angezeigten Tabelle ein Wert geändert, so teilt der Controller dies dem Model mit, welches wiederum die View darüber informiert - der neue Wert wird angezeigt. Der Controller ist also die Logik der Anwendung.

Der Vorteil dieser Aufgabenverteilung ist zum einen die Möglichkeit der Aufteilung in logische, unabhängige Klassen, wie auch die Möglichkeit, jeden der drei Teile jederzeit auszutauschen. Hierdurch ist bei Swing der Austausch des Look-and-Feels zur Laufzeit möglich.

Zusätzlich können Spezialisten für die einzelnen Teilaufgaben eingesetzt werden - der Designer entwickelt ein eigenes Look-and-Feel, während sich der Datenbankprogrammierer um das Model kümmert.

Model-View-Controller bei Swing

Bei Swing werden die View und der Controller zusammengefasst als so genantes UI Delegate. Die Klasse javax.swing.plaf.ComponentUI besitzt dazu Unterklassen für die unterschiedlichen Oberflächenelemente. Diese Klasse kümmert sich neben dem Aussehen um das Verhalten der Oberfläche, so dass beispielsweise Windows-typische Tastenkürzel beim Windows-Look-and-Feel die gleiche Reaktion hervorrufen.

Viele Swing-Komponenten besitzen ein oder mehrere Models, die der Programmierer erweitern kann. Eine typische Aufgabenstellung ist hierbei die Anbindung einer Datenbank für die Lieferung der Daten.

Zu diesem Zweck besitzen alle Datenmodelle eine Methode, um die Anzahl der Daten zu bekommen und eine Methode, um ein bestimmtes Datum (z.B. über eine Indexangabe) zu liefern.

Zudem ist allen Models gemeinsam, dass die Hauptkomponente sich bei dem Model als Listener registriert. Damit hat das Model die Möglichkeit, bei Veränderung die Komponente darüber zu benachrichtigen. Die Komponente wird dann dazu passend die Oberfläche neu zeichnen und die benötigten Daten aus dem Model holen.

Neben der reinen Datenquelle kann ein Modell auch manipulierte Daten liefern. Dies könnte beispielsweise durch eine Sortierung oder ein Ausblenden von bestimmten Daten geschehen.

Als Beispiel soll ein ComboBoxModel dienen, bei denen man eine Auswahl der Daten über einen Suchstring vornehmen kann. Wenn hier die Methode setSearchString(String searchString) aufgerufen wird, soll das Model nur die Datenelemente zeigen, die mit diesem String anfangen.

  class SuchstringComboBoxModel extends 
                                  DefaultComboBoxModel {
   // Suchstring, mit dem alle 
   // Elemente anfangen müssen
   private String searchString = "";

   /**
    * Konstruktor
    */
   public SearchStringComboBoxModel(String[] textStrings) {
     // String-Array wird in der 
     // geerbten Klasse gespeichert
     super(textStrings);
   }

   public void setSearchString(String searchString) {
     this.searchString = searchString;
     // Die ComboBox benachrichtigen, 
     // dass sich die Daten komplett geändert haben
     fireContentsChanged(this, 0, getSize()); 
   }

   /**
    * Liefert die Anzahl der Werte, die mit 
    * suchString anfangen
    */
   public int getSize() {
     int size = 0;
     for (int i = 0; i < super.getSize(); i++)
       if (super.getElementAt(i).toString().
                 startsWith(searchString))
          ++size;
     return size;
   }

   /**
    * Liefert das Element mit dem angegebenen 
    * Index aus der Menge der Elemente, die 
    * mit searchString anfangen
    */
   public Object getElementAt(int index) {
     for (int i = 0; i < super.getSize(); i++)
         if (super.getElementAt(i).toString().
                   startsWith(searchString))
            if (index-- == 0)
               return super.getElementAt(i);
     return null;
   }
  }

Die Benachrichtigung über die Änderungen sollte möglichst nur die Daten betreffen, die sich wirklich geändert haben. Zu diesem Zweck gibt es in den meisten Models Methoden, die beispielsweise den Bereich angeben, in der sich geänderte Daten befinden. Dadurch kann die Oberfläche selbst entscheiden, ob sie neu gezeichnet werden muss oder nicht. Dies steigert in vielen Fällen die Performance (z.B. bei großen Tabellen).

Renderer

Bei den Komponenten JList, JTree und JTable ist es möglich, das Aussehen einer Zelle selbst zu bestimmen. Sind die dargestellten Daten beispielsweise Strings (etwa ein Name), so kann eine Zelle mit einem JLabel dargestellt werden, bei dem der angezeigte Text dem String entspricht.

Die ganze Sache verhält sich natürlich anders, wenn die dargestellten Daten keine Strings sind, sondern z.B. eigene Klassen. Wie soll die Oberflächenkomponente ein Objekt der Klasse Automobil darstellen?

Für diesen Fall lässt sich ein eigener, so genannter Renderer schreiben, der das Automobil beispielsweise mit einem Foto, der Automarke und dem Modell darstellt. Grundsätzlich muss ein Renderer lediglich ein Exemplar von Component sein, so dass selbst Konstrukte wie ein Baum in einer Zelle einer Tabelle möglich sind.

Der Standard-Renderer der meisten Swing-Komponenten verwendet ein JLabel zum Anzeigen, da jedes Objekt eine String-Repräsentation besitzt (über die toString()-Methode von der Klasse Object). Eine JTable besitzt aber beispielsweise noch einen entsprechenden Renderer für Icons, Boolean-Exemplare und Unterklassen von Number.

Um einen eigenen Renderer zu schreiben, ist es am einfachsten, die Standardimplementierung zu überschreiben. Am Beispiel der JList ist dies DefaultListCellRenderer. Diese Klasse ist von JLabel abgeleitet.

Das in diesem Beispiel zugrundeliegeende Model liefert immer Exemplare der eigenen Klasse Autombil.

  public Component getListCellRendererComponent(
          JList list, Object value, int index, 
          boolean isSelected, boolean cellHasFocus) {
    // Erstmal das JLabel der Super-Klasse nehmen
    JLabel label = (JLabel) 
      super.getListCellRendererComponent(
            list, value, index, isSelected, cellHasFocus);
    // Nun das Label gemäß der Klasse
    // Automobil anpassen
    Automobil auto = (Automobil) value;
    label.setText(auto.getMarke()+": "+auto.getModell());
    label.setIcon(auto.getBild());
    return label;
  }

Wer die eigene Implementierung eines Renderers bevorzugt, beispielsweise weil die Anzeige nur durch eine komplexere Komponente sinnvoll ist, muss hier das Interface ListCellRenderer implementieren.

In diesem Beispiel möchten wir eine schönere Ausgabe erzielen. Damit dies möglich ist, muss eine Zelle mittels JPanel in zwei Bereiche geteilt werden: Links eine Grafik, rechts der Text mehrzeilig.

Abbildung 8.33: Eigenener Renderer
Abbildung 8.33

Ein Renderer wird sehr häufig aufgerufen (beispielsweise wenn gleichzeitig 20 Einträge gezeigt werden sollen). Daher sollte man darauf achten, dass die Komponenten nicht lange zum Anzeigen benötigen und dass der Renderer beim Aufruf seiner Render-Methode keine überflüssigen Objekte erzeugt.

Wenn man den Renderer selbst implementiert, sollte man zudem die korrekte Farbgebung beachten. Die Komponente, die den Renderer anzeigt, hat dafür die Methoden getForeground(), getSelectionForeground(), getBackground() und getSelectionBackground(). Zusätzlich sollte man sich um den Fokus kümmern. Dieser wird durch einen Rahmen gezeichnet, den man beispielsweise bei einer JList über die Methode getBorder("List.focusCellHighlightBorder") der Klasse UIManager ermitteln kann. Damit die Größe konstant bleibt, auch wenn das gerenderte Objekt keinen Focus besitzt, wird ansonsten ein leerer Rahmen gezeichnet.

  class AutoMobilListCellRenderer extends JPanel 
    implements ListCellRenderer {
    private JLabel image = new JLabel();
    private JLabel text = new JLabel("<html></html>");
    private final static Border NO_FOCUS_BORDER = 
      new EmptyBorder(1, 1, 1, 1);

    AutoMobilListCellRenderer() {
      setLayout(new BorderLayout());
      add(image, BorderLayout.WEST);
      add(text, BorderLayout.CENTER);

      text.setOpaque(false);
      image.setOpaque(false);
    }

    public Component getListCellRendererComponent(
             JList list, Object value, int index, 
             boolean isSelected, boolean cellHasFocus) {
      if (value != null && value instanceof AutoMobil) {
         AutoMobil auto = (AutoMobil) value;
         image.setIcon(auto.getBild());
         text.setText("<html> <b>Marke</b>: "+
                      "auto.getMarke()+"<br> "+
                      "<b>Modell</b>: "+
                      auto.getModell()+
                      "</html>");
      } else {
        image.setIcon(null);
        text.setText("<html></html>");
      }
      setForeground(isSelected ? 
           list.getSelectionForeground() : 
           list.getForeground());
      setBackground(isSelected ? 
           list.getSelectionBackground() : 
           list.getBackground());
      setBorder((cellHasFocus) ? 
           UIManager.getBorder(          
                  "List.focusCellHighlightBorder") : 
           NO_FOCUS_BORDER);
      return this;
    }
  }

Material zum Beispiel

Editoren

Wie der Renderer muss ein ensprechender Editor in Abhängigkeit zum Datum definiert werden. Während die Standardimplementierung bei einer JList nur das Editieren eines Strings erlaubt, muss zum Editieren eines Automobils eine komplexere Oberfläche geliefert werden.

Ähnlich wie bei den Renderern definiert ein Interface die Schnittstelle zwischen dem Oberflächenelement und der Editorkomponente.

Typischerweise ist der Ablauf wie folgt:

Die unterschiedlichen Komponenten unterscheiden sich sehr stark in der Art, wie sie mit ihren Editoren kommunizieren, so dass dies in der entsprechenden Klasse nachgelesen werden sollte.

Ein Beispiel für einen Tabellen-Editor befindet sich auf der beiliegenden CD. Die folgenden Methoden sind durch das Interface TableCellEditor definiert.

Die Registrierung eines Editors geschieht bei einer JTable typischerweise über eine anzugebende Klasse. Soll eine Zelle editiert werden, wird anhand der Klasse des Wertes der passende Editor gesucht.

  /**
   * Liefert die Komponente, die zum Editieren 
   * benutzt werden soll.
   */
  public Component getTableCellEditorComponent(
       JTable table, Object value, boolean isSelected, 
       int row, int column);

  /**
   * Über diese Methode registriert sich die 
   * Tabelle beim Editor. Über das 
   * Listener-Interface wird der Tabelle dann
   * auch das Ende des Editiervorganges 
   * mitgeteilt.
   */
  public void addCellEditorListener(
                   CellEditorListener l);

  /**
   * Wird von der Tabelle aufgerufen, wenn der 
   * Editiervorgang abgebrochen werden soll (z.B.
   * durch einen Klick auf eine andere Zelle.
  */
  public void cancelCellEditing();

  /**
   * Wenn der Editiervorgang erfolgreich abgeschlossen 
   * werden konnte, wird über diese Methode der neue
   * Wert abgefragt.
   */
  public Object getCellEditorValue(); 

  /**
   * Vor dem Editiervorgang fragt die Tabelle, ob 
   * die Zelle über dieses Event editierbar sein
   * soll.
   */
  public boolean isCellEditable(EventObject anEvent);

  /**
   * Hier deregistriert sich die Tabelle ggf.
   */
  public void removeCellEditorListener(
                   CellEditorListener l);

  /**
   * Gibt an, ob die Zelle zum Editieren vorher 
   * selektiert sein soll.
   */
  public boolean shouldSelectCell(EventObject anEvent);

  /**
   * Wenn der Editiervorgang von aussen gesteuert
   * planmäßig gestoppt werden soll, wird diese
   * Methode aufgerufen. Der Rückgabewert sagt
   * aus, ob der Stoppvorgang erfolgreich 
   * durchgeführt werden konnte.
   */
  public boolean stopCellEditing();


 Inhaltsverzeichnis   Auf Ebene Zurück   Seite Zurück   Seite Vor   Auf Ebene Vor   Eine Ebene höher   Index

Copyright © 2002 dpunkt.Verlag, Heidelberg. Alle Rechte vorbehalten.