Skip to Content
Technical Articles

Let’s build an Offline Hybrid UI5 Application from scratch – Part 2 – Implementing CUD, Flush and Refresh

In Part 1, we built a simple read-only offline Stock Application. In a real-world scenario, an offline-enabled application would also allow modification of records even without an internet connection. In this blog post, we’ll implement CUD (Create, Update, Delete) in our app and explore how it works under the hood with Offline OData.

Implement Create

Let’s get right into it and implement create functionality for our Products list. Let’s add an ‘Add (+)’ button that opens a New Product form dialog, a form dialog and the create (Post) function.

Home.view.xml

<mvc:View controllerName="zoffline.demo.OfflineDemo.controller.Home" xmlns:html="http://www.w3.org/1999/xhtml" xmlns:mvc="sap.ui.core.mvc"
	displayBlock="true" xmlns="sap.m" xmlns:form="sap.ui.layout.form">
	<App id="idAppControl">
		<pages>
			<Page title="Stock App Offline Demo">
				<content>
					<VBox alignItems="Center">
						<form:SimpleForm id="stockDetailForm" title="Stock Details" layout="ResponsiveGridLayout" labelSpanXL="4" labelSpanL="4" labelSpanM="4"
							labelSpanS="12" adjustLabelSpan="false" emptySpanXL="3" emptySpanL="3" emptySpanM="3" emptySpanS="0" columnsXL="1" columnsL="1" columnsM="1"
							singleContainerFullSize="false">
							<form:content>
								<Label text="Product Id"/>
								<Text text="{offline>ProductId}"/>
								<Label text="Quantity"/>
								<Text text="{path: 'offline>Quantity', type: 'sap.ui.model.type.Decimal'}"/>
								<Label text="Last Updated"/>
								<Text text="{path: 'offline>UpdatedTimestamp', type: 'sap.ui.model.type.Date', formatOptions: { style: 'short', pattern: 'dd/MM/yyyy'}}"/>
							</form:content>
						</form:SimpleForm>
					</VBox>
					<List items="{path: 'offline>/Products',  sorter: [{ path: 'UpdatedTimestamp', descending: true }]}" growing="true" growingThreshold="5">
						<headerToolbar>
							<Toolbar>
								<content>
									<Title text="Product List" level="H2"/>
									<ToolbarSpacer/>
									<Button press="onPressAddProduct" icon="sap-icon://add"/>
								</content>
							</Toolbar>
						</headerToolbar>
						<ObjectListItem title="{offline>Name}" type="Active" press="onItemPress"
							number="{ parts:[{path:'offline>Price'},{path:'offline>CurrencyCode'}], type: 'sap.ui.model.type.Currency', formatOptions: {showMeasure: false} }"
							numberUnit="{offline>CurrencyCode}">
							<firstStatus>
								<ObjectStatus text="{offline>Category}"/>
							</firstStatus>
							<attributes>
								<ObjectAttribute text="{offline>ProductId}"/>
								<ObjectAttribute text="{offline>ShortDescription}"/>
							</attributes>
						</ObjectListItem>
					</List>
					<List items="{offline>/Suppliers}" headerText="Supplier List" growing="true" growingThreshold="5">
						<StandardListItem title="{offline>SupplierName}"/>
					</List>
				</content>
			</Page>
		</pages>
	</App>
</mvc:View>

Home.controller.js

sap.ui.define([
	"sap/ui/core/mvc/Controller", "sap/m/MessageToast"
], function (Controller, MessageToast) {
	"use strict";
	return Controller.extend("zoffline.demo.OfflineDemo.controller.Home", {
		onItemPress: function (oEvt) {
			var oContext = oEvt.getSource().getBindingContext("offline");
			this.getView().byId("stockDetailForm").bindElement({
				path: oContext.getPath() + "/StockDetails",
				model: "offline"
			});
		},
		onPressAddProduct: function () {
			if (!this._oAddProductDialog) {
				this._oAddProductDialog = sap.ui.xmlfragment(
					"zoffline.demo.OfflineDemo.view.fragments.AddProduct",
					this
				);
				this.getView().addDependent(this._oAddProductDialog);
			}
			this._oAddProductDialog.open();
		},
		onCloseNewProductDialog: function () {
			this.getView().getModel("data").setData({}); //reset data if dialog is closed
			this._oAddProductDialog.close();
		},
		onSubmitNewProduct: function () {
			var oPayload = this.getView().getModel("data").getData();
			this.getView().getModel("offline").create("/Products", oPayload, {
				success: function (oData) {
					this.getView().getModel("data").setData({});//reset data model after successful post
					this._oAddProductDialog.close(); //close the dialog on success
				}.bind(this),
				error: function (oErr) {
					MessageToast.show("An error occured" + oErr);
				}
			});
		}

	});
});

Add a new JSON model in manifest.json

		"models": {
			"i18n": {..},
			"offline": {..},
			"data": {
				"type": "sap.ui.model.json.JSONModel"
			}
		}

Add a new ‘fragments’ folder in the ‘view’ folder and create an ‘AddProduct.fragment.xml’ file

AddProduct.fragment.xml

