Architecture

Binding to the Domain Model (Data on Multiple Layers)

As implicated in Data Binding with PMOs a linkki application usually consists of multiple layers.

layers

This image intends to highlight where the presentation model layer is situated with respect to UI and domain model.

The rightmost layer is some kind of a persistence layer, for example a database. To the left are the domain model layer, the presentation model layer and finally the UI. The UI data binding works between the UI and the presentation model. It is responsible for keeping them in sync.

Very often the presentation model and the domain model have similar structure and properties. In that case the "model binding" can be used. "Model Binding", short-hand for Domain Model Binding, is a linkki feature for reducing delegate code in PMO classes. It is aimed at the "simple" case of UI binding, in which properties from the domain model are directly presented in the UI, meaning that no special formatting or conversion needs to take place before the value can be shown.

To use this feature, the UI needs to be created using a layout annotation, and the instance field or the getter method for the model object in the PMO needs to be marked with the @ModelObject annotation:

    @ModelObject
    private final Person person;
    @ModelObject
    public Person getPerson() {
        return person;
    }

For example, the firstname property from the model can be used without conversion in the UI:

    @UITextField(position = 10, label = "First Name")
    public String getFirstName() {
        return getPerson().getFirstname();
    }

In this case the getter method ensures a direct delegation to the model attribute. Therefore, the same behavior can be achieved using the model binding:

    @UITextField(position = 10, label = "First Name", modelAttribute = "firstname")
    public void firstName() {
        // model binding
    }

Model binding avoids unnecessary code dealing with delegation and is thus a "shortcut".

ModelBinding
Figure 1. Model Binding

Names of Model Attributes

To use the model binding feature, the property modelAttribute must be set in the UI Annotation. The data type of the property is String. The value of modelAttribute defines the name of the corresponding property in the domain model class. If no matching property exists in the domain model class, an exception is thrown during generation of the UI.

If modelAttribute is given, linkki ignores the return value and the body of the method in the PMO. Therefore the method body can remain empty and the return type be void. The data binding will skip the PMO method and call the corresponding method in the domain model directly.

In the example above the method in the PersonSectionPmo is called firstName(). In the domain model, however, it is called firstname or getFirstname(). It is possible to name properties in PMO and domain model differently.

linkki searches the model for all required methods. If the setter method exists in the model but needs to be ignored in the UI, the shortcut cannot be used. Model attributes can also be set on annotated getter methods. In that case linkki searches for all methods in the model that it can’t find in the PMO.

If the modelAttribute is set in the annotation of a non getter method, the whole method name is treated as a PMO property. This name is then used to bind UI Properties such as enabled state, visibility or available values.

Multiple Model Objects

Model binding can also be used if a PMO has multiple model objects. In this case, the property modelObject can be set in the UI annotation. The value of modelObject defines which model object of the PMO should be used for model binding. As identifier of model objects the name given in the @ModelObject annotation is used.

@UISection
public class ContractSectionPmo {

    private final Contract contract;


    public ContractSectionPmo(Contract contract) {
        this.contract = contract;
    }


    @ModelObject
    public Person getPolicyHolder() {
        return contract.getPolicyHolder();
    }

    @ModelObject(name = "IP")
    public Person getInsuredPerson() {
        return contract.getInsuredPerson();
    }


    @UITextField(position = 10, label = "First Name PH", modelAttribute = "firstname")
    public void firstNamePolicyHolder() {
        // model binding
    }

    @UITextField(position = 20, label = "First Name IP", modelAttribute = "firstname", modelObject = "IP")
    public void firstNameInsuredPerson() {
        // model binding
    }

}

In the example above the annotation of firstNameInsuredPerson() defines that the property firstname on model object IP shall be called. To determine the value for the presentation on the UI, the data binding proceeds as follows:

  • Call the method getInsuredPerson() on the PMO, as that method is annotated with @ModelObject("IP")

  • Save the returned Person object

  • Call the getter method for firstname on the found Person object

If the annotation contains no property modelObject, the method annotated with @ModelObject (without name) is called. Correspondingly the model object for firstNamePolicyHolder() is determined via getPolicyHolder().

null Model Object

When using model binding, linkki would throw an exception if the model object is null upon creation or update until now. With this change, linkki would gracefully handle the null value.

How the null value is handled depends on the respective aspect definition. Aspect definitions that are provided by linkki are adjusted accordingly. In case of the value aspect, the field value is cleared if the model object is null. Boolean valued aspects such as enabled or visible are set to false.

Custom aspect definition may need to be adjusted to be able to handle null value. If model object is null upon update, PropertyDispatcher#pull now returns null instead of throwing an exception. This can then be handled in the UI updater created in LinkkiApsectDefinition#createUiupdater.

When using ModelToUiAspectDefinition, a new method #handleNullItem has been introduced which can be overridden to define the null handling. The behavior defaults to calling the component value setter created by #createComponentValueSetter with null.

To be able to handle null values for generic model objects properly when using the PropertyDispatcherFactory, MemberAccessors#getType(Member) has been deprecated and replaced by MemberAccessors#getType(Member, Type), where Type is the concrete PMO class.