"onBeforeRendering" or "onAfterRendering" is not called every time the view is opened - sapui5

In my UI5 app, I have a view with a table (sap.m.Table), populated by data coming from the back-end at onInit hook. The problem is that onInit is executed only once per view instance:
It is only called once per View instance, unlike the onBeforeRendering and onAfterRendering hooks.
And if a user decides to leave this view (e.g., back navigation) and to reopen it later, the onInit will not be recalled, and thus the data will not be retrieved again, and the table content will not reflect the possible changes.
To ensure that the data are retrieved every time the view is opened, I tried to get the data at onBeforeRendering, but this hook is also called just once. The only way, that I have found, to force onBeforeRendering to be called every time the view is opened, is to add the following code into onInit method:
onInit: function () {
this.getView().addEventDelegate({
onBeforeShow: this.onBeforeShow,
}, this);
}
My questions:
Why, without the code snippet above in onInit, is the onBeforeRendering not triggered every time the view is displayed?
What does exactly the code snippet above do?
The alternative technique: to use patternMatched and routeMatched. But which one of these three approaches is more common?

Why (...) is the onBeforeRendering not triggered every time the view is displayed?
I think there is a misconception of what "rendering" means. In UI5, when a control is "rendering", its corresponding HTML element is being modified or created in the DOM by the RenderManager. I.e. onBeforeRendering means literally "before the render function of the control (here: View) is called".
onBeforeRendering does not mean that it's called before the paint event from the browser (For that, modern browsers provide high-level APIs such as Intersection Observer).
Rendered controls can be in the DOM but not visible in the viewport at the same time.
Coming back to the question; the reason why on*Rendering is not triggered, is because the control has been already rendered before. This can be seen when user navigates via navTo and then back again. The corresponding view element is already in the DOM, so there is no need to call render again, meaning no on*Rendering triggered.
What does the code snippet exactly do?
this.getView().addEventDelegate({
onBeforeShow: this.onBeforeShow,
}, this);
addEventDelegate adds a listener to the events that are fired on the control (not by the control).
E.g.: The view contains events like afterInit, beforeExit, ...
Doing addEventDelegate({onAfterInit}) won't work since afterInit is fired by this control (view).
Doing addEventDelegate({onmouseover}) works since it's fired on this control.
The same applies to the onBeforeShow. The view doesn't contain any events like beforeShow, afterShow, etc.. Those are fired on the view by the NavContainer (e.g. by <App> on its child, view). Documentation about those events can be found in:
API reference: sap.m.NavContainerChild
Topic Events Fired on the Pages
See also a similar question https://stackoverflow.com/questions/44882085/why-does-onbeforefirstshow-work/44882676
The alternative technique: to use patternMatched (...). But which one of these three approaches is more common?
By "three approaches" I assume you mean:
on*Rendering (1st approach),
on*Show (2nd approach),
and the above mentioned routing events like patternMatched (3rd approach).
The answer is, as always, it depends on what you're trying to achieve. But usually, we:
Use the 2nd approach (NavContainerChild events) if the application does not have a routing concept (no sap.ui5/routing in manifest.json).
Use the 2nd approach with onAfterShow if the intent is to set initial focus after the view is displayed. See How to Set Initial Focus in a View?
Use the 3rd approach to get notified about the route pattern being matched. This approach is commonly used to do something every time the view (NavContainerChild) is displayed, for example, to do Context Binding after navigating to a detail page. How it works:
When router.navTo() is called, the next corresponding view and controller are created.
In onInit of the newly created controller, you assign a patternMatched handler.
On navigation, the URL hash value will change. The router (internally the HashChanger) notices the URL change, leading to Route firing patternMatched by which your handler will be invoked. See the TL;DR below.
⚠️ Personal opinion: avoid 1st approach as an application developer. Avoid doing anything in onBeforeRendering and in onAfterRendering since it's unpredictable how often the render function is called from the viewpoint of the application. For control developers, those hooks are absolutely necessary. But for application developers, there are often better alternatives.
TL;DR
Forget on(Before|After)Rendering. Use the (pattern)Matched event from the route instead:
{
// Controller
onInit() {
const myRoute = this.getOwnerComponent().getRouter().getRoute("routeName");
myRoute.attachPatternMatched(this.onMyRoutePatternMatched, this);
},
onMyRoutePatternMatched(event) {
// your code when the view is about to be displayed ..
},
}