<core:FragmentDefinition xmlns="sap.m" xmlns:l="sap.ui.layout" xmlns:f="sap.ui.layout.form" xmlns:core="sap.ui.core">
	<Dialog title="Create Product">
		<content>
			<f:SimpleForm layout="ResponsiveGridLayout" columnsM="1" labelSpanM="12" columnsL="1" labelSpanL="6">
				<f:content>
					<Label text="Product Id"/>
					<Input value="{data>/ProductId}"/>
						<Label text="Product Name"/>
					<Input value="{data>/Name}"/>
					<Label text="Category"/>
					<Input value="{data>/Category}"/>
					<Label text="Category Name"/>
					<Input value="{data>/CategoryName}"/>
					<Label text="Short Description"/>
					<Input value="{data>/ShortDescription}"/>
					<Label text="Price"/>
					<Input value="{data>/Price}" type="Number"/>
				</f:content>
			</f:SimpleForm>
		</content>
		<beginButton>
			<Button text="Submit" press="onSubmitNewProduct" type="Emphasized"/>
		</beginButton>
		<endButton>
			<Button text="Close" press="onCloseNewProductDialog"/>
		</endButton>
	</Dialog>
</core:FragmentDefinition>

Sending and receiving changes to the backend

At this point, we have a basic create function that will create a new Product entity even without network connectivity. Note that the code is the same as a normal application, so I will leave it up to you to implement the update and delete methods.

However, all these changes are only written in the local offline store.

To send requests back to the server, a flush method has to be explicitly called – think of flush as an upload function. A refresh method, on the other hand, downloads data from the backend (new records, updates) into the entity store.

Working with the Offline Store

Early on in my learning journey, I had a misconception that the offline store is only used when the device is offline – but this isn’t the case. When an offline store is opened for a particular OData service, all operations that are made against that service will be stored in a local request queue until a flush operation is called. The request queue can contain multiple create, update and delete operations.

The above diagram has been simplified for easier understanding. A more extensive sequence flow diagram is available here.

Key points:

  1. When a CUD operation is called, the request (create, update, remove) is stored in the request queue.
  2. The operation is performed on the local database (entity store)
  3. local id is created for new entities. This local id can be used to query the local record.
  4. When the flush method is called:
  • All requests in the request queue will be uploaded to the backend through mobile services – network connectivity is required when performing a flush.
  • The local ID will be resolved to real IDs from the server.
  • The response, including errors, are returned by the OData producer through mobile services.

 

Implementing flush and refresh

Flush and refresh are methods of the sap.OfflineStore and should be called explicitly. When to call these methods is exactly up to your data synchronization strategy. A common strategy would be to call the refresh after a successful flush, to get the latest changes from the backend after submitting your own changes.

In our app, let’s modify the submit function and add a simple script that will flush the changes if the device is online, then subsequently call the refresh method.

 

Home.controller.js

		onSubmitNewProduct: function () {
			var oPayload = this.getView().getModel("data").getData();
			this.getView().getModel("offline").create("/Products", oPayload, {
				success: function (oData) {
					this.getView().getModel("data").setData({}); //reset data model after successful post
					this._oAddProductDialog.close(); //close the dialog on success
					if (navigator.onLine) {
						//flush if device is online
						sap.hybrid.flushStore()
					}
				}.bind(this),
				error: function (oErr) {
					MessageToast.show("An error occured" + oErr);
				}
			});
		}

Let’s then implement the flush method:

sap-mobile-hybrid.js

	flushStore: function () {
                var _refreshSuccessCallback= function (o) {
		       //show a message here
		};
		var _refreshErrorCallback= function (o) {
				
		};

		var _flushStoreCallback = function (o) {
			store.refresh(_refreshSuccessCallback, _refreshErrorCallback)
		};
		var _errorCallback = function (o) {
			console.log(o)
		};
		if (store) {
			//offline store is opened
			store.flush(_flushStoreCallback, _errorCallback , null);
		} else {
			console.log("The store must be open before it can be flushed");
		}
	}

What about errors?

Unlike a standard online application, success and error responses from the backend are not provided to the flush method callbacks. What the callbacks represent is the success (or failure) of the HTTP request (due to network unavailability, for example)

For example, if a new record that violates a business rule or throws an error is created and flushed, the success callback will still be called, as long as the HTTP request for the flush is successfully sent to the backend.

To read and handle error messages, a standard entity set named ‘ErrorArchive’ is provided. The relevant error responses, and original request body is available to be read, handled and resent to the backend. Try it for yourself. More on the entity properties here.

In addition, trace logs can be recorded in Mobile Services cockpit under Analytics > Network Traces. The HAR file can be downloaded and viewed using a HAR Viewer.

Wrap-up

With this in mind, server-generated data (document numbers, for example) are not always readily available after creation in the app, hence the technical design and data flow must be carefully considered for different scenarios in an offline application. Common questions that should be asked include:

  1. Which data and functions should be available offline?
  2. Network speed (a flush and refresh can be performed when a network is available, but these actions may lead to performance or UI-blocking issues on a slow connection network.
  3. How often or when should data be refreshed?
  4. How should errors be presented to the user? (given that there might be delays on the error response)
  5. How should server-side validations be designed for an offline application?

References

OfflineStore API

ErrorArchive 

Error Handling

 

Be the first to leave a comment
You must be Logged on to comment or reply to a post.