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:
The self-defined @UIRadioButtonGroup
annotation type can be used inside a PMO:
@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.
@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.
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().getDeclaredConstructor().newInstance();
} catch (ReflectiveOperationException | IllegalArgumentException | SecurityException 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
|
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
:
-
The
ValueAspectDefinitionCreator
is an existingAspectDefinitionCreator
that is reused by theUIRadioButtonGroup
to define the aspect value change. -
UIRadioButtonGroupFieldAspectDefinitionCreator
defines all other Aspects using aCompositeAspectDefinition
:
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:
|
Defines an aspect that updates the set of available values of HasItems. |
|
Aspect definition for read-only state. |
|
Aspect definition for caption binding. Assumes that the |
|
Aspect definition for EnabledType. |
|
Aspect definition for value change. Defines that the data source will get/set values through
Aspects by providing a handler in |
|
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. |
|
Aspect definition for RequiredType. Assumes that the given component is an |
|
Aspect definition for VisibleType. |