You should use
onInit: function() {
let route = this.getOwnerComponent().getRouter().getRoute("yourroutename");
route.attachPatternMatched(this.onRoutePatternMatched, this);
// ...
},
This route event will be triggered every time the route pattern is matched in your routing config. In the above example, the function onRoutePatternMatched will be called every time the route is loaded through navigation.
onRoutePatternMatched: function(event) {
// this logic will repeat itself every time the route pattern is matched
},

Question #3:
patternMatched is going to get hit when your router is matched on the URL or on the router.navTo method.
routeMatched is going to get hit on the same occasion as patternMatched and when its child routes get navigated to.
Imagine, you have a master view on route A and it’s detail on route B. If the user navigates directly to route B, it makes sense to render the target associated to route B and also the target associated to route A.
To conclude:
patternMatched: direct route match
routeMatched:
The pattern of a route in this router.
The pattern of its sub-route.
The pattern of its nested route. When this occurs, the nestedRoute parameter is set with the instance of nested route.

The answers of Boghyon Hoffmann and Bernard are currently the best solution to the problem of needing an event to perform some action when the page is navigated to.
There is an option where it's possible to configure the target of a route to clear the control aggregation and force it to be re-rendered every time it is navigated to. With this configurantion the events onBeforeRendering and onAfterRendering will be called when navigating to the page. However, it is not a recommended solution as it will cause performance issues with the constant re-rendering and it should set to false when working with sap.m.NavContainer or it would cause transitions stop happening and other issues. If you are working on a legacy project that uses sap.ui.ux3.Shell, which is currently deprecated, it is a possible solution.
So to configure this option we need to set the option clearControlAggregation to true in the manifest.json, it is false by default when using sap.m.routing.Router.
In this link you can check all the options you have for the route targets:
https://sapui5.hana.ondemand.com/#/api/sap.ui.core.routing.Targets
"config": {
"routerClass": "sap.ui.core.routing.Router",
"viewPath": "TestApp.view",
"viewType": "XML",
"clearControlAggregation": true,
"controlId": "Main",
"controlAggregation": "pages",
"transition": "slide"
}
It is also possible to add this option only for a specific target like this:
"targets": {
"Test1": {
"viewType": "XML",
"viewName": "Test1",
"clearControlAggregation": true,
"title": "Test1"
},
"Test2": {
"viewType": "XML",
"viewName": "Test2",
"title": "Test2"
}
}

Related

Not able to navigate from one view to other

I am trying to navigate from one view to another in UI5 application but I am not able to do so. I recently created a copy of an existing view A and renamed it to B. Then I copied the controller of A and renamed it as controller of B. I now want to navigate from A to B view. I have declared the view B in routes just like A (renamed wherever necessary) but I am still not able to navigate. When I click on button in view A, I want to display B. But nothing happens. Console shows no error either. What can be the issue?
In Controller of A:
buttonClick: function(event) {
vc.getOwnerComponent().getRouter().navTo("stockDetails", {
"companyId": vc.companyId,
"stockroomId": vc.stockroomId,
"order": vc.orderNo
}, false);
},
In manifest.json:
Under routes:
{
"name": "stockDetails",
"pattern": "company/{companyId}/stockrooms/{stockroomId}/order/{order}",
"titleTarget": "",
"greedy": false,
"target": [
"menu",
"stockDetails"
]
}
Under targets:
"stockDetails": {
"viewType": "XML",
"transition": "slide",
"clearAggregation": true,
"viewName": "stockroom.stockDetails", //stockDetails View B
"viewLevel": 2,
"controlAggregation": "pages",
"controlId": "app"
}
Here are some possible causes..
Multiple Targets for <App>
It looks like you're trying to assign multiple targets ("menu", "stockDetails") to the same aggregation ("pages") at the same time. Unless it's using master-detail layout or a parent target is involved, try to assign only the right target for the right aggregation. In case of sap.m.App:
{
"name": "stockDetails",
"pattern": "...",
"target": "stockDetails"
}
Otherwise, you'll encounter unexpected behaviors depending on the implementation of the routerClass module.
Identical Patterns
Although the route name may be different ("stockDetails"), you probably have kept the route pattern when copy-pasting from the source route (A).
If it's true, you'll need to provide a distinct pattern for the route "stockDetails" as well. When identical patterns exist, the first route is always taken into account over the others.*
The sequence of the routes in the routes definition is important. As soon as a pattern is matched, the following patterns are ignored.[src]
Here, you can try to define two identical patterns and set the hash accordingly. You'll see that only the first pattern is matched which would be, in your case, the view A.
* Unless the property greedy is active in one of the other routes.
Not sure if you are using the right event for button click.
Can you try using the "press" event, and not the "buttonClick" event?
Before that, try putting a "console.log("Event Firing OK") within your buttonClick event handler, and check the console if the message is coming up. If not, you know the event is not even getting fired, and hence, your navigation code is never executed.
REFERNCE : https://sapui5.hana.ondemand.com/#/api/sap.m.Button/events/press
Best Regards,
Gopal Nair.

