This blogs explains a particular case when a jQuery Ajax callback might not be get called once the request is completed/succeed/failed. In this case, the callback is not found due to incorrect definition of a javascript context. In other words, the request is done but your controller does not respond to it.
I decided to create this blog after a little headache I had until 5 minutes ago but which started last week.
Use jQuery.ajax to call a service
If you are starting to deep dive in SAPUI5/OpenUI5/Hana development/jQuery/Ajax/Javascript like me, there is a good change of you having asked such question:
"How do I call a service on a server from SAPUI5? For example, a XSJS of my Hana server"
Asking the right questions lead to finding the right answers. I had this doubt recently and after reading some documentation, checking GitHub repositories, reviewing openSAP courses and watching some Thomas Jung's videos.... I realized that jQuery could solve this problem with its "ajax" function. Like the snippet below:
// (...) var l_url = "/my/package/project/services/do_stuff.xsjs"; // Some back end guru coded some server side javacript here jQuery.ajax( { url: l_url, data: { first_name: sap.ui.getCore().byId("txf_first").getValue(), last_name: sap.ui.getCore().byId("txf_last").getValue() }, type: "POST", } // (...)
Handling Ajax response with jQuery
After succeeding on answering your first doubt, the next one follows:
"How can I handle server's response with Ajax?"
It's easy to see that jQuery's documentation specifies some parameters where you can include your own callback functions as below.
// (...) var l_url = "/my/package/project/services/do_stuff.xsjs"; // Some back end guru coded some server side javacript here jQuery.ajax( { url: l_url, error: function(){ console.log("OMG! This would never happen according to the functional consultant!"); }, success: function(){ console.log("Holy moly! It works!"); }, data: { first_name: sap.ui.getCore().byId("txf_first").getValue(), last_name: sap.ui.getCore().byId("txf_last").getValue() }, type: "POST", } // (...)
Life is happy. You can handle anything you want from now on. However, maybe you have some big/complex handling logic which shouldn't be included directly inside a anonymous function of an ajax call. Maybe it's a good idea to have a specify function inside your MVC controller for that. For example, as shown below.
sap.ui.controller("registration.main", { /** * Called when a controller is instantiated and its View controls (if available) are already created. * Can be used to modify the View before it is displayed, to bind event handlers and do other one-time initialization. * @memberOf currencyregistration.main */ onInit: function() { }, /** * Similar to onAfterRendering, but this hook is invoked before the controller's View is re-rendered * (NOT before the first rendering! onInit() is used for that one!). * @memberOf currencyregistration.main */ // onBeforeRendering: function() { // // }, /** * Called when the View has been rendered (so its HTML is part of the document). Post-rendering manipulations of the HTML could be done here. * This hook is the same one that SAPUI5 controls get after being rendered. * @memberOf currencyregistration.main */ // onAfterRendering: function() { // // }, /** * Called when the Controller is destroyed. Use this one to free resources and finalize activities. * @memberOf currencyregistration.main */ // onExit: function() { // // } onPressRegister: function(){ var l_url = "/my/package/project/services/do_stuff.xsjs"; jQuery.ajax( { url: l_url, error: this.onRequestError, success: this.onRequestSuccess, data: { first_name: sap.ui.getCore().byId("txf_first").getValue(), last_name: sap.ui.getCore().byId("txf_last").getValue() }, type: "POST", }); }, onRequestError: function(){ console.log("OMG! This would never happen according to the functional consultant!"); // show message // (...) // create log // (...) // redirect page // (...) }, onRequestSuccess: function(){ console.log("Holy moly! It works!"); // bind control // (...) // Adjust layout // (...) // show message // (...) }, });
Now you have a cleaner code. Great!
Looking at the bigger picture
Note that in my example the ajax call is done inside a function called "onPressRegister". This was on purpose and this information is crucial to understand the problem I want to show.This function is triggered when a button from my view is pressed. The link between the "press" event and the controller function is done initially inside the view. More specifically, inside the Button constructor.
sap.ui.jsview("registration.main", { /** Specifies the Controller belonging to this View. * In the case that it is not implemented, or that "null" is returned, this View does not have a Controller. * @memberOf registration.main */ getControllerName : function() { return "registration.main"; }, /** Is initially called once after the Controller has been instantiated. It is the place where the UI is constructed. * Since the Controller is given to this method, its event handlers can be attached right away. * @memberOf registration.main */ createContent : function(oController) { var layout = new sap.ui.commons.layout.VerticalLayout(); layout.addContent( new sap.ui.commons.Label({ labelFor: "txf_first", text: "First Name", })); layout.addContent( new sap.ui.commons.TextField("txf_first",{ maxLength : 20, })); layout.addContent( new sap.ui.commons.Label({ labelFor: "txf_last", text: "Last Name", })); layout.addContent( new sap.ui.commons.TextField("txf_last"),{ maxLength : 30, }); layout.addContent( new sap.ui.commons.Button("btn_register", { text: "Register", press: oController.onPressRegister, // HERE!! When the button is pressed, this controller function is called })) return layout; } });
Remember when we replaced our anonymous callback functions with named controller functions? On that step, we broke our code. I really suggest you to give it a try creating a simple SAPUI5 application and creating a simple XSJS as below.
do_stuff.xsjs
var first = $.request.parameters.get("first_name"); var last = $.request.parameters.get("last_name"); if (first != last) { $.response.status = $.net.http.OK; $.response.setBody("OK"); } else { $.response.status = $.net.http.NOT_ACCEPTABLE; $.response.setBody("NOT OK"); }
Be aware with your JavaScript context
Why? You might ask. If you debug your SAPUI5 application with Firebug for example you will notice that "this.onRequestError" and "this.onRequestSuccess" are undefined inside controller function "onPressRegister". This looks crazy at first, second, third, Nth sight... but it is the truth.
You might have seen some controller logic where a function inside a controller calls another one using this.anotherFunction. You can definitely do that if the context on which the first function is called is the controller itself. As it happens with the "hook" functions onInit and onExit for example.
So where is the catch?
You might read jQuery documentation all day long. Unfortunately, you will be cold as I was for some time. There is nothing wrong with the Ajax call. The problem is with our attachment between the "press" event of the button and the controller function.
Let's go back to our button constructor.
layout.addContent( new sap.ui.commons.Button("btn_register", { text: "Register", press: oController.onPressRegister, // When the button is pressed, this controller function is called })
If you attach a controller function like this, it will be called using the button context and not the controller context. "press" is a button property which receives a function. That function might be a property from your controller. However, it doesn't mean that the context inside this function is the controller. Actually, it will be the button by default. The same issue happens if you use the "attachPress" function without filling the "
" parameter. Let's see how we can fix this.oListener
First, we will not attach the controller method to the button event inside our view anymore.
layout.addContent( new sap.ui.commons.Button("btn_register", { text: "Register", // press: oController.onPressRegister, // This link will be done inside the controller })
We will make do this with our onInit function from the controller using the attachPress function aforementioned.
onInit: function() { sap.ui.getCore().byId("btn_register").attachPress(null, this.onPressRegister, this); // The 3rd parameter means the context on which the function will be called. // Here "this" means "this controller" and not "this button" },
And this solves the "ajax problem". You could also use this method inside your view and passing the controller reference inside the last parameter as well.
Conclusion
So in the end of the day the problem was not jQuery neither Ajax. The problem was the javascript scope or context if you will. So, this situation could occur in any event which is not properly registered no matter if you are using ajax or not. I hope this post emphasizes the use of onInit to register listeners avoiding views to become root cause of controller issues.
This might sound too basic for some front end developers or maybe too specific but I'm sure that lots of people will get into this trouble on their first steps with jQuery/Ajax/SAPUI5/Javascript/MVC.
Do not forget to comment with your thoughts!
Fábio Pagoti