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:
- When a CUD operation is called, the request (create, update, remove) is stored in the request queue.
- The operation is performed on the local database (entity store)
- A local id is created for new entities. This local id can be used to query the local record.
- 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:
- Which data and functions should be available offline?
- 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.
- How often or when should data be refreshed?
- How should errors be presented to the user? (given that there might be delays on the error response)
- How should server-side validations be designed for an offline application?
Part 1 – Let’s build an Offline Hybrid UI5 Application from scratch
References
Hi Greg, thank you for your great blog.
Nowadays, I am focusing on offline app. topic and when I saw your blog I immediately tried to implement offline application.
When my application run as a web application on browser, I can see Product List etc. So, it works fine on pc without any errors (but I am not sure about of offline part is ok or not)
But I need your support for mobile offline scenario. When I try to run apk file on phone, it gives error message about metadata when starting. It shows just error thus, I couldn't try offline scenario.
Also, I read @Ludo's blog and in my some trying I mixed your blog and Ludo's blog for solving.
Although, I tried different many kind of path for solving error. it unfortunately did not work on phone.
Could you help me, please?
Hi Mehmet,
Thanks for reading.
Mobile services acts as a proxy between the app and the backend so a destination has to be mapped in mobile services to a cloud platform destination.
Can you check your destination config in mobile services and check it’s configured properly?
The 'Name' value should match the backend connection string in your screenshot.
Make sure it’s mapped to a valid destination in cloud platform:
Cheers
It works...
Greg, thank you for your supporting.
pls send me the screenshot ..
thank you .
regard
jai sharma
I followed your steps, here it is Connection to “mssampledata” established. Response returned: “200: OK”.
Now I am getting error “mssampledata catalog service is unavailable.” what will be the issue.
Kindly help
Thanks Greg for both of the blogs, added in my try list:)
Hi Greg,
I have one more question, can I use on-premise SAP Mobile Platform in this offline architecture and how?
Hello,
I have a problem, when I create an object in offline mode and then I try to update it before flush the changes, when the sync starts the changes are not being uploaded to my backend, could you help me pls? what is the correct way to update a local object before the flush?.
Thanks
hi Greg Cariño
I am using the sampleservices product set and facing an issue while doing a create. The model.create method goes into sucess callback but the success object is returned as blank
the value for all properties is returned as null despite sending the entered values correctly during create call. Hence i am seeing blank line item getting added into the master list of products.
Hi Greg,
I had a requirement where i need to post header and line items. I am able to post header level data but unable to post header and item level data at once. I tried with multiple open stores , $batch odata. Can you suggest on this.