Best practice to lazy load data on tab click

Within the onBeforeRendering() function of a view how should I determine if a specific node is present in the model and modify the model to include the node if not? This question is about lazy loading of data to the model for data that will be infrequently accessed by users but would have a performance penalty if loaded with initial model data.
My use case has a list page leading to a detail page for whatever item in the list the use clicks. On the detail page is a set of tabs which expose sub-details related to the selected detail. Specifically a long text description of a the brief for a task.
The tab control leads via manifest.json style routing to display a view in the tabs content area.
This is my current working code which is within the onBeforeRendering event of the view controller:
onBeforeRendering: function(oEvent){
var sPath = this.getView().getBindingContext("Projects").getPath(); // something like /task/6
console.log('Path='+sPath)
var oModel = this.getView().getModel("Projects");
var oTask = oModel.getProperty(sPath + "/brief");
if (oTask) { // looks like /brief exists so must already have loaded the brief
// nothing to do
console.log('Use existing data')
}
else { // /brief not yet present so we need to load it up
console.log('Load new data')
oModel.setProperty(sPath + "/brief", "This is the brief...") // replace with loaddata() from server, use attachRequestCompleted to call function to modify model.
}}
Question - is this the correct approach?
Edit: Based on discussion in this question I modified my code to use an event that fires per display of the view. onBeforeRendering turned out to run without much apparent predictability - which I am sure it has but in any case I wanted a one-per-display event. Also, I fleshed out the code further but retained the basic structure and it appears to do what I wanted.
This is a valid approach. But you should think aboute following use case: What happens if the data you loaded have been changed at the backend? The JSONModel does not give you any support here as it acts dumb data store only.

Waiting for sap.ui.table.Table rendered after bound model has changed

