SAPUI5 Getting Started (SAP HANA)

Aus MattWiki

This page contains basic knowledge on how to start with SAPUI5 development on SAP HANA development stack and also how the SAPUI5 application looks like.

The repository for the examples can be found here: https://github.com/mattxdev/opensap-hana7

SAPUI5 App Structure

Dynamic odataView App

We create an app odataView. All of its files will be located inside

/web/resources/odataView/

Content:

Relative Path File Function
./ index.html
  • Load SAPUI5 bootstrap
  • Load error handler
  • Load startup.js for session info, unified shell
./ Component.js
  • Represents the real start of the application
  • Initialize SAPUI5 component
  • Define manifest
  • Create instance of JSON configuration model
  • Load first view
./ manifest.json

Specifies:

  • libraries and versions
  • device types and supported themes
  • data sources
  • root view
  • dependencies
  • odata models (Binding data source to model)
  • text bundles
./i18n/ i18n_en.properties
  • text bundles for language EN
./view/ App.view.xml

Contains:

  • Shell control and overall flow of the page
  • Fragment for embedded areas
./view/ MRead.fragment.xml

Contains:

  • Input fields
  • Fragment for header area
  • Fragment for item area
./view/ MTableHead.fragment.xml
  • SmartTable control for display of Purchase Order Header returned by XSODATA service
./view/ MTableItem.fragment.xml
  • SmartTable control for display of Purchase Order Items
./controller/ App.controller.js

Contains methods:

  • onInit: Model binding to table
  • callMultiService: Handles call of odata service to dynamically create columns in table controls
  • callExcel: Calls download of PO data in Excel format
./controller/ Base.controller.js

Base controllers can contain functions and share them to multiple app controllers.

./model/ formatter.js, grouper.js, GroupSortState.js, models.js

Model subdirectory contains reusable functions like formatters

Basic OData App Skeleton from SAP

Source: https://github.com/SAP-samples/hana-xsa-opensap-hana7/blob/snippets_2.3.2/ex4/odataBasic.zip

The file contains all necessary subfolders and should be imported in your project into this module:

/web/resources

This will create a subfolder for the skeleton app named:

/web/resources/odataBasic

Tasks to be done in order to bind an OData service to the SmartTable defined wihtin the App.view in the skeleton app:

File Section Task
manifest.json "sap.app"
  • Specify data source
  • Define model
manifest.json "models"
  • Create model
  • Bind model to data source defined above
controller/App.controller.js onInit
  • Set model created above to oTable table control
  • Specify visible fields for SmartTable

Text Bundles

Text bundles are best located in a subfolder of:

/web/resources/

Contents:

Relative Path File Function
./i18n/ messagebundle.properties Default language message bundle
./i18n/ messagebundle_de.properties Language DE message bundle

Add code:

/* Language Resource Loader */
jQuery.sap.require("jquery.sap.resources");
var sLocale = sap.ui.getCore().getConfiguration().getLanguage();
var oBundle = jQuery.sap.resources({url: "./i18n/messagebundle.properties", locale: sLocale});

// create the button instance
var myButton = new sap.m.Button("btn");

// set properties, e.g. the text (there is also a shorter way of setting several properties)
// myButton.setText("Hello World!");
myButton.setText(oBundle.getText("helloworld"));

Listing: https://github.com/mattxdev/opensap-hana7/blob/master/web/resources/index.html

Local SAPUI5 Micro Service

SAPUI5 libraries can be used from the public SAPUI5 libraries.

Since SAP HANA 2.0 SPS 03 XS Advanced is capable of providing a micro service itself for local consumption.

A local micro service can be created either in XS Advanced Cockpit at

  • HANAExpress -> development space -> Services -> Service Marketplace -> sapui5_sb

or via command line with:

xs create-service sapui5_sb sapui5-1.52 openSAPHANA_00-ui5

The service has to be added in:

