Search code examples
data-bindingentityvaadin

Vaadin: How to bind entity "meta data"


I'm tring to figure out how to bind meta data about an entity that's not an actual field in the entity. For example, consider the following:

Task:
    title: string
    description: string
    ...
    notes: List<TaskNote>

I'd like to be able to display the Task in a grid, or in an "editor" form and display, say the count of TaskNotes associated with a Task. So, in this case, the count is simply the number of items in the notes list.

How do I create a data binding for something like this? What are the best practices/techniques?


Solution

  • A Binder is used for binding Fields into a specific bean's Properties. A Person bean could have the Properties "first name", "last name" and "birth year". However, Field and Property are quite abstract concepts, leaving you a lot of room to maneuver.

    • A Field is something, typically a Component, that implements the HasValue interface. The HasValue interface (and thus the Field) has a Java type, such as String.
    • A Property is essentially a getter/setter pair. The pair must have a matching type - the getter's return type must match what the setter takes as an input.
      • The Property's getter can be provided by implementing the ValueProvider functional interface, which, in a simple form, can be a Lambda expression such as person -> person.getFirstName()
      • the setter can be provided with an implementation of the Setter functional interface. A simple example as a Lambda would be (person, newFirstName) -> person.setFirstName(newFirstName).

    When you create a Binder, you provide the ValueProvider and Setter in one way or another; the most explicit way would be when you're binding like this:

    Binder<Person> binder = new Binder<>();
    TextField firstNameField = new TextField("First name");
    binder.forField(firstNameField).bind(valueProvider, setter);
    

    When you're dealing with something that is a part of a bean, but not a Property, you have a couple of different options. For example, the Person's "age" can be calculated as a function of their "birth year". Let's say we only want to be able to edit the birth year, but we also want to display the age as a read-only value. It could also be a discount percentage based on age or something.

    The first, easiest approach is to use a Field, but only with a ValueProvider and no Setter. So you could have something like this (using TextField for simplicity):

    Binder<Person> binder = new Binder<>();
    TextField ageField = new TextField("Age (calculated)");
    binder.forField(ageField).bind(person -> calculateAge(person.getBirthYear()), null);
    

    The downside of this approach is that you're still using a TextField, which is an editor component. You can set it to read-only mode so the user can't change the value, but it still looks like you might be able to edit it at some point.

    Another thing you can use is a ReadOnlyHasValue, which is an interface for creating a Property without a Setter. It's a bit clunky, but you can use it with binding:

    Span ageDisplay = new Span();
    ReadOnlyHasValue<String> ageField = new ReadOnlyHasValue<>(text -> ageDisplay.setText(text));
    binder.forField(ageField).bind(person -> calculateAge(person.getBirthYear()), null);
    

    Finally, as ValueProvider and Setter are just Java interfaces, you can implement them with any kinds of side effects that you might want. So you can piggyback any logic to a Property's changes:

    Binder<Person> binder = new Binder<>();
    Span ageDisplay = new Span();
    TextField birthYear = new TextField();
    binder.forField(birthYear)
      .bind(person -> { // ValueProvider
        return person.getBirthYear();
      }, (person, newBirthYear) -> { // Setter
        person.setBirthYear(newBirthYear);
        ageDisplay.setText(calculateAge(newBirthYear));
      });