Extending linkki

Aspects

A primary functionality of linkki is the data binding. Most data binding frameworks only concentrate on synchronizing the value between model and UI. linkki can bind more than just the value. In fact, almost every part of the UI state can be taken into account for data binding with linkki. The most common parts of the UI state are for example the enabled state or the visibility. But there are also additional data that may be bound to a component such as the tooltip, available values for selection or CSS class names. For a property, every such UI state part is a so called aspect of the property.

Using an Apsect in a PMO

The following example shows two aspects for the property "name".

Value and enabled aspect
    @UITextField(position = 1, label = "Name", enabled = DYNAMIC)
    public String getName() {
        return partner.getName();
    }

    public void setName(String name) {
        partner.setName(name);
    }

    public boolean isNameEnabled() {
        return partner.getType() == PartnerType.NATURAL_PERSON;
    }

The most important aspect is the value aspect. It is also mandatory for the data binding to function. The value aspect is dynamically determined by the getter and setter methods of the property.

The second aspect that can be seen is the enabled aspect. This state is defined by the method isNameEnabled.

Each aspect has a name. The enabled aspect has the name "enabled" whereas the value aspect has the empty String as name. In general, the state of the aspect "aspect" for property "property" is determined by the method is/get<Property><Aspect> and set<Property><Aspect>.

Aspects can also apply to the whole PMO if it is also bound to a UI component. In this case, the property name is the empty string.

Aspect on PMO class
@BindStyleNames
@UISection(caption = "Partner")
public class PartnerSectionPmo {

    public List<String> getStyleNames() {
        if (partner.getType() == PartnerType.NATURAL_PERSON) {
            return Arrays.asList("naturalperson");
        } else {
            return Collections.emptyList();
        }
    }
}

Creating a New Aspect

In addition to built-in aspects, you can easily create your own. This guide walks you through the process of implementing a simple aspect that allows components to show a dynamically bound tooltip. In this example, you will build the aspect as a standalone annotation that can be used in addition to any BindingDefinition.

A similar aspect is already included in linkki. It is only used as an example due to its simplicity.
AspectDefinition

An aspect definition defines how an aspect is created. To define your own definition you have to implement LinkkiAspectDefinition with the following two methods:

  • initModelUpdate(PropertyDispatcher, ComponentWrapper, Handler): Method that registers a listener to the wrapped UI component which react to changes in the UI. This method is only mandatory if the defined aspect needs to write into the model.

  • createUiUpdater(PropertyDispatcher, ComponentWrapper): Creates a Handler that is triggered when the UI has to be updated.

We want to define an aspect that can either have a static value or read a value from a get<Property>Tooltip() method, depending on a TooltipType enumeration:

BindTooltipAspectDefinition
public class BindTooltipAspectDefinition implements LinkkiAspectDefinition {

    public static final String NAME = "tooltip";

    private final TooltipType tooltipType;

    private final String staticValue;

    public BindTooltipAspectDefinition(TooltipType tooltipType, String staticValue) {
        this.tooltipType = tooltipType;
        this.staticValue = staticValue;
    }

    @Override
    public Handler createUiUpdater(PropertyDispatcher propertyDispatcher, ComponentWrapper componentWrapper) {
        Aspect<String> aspect = createAspect();
        Consumer<String> setter = componentWrapper::setTooltip;
        if (tooltipType == TooltipType.STATIC) {
            setter.accept(propertyDispatcher.pull(aspect));
            return Handler.NOP_HANDLER;
        } else {
            return () -> setter.accept(propertyDispatcher.pull(aspect));
        }
    }

    public Aspect<String> createAspect() {
        if (tooltipType == TooltipType.STATIC) {
            return Aspect.of(NAME, staticValue);
        } else {
            return Aspect.of(NAME);
        }
    }


    public static enum TooltipType {

        STATIC,

        /**
         * Tooltip is bound to the property using the method get&lt;PropertyName&gt;Tooltip().
         */
        DYNAMIC;
    }

}
  • initModelUpdate is not needed in this example because the tooltip won’t change in the model upon UI change.

  • In the method createUiUpdater, the propertyDispatcher is asked for the tooltip value which is then set in the component using ComponentWrapper#setTooltip. The propertyDispatcher needs an aspect to know which value it should retrieve, and how. This aspect is created in the method createAspect().

  • createAspect() creates the aspect depending on the values given in the annotation.

Create a New Aspect Annotation

We first create a new annotation that later links the annotated component to our aspect definition:

@Retention(RetentionPolicy.RUNTIME)
@Target(value = { ElementType.METHOD })
public @interface BindTooltip {

    TooltipType tooltipType() default TooltipType.STATIC;

    String value() default "";

}
  • The annotation must have the retention policy RetentionPolicy#RUNTIME to be able to be discovered by linkki at runtime.

  • The attribute tooltipType determines whether the tooltip is static text or is provided by a method dynamically.

  • If the tooltipType is BindTooltipType#STATIC, the attribute value allows the user to define the content of the tooltip.

Using the name value for an annotation’s attribute allows users to omit the attribute name if it is the only attribute: @BindTooltip("My tooltip") instead of @BindTooltip(value="My tooltip")

@LinkkiAspect Annotation

Lastly, BindTooltip has to be annotated with @LinkkiAspect to tell linkki how to create the aspect from the annotation. The value for that annotation is a class implementing AspectDefinitionCreator<BindTooltip> that can be created as an inner class:

@LinkkiAspect(BindTooltipAspectDefintionCreator.class)
public @interface BindTooltip {
    class BindTooltipAspectDefintionCreator implements AspectDefinitionCreator<BindTooltip> {

        @Override
        public LinkkiAspectDefinition create(BindTooltip annotation) {
            return new BindTooltipAspectDefinition(annotation.tooltipType(), annotation.value());
        }
    }

Inheritance

Aspects are defined in annotations. In general, annotations on methods are not inherited. Thus, an aspect annotation has to be annotated again for a overridden method, if the aspect should also be effective in the subclass.

linkki provides a meta annotation for aspect annotations that can be annotated on classes and interfaces to be inherited: @InheritedAspect. If an aspect annotation is annotated with @InheritedAspect, the aspect annotation is also taken into account although it is only annotated on one of the super classes or interfaces.

Note that this mechanism is not without limitations. For the entire inheritance hierarchy, only one annotation per annotation type is taken into account. If the annotation type appears multiple times, the annotation on the PMO class itself is preferred over the one on the super class, which is preferred over annotations of any of the implemented interfaces.

As the inheritance structure can be very complex, the usage of @InheritedAspect should be carefully evaluated when defining your own aspect.