Path File Function
/ mta.yaml Create new resource and add it to modules
/web/ xs-app.json Use replace capability of app router to insert UI5 service URL dynamically
/web/ index.html Fetch bootstrap from resource by utilizing variable

How to Implement

Dynamic Table Field List Based on OData Service Metadata

This example is based on the odataView App from above.

Visible table columns can eiter be hard coded in controller/App.controller.js in OnInit with oTable.setInitiallyVisibleFields:

onInit: function() {
  this.getView().addStyleClass("sapUiSizeCompact"); // make everything inside this View appear in Compact mode
  var oConfig = this.getOwnerComponent().getModel("config");
  var userName = oConfig.getProperty("/UserName");
  var bpModel = this.getOwnerComponent().getModel("bpModel");
  var oTable = this.getView().byId("bpTable");
		
  oTable.setModel(bpModel);
  oTable.setEntitySet("BusinessPartners");
  oTable.setInitiallyVisibleFields("PARTNERID,COMPANYNAME,PARTNERROLE");
},

Or they can be made visible dynamically by looking them up in the meta data of the service at dataServices.schema[0].entityType[0].property:

function fnLoadMetadata() {
  try {
    oTable.setModel(bpModel);
    oTable.setEntitySet("BusinessPartners");
    var oMeta = bpModel.getServiceMetadata();
    var headerFields = "";
    for (var i = 0; i < oMeta.dataServices.schema[0].entityType[0].property.length; i++) {
      var property = oMeta.dataServices.schema[0].entityType[0].property[i];
      headerFields += property.name + ",";
  	 }
  	 oTable.setInitiallyVisibleFields(headerFields);
  } catch (e) {
  	 console.log(e.toString());
  }
} 
bpModel.attachMetadataLoaded(bpModel, function() {
  fnLoadMetadata();
});
fnLoadMetadata();

Hint: If your service metadata has multiple tables then you probably have to loop through the different entityType or even schema.

OData CRUD with Input List and SmartTable

Based on the odataView app from above the following has to be changed:

(The results are implemented in the odataCRUD app.

Model in manifest.json has to be changed:

    "userModel": {
      "dataSource": "userService",
      "type": "sap.ui.model.odata.v2.ODataModel",
      "preload": true,
      "settings": { 
        "useBatch": false,
        "json": true,
        "defaultBindingMode": "TwoWay",
        "defaultUpdateMethod": "PUT"
      }
    },  

Notably is the defaultBindingMode TwoWay. This means that changes, which happen in the odata Service, are automatically reflected in bound UI controls and vice versa. When changes happen in the UI controls, they are automatically reflected in the model and sent to the server.

New event handlers in app controller for the buttons:

  • callUserService -> for create operation -> calls the oModel.create function
  • callUserUpdate -> for update operation -> calls the oModel.submitChanges function

Batch Insert with XMLfragment based Dynamically Created Dialog

Batching enables serverside parallelization if the queries are sent to the server as a batch operation.

Example Application: odataCRUD --> See event handler for onBatchDialogPress and the respective buttons and fragments.

Event handler getItem creates dynamically dialog elements in the UI for add and delete icon as well as input fields for first name, last name and email.

Event handler onSubmitBatch handles the batch processing. For that first a JSON object with all users will be created.

Class sap.ui.model.odata.v2.ODataModel parameter oParams.usetBatch = true overwrites the settings from the manifest.json. Also a new model will be created to use the batch option.

This enables SAPUI5 to communicate with a odata service with different settings.

When batch is used a mParams.groupId will be available. It should be set.

batchModel.create sends the records actually one by one to the server. But as the model was created with the useBatch option all the records will be sent to the server as a batch after all commands are done on the client side.

Listing: https://github.com/mattxdev/opensap-hana7/blob/master/web/resources/odataCRUD/controller/App.controller.js

Documentation: https://sapui5.hana.ondemand.com/#/api/sap.ui.model.odata.v2.ODataModel

OData V2 Deep Insert with Content-ID and Links

Example Application:

/web/resource/odataDeep

Deep inserts allow to insert data into tables with parent-child relationship and ensure that all the inserted records of both tables are correctly related to each other.

In the example below this actually happens by inserting records into both tables seperately using content ID and then updating the second table to have the same primary keys as the first table.

Definition of OData Service businessPartnersAddresses.xsodata:

service  { 
  
  "MD.BusinessPartner"
      as "BusinessPartners"
      navigates ("ToAddresses" as "AddRef")
      create events(before 
        "sap.hana.democontent.epm.services:businessPartnersAddresses.xsjslib::bp_create_before_exit");
	
  "MD.Addresses"
      as "Addresses"
      without ("POINT")
      create events(before 
        "sap.hana.democontent.epm.services:businessPartnersAddresses.xsjslib::address_create_before_exit");
 	
  association "ToAddresses" principal "BusinessPartners"("ADDRESSES.ADDRESSID")
      multiplicity "1" 
      dependent "Addresses"("ADDRESSID") multiplicity "1"
        over "MD.Addresses"
        principal("ADDRESSID") dependent ("ADDRESSID")
        update using 
          "sap.hana.democontent.epm.services:businessPartnersAddresses.xsjslib::assocation_create_exit";	
}

The service has three user exits:

  • bp_create_before_exit: Generates partnerID from sequence and fills some fields before create
  • address_create_before_exit: Generates addressID from sequence and fills some fields before create
  • association_create_exit: Fetches both previously created records and updates the business partner record with the addressID from the address record.

Listing of businessPartnersAddresses.xsjslib containing the three exits:

https://github.com/mattxdev/opensap-hana7/blob/master/xsjs/lib/sap/hana/democontent/epm/services/businessPartnersAddresses.xsjslib

In order to use batch with content-ids the SAPUI5 odata object cannot be used. Instead the request will be build by the controller during run time. Calling of the services utilizes jQuery.ajax and must also handle CSRF tokens.

index.html

...
<script type="text/javascript" src="/common/csrf.js" ></script> 
...

Listing of app controller which builds the complete OData batch request (unlike earlier SAPUI5 exercises where the framework did all the work):


Notice the Content-ID option. It works as a place holder ($1 and $2) to represent the keys which will not be generated until the server side has processed the work. The $1 and $2 placeholders can be used in the association URL and work as placeholders for actual values. XSODATA framework on the server side will replace this values during processing.

Fiori Annotations with OData V4 and CDS

Example Application:

/web/resource/poListV4

OData V4 and CDS enable a large range of metadata definitions for Fiori applications to allow nearly everything about the UI like:

  • Value helps
  • Sort Options
  • Output columns

The metadata can be placed at different levels of the CDS like:

  1. Lowest level: At the entity definitions
  2. At the service level or UI page
Types of annotations
Type Description
title Text description used as field label and column headers. Can be used at reusable type level or within entity or view definition
FieldControl Set state of input field to i.e. Read Only or Mandatory
Measures i.e. amount of fields gets measure from currency column
ValueList i.e. currency column which points to foreign entity CURRENCY. This allows to pupulate drop down list boxes or fill value helps within the UI from this definition.

Code excerpts for entity level annotations from /db/PurchaseOrder.cds:

type HistoryT { 
	CREATEDBY : BusinessKey @(title: '{i18n>CreatedBy}', Common.FieldControl: #ReadOnly);
	CREATEDAT : SDate  @(title: '{i18n>CreatedAt}',  Common.FieldControl: #ReadOnly);
	CHANGEDBY : BusinessKey  @(title: '{i18n>ChangedBy}',  Common.FieldControl: #ReadOnly);
	CHANGEDAT : SDate  @(title: '{i18n>ChangedAt}',  Common.FieldControl: #ReadOnly);
};

entity Headers {
	key PURCHASEORDERID : Integer @(
			title: '{i18n>po_id}', 
			Common.FieldControl: #Mandatory, 
			Search.defaultSearchElement, 
			Common.Label: '{i18n>po_id}'
		);
		ITEMS           : association to many Items on ITEMS.POHeader = $self @(
			title: '{i18n>po_items}', 
			Common: { Text: {$value: ITEMS.PRODUCT, "@UI.TextArrangement": #TextOnly }}
		);
		HISTORY         : HistoryT;
		NOTEID          : BusinessKey null @title: '{i18n>notes}';
		PARTNER         : BusinessKey @title: '{i18n>partner_id}';
		CURRENCY        : CurrencyT @(
			Common: {
				Text: {$value: CURRENCY.CURRENCY, "@UI.TextArrangement": #TextOnly},
				ValueList: {entity: 'CURRENCY', type: #fixed},
				ValueListWithFixedValues
			}
		);
		GROSSAMOUNT     : AmountT @( title: '{i18n>grossAmount}', Measures.ISOCurrency: currency);
		NETAMOUNT       : AmountT @( title: '{i18n>netAmount}', Measures.ISOCurrency: currency);
		TAXAMOUNT       : AmountT @( title: '{i18n>taxAmount}', Measures.ISOCurrency: currency);
		LIFECYCLESTATUS : StatusT @(title: '{i18n>lifecycle}', Common.FieldControl: #ReadOnly);
		APPROVALSTATUS  : StatusT @(title: '{i18n>approval}', Common.FieldControl: #ReadOnly);
		CONFIRMSTATUS   : StatusT @(title: '{i18n>confirmation}', Common.FieldControl: #ReadOnly);
		ORDERINGSTATUS  : StatusT @(title: '{i18n>ordering}', Common.FieldControl: #ReadOnly);
		INVOICINGSTATUS : StatusT @(title: '{i18n>invoicing}', Common.FieldControl: #ReadOnly);
}

Full listing: https://github.com/mattxdev/opensap-hana7/blob/master/db/PurchaseOrder.cds

Code excerpt from /srv/fiori-annotations-my-service.cds for service level UI annotations:

using CatalogService as cats from './my-service'; 

annotate cats.POItemsView with @( // header-level annotations
// ---------------------------------------------------------------------------
// List Report
// ---------------------------------------------------------------------------
  // Product List
  UI: {
    LineItem: [ 
      {$Type: 'UI.DataField', Value: PO_ITEM_ID, "@UI.Importance":#High},
      {$Type: 'UI.DataField', Value: PRODUCT_ID, "@UI.Importance": #High},
      {$Type: 'UI.DataField', Value: PARTNER_ID, "@UI.Importance": #High},
      {$Type: 'UI.DataField', Value: PARTNERS.COMPANYNAME, "@UI.Importance": #Medium},			
      {$Type: 'UI.DataField', Value: AMOUNT, "@UI.Importance": #High},
      {$Type: 'UI.DataField', Value: CURRENCY_CODE, "@UI.Importance": #Medium},			
    ],
    PresentationVariant: {
      SortOrder: [ {$Type: 'Common.SortOrderType', Property: PO_ITEM_ID, Descending: false}, 
                   {$Type:  'Common.SortOrderType', Property: PRODUCT_ID, Descending: false} ]
	}
  }
);

Full listing: https://github.com/mattxdev/opensap-hana7/blob/master/srv/fiori-annotations-my-service.cds

Fiori allows XML templating. It will be implemented in the Main.view.xml by making use of template:with and template:repeat elements. This allow iterating over OData Annotations in order to control the UI output dynamically.

Example code excerpts from /web/resource/poListV4/view/Main.view.xml:

<template:with path="entityType>@com.sap.vocabularies.UI.v1.LineItem" var="lineItem">
<columns> <template:repeat list="{lineItem>}" var="field"> <template:if test="{= ${field>@com.sap.vocabularies.UI.v1.Importance/$EnumMember} !== 'com.sap.vocabularies.UI.v1.ImportanceType/Low' }"> <template:then> <Column> <Label design="{:= ${field>@com.sap.vocabularies.UI.v1.Importance/$EnumMember} === 'com.sap.vocabularies.UI.v1.ImportanceType/High' ? 'Bold' : 'Standard'}" text="{field>@@label}"/> </Column> </template:then> </template:if> </template:repeat> </columns> <ColumnListItem> <template:repeat list="{lineItem>}" var="field"> <Text text="{field>Value/@@value}"/> </template:repeat> </ColumnListItem>
</template:with>

Full listing: https://github.com/mattxdev/opensap-hana7/blob/master/web/resources/poListV4/view/Main.view.xml

Consume OData V4 Service with Create Option

Example Application:

/web/resource/odataCRUDV4

In order to use OData V4 appropriate settings for data source and model are necessary in manifest.json, i.e.:

...
"sap.app": {
	...
	"dataSources": {
		"userService": {
			"uri": "/odata/v4/opensap.hana.CatalogService/",
			"type": "OData",
			"settings": {
				"odataVersion": "4.0"
			}
		}
	}
	...
...

"sap.ui5": {
	...
	"models": {
		"": {
			"type": "sap.ui.model.json.JSONModel",
			"settings": {
				"defaultBindingMode": "TwoWay"
			}
		},
		"userModel": {
			"dataSource": "userService",
			"type": "sap.ui.model.odata.v4.ODataModel",
			"preload": false,
			"settings": {
				"synchronizationMode": "None",
				"operationMode": "Server",
				"autoExpandSelect": true
			}
		},
		...
	}
}

Entire listing: https://github.com/mattxdev/opensap-hana7/blob/master/web/resources/odataCRUDV4/manifest.json

As the SmartTable control does not yet allow OData V4 model (in early 2019) the m.table control will be used in App.view.xml:

<columns> <Column> <header><Label text="User ID"/></header> </Column> <Column> <header><Label text="First Name"/></header> </Column> <Column> <header><Label text="Last Name"/></header> </Column> <Column> <header><Label text="Email"/></header> </Column> </columns> <items> <ColumnListItem> <cells> <Input value="{path: 'userModel>USERID'}" name="PERS_NO" editable="false"/> <Input value="{path: 'userModel>FIRSTNAME'}" name="FIRSTNAME"/> <Input value="{path: 'userModel>LASTNAME'}" name="LASTNAME"/> <Input value="{path: 'userModel>EMAIL'}" name="E_MAIL"/> </cells> </ColumnListItem> </items>

Entire listing: https://github.com/mattxdev/opensap-hana7/blob/master/web/resources/odataCRUDV4/view/App.view.xml

In order to use OData V4 the App.controller.js has to account for that. This happens by creating a binding the new data to the OData V4 model. Besides that no special model functions have to be called:

...
callUserService: function () { 

	try { 
		var oModel = this.getOwnerComponent().getModel("userModel");
		var result = this.getView().getModel().getData();
		var oList = this.byId("userTable"),
			oBinding = oList.getBinding("items"),
			// Create a new entry through the table's list binding
			oContext = oBinding.create({
				"USERID": 0,
				"FIRSTNAME": result.FirstName,
				"LASTNAME": result.LastName,
				"EMAIL": result.Email
			});

		// Note: this promise fails only if the transient entity is deleted
		oContext.created().then(function () {
			sap.m.MessageBox.alert("User created: " + oContext.getProperty("USERID"));
		}, function (oError) {
			sap.m.MessageBox.alert(oError.toString());
		});
	} catch (err) {
		sap.m.MessageBox.alert(err.toString());
	}
},

callUserUpdate: function () {
	var oModel = this.getOwnerComponent().getModel("userModel");

	var mParams = {};
	mParams.error = function () {
		sap.m.MessageToast.show("Update failed");
	};
	mParams.success = function () {
		sap.m.MessageToast.show("Update successful");
	};

	oModel.submitChanges(mParams);
} ,
...

Entire listing: https://github.com/mattxdev/opensap-hana7/blob/master/web/resources/odataCRUDV4/controller/App.controller.js