Extending linkki

Custom UI element annotation

It is possible to keep the clean structure of PMO code even if the application needs UI elements that are not part of linkki's UI element set by creating custom annotations for UI elements. The following example shows how to create a custom annotation for a radio button group:

radiobuttongroup

The self-defined @UIRadioButtonGroup annotation type can be used inside a PMO:

Usage of @UIRadioButtonGroup
    @UIRadioButtonGroup(position = 30, label = "Gender", buttonAlignment = AlignmentType.HORIZONTAL, content = AvailableValuesType.ENUM_VALUES_EXCL_NULL, //
            itemCaptionProvider = GenderCaptionProvider.class, modelAttribute = Contact.PROPERTY_GENDER)
    public void gender() {
        /* model binding only */
    }

Annotation Type

The next listing shows the whole annotation type UIRadioButtonGroup. The relevant concepts and requirements are discussed below.

Implementation of the annotation type UIRadioButtonGroup
@Retention(RUNTIME) (1)
@Target(METHOD) (1)
@LinkkiPositioned (2)
@LinkkiAspect(UIRadioButtonGroupFieldAspectDefinitionCreator.class) (3)
@LinkkiAspect(ValueAspectDefinitionCreator.class) (3)
@LinkkiBoundProperty (4)
@LinkkiComponent(UIRadioButtonGroupComponentDefinitionCreator.class) (5)
public @interface UIRadioButtonGroup {

    /**
     * Defines the position of the UI element.
     *
     * @return the defined position
     */
    @LinkkiPositioned.Position (2)
    int position();

    /**
     * The label that is displayed to describe the {@link RadioButtonGroup}. The default is an empty
     * String.
     *
     * @return the label
     */
    String label() default ""; (3)

    /**
     * Specifies the source of the available values, the content of the combo box.
     *
     * @see AvailableValuesType
     *
     * @return the type of the content
     */
    AvailableValuesType content() default AvailableValuesType.ENUM_VALUES_INCL_NULL;

    /**
     * Defines whether the {@link RadioButtonGroup} is enabled, disabled or dynamically enabled. The
     * default value is {@link EnabledType#ENABLED}.
     *
     * @return the {@link EnabledType}
     */
    EnabledType enabled() default ENABLED;

    /**
     * Defines the {@link RequiredType} of the {@link RadioButtonGroup}. The default value is
     * {@link RequiredType#NOT_REQUIRED}.
     *
     * @return the specified {@link RequiredType}
     */
    RequiredType required() default NOT_REQUIRED;

    /**
     * Defines the {@link VisibleType} of the {@link RadioButtonGroup}. The default value is
     * {@link VisibleType#VISIBLE}.
     *
     * @return the specified {@link VisibleType}
     */
    VisibleType visible() default VISIBLE;

    /**
     * An {@link ItemCaptionProvider} provides the caption of each radio button. By default the
     * {@link DefaultCaptionProvider} which invokes the {@code getName()} method is used.
     *
     * @return the specified {@link ItemCaptionProvider}
     */
    Class<? extends ItemCaptionProvider<?>> itemCaptionProvider() default DefaultCaptionProvider.class;

    /**
     * If provided <b>linkki</b> uses the {@code ModelObject} for model binding.
     *
     * @return the name of the {@code ModelObject}
     */
    @LinkkiBoundProperty.ModelObject (4)
    String modelObject() default ModelObject.DEFAULT_NAME;

    /**
     * If provided <b>linkki</b> accesses the given model attribute for model binding.
     *
     * @return the name of the {@code ModelAttribute}
     */
    @LinkkiBoundProperty.ModelAttribute (4)
    String modelAttribute() default "";