I have sap.ui.table.Table which rows are bound to an JSONModel.
var oListModel = new sap.ui.model.json.JSONModel();
//oTable created here (sap.ui.table.Table)
oTable.setModel(oListModel);
oTable.bindRows("/");
When the table is rendered, i.e. the DOM is created for this table, i need to reference to the DOM to pass the DOM elements (table rows) to a library which will make them draggable.
My problem is: How do i know when the DOM for the table is created after the model has been changed and the table is rerendered? I didnt find any listener. The view controller's listener onAfterRendering() didn't help me.
The model is filled with data after a XMLHTTPRequest is successful. When i set the model in the success handler of the xht request and try to access the DOM elments directly afterwards they don't exist yet.
Thank you for your help!
You can add an event delegate
var oMyTable = new sap.ui.table.Table();
oMyTable.addEventDelegate({
onAfterRendering: function() {
$table = this.getDomRef() ....
}
});
a better way is to extend the control see Using addDelegate to extend a control to use third party functionality
UPDATE
the example in that blog doesn't work anymore fixed here
I had a similar issue recently where i had to access an element after a smartform (where the element is present) rendered onto the view. I posted my query here as well as my final solution (based on the accepted answer of #Dopedev). I am using XML views - including nested XML views - here however and the onAfterRendering didn't help for me as well.
I guess you could look at this solution - like it's mentioned in the accepted answer, it may not be an optimal solution, but it sure works. In my case there is not much of a performance issue with the binding of the DOMNodeInserted since it is being called in the onAfterRendering of the nested view that consists of only the smartform with an immediate unbinding upon finding.
The condition if (id === "yourtableid") { } should be enough to identify and pass on. Since you have a table and thus several child nodes, unbinding is imperative at this point.
Mutation Observer is the preferred method but i guess you may need to check the browser compatibility table at the end of the page to see if it matches your requirements. There is an example here. I have used Mutation Observer (outside of a SAPUI5/openUI5 environment) earlier and found it very convenient(and performant) to listen to DOM insert events. In fact the sap.ui.dt package consists of MutationObserver.js

GWTP Presenter prepareFromRequest - load data into form retrieved from event

I have been trying GWTP for the past couple of weeks and building a small project with it.
Here's the question :
I have a grid widget (attached screenshot) which shows a list of data. On selection of a checkbox of a row and clicking on Edit Request, I get into a detail page.
Since I have all the data (model) to be shown in the detail page in the summary screen presenter itself, I don't want to fetch it from the database again.
So, I did the following :
On selection and clicking edit request, I get the selected model
Make a place request to the detail page
Fire an edit event and pass the selected model as parameter.
I understand that I am doing it wrong because when I select an item and hit Edit Request, the detail page does not receive the selected item yet. It just shows a blank page with no data filled in (obviously, because the place has been reached much before the event has been fired).
Current code :
RequestModel selectedItem = getView().getGrid().getSelectionModel().getSelectedItem();
PlaceRequest placeRequest=new PlaceRequest(NameTokens.initiationedit);
getEventBus().fireEvent(new RequestEditEvent(selectedItem, PHASE_INITIATION));
placeManager.revealPlace(placeRequest);
Personally thought solution : Instead of firing an event, I could make a placerequest with a parameter of the selected item's id and then override the useManualReveal and the prepareFromRequest to fetch data fresh from database.
But is there a way I could avoid database call for the pre-existing data.
If you want to keep your current "RequestEditEvent" solution, then make sure to use #ProxyEvent (see http://code.google.com/p/gwt-platform/wiki/GettingStarted?tm=6#Attaching_events_to_proxies).
Another possibility may be to invert the direction of the event: In your details presenter, fire an event which requests data from the overview presenter.
However, when using the same data across multiple presenters, then it may be a good idea to keep the data in a central model: Create a model class (#Singleton) and inject it everywhere you need it. Then you can make a lookup (by id) from that local model instead of asking the server. In this case, you don't need any events - just the id somewhere, e.g. as a place parameter, or even as a "currentItemId" in the model.
Based on the answer by #Chris Lercher, I used the ProxyEvent. The implementation details are as follows.
In my RequestEditPresenter (the details presenter), I implemented the event handler RequestEditHandler as in
public class RequestEditPresenter extendsPresenter<RequestEditPresenter.MyView, RequestEditPresenter.MyProxy> implements RequestEditHandler{
then in the same RequestEditPresenter override the method in the RequestEditHandler as in
#Override
#ProxyEvent
public void onRequestEdit(RequestEditEvent event) {
getView().setModelToView(event.getRequestModel());
...various other initiation process...
placeManager.revealPlace(new PlaceRequest(NameTokens.initiationedit));
}
Since the Details presenter was a Place, I used the placeManager. For presenters that do not have a NameToken, just call the forceReveal() method

ExtJS 4 - How to load grid store with its params carrying latest values from a form?

I have a window with a search form at the top and grid at the bottom.
User can enter values in the search form and click button - Get Records.
At the click of this button, I load the store of the grid by passing the values in form fields as parameters in following way:
store.load({
params:{
key1:Ext.getCmp('field1').getValue();
}
});
I tried giving parameters in the store proxy itself, but it unfortunately always takes up initial values (values when the form is rendered) and not the latest one entered by the users in the form fields. Following is the method I used for assigning values to params while creating the store:
extraParams:{
key1:Ext.getCmp('field1').getValue();
}
I wanted to seek guidance at two things:
a. While defining a store, can I ensure that store takes latest/current values from the form fields before querying server, so that I don't have to provide these values while calling load function?
This becomes more necessary as I have a paging toolbar at the bottom which carries a refresh button (along with next, last, previous, first icons for navigation).
Now, whenever user clicks at refresh (or any navigation icon), the store gets loaded without the query parameters.
Thus the second thing is:
b. If the answer of 'a' is that - Pass the latest values to parameters manually when calling load function - then how can I write the handler for 'refresh' button and navigation icons (that is, next, last, previous and first) in the paging toolbar, so that I can pass the latest form values to load function.
Thanks for any help in advance.
PS: I am using ExtJS 4.
yourStore.on('beforeload',function(store, operation,eOpts){
operation.params={
status:cmbStatus.getValue(),
value:txtBuscarPor.getValue(),
empresa:'saasd',
app:'dsads'
};
},this);
Related to your question (b) (and because you especially asked for this in the comments section):
I see only one standard way to hook into the PagingToolbar button handlers which is very limited.
Ext.toolbar.Paging fires a 'beforechange' event before it actually changes the current page. See API docs. A listener that returns false will stop the page change.
All other methods require extending Ext classes which wouldn't be a problem if the ComboBox would make it easier to use your own implementation of BoundList (which is the class that renders the dropdown) or pass through config parameters to BoundList resp. the paging toolbar.
I tried to bring this lack of flexibility up on the Ext message board once but was pretty much ignored.
A possible solution for this is to use 'beforeload' event of the store and provide the list of parameters in it. This way, whenever the store is loaded, then its beforeload event is fired and the values picked up are always the latest. Hope this helps someone looking for something similar.