$this ModelAn alternative to writing setter wrappers in your composite control: expose its state via a model, and let the inner controls bind to it. OpenUI5 propagates property changes for you.
When a composite control has only one or two properties, overriding their dedicated setters (setMyValue, setLabel, etc.) to forward values to the inner controls is straightforward — see Building Standard Composite Controls. For composite controls with many properties, or with several inner controls reacting to the same property, those per-property setter overrides become repetitive.
An alternative is to expose the composite’s state via a model and let the inner controls bind to it. This shifts the synchronization out of imperative code into declarative bindings, and OpenUI5 handles the updates automatically. By convention — inherited from the XMLComposite era — the model is registered under the name $this, so that any reader of an inner control’s binding can tell at a glance: “this binding pulls from the surrounding composite”.
Note:
Use plain setter forwarding when you have one or two properties; reach for the model pattern when you have many, or when migrating from
XMLComposite.
$this Binding PatternThe following helper is a pattern that application teams can copy into their codebase and adapt as needed. It is small, self-contained, and has no dependencies beyond sap.ui.core.Control and sap.ui.model.json.JSONModel. The code snippets below use CompositeHelper as the file and function name — this is just a naming convention for the example; you can choose any name that fits your codebase.
The pattern has three responsibilities, all wired through a single JSONModel registered under $this:
$this on the control.propertyChange. Listen for it and write the value back into the composite via setProperty(..., true).setProperty, so that programmatic changes from outside (composite.setMyValue("x")) also update the model.These two directions share a re-entry guard, so that one does not trigger the other in a loop.
sap.ui.define([
"sap/ui/core/Control",
"sap/ui/model/json/JSONModel"
], function(Control, JSONModel) {
"use strict";
/**
* Activates the $this-model pattern on a composite control instance.
*
* @param {sap.ui.core.Control} oControl - the composite control instance (typically `this` from init)
*/
return function CompositeHelper(oControl) {
const oModel = new JSONModel({});
oControl.setModel(oModel, "$this");
let bSuppressUpdate = false;
// Inner control -> outer control: a bound inner control changed something.
oModel.attachPropertyChange(function(oEvent) {
if (bSuppressUpdate) {
return;
}
const sProperty = oEvent.getParameter("path").slice(1); // "/myValue" -> "myValue"
const vValue = oEvent.getParameter("value");
Control.prototype.setProperty.call(oControl, sProperty, vValue, true);
});
// Outer control -> model: programmatic setter calls reach the model.
oControl.setProperty = function(sName, vValue, bSuppressInvalidate) {
bSuppressUpdate = true;
try {
oModel.setProperty("/" + sName, vValue);
} finally {
bSuppressUpdate = false;
}
return Control.prototype.setProperty.call(oControl, sName, vValue, bSuppressInvalidate);
};
};
});
Note:
The helper assumes the composite inherits directly from
sap.ui.core.Control. If your composite extends a more specialized base class (for example,sap.m.InputBase), replaceControl.prototype.setPropertywith the corresponding base class’s prototype so that the framework’s setter logic for that base class still runs.
Three notes on the implementation:
bSuppressUpdate) is a defensive measure. With the current JSONModel, the wrapped setProperty writing into the model does not by itself trigger a propertyChange event — that event is fired by the binding layer when an inner control writes back via two-way binding. The guard ensures that if a subclass or a side-effect setter ever introduces additional model writes during the wrapper’s execution, those writes do not re-enter the wrapper. Without the guard, such an extension could cause an infinite loop.Control.prototype.setProperty.call(...) bypasses the wrapper but still goes through the framework’s normal setter logic (validation, invalidation, change events). Calling oControl.setProperty(...) from the listener would re-enter the wrapper.true as the third argument in the listener path skips the re-render: the change originated in the inner control’s DOM, so re-rendering the composite would discard work the inner control already did.This is a LabeledInput composite — a label, an input, and a status text below it, all bound to three properties of the composite (label, value, status):
sap.ui.define([
"sap/ui/core/Control",
"sap/m/Label",
"sap/m/Input",
"sap/m/Text",
"my/app/util/CompositeHelper"
], function(Control, Label, Input, Text, CompositeHelper) {
"use strict";
const LabeledInput = Control.extend("my.app.LabeledInput", {
metadata: {
properties: {
"label": { type: "string", defaultValue: "" },
"value": { type: "string", defaultValue: "" },
"status": { type: "string", defaultValue: "" }
},
aggregations: {
"_label": { type: "sap.m.Label", multiple: false, visibility: "hidden" },
"_input": { type: "sap.m.Input", multiple: false, visibility: "hidden" },
"_status": { type: "sap.m.Text", multiple: false, visibility: "hidden" }
}
},
init() {
Control.prototype.init.apply(this, arguments);
CompositeHelper(this);
this.setAggregation("_label", new Label({
text: "{$this>/label}"
}));
this.setAggregation("_input", new Input({
value: "{$this>/value}"
}));
this.setAggregation("_status", new Text({
text: "{$this>/status}"
}));
},
renderer: {
apiVersion: 4,
render(oRm, oControl) {
oRm.openStart("div", oControl);
oRm.class("LabeledInput");
oRm.openEnd();
oRm.renderControl(oControl.getAggregation("_label"));
oRm.renderControl(oControl.getAggregation("_input"));
oRm.renderControl(oControl.getAggregation("_status"));
oRm.close("div");
}
}
});
return LabeledInput;
});
Two things to note:
{$this>/label}, {$this>/value}, {$this>/status} — the same syntax that an XMLComposite fragment uses. After CompositeHelper(this) runs in init, the bindings resolve.oLabeledInput.setValue("foo") updates the model, which updates the bound Input. Typing into the Input updates the model, which writes the value back into the composite’s value property.Note:
If you override a setter on the composite to do additional work (for example, a
setEditablethat also clears the validation state), the override must end withthis.setProperty(...), not by writing the model directly. The wrappedsetPropertykeeps the model in sync; writing the model directly skips the framework’s setter logic.
// Side-effect setter — composite's own setEditable does additional cleanup.
setEditable(bEditable) {
this.clearValidationState(); // composite-specific side effect
return this.setProperty("editable", bEditable);
}
XMLCompositeXMLComposite automatically registers a $this backing model for property bindings, so that fragment bindings such as {$this>/text} work without any explicit setup. The pattern on this page reconstructs that behavior with a JSONModel. Property bindings stay character-identical — only the model is now activated by your code in init.
One important difference: the $this model in XMLComposite could also expose aggregations. The pattern on this page covers properties only. For aggregations, see Forwarding Aggregations.
| `XMLComposite` feature | What is lost during migration | Compensation |
|---|---|---|
| Automatic `$this` model registration | Bidirectional property bindings between inner controls and outer control | The `$this` binding pattern on this page |
Related Information