MVC architecture

Every SAPUI5 app separates three pieces: the view (XML, defines the layout), the controller (JavaScript, holds the logic) and the model (the data, typically an ODataModel or a JSONModel). This separation lets you test the logic without touching the UI.

XMLContractList.view.xml
<mvc:View controllerName="com.dev.app.controller.ContractList"
  xmlns:mvc="sap.ui.core.mvc" xmlns="sap.m">
  <Table id="contractTable" items="{/Contracts}">
    <columns>
      <Column><Text text="ID" /></Column>
      <Column><Text text="Manager" /></Column>
    </columns>
    <items>
      <ColumnListItem press="onContractPress">
        <cells>
          <Text text="{ContractId}" />
          <Text text="{ContractManager}" />
        </cells>
      </ColumnListItem>
    </items>
  </Table>
</mvc:View>

The controller

The controller receives events from the view and decides what to do: navigate, call the model, open a dialog. It always extends sap.ui.core.mvc.Controller.

JavaScriptContractList.controller.js
sap.ui.define([
  "sap/ui/core/mvc/Controller",
  "sap/m/MessageToast"
], function (Controller, MessageToast) {
  "use strict";

  return Controller.extend("com.dev.app.controller.ContractList", {

    onContractPress: function (oEvent) {
      const oContext = oEvent.getSource().getBindingContext();
      const sId = oContext.getProperty("ContractId");
      MessageToast.show(`Opening contract ${sId}`);
      this.getOwnerComponent().getRouter()
        .navTo("detail", { contractId: sId });
    }

  });
});

The OData model

The ODataModel connects the app to the SAP backend (a CDS view exposed as a service, typically via RAP). The {/Contracts} binding in the view resolves automatically against this entity.

JavaScriptComponent.js — excerpt
const oModel = new ODataModel("/sap/opu/odata4/sap/zcontract_srv/srvd/sap/zui_contract/0001/", {
  synchronizationMode: "None",
  operationMode: "Server",
  autoExpandSelect: true
});
this.setModel(oModel);

Fiori Elements: when not to write the view by hand

When the app follows a standard pattern (List Report + Object Page), Fiori Elements generates the UI from the CDS view's annotations, with no XML or controller to write. It's only customised via extensions (metadata extensions, controller extensions) when the standard doesn't cover a specific case.

Rule of thumb: always start with Fiori Elements. Only fall back to Freestyle SAPUI5 when there's a very specific interaction that standard annotations can't express.

Routing and navigation

SAPUI5 apps define their navigation in manifest.json: routes map URL patterns to views, so the browser back/forward buttons and deep links (bookmarking a specific object page) work out of the box.

JSONmanifest.json — routing excerpt
{
  "routing": {
    "routes": [
      { "name": "list", "pattern": "", "target": "list" },
      { "name": "detail", "pattern": "Contract/{contractId}", "target": "detail" }
    ],
    "targets": {
      "list": { "viewName": "ContractList", "viewLevel": 1 },
      "detail": { "viewName": "ContractDetail", "viewLevel": 2 }
    }
  }
}

Reading the pattern parameter back inside the target controller:

JavaScriptContractDetail.controller.js
onInit: function () {
  this.getOwnerComponent().getRouter()
    .getRoute("detail")
    .attachPatternMatched(this._onRouteMatched, this);
},

_onRouteMatched: function (oEvent) {
  const sContractId = oEvent.getParameter("arguments").contractId;
  this.getView().bindElement(`/Contracts('${sContractId}')`);
}

Error and busy-state handling

A production Fiori app needs to show a busy indicator during requests and surface backend errors without a blank screen. UI5 gives every model a metadataFailed and requestFailed event for exactly this.

JavaScripterror-handling.js
const oModel = this.getView().getModel();

oModel.attachRequestFailed(function (oEvent) {
  const oParams = oEvent.getParameters();
  MessageBox.error(`Request failed: ${oParams.message}`);
});

this.getView().setBusyIndicatorDelay(0);
this.getView().setBusy(true);
oModel.read("/Contracts", {
  success: () => this.getView().setBusy(false),
  error: () => this.getView().setBusy(false)
});
UX detail that matters: setting setBusyIndicatorDelay(0) shows the spinner immediately for slow requests, but for fast ones (<300ms) a spinner flash feels worse than no feedback at all — a small delay (200-300ms) is usually the better default.
JavaScript Previous