Seiten

Sonntag, 7. Juli 2013

Vaadin: Markierte Zeile einer Tabelle editierbar machen

Hier ein kleines Tutorial über den Umgang mit Vaadin-Tabellen und deren Editierbarkeit. Der Anwendungsfall (aus der Praxis) ist simpel:

Eine Tabelle enthält Entitäten des Typs Parameter. Ein Parameter besteht aus dem Parameter-Namen, einer Beschreibung, einem Standard-Wert und einem benutzerdefinierten Wert (welcher den Standard-Wert ersetzt). Die Vaadin-Tabelle enthält einen Container mit Parameter-Beans und besteht somit aus den vier Spalten Name, Beschreibung, Standard-Wert, benutzerdefinierter Wert. Name, Beschreibung und Standard-Wert sind fix, während der benutzerdefinierte Wert editierbar sein soll.

Ziel


Um Vaadin-Tabellen editierbar zu machen existiert die Methode setEditable(boolean b). Wird dieser ein true übergeben wird die gesamte Tabelle editierbar. Sprich: Alle Zellen werden zu Textfeldern. Oftmals ist dies natürlich nicht gewünscht da beispielsweise bestimmte Spalten nicht editierbar sein sollen (wie in unserem Fall). Um das Verhalten der Tabelle den eigenen Wünschen anzupassen muss man die von der Tabelle verwendete TableFieldFactory überschreiben (standardmäßig wird hier eine Instanz der DefaultFieldFactory verwendet, welche sämtliche Zellen der Tabelle editierbar macht). An dieser Stelle möchte ich etwas vorgreifen und schonmal die Codestelle zeigen, in welcher der Tabelle die neu implementierte FieldFactory zugewiesen wird. Die entsprechende Implementierung unserer eigenen TableFieldFactory folgt dann im Anschluss.

parametersTable.addItemClickListener(new ItemClickEvent.ItemClickListener() {
            @Override
            public void itemClick(ItemClickEvent itemClickEvent) {
                final CrawlerParameter crawlerParameter = (CrawlerParameter) itemClickEvent.getItemId();
                if(crawlerParameter == null){
                    buttonLeiste.setVisible(false);
                    parametersTable.setEditable(false);
                } else {
                    buttonLeiste.setVisible(true);
                    parametersTable.setTableFieldFactory(new EditSelectedParamFieldFactory(crawlerParameter));
                    parametersTable.setEditable(true);
                }
            }
        });
Der Tabelle (parametersTable) wird ein ItemClickListener hinzugefügt welcher die itemClick()-Methode ausführt wenn eine Zeile der Tabelle durch den Benutzer markiert wurde. Innerhalb dieser Methode wird zuerst der vom Benutzer ausgewählte Parameter in einer entsprechenden Variable gespeichert. Achtung: Den aktuellen Parameter bekommt man nicht via parametersTable.getValue()! Dies liefert nicht den aktuell angeklickten Parameter, sondern den zuvor ausgewählten (Bei Benutzung dieser Methode ist man also immer genau einen Schritt hinterher). Der aktuell ausgewählte Parameter ist per getValue() erst NACH Durchführung der itemClick()-Methode abrufbar. Anschließend wird überprüft ob überhaupt ein Parameter ausgewählt ist. Ist dies nicht der Fall soll die Tabelle auch nicht editierbar sein, andernfalls bekommt die Tabelle eine neue FieldFactory - nämlich unsere eigene Implementation. Diese bekommt als Parameter den angeklickten Parameter übergeben (die Implementation folgt gleich). Anschließend wird die Tabelle editierbar gemacht (per setEditable(true)). Nun ist die Spalte BenutzerdefWert der ausgewählten Zeile editierbar.

Lösen wir das Rätsel und schauen uns die EditSelectedParamFieldFactory-Implementation an:

public class EditSelectedParamFieldFactory extends AbstractFieldFactory {

    private CrawlerParameter selectedCrawlerParameter;

