Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom Control + Model/Aggregation handling best practices(?) #13

Closed
wridgeu opened this issue Nov 5, 2022 · 2 comments · Fixed by #14
Closed

Custom Control + Model/Aggregation handling best practices(?) #13

wridgeu opened this issue Nov 5, 2022 · 2 comments · Fixed by #14
Labels
help wanted Extra attention is needed question Further information is requested

Comments

@wridgeu
Copy link
Owner

wridgeu commented Nov 5, 2022

Basic question: What are the best practices when it comes to model handling with/within custom controls?

What is the use-case/problem?

Basically I'm retrieving data from the backend, this can have any shape i.e.

{
	"id": "someId",
	"value": "someBaseValue",
	"items": [{
			"value": "Test"
		},
		{
			"value": "Test2"
		}
	]
}

So retrieve Data via OData V2, set our data into our internal JSONModel (state etc.) and use it.

Now at best I'd want to bind/set this data to my custom control which, based on this, renders different aggregations. For example if there are 2 items, there are 2 aggregations, if it's 3 then 3 accordingly. These aggregations should also be able to use the values within the items for displaying certain information.

In a best case scenario I'd like to rely on the model itself and don't do much manual work. So whenever the model updates, I'd expect a "rerender" of my custom control with updated content/aggregations.

Now I have dug quite deep into UI5 itself and figured out that the "issue" is, that using setProperty for updating (if it isn't the root), updates the .. well .. property only and therefore all the references of it which leads UI5 to think that there are no changes that require any rerendering during diffing within ManagedObject.js, or a bit deeper/earlier already within Model.js. This is because, as it is directly updated by reference the oldValue is equal to the new Value.

image
image

Generally saying (even though probably not as performant) this can be ... worked around by using setData (which setProperty uses under the hood for root changes) optionally also using the merge flag of setData. This however, technically, always creates a new object, thus creating a new reference which then again "fixes" my issue but doesn't seem "proper"/performant.

This behaviour can be tested using this repository (the one the issue was created on)

_setupCustomControlWithReactiveModelUsage() {
// create initial model
this.customViewModel = new JSONModel({
rootNode: {
demoData1: "initialValue1",
demoData2: "initialValue2",
demoData3: "initialValue3",
}
})
// create custom control and bind against rootNode of model
// https://jsbin.com/fosiya/edit?html,css,js,output → https://youtu.be/W3Qkev2yk9w
const customControlWithModel = new CCModelReactive({ value: "{/rootNode}" })
// this would be the way in case the model has a name
// const customControlWithModel = new CCModelReactive({ value: "{someName>/rootNode}" })
// customControlWithModel.setModel(this.customViewModel, someName)
// here the model is put "inside" the custom control, alternatively
// you can put it into the view or anywhere above within the tree
// the managedmodel will traverse and look for a match
customControlWithModel.setModel(this.customViewModel)
// this.getView().setModel(this.customViewModel)
// insert the custom control into the flexbox (within the view)
this.byId("container").addItem(customControlWithModel)
// dynamically create a custom button to alter the demo data at runtime
this.byId("container").addItem(new Button("changeDataButton", {
text: "adjust model at runtime",
icon: "sap-icon://incident",
press: () => {
// setProperty won't work as the value comparison between old/new fails → references are directly adjusted
// updateBinding/refresh have the same "issue", no diff can be created thus nothing will be adjusted
// this.customViewModel.setProperty("/rootNode/demoData1", Math.random()) // does update the model, doesn't retrigger rendering
this.customViewModel.setData({
rootNode: {
demoData1: Math.random(),
demoData2: Math.random(),
demoData3: Math.random(),
newlyAddedData: Math.random(),
}
}, true)
}
}))
&& https://github.com/wridgeu/UI5-control-example/blob/master/webapp/controls/CCModelReactive.js -- simply adjust the press event handler to use setProperty. You can observe that the model itself is changed but the UI won't get rerendered. While using setData it does.

All of this is easier with primitives which are more common use cases and examples shown everywhere. What came to mind was the sap.m.Tree (or maybe even the List itself as Tree is based on parts of it) as the Tree can take in quite a complex structure (as JSON Binding) and I think updating models, automatically takes care of updating everything there too. But ... I'd need quite a lot more time figuring all this out.

@wridgeu wridgeu added help wanted Extra attention is needed question Further information is requested labels Nov 5, 2022
@wridgeu
Copy link
Owner Author

wridgeu commented Nov 5, 2022

@wridgeu
Copy link
Owner Author

wridgeu commented Nov 30, 2022

After some more research on how to properly work with aggregation I've finally wrapped my head around it (I think/hope :^)).

There are two different PoVs to consider. Will it be used from the "outside" so with Binding (e.g. from the XML View or ofc via a controller etc.) or from the inside, within the control.

What finally made it click better for me was the comment here.

You won't find any manually created "rows"-aggregation within the control, so no magic setRows or setAggregation('rows', ..). This value will purely come from a binding and what it'll be is a sap.ui.table.Row and thus it'll "magically" be rendered. Not really magically ... but since you're not manually creating these rows anywhere within the control, you'll just have rows at runtime when calling getRows like here.

These will be created from the binding mechanism itself behind the scenes for you. Like the factory functions you can write yourself or templates you can hand over for the creation of aggregations based on a binding. But again, different PoVs (Control Developer / Consumer or App Developer) here.

Based on more UI5 Standard that I've crawled through it is also perfectly fine to create UI5 controls within a control. I was wondering whether or not this is ... "dirty" because it kind of makes you feel that way as you can define a custom control a renderer and all those things while the renderer itself can use basic HTML but you can still throw UI5 itself in there ... it can get confusing and I for one ended up questioning my decisions more than once and didn't know if things were "best practice" or "supposed to be like this" as it ... didn't feel "clean" but what is clean anyway? It just really depends how you look at it and as I can tell by now, it is perfectly fine.

An example would be the MessagePage or even the custom control tutorial that even makes use of "private" aggregations so the PoV within this tutorial is full-on custom control development and nothing from the outside or rather to abstract the aggregations and their behaviour through a common interface (the value property) but manage them completely internally (within the control) without exposing them.

A more specific example also based on the MessagePage would be:

  • control priv. method for getting/setting (managing) an aggregation → _getDescription
  • ... which is used within the renderer (which is neatly structured) to render the control within renderContent
  • ... which is then called in the main render method

Regarding the initial issue

I've pivoted from handing over an entire object (with type object, so in the JS world ...) into a property to actually bind the data against some property like "boxes" or "segments". The data is the same, an array of objects (items). So from the outside there is the usage of the custom control where a JSONModel (ViewModel in this case) is bound against. At runtime the custom control of the outer box has an aggregation (of a custom item so to say) and due to the binding will have X custom items to render.

I'll update this Issue once the "final" or rather improved approach is added to this repository as another custom control example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed question Further information is requested
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant