Skip to Content

Table of contents

1. Introduction

2. Going deeper

2.1. Preparation

2.2. New controllers, views and classes

2.2.1. App.controller.js/App.view.XML modification

2.2.2. BaseController.js

2.2.3. ErrorHandler.js

2.2.4. Main.controller.js/Main.view.XML

2.2.5. NotFound.controller.js/NotFound.viiw.XML

2.3. Component, Manifest and Routing

2.3.1. Component

2.3.2. Manifest

2.3.3. Routing

3. Final toughts

 

1. Introduction

If you are digging deeper and deeper into UI5 app developement, sooner or later you realize that you need something more than standard templates WebIDE gives to you. In this blog post I’ll try to show you how to build your own template app with all core functionalities every app (simple or complex) needs.

For that purpose I will use following:

  • IDE: SAP WebIDE Personal Edition
  • View format: XML
  • oData version: v2
  • SAPUI5 Version: 1.52

 

2. Going deeper

2.1. Preparation

Let’s create simple application by choosing “File->New->Project from Template” from WebIDE menu. Then slelect “SAPUI5 Application” as your template project:

Name it “AppTemplate” for the purpose of this tutorial. Change First view name for “Main”:

Click “Finish”.

After that you should get something like this:

Now we want to add/change few things which make your template actually universal.

2.2. New controllers, views and classes

We look first at the controller and view folders. If we want that our application will be ease to extend and what’s more important – to understand for future developers, we need to create four additional controllers/classes and two views:

Controllers/classes:

  • App.controller.js – it will act as container for all views (Control id in routing configuration)
  • BaseController.js – it will simplify reuse of your code in application.
  • ErrorHandler.js  – it will help you to show the user information about service/server errors.
  • NotFound.controller.js – It show user information in case of errors in navigation

Views:

  • App.view.XML – it will act as container for all view (Control id in routing configuration)
  • NotFound.view.XML – for showing user information in case of errors in navigation

If we get this all together you should see something similar to this:

Now when we have created new controllers/classes and views we need to discuss what they actually do in our application. Additionally we need to do some modifications in code to suit to our needs. Let’s do this one by one.

2.2.1. App.controller.js/App.view.XML modification

App.controller.js/App.view.XML – it acts like container for all views, it inherit from BaseController and play a main role in routing process.

App.controller.js

sap.ui.define([
	"./BaseController",
	"sap/ui/model/json/JSONModel",
	"sap/ui/core/routing/History"
], function(BaseController, JSONModel, History) {
	"use strict";

	return BaseController.extend("AppTemplate.controller.App", {
		onInit: function() {
			
			// declare controller variable
			var oViewModel,
				fnSetAppNotBusy,
				iOriginalBusyDelay = this.getView().getBusyIndicatorDelay();

			/* 
			Create the model for your view. 
			This model can set/reset all specific view paramaters.
			In this case we want to set busy and delay values
			*/
			oViewModel = new JSONModel({
				busy: true,
				delay: 0
			});

			/*
			set model to our view which actually will be set in BaseController.
			This is simple example of resuing our code. You can always refer to
			function in BaseController from any view in your app
			*/
			this.setModel(oViewModel, "appView");

			// function remove busy indicator
			fnSetAppNotBusy = function() {
				oViewModel.setProperty("/busy", false);
				oViewModel.setProperty("/delay", iOriginalBusyDelay);
			};

			/* 
			use promise to check if metadata from our service is loaded.
			If so run function fnSetAppNotBusy which set busy to false.
			It will run only once just on start of the application.
			The user wont be able to do anything unless service is avilable 
			*/ 
			this.getOwnerComponent().getModel().metadataLoaded().
			then(fnSetAppNotBusy);

			// apply content density mode to root view
			this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass());
		}
	});
});

Note that as I said earlier we inherit from BaseController.js class in every view. Once again in this way we can easily reuse our code.

 

App.view.XML

<mvc:View controllerName="AppTemplate.controller.App" xmlns:html="http://www.w3.org/1999/xhtml" xmlns="sap.m" xmlns:mvc="sap.ui.core.mvc" displayBlock="true" 
busy="{appView>/busy}" 
busyIndicatorDelay="{appView>/delay}">
	<App id="app"/>
</mvc:View>

Note that here we set properties values we have defined in controller (we named oViewModel as “appView”).

2.2.2. BaseController.js

It’s the class where you can put all the code you want to reuse. Since all views inherit from BaseController.js you can always simple refer to function in that class from any view. You can write here e.g. common formatters, you can use across all views.

sap.ui.define([
	"sap/ui/core/mvc/Controller",
	"sap/ui/core/format/DateFormat",
	"sap/ui/core/MessageType",
	"sap/ui/core/routing/History"
], function(Controller, History) {
	"use strict";

	return Controller.extend("AppTemplate.controller.BaseController", {

		/*
		Using this function you can manipulate router on any view.
		Just remember to call it first on view
		*/
		getRouter: function() {
			return sap.ui.core.UIComponent.getRouterFor(this);
		},

		/*
		By calling this function in your view you can set translated texton view.
		Example: 'this.getResourceBundle().getText("Main.saveButton");'
		*/
		getResourceBundle: function() {
			return this.getOwnerComponent().getModel("i18n").getResourceBundle();
		},

		/*
		Use this when you set an action on Back button on any view.
		If you have history it will show you the previous view
		if not router wil redirect you to first view of your app
		*/
		onNavBack: function(oEvent) {
			var oHistory, sPreviousHash;
			oHistory = History.getInstance();
			sPreviousHash = oHistory.getPreviousHash();
			if (sPreviousHash !== undefined) {
				window.history.go(-1);
			} else {
				this.getRouter().navTo("appMain", {}, true /*no history*/ );
			}
		},

		/* 
		Getter and setter for model.
		*/
		getModel: function(sName) {
			return this.getView().getModel(sName);
		},

		setModel: function(oModel, sName) {
			return this.getView().setModel(oModel, sName);
		}
	});
});

2.2.3. ErrorHandler.js

It is built for error handling in our app. Instance of this object should be created in Component.js. It will automatically show service/server error to the user.

sap.ui.define([
	"sap/ui/base/Object",
	"sap/m/MessageBox"
], function(Object, MessageBox) {
	"use strict";

	return Object.extend("AppTemplate.controller.ErrorHandler", {

		/**
		 * Handles application errors by automatically attaching to the model events and displaying errors when needed.
		 * @class
		 * @param {sap.ui.core.UIComponent} oComponent reference to the app's component
		 * @public
		 * @alias mycompany.myapp.controller.ErrorHandler
		 */
		constructor: function(oComponent) {
			this._oResourceBundle = oComponent.getModel("i18n").getResourceBundle();
			this._oComponent = oComponent;
			this._oModel = oComponent.getModel();
			this._bMessageOpen = false;
			this._sErrorText = this._oResourceBundle.getText("errorText");

			this._oModel.attachMetadataFailed(function(oEvent) {
				var oParams = oEvent.getParameters();
				this._showMetadataError(oParams.response);
			}, this);

			this._oModel.attachRequestFailed(function(oEvent) {
				var oParams = oEvent.getParameters();

				// An entity that was not found in the service is also throwing a 404 error in oData.
				// We already cover this case with a notFound target so we skip it here.
				// A request that cannot be sent to the server is a technical error that we have to handle though
				if (oParams.response.statusCode !== "404" || (oParams.response.statusCode === 404 && oParams.response.responseText.indexOf(
					"Cannot POST") === 0)) {
					this._showServiceError(oParams.response);
				}
			}, this);
		},

		/**
		 * Shows a {@link sap.m.MessageBox} when the metadata call has failed.
		 * The user can try to refresh the metadata.
		 * @param {string} sDetails a technical error to be displayed on request
		 * @private
		 */
		_showMetadataError: function(sDetails) {
			MessageBox.error(
				this._sErrorText, {
					id: "metadataErrorMessageBox",
					details: sDetails,
					styleClass: this._oComponent.getContentDensityClass(),
					actions: [MessageBox.Action.RETRY, MessageBox.Action.CLOSE],
					onClose: function(sAction) {
						if (sAction === MessageBox.Action.RETRY) {
							this._oModel.refreshMetadata();
						}
					}.bind(this)
				}
			);
		},

		/**
		 * Shows a {@link sap.m.MessageBox} when a service call has failed.
		 * Only the first error message will be display.
		 * @param {string} sDetails a technical error to be displayed on request
		 * @private
		 */
		_showServiceError: function(sDetails) {
			if (this._bMessageOpen) {
				return;
			}
			this._bMessageOpen = true;
			MessageBox.error(
				this._sErrorText, {
					id: "serviceErrorMessageBox",
					details: sDetails,
					styleClass: this._oComponent.getContentDensityClass(),
					actions: [MessageBox.Action.CLOSE],
					onClose: function() {
						this._bMessageOpen = false;
					}.bind(this)
				}
			);
		}
	});
});

2.2.4. Main.controller.js/Main.view.XML

It’s a main controller and view of our template app. When application starts, this view will be presented to the user

Main.controller.js

