Loading...

View models for Java components

February 4, 2020

One thing that I really like with the current breed of template-based frontend frameworks is how you just define how your components should be configured based on the view’s state, and then the framework takes care of the rest. Based on a binding such as <vaadin-button ?disabled=${!this.hasValidationError}>, the button is automatically disabled or enabled simply by updating the value of the `hasValidationErrors property.

I set out to investigate how the same concept could be applied for typical Vaadin views built using Java. I ended up building a small framework on top of Vaadin that you can find at https://github.com/Legioth/reactivevaadin.

I would now like to hear what you think about this way of handling the data that is shown in Vaadin UIs. Please leave a comment below or as an issue in the GitHub repository.

A model for viewing an order

Let’s build a component for showing information about an order object. The component is internally using a view model to manage the data that is displayed, even though this isn’t in any way visible to the end user. You can see a live example with some extra tweaking controls running here. All the differences will be in what the code looks like.

OrderPanel-1

A view model contains all the dynamic data that is used to configure the components in a view. You should avoid making changes directly to the UI components, and instead configure them to pull data from the model. But first you need that view model. It’s made up of properties related to the Java bean that is used by the view.

public class OrderModel extends AbstractBeanModel<Order> {
    public final Property<String> customerName =
        createProperty(Order::getCustomerName);
    public final Property<Money> discount =
        createProperty(Order::getDiscount);

    public OrderModel() {
        super(Order::new);
    }
}

The model is still incomplete, but we can already create a simple OrderPanel component that uses an OrderModel instance to show details about an order bean. It’s still missing all the fun stuff such as a list of ordered products, but it’s a start.

public class OrderPanel extends VerticalLayout {
    private OrderModel model = new OrderModel();

    public OrderPanel() {
        Span customerName = new Span();
        Span discount = new Span();

        Bindings.bindText(customerName, "Customer: %s", model.customerName);
        Bindings.bindText(discount, "Discount: $%s", model.discount);

        add(customerName, discount);
    }

    public void setOrder(Order order) {
        model.read(order);
    }
}

The setOrder method only reads values from the bean into the model instance. All the magic happens through the Bindings.bindText method calls. Whenever the property value changes, its value is formatted using the printf-style format string and then the result is set as the text content of the component.

Note

There could be new API in the components instead of static helpers if this would be built in to the core framework. We could then write something like customerName.setText("Customer: %s", model.customerName) instead of using the Bindings class. Kotlin users have the upper hand here thanks to extension functions.

Properties based on other properties

In addition to properties that are populated directly from a bean, you can also define properties by converting or combining other properties. The derived property listens to changes from the original properties so you can use it to create bindings in the same way as with any other property.

Let’s add a property to our model for keeping track of whether there is a discount.

public final Flag hasDiscount =
    discount.test(new BigDecimal("0.00")::equals).not();
Note

Cannot use BigDecimal.ZERO here since we’re using two decimals for all values and equals requires matching precisions.

Flag is a property that has a boolean value. It gives some additional convenience methods such as not() which is used here to define a property that negates the value of the flag created by test(). We can use this property to create a binding that toggles a CSS class name so that some additional styling is used if there is a discount.

Bindings.bindClassName(discount, "discount", model.hasDiscount);

Settable properties for view configuration

All dynamic component configuration is not based on a bean like Order. The component can also have unrelated configuration properties, such as whether the order discount should be highlighted.

We would thus need a setter in the component that changes the value of a property. For this, we’re using a ModelProperty which is a simple standalone property with a settable value. We set true as the default value.

private final ModelProperty<Boolean> highlightDiscount = new ModelProperty<>(true);

public void setHighlightDiscount(boolean highlightDiscount) {
    this.highlightDiscount.setValue(highlightDiscount);
}

This property can be used to supplement the class name binding so that the class name is only set when both model.hasOrder and highlightDiscount are true.

Bindings.bindClassName(discount, "discount",
    model.hasDiscount.and(highlightDiscount));

This is the first example that really shows the benefits of deriving all component configuration from model properties. With a traditional way of configuring the components, you must check if the classname should be toggled both when setting and order and when changing the highlight configuration.

Note

It’s not necessary to define the property and the setter in the OrderPanel component. It could just well be defined by a parent component and then passed down to OrderPanel as a Property instance. A binding has a listener registered with its property only while the target component is attached. You don’t need to worry about memory leaks even if you share the same property instance between components with different life cycles.

Repeating multiple components based on a list

Our order model and panel are not yet complete - we’re still missing the list of order lines that each define a product and how many pieces of that product is ordered. We need to create a basic OrderLineModel class similar to OrderLine and then we define a property for the lines in OrderModel. The model property gets a list of OrderLine beans from the Order, creates an OrderLineModel instance for each order and then wraps up those in a java.util.List instance.

public final ModelList<OrderLineModel> lines =
    createList(Order::getOrderLines, OrderLineModel::new);
Note

Vaadin already has a concept for a sequence of items that can fire events when something is changed, namely DataProvider. It would indeed be possible to create adapters that allow using one in place of the other. It is left as an exercise for the reader to come up with ideas about where that could make sense.

Note

A separate OrderLineModel class is not really needed for this example since order line values don’t change on the fly. It will be truly useful later on when we create a form around the same model.

To show the order lines as a part of OrderPanel, we use a repeat binding that creates a component for each item in the model list. The binding automatically adds, removes, or rearranges the components if the list is modified. The component uses bindings to its the line order model to update itself based on changes to any used property value.

Bindings.repeat(orderLines, model.lines, model -> {
    Paragraph lineComponent = new Paragraph();
    Bindings.bindText(lineComponent, "%s %s x $%s = $%s",
        model.product.map(Product::getName),
        model.amount,
        model.product.map(Product::getPrice),
        model.price);
    return lineComponent;
});
Note

Binding text with a format string can use multiple properties, just like with the String.format method. The text is updated whenever any of the properties change.

model.price is a derived property defined in OrderLineModel. It takes the price of the product and multiplies it with the amount after converting it into a BigDecimal.

public final Property<BigDecimal> price =
    product.map(Product::getPrice)
           .combine(amount.map(BigDecimal::valueOf), BigDecimal::multiply);

Derived properties based on a list

Similarly to ModelProperty, ModelList can also be used as a starting point for creating derived lists or single-item properties. You do this using operations familiar from Java’s Stream API. We can define some additional properties in OrderModel that summarize the status of all the order lines.

public final Flag allInStock =
    lines.mapProperties(line -> line.product)
         .allMatch(Product::isInStock);

public final Property<BigDecimal> linePriceSum =
    lines.mapProperties(line -> line.price)
         .reduce(BigDecimal.ZERO, BigDecimal::add);

mapProperties expands a Property to its value and also sets up a listener for value changes from that property in addition to listening for changes to the list itself. The rest is really similar to the Stream API except that the result is a Property or Flag which will fire events whenever anything used for the computation has changed.

We can finally use these properties to complete the OrderPanel component.

Bindings.bindVisible(allInStock, model.allInStock);
Bindings.bindText(total, "Total $%s - $%s = $%s",
    model.linePriceSum,
    model.discount,
    linePriceSum.combine(discount, BigDecimal::subtract));

An alternative way of deriving properties

If you don’t like the functional style of creating derived properties using operations such as mapProperties and reduce, you can also create a derived property based on a single callback that explicitly registers a dependency on any property that it uses.

With that approach, the price property in the order line model can be defined in this way.

public final Property<BigDecimal> price =
    Property.computed(using -> {
        Product product = using.valueOf(this.product);
        int amount = using.valueOf(this.amount);

        return product.getPrice().multiply(new BigDecimal(amount));
});

Correspondingly, the sum of all order line prices could be defined like this.

public final Property<BigDecimal> linePriceSum =
    Property.computed(using -> {
        BigDecimal sum = BigDecimal.ZERO;
        for (OrderLineModel line : using.iterableOf(lines)) {
            sum = sum.add(using.valueOf(line.price));
        }
        return sum;
});

Forms based on a view model

The view model has so far been mostly static, except for importing values from another order bean or toggling whether discounts are highlighted. The same principles with a view model, properties and so on can also be used for data that is being edited by the user, i.e. for forms.

A live demo of the fully implemented form runs here.

OrderForm

The first step towards this is to update the view model to not only support reading values from an order bean, but to also be able to write model values back to an order bean instance. To do this, we change the createProperty calls to also configure a setter for each property At the same time, we can also change the field types to ModelProperty since the property can now accept new values in other ways than by reading them from a bean.

public final ModelProperty<String> customerName =
    createProperty(Order::getCustomerName, Order::setCustomerName);
public final ModelProperty<BigDecimal> discount =
    createProperty(Order::getDiscount, Order::setDiscount);

The property for the order lines can be left as before as long as the list returned by Order::getOrderLines is mutable.

To manage the overall status of the form, e.g. whether the user has made any changes or whether there are any validation errors, we will create a FormBindings instance. To get a simple initial version of our form, we create some field components and use the form instance to create field bindings between the field component and the corresponding property in the model. We also copy the status labels from the OrderPanel implementation.

public class OrderForm extends VerticalLayout {
    private OrderModel model = new OrderModel();

    public OrderForm() {
        TextField customerName = new TextField("Customer name");
        BigDecimalField discount = new BigDecimalField("Discount");
        Span total = new Span();
        Span allInStock = new Span("All products are in stock");

        FormBindings form = new FormBindings();
        form.bind(customerName, model.customerName);
        form.bind(discount, model.discount);

        Bindings.bindText(total, "Total $%s", model.totalPrice);
        Bindings.bindVisible(allInStock, model.allInStock);

        add(customerName, discount, allInStock, total);
    }

    public void setOrder(Order order) {
        model.read(order);
    }
}

Update a bean based on view model values

Changes to the fields are automatically propagated to the property instances in the view model. Thanks to the bean setters defined for the properties, we can use model.write(order) to write out the values to an existing order instance, or we can use model.createAndWrite() to create a new instance and populate it.

We can create a submit button that does either of those depending on whether there is currently an actively edited order or not.

public void setOrder(Order order) {
    if (order == null) {
        model.reset();
        submitButton.setText("Create order");
        submitCommand = () -> {
            Order newOrder = model.createAndWrite();
            orderService.saveNewOrder(newOrder);
        };
    } else {
        model.read(order);
        submitButton.setText("Update order");
        submitCommand = () -> {
            model.write(order);
            orderService.updateExistingOrder(order);
        };
    }
}

Validate and control the submit button

Next, let’s define some simple validation rules for our bindings and hook it up so that the submit button is enabled only when there are changes but no validation errors. Let’s set the customer name as required, add a converter that always stores the discount with two decimals, and require that the discount is positive.

form.bind(customerName, model.customerName)
    .setRequired("Must enter customer name");
form.forField(discount)
    .withConverter(OrderForm::adjustScale, modelValue -> modelValue)
    .withValidator(value -> value.equals(value.abs()),
        "Discount cannot be negative")
    .bind(model.discount);

The fields will now be marked as invalid but the user can still submit the incomplete form. We should disable the submit button when any binding in form has a validation error. While we’re at it, we can also make the button disabled if there are not yet any changes. FormBindings provides Flag for each of those cases, which we can combine for the disabled state of the button.

Bindings.bindEnabled(submitButton,
    form.hasChanges.and(form.isValid));
Note

There should probably be a ready-made shorthand binding for this in FormBindings so that you could just use something like form.canSubmit. This is once again a case where I choose to show a more explicit variant just to make the relationship between different concepts as clear as possible.

Editing order lines

The last part of the order edit form is to make it possible to add, remove and edit order lines. For this, we need a separate OrderLineForm component that is created based on an OrderLineModel instance so that all changes are applied directly to the same view model that is used by the rest of the form. Similarly to the readonly OrderPanel case, we can use a repeat binding to automatically add and remove line forms when lines are added or removed in the model. The line editor uses the same FormBindings instance so that hasChanges and isValid also takes the order lines into account.

Bindings.repeat(lines, model.lines, line -> new OrderLineForm(line, form));

The OrderLineForm component implementation is left as an exercise to the reader since it doesn’t introduce any new concepts.

To add order lines, we add a button that creates a new order line model and adds it to the lines list. The repeat binding will take care of adding a corresponding line editor component. We preselect a product so that the rest of the logic wouldn’t have to deal with potential null values. We also set the amount to 1.

Button addOrderLine = new Button("+", clickEvent -> {
    OrderLineModel newLine = new OrderLineModel();
    newLine.product.setValue(orderService.getDefaultProduct());
    newLine.amount.setValue(1);
    model.lines.add(newLine);
});
Note

Updating the logic to deal with null values in the product property isn’t very complicated. In most cases, we just need to update derived property mappings such as product.map(Product::getPrice) to define a default value to use if the property value is null, i.e. product.mapOrDefault(Product::getPrice, BigDecimal.ZERO).

The very last step is to make it possible to remove order lines. We add a remove button in the line editor component to run a callback passed to its constructor. The callback only needs to remove the line model from the list of lines models. The repeat binding takes care of removing the line editor component while FormBindings works so that it doesn’t consider bindings to field components that are not attached.

Bindings.repeat(lines, model.lines,
    line -> new OrderLineEditor(line, form,
        () -> model.lines.remove(line)));

That’s it. We’re done.

What do you think?

Setting up properties for a view model takes a little bit of extra code, but it helps make the UI logic so much simpler when any change is automatically propagated to all places that do anything based on that value.

Do you think this approach would make sense for whatever UI logic you’re currently building? What parts are difficult to understand? Can the API be improved? Please leave a comment!

If you want to play with the examples or build your own example, then you can get all the code from https://github.com/Legioth/reactivevaadin.

Disclaimer

This project is experimental and you probably should not use it in production application. Vaadin does not give any warranty, support or guarantees of applicability, usefulness, availability, security, functionality, performance etc.

Comments ()