    public EditSelectedParamFieldFactory(final CrawlerParameter selectedCrawlerParameter){
        this.selectedCrawlerParameter = selectedCrawlerParameter;
    }
    @Override
    public Field<?> createField(Container container, Object aParameter, Object spaltenname, Component uiContext) {
        CrawlerParameter parameter = (CrawlerParameter) aParameter;
        switch(spaltenname.toString()){
            case NAME:
                return null;
            case DESCR:
                return null;
            case DEFAULT_VALUE:
                return null;
            case USER_VALUE:
                if(selectedCrawlerParameter != null){
                    if(selectedCrawlerParameter.equals(parameter)){
                        if(parameter.getDefaultValue().getClass() == Boolean.class) {
                            return new CheckBox();
                        }
                        if(parameter.getDefaultValue().getClass() == Integer.class) {
                            final TextField integerTextField = new TextField();
                            configureField(integerTextField, Integer.class);
                            return integerTextField;
                        }
                        if(parameter.getDefaultValue().getClass() == Double.class) {
                            final TextField doubleTextField = new TextField();
                            configureField(doubleTextField, Double.class);
                            return doubleTextField;
                        }
                        if(parameter.getDefaultValue().getClass() == String.class) {
                            final TextField textField = new TextField();
                            configureField(textField, String.class);
                            return textField;
                        }
                    }
                } else {
                    return null;
                }
            default:
                return null;
        }
    }
}


Wie bereits erwähnt, muss ein selbst implementierte FieldFactory das Interface TableFieldFactory implementieren. Warum erweitert unsere Klasse also AbstractFieldFactory anstatt TableFieldFactory zu implementieren? Ganz einfach: Die Klasse AbstractFieldFactory ist abstrakt und implementiert das TableFieldFactory-Interface (zusätzlich wird in ihr die (für uns nun nicht relevante) Methode configureField() implementiert, welche die zu erstellenden Felder je nach enthaltenem Datentyp mit entsprechenden Validatoren etc. versieht). Wir implementieren also die Methode  public Field<?> createField(Container container, Object itemId, Object propertyId, Component uiContext) des TableFieldFactory-Interfaces. Außerdem besitzt die Klasse ein Attribut, welches den aktuell ausgewählten Parameter speichert. Für jede Zelle der Tabelle wird dann die Methode durchlaufen. Innerhalb der Methode wird bei jedem Durchlauf zunächst geprüft in welcher Zeile (CrawlerParameter parameter) und in welcher Spalte (switch(spaltenname.toString()) wir uns befinden. Befinden wir uns in einer Zelle der Spalten Name, Beschreibung oder StandardWert ist eine Prüfung der Zeile unnötig, da Zellen dieser Spalten eh nicht editierbar sein sollen, egal ob es sich um die ausgewählte Zeile handelt oder nicht. Daher wird in diesen Fällen ein null-Wert zurückgegeben --> Die Zelle ist nicht editierbar. Falls wir uns in der BenutzerdefWert-Spalte befinden wird überprüft ob es sich um die vom Benutzer ausgewählte Zeile handelt. Falls nicht, wird ebenfalls ein null zurückgegeben. Falls doch wird überprüft welchen Datentyp der Parameter besitzt (ist es ein boolescher Parameter, ein Integer-Parameter, ...?). Anschließend wird ein entsprechendes TextFeld (oder eine CheckBox bei einem booleschen Parameter) erstellt und mit entsprechenden Convertern/Validatoren etc. versehen. Anschließend wird das TextFeld bzw. die CheckBox zurückgegeben. Diese Zelle ist also nun editierbar. Damit haben wir das Ziel erreicht: Nur der benutzerdefinierte Wert des aktuell ausgewählten Parameters ist editierbar.

Zusammenfassung
Soll nur die aktuell ausgewählte Zeile (bzw. Teile davon) der Tabelle editierbar sein so muss folgendes getan werden:

  • Tabelle einen ItemClickListener hinzufügen
  • Im ItemClickListener wird der Tabelle eine Instanz einer selbst erstellten, das Interface TableFieldFactory implementierenden, Klasse zugewiesen (setTableFieldFactory(new MyFieldFactory(currentSelectedEntity)) und die Tabelle auf editable gesetzt (setEditable(true)).
  • In der eigenen Implementierung der TableFieldFactory (welche per Konstruktor-Parameter die selektierte Entität übergeben bekommt) wird überprüft ob es sich um eine Zelle handelt welche sich in der selektierten Zeile (und in einer editierbaren Spalte) befindet. Ist dies der Fall wird eine entsprechende Eingabe-Komponente (z.B. ein Textfeld oder eine CheckBox) erstellt und zurückgegeben (andernfalls wird null zurückgegeben und die Zelle ist nicht editierbar).
Hier werden alle Zellen der Spalte BenutzerdefWert editierbar gemacht, unabhängig von der Selektion