Das NetBeans-Lookup für Nicht-RCP-Projekte benutzen

Im Artikel Java-Aktionen richtig benutzen überlegte ich, wie Java-Actions ihre Daten erhalten und sich aktivieren oder deaktivieren abhängig davon, ob es relevante Daten gibt. Ich wollte nicht Swing-Components nach Daten befragen, die Actions dort als Listener registrieren und auf Statusänderungen des GUI reagieren. Als Lösung schlug ich Lookups vor – Container mit Elementen, die Beobachter benachrichtigen, falls sich ihr Inhalt ändert.

Ein solches Lookup benutzen NetBeans Platform-Anwendungen, es kann in Nicht-Platform-Anwendungen eingesetzt werden. Die Projekte integrieren dazu das JAR org-openide-util-lookup.jar, es ist unterhalb des NetBeans-Installationsverzeichnisses im Verzeichnis platform/lib.

Zugrunde liegende Idee

  • Ein Anwendungsmodul hat einen oder mehrere Lookups
  • Wird im GUI etwas ausgewählt oder abgewählt, gelangen relevante Daten, die das GUI-Element repräsentiert, in eines der Lookups
  • Die Actions beobachten das Lookup: Ist es leer, sind sie deaktiviert, enthält es Elemente, sind sie aktiviert. In actionPerformed() benutzen Actions die Elemente des Lookups.

Das NetBeans-Lookup benachrichtigt Beobachter nur, falls Daten eine bestimmten Typs im Lookup sind: Eine Action, die Personen in eine Datenbank speichert, will in der Regel nicht wissen, ob und wieviele Bücher im Lookup sind.

Beispiels-Implementierung

Ich habe ein Beispiels-Projekt implementiert und in ein ZIP-Archiv gepackt. Nach dem Entpacken kann es mit NetBeans geöffnet werden. Das Kontextemenü des Projects-Window bietet Run oder Debug an (auf die Klasse Main mit der rechten Maustaste klicken).

(Nur) Der Übersichtlichkeit wegen verzichtete ich bei den Beispielen auf Parameterüberprüfung, in „Produktions-Code“ sollten die Parameter überprüft werden, beispielsweise eine NullPointerException geworfen, falls null überreicht wird anstelle einer erwarteten Objektreferenz.

LookupAction

Die LookupAction habe ich kopiert aus dem Projekt JPhotoTagger. LookupActions beobachten einen Lookup. Bei Aufruf von actionPerformed() ruft die LookupAction bei den spezialisierten Actions die abstrakte Methode actionPerformed(Collection<? extends T> lookupContent) auf, mit isEnabled(Collection<? extends T> lookupContent) können sie entscheiden, ob sie aktiviert sein sollen, in der Regel werden sie true liefern, falls der Lookup-Content Elemente enthält.

public abstract class LookupAction<T> extends AbstractAction implements LookupListener {

    private final Class<? extends T> lookupResultClass;
    private final Lookup lookup;
    private Lookup.Result<? extends T> lookupResult;

    protected LookupAction(Class<? extends T> lookupResultClass, Lookup lookup) {
        this.lookupResultClass = lookupResultClass;
        this.lookup = lookup;
        setLookupResult();
    }

    protected abstract boolean isEnabled(Collection<? extends T> lookupContent);

    protected abstract void actionPerformed(Collection<? extends T> lookupContent);

    private void setLookupResult() {
        if (lookupResult == null) {
            lookupResult = lookup.lookupResult(lookupResultClass);
            lookupResult.addLookupListener(this);
            resultChanged(null);
        }
    }

    @Override
    public void resultChanged(LookupEvent evt) {
        setEnabledInDispatchThread();
    }

    private void setEnabledInDispatchThread() {
        final boolean isEnabled = isEnabled(lookupResult.allInstances());

        if (EventQueue.isDispatchThread()) {
            setEnabled(isEnabled);
        } else {
            EventQueue.invokeLater(new Runnable() {

                @Override
                public void run() {
                    setEnabled(isEnabled);
                }
            });
        }
    }

    protected Collection<? extends T> getLookupContent() {
        return (lookupResult == null)
                ? Collections.<T>emptyList()
                : lookupResult.allInstances();
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        setLookupResult();
        actionPerformed(lookupResult.allInstances());
    }
}

Eigenes Lookup

Die Klasse ModifiableLookup liefert ein Lookup und bietet Methoden an, dessen Inhalt zu modifizieren.

public final class ModifiableLookup implements Lookup.Provider {

    private final InstanceContent content = new InstanceContent();
    private final Lookup lookup = new AbstractLookup(content);

    @Override
    public Lookup getLookup() {
        return lookup;
    }

    public void add(Object content) {
        this.content.add(content);
    }

