A form from properties
A row that names a property instead of a control does not need a control, a label, or a value: the builder makes the control from the property's metadata, labels it from the metadata, and binds it to the property.
Track track = getSharedContext().get(Track.class, 1L);
FormBuilder fb = new FormBuilder(cp);
fb.property(track, Track_.name()).control();
fb.property(track, Track_.composer()).control();
fb.property(track, Track_.unitPrice()).control();
fb.property(track, Track_.genre()).control();
- What the four lines did
- Naming the property
- Overriding what the metadata says
- readOnly and disabled
- The rest of the chain
What the four lines did
control() with no argument is what asks for a control to be made. For each
row, the builder
- reads the property's metadata -
Track.nameis a mandatoryStringof 200 characters whose label is "Title",Track.unitPriceis money,Track.genreis a relation to another entity, - asks the
ControlCreatorRegistryfor the control that fits it, which is aText2<String>for the first, aText2<BigDecimal>with a money converter for the third and aLookupInput2<Genre>for the last, - labels it with the property's label and marks it mandatory when the property is,
- and binds the control's value to that property of that instance.
Which control comes out of which property is decided by the registry and is
described with the controls themselves - see
text and value input and
metadata. The binding is the
ordinary data binding:
typing in the box changes the Track instance, and changing the instance
changes the box.
Naming the property
fb.property(track, Track_.name()).control(); // the generated QField
fb.property(track, "name").control(); // by name
The QField form is the one to use: Track_ is generated from the entity, so a
renamed property is a compile error rather than a runtime one, and the chain
knows the property's type - which is what lets control(control) check that the
control fits the property. See
typed properties.
Naming the property as a String gives a chain that does not know the type: it
takes any IControl, and it is the chain that has converter() on it, because
the converter cannot be checked against the property's type either way.
In Kotlin a property reference does the same as the QField:
fb.property(track, Track::name).control()
Overriding what the metadata says
fb.property(track, Track_.composer()).label("A label of my own").control();
fb.property(track, Track_.unitPrice()).mandatory().hint("What the shop charges").control();
fb.property(track, Track_.genre()).control(ComboLookup2.class);
fb.property(track, Track_.name()).label("A control of my own").control(ownControl);
Every step of the chain overrules the metadata for that one row.
| Ending the row with | What you get |
|---|---|
control() |
the control the registry picks for the property |
control(Class) |
the control the registry picks of that class - genre is a relation, which becomes a LookupInput2 by default and a ComboLookup2 when asked for one |
control(control) |
a control you made yourself, bound and laid out the same way |
control(control, converter) |
the same, with an IBidiBindingConverter between the control's type and the property's |
control(Class) only works where a control creator can deliver that class for
that property. Asking for a class no creator makes for the property - a
TextArea for a property with no textarea hint, say - ends in No control
factory found.
readOnly and disabled
Both can be set outright, bound to a property, or set for a run of rows:
FormBuilder fb = new FormBuilder(cp);
fb.readOnlyAll(this, "locked"); // from here on: bound to page.locked
fb.property(track, Track_.name()).control();
fb.property(track, Track_.composer()).control();
fb.readOnlyAllClear(); // and no longer
fb.property(track, Track_.unitPrice()).control();
| On the builder | On one row |
|---|---|
readOnlyAll(instance, property) / readOnlyAllClear() |
readOnly(), readOnly(boolean), readOnly(instance, property) |
disabledAll(instance, property) / disabledAllClear() |
disabled(), disabled(boolean), disabled(instance, property) |
disabledBecauseAll(instance, property) / disabledBecauseClear() |
disabledBecause(String), disabledBecause(instance, property) |
The row wins over the builder: a readOnly() on the row is used even when
readOnlyAll() is in force. The bound forms are ordinary bindings, so the
control follows the property as it changes - tick the box on the demo page above
and the two controls under it become read only while the third does not.
disabledBecause() disables the control and says why, which the control shows
as its tooltip. It is set on the control's own disabledBecause property; a
control that has no such property is simply disabled.
The rest of the chain
| Method | What it does |
|---|---|
mandatory(), mandatory(boolean) |
override the metadata's idea of whether a value is required |
hint(String), hint(IBundleCode) |
the explaining text, over the property's default hint |
errorLocation(String) |
where errors on this control are reported; the label text by default |
testId(String) |
the test id for Selenium tests; the property's name by default |
cssLabel(String), cssControl(String) |
a class on the label or the control part of this pair |