sap.ui.define([
	"./BaseController",
	"sap/ui/model/json/JSONModel"
], function(BaseController, JSONModel) {
	"use strict";

	return BaseController.extend("AppTemplate.controller.Main", {
		/*
		This function will run only once at the first call of the view
		*/
		onInit: function() {
			//put your logic here

			// As always, create view model here
			var oViewModel = new JSONModel({
				calSelectedDate: new Date(),
				isDateModified: false
			});
			
			// and set it for this view
			this.setModel(this.oViewModel, "MainView");

			// call instance of the router and check is it correct
			var oRouter = this.getRouter();
			oRouter.getRoute("appMain").attachMatched(this._onRouteMatched, this);
		},

		// checking if declared route is matched
		_onRouteMatched: function(oEvent) {
			/*
			Put you logic here.
			Note that this will be call by every enter into this view.
			Once again if you want something to run only once add that code to onInit function.
			*/
		}
	});
});

Note that Controller was changed to BaseController.

 

Main.view.XML

<mvc:View controllerName="AppTemplate.controller.Main" xmlns:html="http://www.w3.org/1999/xhtml" xmlns:mvc="sap.ui.core.mvc"
	displayBlock="true" xmlns="sap.m">
	<App>
		<pages>
			<Page title="{i18n>title}">
				<content>
				</content>
			</Page>
		</pages>
	</App>
</mvc:View>

Here you don’t have to change anything. You will fill this out as you like in development process.

2.2.5. NotFound.controller.js/NotFound.view.XML

When user enter wrong url, choosing wrong not existing data etc., you can show this to her/him.

NotFound.controller.js

sap.ui.define([
	"./BaseController"
], function(BaseController) {
	"use strict";

	return BaseController.extend("AppTemplate.controller.NotFound", {

	});
});

Just remember to change Controller to BaseController.

 

NotFound.view.XML

<mvc:View controllerName="AppTemplate.controller.NotFound" xmlns="sap.m" xmlns:mvc="sap.ui.core.mvc">
	<MessagePage title="{i18n>NotFound}" text="{i18n>NotFound.text}" description="{i18n>NotFound.description}" showNavButton="true"
		navButtonPress="onNavBack"/>
</mvc:View>

Here you have to use some of translation elements plus onNavBack function we defined in BaseController.js.

2.3. Component, Manifest and Routing

Component and manifest are the “heart” of your app. Component.js runs before all of the views. In manifest you will put all configuration of your app (e.g. routing configuration).

2.3.1. Component

sap.ui.define([
	"sap/ui/core/UIComponent",
	"sap/ui/Device",
	"AppTemplate/model/models",
	"AppTemplate/controller/ErrorHandler"
], function(UIComponent, Device, models, ErrorHandler) {
	"use strict";

	return UIComponent.extend("AppTemplate.Component", {

		metadata: {
			manifest: "json"
		},

		/**
		 * The component is initialized by UI5 automatically during the startup of the app and calls the init method once.
		 * @public
		 * @override
		 */
		init: function() {
			// call the base component's init function
			UIComponent.prototype.init.apply(this, arguments);

			// initialize the error handler with the component
			this._oErrorHandler = new ErrorHandler(this);

			// set the device model
			this.setModel(models.createDeviceModel(), "device");

			// create the views based on the url/hash
			this.getRouter().initialize();
		},

		destroy: function() {
			this._oErrorHandler.destroy();
			// call the base component's destroy function
			UIComponent.prototype.destroy.apply(this, arguments);
		},

		getContentDensityClass: function() {
			if (this._sContentDensityClass === undefined) {
				// check whether FLP has already set the content density class; do nothing in this case
				if (jQuery(document.body).hasClass("sapUiSizeCozy") || jQuery(document.body).hasClass("sapUiSizeCompact")) {
					this._sContentDensityClass = "";
				} else if (!Device.support.touch) {
					// apply "compact" mode if touch is not supported
					this._sContentDensityClass = "sapUiSizeCompact";
				} else {
					// "cozy" in case of touch support; default for most sap.m controls, but needed for desktop-first controls like sap.ui.table.Table
					this._sContentDensityClass = "sapUiSizeCozy";
				}
			}
			return this._sContentDensityClass;
		}
	});
});

2.3.2. Manifest

