Zeitsparende UI-Erstellung

Formulare kommen in Business-Applikationen sehr häufig vor und werden von Benutzern stärker wahrgenommen als die im Backend ablaufende Funktionalität. Es ist daher wichtig, dass sie optisch ansprechend und möglichst einfach auszufüllen sind.

Aus Unternehmens- und Entwicklersicht ist es wiederum von Vorteil, wenn Formulare möglichst zeitsparend und einfach, aber trotzdem mit gleichbleibend hoher Qualität implementiert werden können. Dafür habe ich für Cenarion Information Systems auf Basis von GWT UI-Komponenten entwickelt, die nicht grundlegend neue Funktionalität anbieten sollen (so wie es viele Widget-Libraries tun), sondern vorhandene Grundelemente zusammenfassen und deren Verwendung vereinfachen.

Das übergeordnete Element ist das Interface IFormElement.

public interface IFormElement<T> {
    public T getValue();
    public void setValue(T value);
    public void setVisible(boolean visible);
    public void setEnabled(boolean enabled);
    public void setLabelText(String text);
    public boolean isEnabled();
}

Die implementierenden, spezialisierten FormElements stellen dabei im UI jeweils ein Label mit einem (oder im Fall von Radiobuttons/Checkboxen) mehreren) Eingabefeld(er) dar. Jedes dieser Elemente kann mit Standard-Einstellungen zu CSS-Styles, Validierung und Darstellung (Zahlen, Daten) mit nur einer einzigen Zeile im UI-Binder hinzugefügt werden.

<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder" xmlns:g="urn:import:com.google.gwt.user.client.ui" xmlns:cen="urn:import:com.cenarion.client.nexaplus.ui.components">

    <g:HTMLPanel>
        <cen:TextFormElement ui:field="standardText" caption="Standard Textfield" widgetId="standardText" />
    </g:HTMLPanel>
</ui:UiBinder>

Ist das nicht ausreichend, können über das Builder-Pattern sehr einfach einer oder mehrere der Parameter individuell angepasst werden. Eine Initialisierung rein über den UI-Binder ist hier nicht möglich, da dort nur primitive Datentypen bzw. Strings, aber keine Objekte übergeben werden können.

Im UI-Binder wird in diesem Fall außer dem Feldnamen nichts mehr initialisiert:

<g:HTMLPanel>
        <cen:TextFormElement ui:field="customizedText" />
</g:HTMLPanel>

Auf jeden Fall anzugebende Parameter sind beim Builder über den Konstruktor vorgeschrieben, optionale Parameter können über Setter gesetzt werden. Die Anzahl von nötigen Konstruktoren wird dadurch verringert – alle (optionalen) Parameter, die nicht explizit gesetzt werden, werden automatisch mit den jeweiligen Standardwerten initialisiert.

public class TestClass {
    // final ist nicht zwingend nötig, man kann damit aber nicht vergessen, die Komponenten zu initialisieren.
    @UiField(provided=true) final TextFormElement customizedText;

    public TestClass() {
        // ... Validator u. Style erstellen ...
        customizedText = new TextFormElementBuilder("customizedText", "Caption").setStyle(style).setValidator(validator).build();

        // erst wenn jedes mit @UiField gesetzte Element initialisiert wurde (entweder über UI-Binder oder manuell), kann das UI initialisiert werden.
        initWidget(uiBinder.createAndBindUi(this));
    }
}

Die Implementierung des TextFormElement (etwas vereinfacht):

public class TextFormElement extends ValidatedFormElement<String> implements HasBlurHandlers {

    private static final Binder binder = GWT.create(Binder.class);
    interface Binder extends UiBinder<Widget, TextFormElement> { }

    @UiField
    TextBox text;

    /**
     * Verbindungsstück zwischen dem Validierungs-Feedback und der Komponente selbst (erhöht die Kapselung von Feedback).
     */
    private IValueCommunicator<String> valueCommunicator = new IValueCommunicator<String>() {

        @Override
        public void setValue(String value) {
            text.setText(value);
        }

        @Override
        public String getValueOrThrow() throws ParseException {
            return text.getValueOrThrow();
        }
    };