    public void remove(Object content) {
        this.content.remove(content);
    }

    public void set(Collection<?> content) {
        this.content.set(content, null);
    }
}

Beispiel: Ausgewählte Listenelemente in das Lookup einfügen

Will ich ausgewählte Items einer JList in das Lookup einfügen, kann ich folgende Klasse benutzen:

public final class LookupListSelectionListener implements ListSelectionListener {

    private final ModifiableLookup lookup;

    public LookupListSelectionListener(ModifiableLookup lookup) {
        this.lookup = lookup;
    }

    @Override
    public void valueChanged(ListSelectionEvent e) {
        if (!e.getValueIsAdjusting()) {
            JList list = (JList) e.getSource();
            List<Object> selectedValues = Arrays.asList(list.getSelectedValues());
            lookup.set(selectedValues);
        }
    }
}

Beispielsklassen, die in das Lookup sollen

public final class Human {

    private final String name;

    public Human(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return name;
    }
}
public class Dog {

    private final String name;

    public Dog(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return name + " (Wuff)";
    }
}
public final class CreatureListModel extends DefaultListModel {

    private static final long serialVersionUID = 1L;

    public CreatureListModel() {
        addElement(new Human("Anton"));
        addElement(new Human("Berta"));
        addElement(new Human("Cäsar"));
        addElement(new Human("Dora"));
        addElement(new Human("Emil"));
        addElement(new Dog("Fiffi"));
        addElement(new Dog("Waldi"));
    }
}

Kommunikation über ein zentrales Lookup

Ein Anwendungsmodul kann Lookups beispielsweise so bereitstellen:

public final class Lookups {

    public static final Lookups INSTANCE = new Lookups();
    private final ModifiableLookup humansLookup = new ModifiableLookup();

    public ModifiableLookup getHumansLookup() {
        return humansLookup;
    }

    private Lookups() {
    }
}

Erweiterte LookupAction

Die erweiterte LookupAction interessiert sich (nur) für Instanzen von Human

public final class ShowHumansInLookupAction extends LookupAction<Human> {

    private static final long serialVersionUID = 1L;

    public ShowHumansInLookupAction() {
        super(Human.class, Lookups.INSTANCE.getHumansLookup().getLookup());
        putValue(Action.NAME, "Zeige ausgewählte Menschen");
    }

    @Override
    protected boolean isEnabled(Collection<? extends Human> humans) {
        return !humans.isEmpty();
    }

    @Override
    protected void actionPerformed(final Collection<? extends Human> humans) {
        JOptionPane.showMessageDialog(null, humans);
    }
}

In einem GUI benutzen

Die entscheidenden Zeilen in einem GUI, die Action ist einem Button zugeordnet, Model und ListSelectionListener der Liste:

    list.setModel(new CreatureListModel());
    list.addListSelectionListener(new LookupListSelectionListener(Lookups.INSTANCE.getHumansLookup()));
    button.setAction(new ShowHumansInLookupAction());

Worauf achten beim Ausführen der Beispielsanwendung

  • Ist nicht ausgewählt, setzt sich die Action automatisch auf deaktiviert, der Button mit der Action ist deaktiviert („ausgegraut“)
  • Werden ein oder mehrere Menschen ausgewählt, ist die Action aktiviert, der Button reagiert auf Klicks (Betätigung)
  • Sind nur Hunde ausgewählt, ist der Button deaktiviert
  • Die Action erhält nur Menschen: Sind Hunde und Menschen ausgewählt, zeigt bei Klick auf den Button die Action den Lookup-Content an – es sind nur die erwarteten Objekte darin

Vorteile

  • Die Action ist nicht an ein GUI gebunden (sie benötigt nur ein Lookup). Sie ist nicht anzupassen, falls später beispielsweise ein JTree die Geschöpfe anzeigt (getrennt nach Mensch und Tier). Es könnte auch ein Dialog zum Ändern der Daten eines Menschen benutzt werden: Der „Mensch im Dialog“ gelangt in das Lookup, das die Action benutzt.
  • Die Action fragt nicht nach den benötigten Daten, sie erhält sie
  • Die Action aktiviert und deaktiviert sich automatisch
  • Das Lookup könnte über Modulgrenzen hinweg benutzt werden, beispielsweise über das Java-Service Provider Interface (SPI) oder ein globales Lookup, wie es die NetBeans-Platform anbietet. Die Actions anderer Module sind nur mit dem passenden Lookup zu verknüpfen, sie müssen nicht wissen, woher die Daten kommen (wie sie an diese gelangen).
  • Actions sind nur ein Beispiel, es können beliebige Klassen LookupListener sein

Stichwörter: ,

Zu diesem Artikel können keine Kommentare mehr geschrieben werden.