{
	"_version": "1.5.0",
	"sap.app": {
		"id": "AppTemplate",
		"type": "application",
		"i18n": "i18n/i18n.properties",
		"applicationVersion": {
			"version": "1.0.0"
		},
		"title": "{{appTitle}}",
		"description": "{{appDescription}}",
		"sourceTemplate": {
			"id": "ui5template.basicSAPUI5ApplicationProject",
			"version": "1.40.12"
		},
		"dataSources": {
			"main": {
				"uri": "/sap/opu/odata/SAP/YOUR_ODATA/",
				"type": "OData",
				"settings": {
					"odataVersion": "2.0",
					"localUri": "localService/metadata.xml"
				}
			}
		}
	},

	"sap.ui": {
		"technology": "UI5",
		"icons": {
			"icon": "",
			"favIcon": "",
			"phone": "",
			"phone@2": "",
			"tablet": "",
			"tablet@2": ""
		},
		"deviceTypes": {
			"desktop": true,
			"tablet": true,
			"phone": true
		},
		"supportedThemes": [
			"sap_hcb",
			"sap_belize"

		]
	},

	"sap.ui5": {
		"rootView": {
			"viewName": "AppTemplate.view.App",
			"type": "XML"
		},
		"dependencies": {
			"minUI5Version": "1.30.0",
			"libs": {
				"sap.ui.core": {},
				"sap.m": {},
				"sap.ui.layout": {},
				"sap.ushell": {},
				"sap.collaboration": {},
				"sap.ui.comp": {},
				"sap.uxap": {}
			}
		},
		"contentDensities": {
			"compact": true,
			"cozy": true
		},
		"models": {
			"i18n": {
				"type": "sap.ui.model.resource.ResourceModel",
				"settings": {
					"bundleName": "AppTemplate.i18n.i18n"
				}
			},
			"": {
				"dataSource": "main",
				"type": "sap.ui.model.odata.v2.ODataModel",
				"settings": {
					"loadMetadataAsync": true,
					"json": true,
					"bJSON": true,
					"defaultBindingMode": "TwoWay",
					"useBatch": true,
					"refreshAfterChange": false,
					"disableHeadRequestForToken": true
				}
			}
		},
		"routing": {
			"config": {
				"routerClass": "sap.m.routing.Router",
				"viewType": "XML",
				"viewPath": "AppTemplate.view",
				"controlId": "app",
				"controlAggregation": "pages",
				"transition": "slide",
				"bypassed": {
					"target": "notFound"
				}
			},
			"routes": [{
				"pattern": "",
				"name": "appMain",
				"target": "main"
			}],
			"targets": {
				"main": {
					"viewName": "Main",
					"viewLevel": 1
				},
				"notFound": {
					"viewName": "NotFound",
					"transition": "show"
				}
			}
		},

		"resources": {
			"css": [{
				"uri": "css/style.css"
			}]
		}
	}
}

Note that in section “dataSources” we need to define our oData. For test purpose you can define here your already existing oData but you also can leave it as it is in our example and change it in the development process.

 

2.3.3. Routing

Let’s talk about routing section in our manifest. Please take a look at following code:

"rootView": {
			"viewName": "AppTemplate.view.App",
			"type": "XML"
		}

Here we have to change root view of our app (App view) where routing configuration will try to find control with given ID.

 

Routing section:

"routing": {
			"config": {
				"routerClass": "sap.m.routing.Router",
				"viewType": "XML",
				"viewPath": "AppTemplate.view",
				"controlId": "app",
				"controlAggregation": "pages",
				"transition": "slide",
				"bypassed": {
					"target": "notFound"
				}
			},
			"routes": [{
				"pattern": "",
				"name": "appMain",
				"target": "main"
			}],
			"targets": {
				"main": {
					"viewName": "Main",
					"viewLevel": 1
				},
				"notFound": {
					"viewName": "NotFound",
					"transition": "show"
				}
			}
		},

You see that we have added here the whole section of routing configuration. Just for now we have only two targets declared: “main” and “notFound”. When you need more views you can simply add it here.

 

There is one last thing when you working with WebIDE personal edition (for the local test only). You need to define destination with oData target. This is done in neo-app.json:

{
      "path": "/sap/opu/odata",
      "target": {
        "type": "destination",
        "name": "NAME_OF_YOUR_DEV_SERVER",
        "entryPath": "/sap/opu/odata"
      },
      "description": "NAME_OF_YOUR_DEV_SERVER"
    }

Just remember that destination should be also created in orion local server configuration under the path:eclipse\config_master\service.destinations\.

Then name “NAME_OF_YOUR_DEV_SERVER” should be the same as name of your destination in orion configuration.

 

3. Final toughts

You can export and then import our newly created “AppTemplate” in WebIDE PE under different name . On the other hand, you can push it on your company internal GIT server and use it with your whole development team. Anyway, I hope it will create the same standard of UI5 application development for whole your team.

To report this post you need to login first.

2 Comments

You must be Logged on to comment or reply to a post.

Leave a Reply