    /**
     * Im Feedback werden Validierungsfehler angezeigt, sobald auf dem TextFormElement ein onBlur-Event ausgelöst wurde und der Inhalt
     * nicht den Vorgaben des Validators entspricht. Dabei kann ein Feedback-Feld auch von mehreren FormElements geteilt werden (etwa
     * um alle Validierungsfehler gesammelt am Ende des Formulars anzuzeigen).
     */
    private TextFormElement(String widgetId, String caption, IStyle style, boolean labelVisible, IValidator<String> validator, Feedback feedback) {
        initWidget(binder.createAndBindUi(this));

        super.init(widgetId, caption, text, feedback,
                style == null ? new NoStyle() : style, labelVisible,
                validator == null ? new StringValidator() : validator,
                valueCommunicator);

        this.text.addBlurHandler(new BlurHandler() {
            @Override
            public void onBlur(BlurEvent event) {
                TextFormElement.this.doValidate(text);
            }
        });
    }

    /**
     * Default-Konstruktor, wird vom UI-Binder verwendet. Alle Parameter müssen zwingend im entsprechenden ui.xml angegeben werden.
     */
    public @UiConstructor TextFormElement(String widgetId, String caption) {
        this(widgetId, caption, null, true, null, null);
    }

    /**
     * Konstruktor, der auf Grundlage des Builders das TextFormElement initialisiert.
     */
    public <T extends ValidatedFormElementBuilder<String, T>> TextFormElement(T builder) {
        this(builder.getWidgetId(), builder.getCaption(), builder.getStyle(), builder.isLabelVisible(), builder.getValidator(), builder.getFeedback());
    }

    @Override
    public void setEnabled(boolean enabled) {
        this.text.setEnabled(enabled);
    }

    @Override
    public boolean isEnabled() {
        return this.text.isEnabled();
    }

    @Override
    public HandlerRegistration addBlurHandler(BlurHandler handler) {
        return this.text.addHandler(handler, BlurEvent.getType());
    }

    /**
     * Die Komponente unterstützt das Editor-Framework.
     */
    @Override
    public LeafValueEditor<String> asEditor() {
        return text.asEditor();
    }

    @Override
    public void setValue(String value) {
        this.text.setValue(value);
        super.doValidate(text);
    }

}

Beim Validieren werden etwaige Fehler vom Validator als Set von Strings zurückgegeben.

public abstract class ValidatedFormElement<T> extends FormElement<T> implements IsEditor<LeafValueEditor<T>> {
    //...

    private Set<String> validate() {
        return validator.validate(communicator);
    }

    protected void doValidate(Widget widget) {
        Set<String> validationErrors = validate();
        if (!validationErrors.isEmpty()) {
            feedback.setFeedback(widget, validationErrors);
        } else {
            feedback.removeFeedback(widget);
        }
    }

    //...
}

Mögliche Fehler bei einem TextFormElement können z.B. Übertreten einer minimalen/maximalen Eingabelänge sein, bei einem IntegerFormElementEingaben von Text oder Eingaben, die über den Wertebereich von Integer hinausgehen.

Einige FormElements (Radiobuttons, Listboxen etc.) unterstützen zusätzlich die Verwendung von generischen Typen, d.h. man kann die von der Serverseite gelieferten Proxies direkt in das FormElement einfüllen und selektierte Werte direkt auslesen, ohne sich um ein Mapping kümmern zu müssen.

Generell erreicht man durch das Verwenden dieser FormElements und durch das damit verbundene automatische Festlegen von Default-Werten eine größere Einheitlichkeit des UIs auch bei mehreren Entwicklern (sowohl optisch als auch im Code selbst), und kann Implementierungseigenheiten und/oder Bugs des verwendeten Frameworks (hier GWT) für den täglichen Gebrauch verstecken.

Lisi Blümelhuber
(Software Developer)