Skip to content

UI5 Design Patterns

This page contains some Design Pattern that are productively used inside UDINA UI5 apps.

Controller Extensions

Controller extensions allow you to add functionality to existing applications while implementing the Mix-in design pattern.

They can be used for extensibility purposes, for example by a customer wishing to extend SAP-delivered applications, or as a reusable part that is added to the original application (Using Controller Extension).

Flexible Programming Model

Working with sap.fe and the Flexible Programming Model, you cannot use a custom BaseController class anymore and instead have to decouple reused functionality using controller extensions!

WARNING

Usage examples will follow, after SAP fixed current transpiler issues!

Service Factory

If the app landscape is growing, at some point there is a need to be able to create reuseable services.

For this reason, SAPUI5 offers a factory method pattern that allows to declaratively define sap.ui.core.Componentservice dependencies.

This feature is not officially supported!

SAPUI5 does not made this feature public available and it is even not documented in the SDK, but SAP internally uses the functionality inside sap.ushell.services and sap.fe.core.services.

Because the API is not public, no types will be generated for TypeScript TypeScript Support

Use is at the risk, that the API will change in the future.

File Hierarchy

The following files are relevant:

├── webapp
│   ├── services
│   │   ├── MyServiceFactory.ts
│   │   ├── enhanceService.ts
│   │   ├── registerServices.ts
│   │   ├── types.ts
│   ├── Component.ts
│   ├── manifest.json

Component Service Dependencies

All services dependencies needs to be defined in the manifest.json file:

json
{
    [...]
    "sap.ui5": {
        "services": {
            "myLocalServiceAlias": {
                "factoryName": "my.ServiceFactory",
                ["startup": "waitFor"],
                ["optional": true],
                ["settings": {
                    "modelName": "my-model"
                }]
            }
        }
    }
    [...]
}
PropertyDescription
myLocalServiceAliasService alias to used for retrieving a service instance using Component.getService("myLocalServiceAlias").
factoryNameThe qualified name of the service factory that is creating the service instance.
startupServices can be optionally instanciated during component start using waitFor option.
optionalThe optional property defines that the service is not mandatory and there will be no error in the log if the service is not available.
settingsThe settings property allows to pass-in additional information that will be accessible through the service context.

Service Factory Registry

All service dependencies needs to be registered using the sap/ui/core/service/ServiceFactoryRegistry:

registerServices.js

ts
import ServiceFactoryRegistry 
    from "sap/ui/core/service/ServiceFactoryRegistry";

ServiceFactoryRegistry.register(
    "ns.MyService", 
    new MyServiceFactory()
);
[...]

There is an issue with the TypeScript and the returned service interface.

With the following prototype, Component.getService() will return the relevant service instance instead of the service interface for further usage:

enhanceService.ts

ts
/*
 * Protoype Service.getInterface to return 
 * instance instead interface.
 * Reason:  returned interface is missing
 *          public functions (TypeScript issue)
*/
import Service from "sap/ui/core/service/Service";

Service.prototype.getInterface = function () {
    // return instance instead interface
    return this;
}

Component Usage

The app component needs to register all service factories:

Component.ts

ts
// Register all services declared in the descriptor
// for components (manifest.json)
import "./services/registerServices";
// Import relevant prototype to fix TS issue for the service 
import "./services/enhanceService";
[...]

Example Service Usage

The following is an example, how to define and implement a custom service by exporting the service and the corresponding service factory, which creates the service instance.

MyServiceFactory.ts

ts
// core dependencies
import Service from "sap/ui/core/service/Service";
import ServiceFactory, { ServiceContext } 
    from "sap/ui/core/service/ServiceFactory";

// additional dependencies
import AppComponent from "../Component";
import XMLView from "sap/ui/core/mvc/XMLView";
import JSONModel from "sap/ui/model/json/JSONModel";
import Log from "sap/base/Log";

// default settings type used by settings context
type MySettings = {
    modelName: "my-model";
};

export class MyService extends Service<MySettings> {
    initPromise!: Promise<MyService>;
    appComponent!: AppComponent;
    rootView: XMLView;
    model: JSONModel;
    
    init() {
        // handle init lifecycle
        const context = this.getContext();
        if (context.scopeType === "component") {
            const settings = context.settings;

            // shortcut to parent compomnent
            this.appComponent = context.scopeObject;

            // create an internally used service model
            this.model = new JSONModel({
                myProperty: "value"
            });

            // register service model for public usage
            this.appComponent.setModel(
                this.model, 
                settings.modelName // manifest injected
            );

            // use component rootView for ux dependencies 
            this.appComponent.rootControlLoaded()
                .then((rootControl) => {
                    this.rootView = rootControl as XMLView;
                    // eg. add ux controls to hierarchy
                    // this.rootView.addDependent(this.dialog);
                }
            );

            // initialize inter service dependency
            this.appComponent.getService("dependendService")
                .then((dependendService) => {
                    this.startDependendFunction(
                        dependendService.getProperty()
                    );
                }).catch(function (e) {
                    Log.error(e);
                });
        }

        // fullfill service for usage
        this.initPromise = Promise.resolve(this);
    }

    exit() {
        // handle exit lifecycle
    }

    startDependendFunction(dependendProperty: string) {
        // some functionality
    }
}

export default class MyServiceFactory 
    extends ServiceFactory<MySettings> { 
        createInstance(serviceContext: ServiceContext<MySettings>)
            : Promise<MyService> {
                const myService = new MyService(serviceContext);
                return myService.initPromise;
    }
}

TypeScript Support

Using TypeScript, transpiler will crash!

Since the relevant classes are not public, sap will not generate the relevant types.

To fix this in such cases, you have to manually add the relevant types.

types.d.ts

ts
// only used and relevant stuff, manually declared!

declare module "sap/ui/core/service/Service" {
    import BaseObject from "sap/ui/base/Object";

    export default class Service<T> extends BaseObject {
        constructor(
            oServiceContext: object
        );
        getContext(): ServiceContext;
    }
}

declare module "sap/ui/core/service/ServiceFactory" {
    import BaseObject from "sap/ui/base/Object";

    export type ServiceContext<T> = {
        scopeObject: Component,
        scopeType: "component",
        settings: any
    }

    export default class ServiceFactory<T> extends BaseObject {
        constructor(
            vService?: string
        );
    }
}

declare module "sap/ui/core/service/ServiceFactoryRegistry" {
    export default class ServiceFactoryRegistry {
        static register(
            sServiceFactoryName: string,
            oServiceFactory: ServiceFactory
        ): ServiceFactoryRegistry;
    }
}