    /**
     * Specifies the alignment of the radio buttons. The alignment can be either
     * {@link AlignmentType#VERTICAL}, which is the default alignment, or
     * {@link AlignmentType#HORIZONTAL}
     *
     * @return the {@link AlignmentType} that is set
     */
    AlignmentType buttonAlignment() default AlignmentType.VERTICAL;
1 Just like any other Java annotation type @UIRadioButtonGroup has to specify @Retention and @Target. More detailed information about annotation types can be found at the Oracle Docs
2 linkki must be able to position the custom UI element. Therefore the annotation type must be annotated with @LinkkiPositioned to indicate that it defines a position property. The actual position() annotation type element is annotated with @LinkkiPositioned.Position.
3 The annotation type elements label(), content(), enabled() and so on are LinkkiAspects that enable the user of the @UIRadioButtonGroup annotation to alter the appearance of the UI element by setting the corresponding parameter to the @UIRadioButtonGroup annotation. It is necessary to provide an AspectDefinition for each Aspect through an AspectDefinitionCreator class.
4 The BoundProperty is necessary for linkki to define the correlation between the UI element and the model side (PMO or model object) for binding purposes.
5 The @LinkkiComponent meta-annotation specifies which ComponentDefinitionCreator class is used. A ComponentDefinitionCreator is necessary to define how to create the actual UI element.

Component Definition

A ComponentDefinition defines how the actual UI element is created. In this case the RadioButtonGroup from the Vaadin framework is created. The following listing shows the UIRadioButtonGroupComponentDefinitionCreator class and the corresponding UIRadioButtonGroupComponentDefinition class.

Implementation of the UIRadioButtonGroupComponentDefinitionCreator
    public static class UIRadioButtonGroupComponentDefinitionCreator
            implements ComponentDefinitionCreator<UIRadioButtonGroup> {

        @Override
        public LinkkiComponentDefinition create(UIRadioButtonGroup annotation, AnnotatedElement annotatedElement) {
            return new UIRadioButtonGroupComponentDefinition(getItemCaptionProvider(annotation),
                    annotation.buttonAlignment());
        }

        private ItemCaptionProvider<?> getItemCaptionProvider(UIRadioButtonGroup uiRadioButtonGroup) {
            try {
                return uiRadioButtonGroup.itemCaptionProvider().newInstance();
            } catch (InstantiationException | IllegalAccessException e) {
                throw new LinkkiBindingException(
                        "Cannot instantiate item caption provider " + uiRadioButtonGroup.itemCaptionProvider().getName()
                                + " using default constructor.",
                        e);
            }
        }
    }

    class UIRadioButtonGroupComponentDefinition implements LinkkiComponentDefinition {

        private ItemCaptionProvider<?> itemCaptionProvider;
        private AlignmentType alignment;

        public UIRadioButtonGroupComponentDefinition(ItemCaptionProvider<?> itemCaptionProvider,
                AlignmentType alignment) {
            this.itemCaptionProvider = itemCaptionProvider;
            this.alignment = alignment;
        }

        @Override
        public Object createComponent(Object pmo) {
            RadioButtonGroup<?> radioButtonGroup = new RadioButtonGroup<>();
            radioButtonGroup.setItemCaptionGenerator(itemCaptionProvider::getUnsafeCaption);
            if (alignment.equals(AlignmentType.HORIZONTAL)) {
                radioButtonGroup.addStyleName(ValoTheme.OPTIONGROUP_HORIZONTAL);
            }
            return radioButtonGroup;
        }
    }

The ComponentDefinition for the UIRadioButtonGroup is not directly specified for the UIRadioButtonGroup by the @LinkkiComponent meta-annotation. Instead, the UIRadioButtonGroupComponentDefinitionCreator which holds a reference to the annotation type and creates the actual UIRadioButtonGroupDefinition is specified. This separation makes ComponentDefinition also usable without annotations and reusable for other annotations.

The separation of DefinitionCreators that are annotated to the annotation type and Definitions without annotation type dependencies is used for BoundProperties and AspectDefinitions as well. All Creator classes contain a create() method, that will return the relevant Definition.

In the example the UIRadioButtonGroupComponentDefinitionCreator holds a reference to the actual UIRadioButtonGroup annotation type and passes the required arguments to create the actual Vaadin UI element to the constructor of UIRadioButtonGroupDefinition. In this specific case the aspects width and ItemCaptionProvider are needed at the time of creation. The UIRadioButtonGroupComponentDefinition#createComponent(Object pmo) method is called by linkki itself.

Bound Property

The property on the model side (PMO / model object) that is considered for data binding is called BoundProperty. In our example the property "gender" inside the PMO that is annotated with @UIRadioButtonGroup is the BoundProperty for this specific RadioButtonGroup. Most notably, BoundProperty is used to determine how an aspect value is retrieved. For example, the enabled aspect of the @UIRadioButtonGroup will be retrieved by the method isGenderEnabled, as the bound PMO property is named "gender".

For UI annotation that are used on methods, a BoundProperty needs to have at least the property name of the PMO property. Optionally, it can contain the model object and attribute for model binding.
Annotations on classes typically use an empty BoundProperty. That way, the aspect methods for the whole PMO do not contain any property name.

The annotation LinkkiBoundProperty points to a BoundPropertyCreator that defines how a BoundProperty should be created from the annotated annotation. The UIRadioButtonGroup uses the existing ModelBindingBoundPropertyCreator that derives the BoundProperty from the PMO property, and also determines the model object and model attribute from annotation attributes that are annotated with @LinkkiBoundProperty.@ModelObject and @LinkkiBoundProperty.@ModelAttribute respectively. This enables binding to the domain model.

Aside from ModelBindingBoundPropertyCreator, linkki also provides some other implementations:

  • EmptyPropertyCreator - Creates an empty BoundProperty

  • SimpleMemberNameBoundPropertyCreator - Derives the BoundProperty from only the PMO property

Aspect Definitions

LinkkiAspects are the aspects that linkki has to take into consideration for the binding between the BoundProperty and the actual UI element. In most cases a change of an Aspect on the model side has to result in a change on the UI element and vice versa, so that the state of the UI reflects the state of the model. An AspectDefinition defines the type of the Aspect and what needs to be done if the Aspect changes.

The UIRadioButtonGroup has the aspects label, content, enabled, required and visible. Every Aspect must be defined within an AspectDefinition. This is done in the UIRadioButtonGroup using AspectDefinitionCreators:

  1. The ValueAspectDefinitionCreator is an existing AspectDefinitionCreator that is reused by the UIRadioButtonGroup to define the aspect value change.

  2. UIRadioButtonGroupFieldAspectDefinitionCreator defines all other Aspects using a CompositeAspectDefinition:

Implementation of the UIRadioButtonGroupFieldAspectDefinitionCreator
    public class UIRadioButtonGroupFieldAspectDefinitionCreator implements AspectDefinitionCreator<UIRadioButtonGroup> {

        @Override
        public LinkkiAspectDefinition create(UIRadioButtonGroup annotation) {

            AvailableValuesAspectDefinition<?> availableValuesAspectDefinition = new AvailableValuesAspectDefinition<RadioButtonGroup<Object>>(
                    annotation.content(),
                    RadioButtonGroup<Object>::setDataProvider);

            EnabledAspectDefinition enabledAspectDefinition = new EnabledAspectDefinition(annotation.enabled());
            RequiredAspectDefinition requiredAspectDefinition = new RequiredAspectDefinition(
                    annotation.required(),
                    enabledAspectDefinition);

            return new CompositeAspectDefinition(new LabelAspectDefinition(annotation.label()),
                    enabledAspectDefinition,
                    requiredAspectDefinition,
                    availableValuesAspectDefinition,
                    new VisibleAspectDefinition(annotation.visible()),
                    new DerivedReadOnlyAspectDefinition());
        }
    }

The UIRadioButtonGroupFieldAspectDefinitionCreator#create() method gets a reference to the UIRadioButtonGroup annotation like other *Creator#create() methods as well. Given this reference it can access all annotation type elements that belong to the aspects and pass them to the appropriate AspectDefinition.

linkki contains many existing AspectDefinitions that can be reused, like the EnabledAspectDefinition in this example. Common AspectDefinitions are:

Table 1. Common LinkkiAspectDefinitions

AvailableValuesAspectDefinition

Defines an aspect that updates the set of available values of HasItems.

BindReadOnlyAspectDefinition

Aspect definition for read-only state.

CaptionAspectDefinition

Aspect definition for caption binding. Assumes that the ComponentWrapper wraps a Component by default.

EnabledAspectDefinition

Aspect definition for EnabledType.

FieldValueAspectDefinition

Aspect definition for value change. Defines that the data source will get/set values through Aspects by providing a handler in #initModelUpdate(PropertyDispatcher, ComponentWrapper, Handler).

LabelValueAspectDefinition

The value aspect for label components. The label is a read-only component, hence this aspect only reads the value from model and updates the UI.

RequiredAspectDefinition

Aspect definition for RequiredType. Assumes that the given component is an AbstractField.

VisibleAspectDefinition

Aspect definition for VisibleType.