Skip to Content
Technical Articles

Offline-capable UI5-Application with IndexedDB and extended OData Model

Abstract

During these times of the pandemic, more and more activities have been shifting to remote work, here in Germany known as “home office”. This additional data traffic has been increasingly burdening the infrastructure and there is a requirement to keep certain functions of an application available, even in areas free of Wi-fi and mobile communications. In this blog post, the local IndexedDB is used to store app data. The SAP oData v2 model is extended so that a failed communication with the SAP backend is automatically recognized. Endpoint, HTTP verb and payload are cached in the IndexedDB and sent again as soon as the application is back online.

Contents

  1. Basics
  2. Creation of the Model Classes
  3. Read Data: Online/Offline
  4. Create Data: Online
  5. Create Data: Offline
  6. Delete Data: Offline
  7. Synchronization
  8. Outlook

 

 

1. Basics

 IndexedDB

The IndexedDB is a Javascript based database. It allows data and files to be saved locally. The data is structured and stored according to the same origin policy per host / port. No fixed columns are used here but objects with keys are stored instead.

 

Scope

The goal is a UI5 application that displays a simple list. The list allows you to add and delete entries when the SAP backend server is available and when there is no connection. The list also offers a synchronization function that transfers all data from the local IndexedDB to the SAP system. The entries from the SAP backend are displayed in the table. Likewise are the entries in the IndexedDB that could not to be save on SAP backend yet. Furthermore, the Model bound to the list, stores delete tokens for those entries, the user wanted to be deleted while there was no server connection. The application always works the same for the user regardless of the network status.

Final%20list%20containing%20data%20from%20both%20backend%20service%20and%20local%20IndexedDB

Final list containing data from both backend service and local IndexedDB

 

OData Service

The associated OData service has only the four fields that are shown in the table. It allows the “Query”, “Create” and “Delete” functions.

SEGW%20project

SEGW project

Service%20response

Service response

 

JS-Classes oData and IndexedDb

In the UI5 application, new classes for the oData Model and the IndexedDB are created.
The sap/ui /model/odata/v2/ODataModel is extended for the oData.js class.
The sap/ui/model/ json/JSONModel is used to access functions of the IndexedDb.js.
Both classes are later instantiated from Component.js.

oData%20and%20IndexedDb%20class

oData and IndexedDb class

 

Failed calls to the SAP backend should be intercepted directly in the OData model. To do this, the standard SAP Odata model must be extended. The required standard CRUD methods “Create”, “Read” and “Remove” are extended for this. Own methods are called in the success and error callbacks, which forward the information to the IndexedDB.

 

oData.js

/**
 *@class oData
 *@classdesc Extends OData Model and offers API to IndexedDB.
 *@extends sap/ui/model/odata/v2/ODataModel
 */
sap.ui.define([
   "sap/ui/model/odata/v2/ODataModel"
], function(ODataModel)
{
   "use strict";

   return ODataModel.extend("ui5.offlineFunct.model.oData", {
      _oComponent: null,

      constructor: function(sServiceURL, mParameters, oComponent)
      {
         this._oComponent = oComponent;
         ODataModel.prototype.constructor.call(this, sServiceURL, mParameters);
      },
 
…

 

2. Creation of the Model Classes

A global JSON model is created to access the functions of the IndexedDB. The database is opened in the constructor. If an open instance of the IndexedDB already exists, the current version is stored globally.
The metadata is loaded from the SAP backend using the extended oData model. Similarly, a JSON “table model” is created. This controls the loading and saving of data.

 

Model%20creation

Model creation

 

IndexedDb.js

sap.ui.define([
   "sap/ui/model/json/JSONModel"
], function(Model)
{
   "use strict";

   var _instance = void 0;
   var version = 0;

   var IndexedDb = Model.extend("ui5.offlineFunct.model.IndexedDb", {

      /**
       *@description Constructor
       *@memberOf IndexedDb
       *@param {object} oComponent - Owner Component
       */
      constructor: function(oComponent)
      {
         if (window.indexedDB === null)
         {
            console.error("Offline store not supported!");
            return null;
         }

         this._oComponent = oComponent;
         Model.prototype.constructor.call(this, {
            "_meta": []
         });
         var request = indexedDB.open("localStorage"); 
         request.onsuccess = function(oEvent)
         {
            IndexedDb._db = oEvent.target.result;
            version = IndexedDb._db.version;
            IndexedDb._db.close();
         };
         IndexedDb._instance = this;
      },

 

Initialization

In order to be able to access the extended ODatamodel class, the model instantiation is removed from manifest.json …

"models":{
"i18n":{
"type":"sap.ui.model.resource.ResourceModel",
"settings":{
"bundleName":"ui5.offlineFunct.i18n.i18n"
}
}
},

…and transfered in the Component.js / models.js:

models.createServiceModel(sServiceUrl, null, this)
   .then(function(oServiceModel)
   {
      oServiceModel.setUseBatch(false);
      console.log("Backend connection established");
      var networkModel = that.getModel("network");
      oServiceModel.attachRequestSent(function()
      {
         networkModel.setBusy(true);
      });
      oServiceModel.attachRequestCompleted(function()
      {
         networkModel.setBusy(false);
         networkModel.setProperty("/connected", true)
      });
      oServiceModel.attachRequestFailed(function()
      {
         networkModel.setProperty("/connected", false)
      })
   })

 

models.js:

createServiceModel: function(sServiceUrl, mParameters, oComponent)
{
   return new Promise(function(resolve, reject)
   {
      try
      {
         var oServiceModel = new oData(sServiceUrl, null, oComponent);
         oComponent.setModel(oServiceModel);
         oServiceModel.setUseBatch(true);

         oServiceModel.metadataLoaded()
            .then(function()
            {
               resolve(oServiceModel);
            });
      } catch (err)
      {
         reject(err);
      }
   });
},

 

 

3. Read Data: Online / Offline

The data is read using the table and odata models. In addition, the “load” method of the TableModel checks whether a table exists in the IndexedDB under the same end point. If this is the case, it is also read out in order to be able to display the data from both sources.
The process that is initiated by reading the “Table1Set” entity is described below. This reading process is called and updated after each create and delete call. This also includes the data from the SAP backend and the IndexedDB. Event S1 symbolizes the start of the reading process.

Read%20process

Read process

 

View1.controller.js

Start the loading process via the table model:

/**
 *@description Load Data
 *@memberOf View1
 */
onLoadTable: function()
{
   this.getView()
      .getModel("TableModel")
      .load("/Table1Set")
      .then(function(data)
      {
         console.log("BE and INDEXEDDB loaded")
      })
      .catch(function(err)
      {
         console.error("Either BE or INDEXEDDB could not be loaded");
      })
},

 

Table.js

Forwarding the request to the oData model:

/**
 *@description Loads data from SPATH
 * through extended odata Model from both backend and Indexed DB
 *@param {string} sPath - Path to Data to be loaded
 */
load: function(sPath)
{
   var oData = this.oComponent.getModel(),
      that = this;
   return new Promise(function(resolve, reject)
   {
      oData.read(sPath, {
         success: function(response)
         {
            that.setProperty("/results", response.results);
            resolve(response.results);
         },
         error: function(err)
         {
            reject(err);
         }
      });
   });
},

 

oData.js

The read function is executed. In the “Success” case, the read result is forwarded to the “_readSuccessCallback” method to check whether data is also stored under the same path locally.

/**
 *@description extended READ
 * on read (error and success) the local INDEXEDDB is also read with the supplied sPath from the original
 * OData Call. Merges data from indexed to to "Results"
 *@memberOf oData
 *@param {String} sPath - A string containing the path to the data which should be retrieved.
 * The path is concatenated to the service URL which was specified in the model constructor. Eg. '/CustomerSet'
 *@param {Object} mParameters - Optional parameter map, see API
 */
read: function(sPath, mParameters)
{
   var that = this;
   mParameters.success = (function(success)
   {
      function fnExtend(data, oData)
      {
         that._readSuccessCallback(data, oData, this)
            .then(function(localData)
            {
               data.results ? data.results = data.results.concat(localData.data) : localData.data ? data.results = localData.data : '';
               success(data, oData);
            });
      }

      return fnExtend.bind(sPath);
   }.bind(sPath))(mParameters.success);

   mParameters.error = (function(error)
   {
      function fnExtend(data)
      {
         that._readErrorCallback(data, this.path, this.params)
            .then(function(localData)
            {
               if (localData.data) localData.params.success(data);
               error(data);
            });
      }

      return fnExtend.bind({
         path: sPath,
         params: mParameters
      });
   }.bind(sPath))(mParameters.error);

   ODataModel.prototype.read.call(this, sPath, mParameters)

},

…

/**
 *@description Extended READ Success function. Tries to read path from BE AND IndexedDB
 *@memberOf oData
 *@param {Object} data - Data resulting from call
 *@param {Object} oData - Overhead Data
 *@param {String} sPath - called Path
 */
_readSuccessCallback: function(data, oData, sPath)
{
   console.log("Server READ SUCCESS: %s ", sPath);
   return this._loadFromIndexedDB(sPath, {})
},

…

/**
 *@description loads a Table from the indexed DB
 *@param {String} sPath - called Path
 *@param {Object} oParams - success / error
 *@return {promise}
 */
_loadFromIndexedDB: function(sPath, oParams)
{
   var that = this;
   return new Promise(function(resolve)
   {
      that._oComponent.getIndexDb()
         .getTable(sPath)
         .then(function(data)
         {
            resolve({
               data: data,
               params: oParams
            })
         });
   })
},

 

IndexedDb.js

getTable first checks whether there is a table under the read sPath locally. If this is the case, each entry is read and returned. Each entry receives an additional attribute for determining the origin.

/**
 *@description loads data from indexeddb.
 * Adds attribute "_origin": "indexeddb" to indexeddb
 *@memberOf IndexedDb
 *@param {String} sTable - Name of table that is read
 *@param {boolean} bIncludeRemove - in cludes entries with property _http === "remove"
 *@returns {array} - read table with extra attribute "_origin": "indexeddb"
 */
getTable: function(sTable, bIncludeRemove)
{
   var that = this;
   return new Promise(function(resolve, reject)
   {
      var request = indexedDB.open("localStorage", version);

      request.onsuccess = function(oEvent)
      {
         IndexedDb._db = oEvent.target.result;
         if (!that._tableAvailable(sTable))
         {
            IndexedDb._db.close();
            return resolve([]);
         }
         var objectStore = IndexedDb._db.transaction([sTable], "readwrite").objectStore(sTable);
         var request = objectStore.openCursor();
         var aTable = [];

         request.onsuccess = function(event)
         {
            var cursor = event.target.result;
            if (cursor)
            {
               cursor.value._origin = "indexeddb";
               if (bIncludeRemove || cursor.value._http === 'create') aTable.push($.extend(cursor.value, cursor.value));
               cursor.continue();
            }
            else
            {
               IndexedDb._db.close();
               resolve(aTable);
            }
         };
      };
   });
},

After the reading process, the table shows the data read from the backend and from the IndexedDB.

 

List%20showing%20both%20BE%20and%20local%20Data

List showing both BE and local Data

 

 

 

4. Create Data: Online

The data is saved via the interface of the table model in the online state. The new data is created via the GUI and transferred to the backend.

Add%20dialog

Add dialog

 

Save%20process

Save process

 

The table shows the data read from the backend.

Save%20result

Save result

 

 

5. Create Data: Offline

For the test case, the network is deactivated via the Chrome browser options. For the user, the creation process on the surface is the same. As in online mode, the data is created via the GUI.

 

Set%20browser%20to%20offline%20mode

Set browser to offline mode

 

Add%20dialog

Add dialog

 

Create%20data%20process

Create data process

 

 

View1.controller.js

After saving, it is read manually (S1)

oTableModel.save(data, "/Table1Set")
   .then(function()
   {
      that.onLoadTable();
   })
   .catch(function()
   {
      that.onLoadTable();
   })
;

 

Table.js

/**
 *@description Creates new table entry
 *@param {Object} data - data to be saved
 *@param {String} sPath - Path to be saved to
 *@returns {promise}
 */
save: function(data, sPath)
{
   var oData = this.oComponent.getModel();
   return new Promise(function(resolve, reject)
   {
      oData.create(sPath,
         data,
         {
            success: function(resp)
            {
               resolve(resp)
            },
            error: function(err)
            {
               reject(err);
            }
         })
   })
},

 

oData.js

After the Createcall has failed as planned the data to be saved, including the path, is forwarded to the IndexedDB. The data is given an additional attribute “_http” with the value “create” for later synchronization.

/**
 *@description extended CREATE
 *@memberOf oData
 *@param {String} sPath - A string containing the path to the collection
 * where an entry should be created.
 * The path is concatenated to the service URL which was specified in the
 * model constructor.
 *@param {Object} oData - Data of the entry that should be created.
 *@param {Object} mParameters - Optional parameter map, refer to API
 */
create: function(sPath, oData, mParameters)
{
   oData = this._trimPayload(oData);
   var that = this;
   mParameters.success = (function(success)
   {
      function fnExtend(data, oData)
      {
         success(data, oData);
         that._createSuccessCallback(data, oData, this)
      }

      return fnExtend.bind({
         path: sPath,
         data: oData
      });
   }.bind({
      path: sPath,
      data: oData
   }))(mParameters.success);

   mParameters.error = (function(error)
   {
      function fnExtend(data)
      {
         that._createErrorCallback(data, this)
            .then(function()
            {
               error();
            });
      }

      return fnExtend.bind({
         path: sPath,
         data: oData
      });
   }.bind({
      path: sPath,
      data: oData
   }))(mParameters.error);

   ODataModel.prototype.create.call(this, sPath, oData, mParameters)
},

…

/**
 *@description Extended CREATE ERROR function
 * Stores data in local Index DB
 *@memberOf oData
 *@param {Object} data - Data resulting from call
 *@param {Object} mParameters - 1: called sPath, 2:saved object
 */
_createErrorCallback: function(data, mParameters)
{
   console.error("CREATE ERROR: %s \n Property %s not Set", data.message, sPath);
   var sPath = mParameters.path;
   var oUnsavedData = mParameters.data;
   oUnsavedData._http = 'create';
   return this._oComponent.getIndexDb()
      .create(oUnsavedData, sPath);
},
 

 

IndexedDb.js

If necessary, a new table (sPath) is first created in the IndexedDB. The data is then stored there.

/**
 *@description stores data in indexeddb and creates Table if necessary
 * adds property _http to save the intended http action (CRUD)
 *@memberOf IndexedDb
 *@param {object} data - data to be stored in indexed db
 *@param {String} sTable - Name of table that data is added to
 */
create: function(data, sTable)
{
   var that = this;
   sTable = that._trim(sTable)[1];
   return Promise.resolve()
      .then(that._createTable.bind(that, sTable))
      .then(that._createData.bind(that, data, sTable))
      .catch(function(err)
      {
         console.error("Data could not be written to INDEXEDDB");
      })
},

…

/**
 *@description creates a new table
 *@memberOf IndexedDb
 *@param {String} sTable - Name of table that data is added to
 */
_createTable: function(sTable)
{
   if (!this._tableAvailable(sTable)) version++;

   var request = indexedDB.open("localStorage", version);
   var indexedDbKey = 'Id';
   return new Promise(function(resolve, reject)
   {
      request.onupgradeneeded = function(oEvent)
      {
         IndexedDb._db = oEvent.target.result;
         var objectStore = IndexedDb._db.createObjectStore(sTable, {
            autoIncrement: false,
            keyPath: indexedDbKey
         });
         objectStore.createIndex('_http', '_http', {unique: false});
         request.onsuccess = function(evt)
         {
            resolve();
         };
         request.onerror = function(oError)
         {
            IndexedDb._db.close();
            reject();
         };
      };
      request.onsuccess = function(oEvent)
      {
         IndexedDb._db = oEvent.target.result;
         resolve();
      };
   })
},

…

/**
 *@description create indexdb entry
 *@memberOf IndexedDb
 *@param {object} data - data to be stored in indexed db
 *@param {String} sTable - Name of table that data is added to
 */
_createData: function(data, sTable)
{
   return new Promise(function(resolve, reject)
   {
      var oTransaction = IndexedDb._db.transaction(sTable, "readwrite");
      var oDataStore = oTransaction.objectStore(sTable);
      oDataStore.add(data);
      IndexedDb._db.close();
      resolve();
   })
}, 

 


The entry can be found in the IndexedDB accordingly. An additional attribute (_http), which stores the http verb, was added for later processing.

Local%20IndexedDB%20containing%20data

Local IndexedDB containing data

 

 

Subsequently, the data is read new and the table shows local data as well as data from the backend.

 

List%20displaying%20IndexedDB%20data

List displaying IndexedDB data

 

 

 

6. Delete Data: Offline

Deletion of data should work the same way as the creation of data, should the application not have access to the SAP systems. The delete function, described below, removes local entries and also creates delete tokens for entries on the backend in the IndexedDB in the offline state.
The application is shifted to offline again in order to delete the first entry in the list (backend data).

Items%20to%20be%20deleted

Items to be deleted

The program sequence is as follows:

Process%3A%20Delete%20Item%20on%20Backend%2C%20in%20offline%20Mode

Process: Delete Item on Backend, in offline Mode

 

View1.controller.js

Process-Start via ViewController:

/**
 *@description Deletes entry
 *@memberOf View1
 *@param {object} oEvent
 */
deleteEntry: function(oEvent)
{
   var that = this;
   var sPath = oEvent.getSource()
      .getBindingContext("TableModel").sPath;
   var oObj = this.getView()
      .getModel("TableModel")
      .getProperty(sPath);
   this.getView()
      .getModel("TableModel")
      .remove(oObj)
      .then(function(data)
      {
         that.onLoadTable();
      })
      .catch(function(error)
      {
         that.onLoadTable();
      })
},

 

Table.js

Forwarding the request:

/**
 *@description removes table entry
 *@returns {promise}
 */
remove: function(data)
{
   var oData = this.oComponent.getModel();
   var sPath = oData.createKey("/Table1Set", {
         "Id": data.Id
      }
   );

   return new Promise(function(resolve, reject)
   {
      oData.remove(sPath,
         {
            success: function(resp)
            {
               resolve(resp)
            },
            error: function(err)
            {
               reject(err);
            }
         },
      )
   })

 

oData.js

After the removal call has failed, the sPath is forwarded to the “_removeErrorCallback” method:

/**
 *@description extended REMOVE
 *@memberOf oData
 *@param {String} sPath - A string containing the path to the collection where an entry should be created.
 * The path is concatenated to the service URL which was specified in the model constructor.
 *@param {Object} mParameters - Optional parameter map, refer to API
 */
remove: function(sPath, mParameters)
{
   var that = this;
   mParameters.success = (function(success)
   {
      function fnExtend(data, oData)
      {
         success(data, oData);
         that._removeSuccessCallback(data, oData, this);
      }

      return fnExtend.bind({path: sPath});
   }.bind({path: sPath}))(mParameters.success);

   mParameters.error = (function(error)
   {
      function fnExtend(data)
      {
         error(data);
         that._removeErrorCallback(data, this.path);
      }

      return fnExtend.bind({path: sPath});
   }.bind({path: sPath}))(mParameters.error);

   ODataModel.prototype.remove.call(this, sPath, mParameters)
},

…

/**
 *@description Extended REMOVE ERROR function.
 * Stores items that are to be deleted in indexedDB
 *@param {Object} data - Data resulting from call
 *@param {String} sPath - called Path, including Key
 */
_removeErrorCallback: function(data, sPath)
{
   return this._oComponent.getIndexDb()
      .remove(sPath)
},

  

IndexedDb.js

As in the case of storing the data in the “Create” process, it is first checked as to whether a “Spath” table already exists. If this is not the case it will then be created. If the _removeData method does not find an entry under the path and key passed, it is stored locally as an entry to be deleted.

/**
 *@description Checks if affected tableentry exists. triggers creation of table if needed.
 * if data cannot be found in indexed db, the system asumes that the entry needs to be saved
 * to be deleted later
 *@memberOf IndexedDb
 *@param {String} sPath - Name of Spath that is read (most of it is also the iDB table name)
 */
remove: function(sPath)
{
   var that = this;
   var sKey = this._getKeys(sPath);
   var sShortPath = this._trim(sPath)[0];

   return Promise.resolve()
      .then(that._createTable.bind(that, sShortPath))
      .then(that._removeData.bind(that, sShortPath, sKey, false))
      .catch(function(err)
      {
         that._removeData(sShortPath, sPath, true)
            .then(function()
            {
               console.log("deleteToken successfully written to IndexedDB")
            })
            .catch(function(err)
            {
             })
      })

},

…

/**
 *@description Removes entry from indexed DB
 * if entry is not found, affected key and table are stored to be dealt with later
 *@memberOf IndexedDb
 *@param {String} sTable - Name of Spath that is read (most of it is also the iDB table name)
 *@param {String} sKey - spath Key
 *@param {Boolean} bDeleteToken - remove a delete token from INDEXED DB (_http: remove).
 * if false, its assumed that a create entry is to be delete after it was successfully send to the backend
 */
_removeData: function(sTable, sKey, bDeleteToken)
{
   var that = this;
   var bIndexedEntryFound = false;

   return new Promise(function(resolve, reject)
   {
      var request = indexedDB.open("localStorage", version);
      request.onsuccess = function(oEvent)
      {
         IndexedDb._db = oEvent.target.result;
         var objectStore = IndexedDb._db.transaction([sTable], "readwrite").objectStore(sTable);
         var request = objectStore.openKeyCursor(sKey);
         request.onsuccess = function(oEvent)
         {
            var cursor = oEvent.target.result;
            if (cursor)
            {
               bIndexedEntryFound = true;
               objectStore.delete(cursor.primaryKey);
               cursor.continue();
            }
            else
            {
               if (!bIndexedEntryFound)
               {
                  if (bDeleteToken)
                  {
                     that.create({ //...create indexeddb table with delete token, if it doesnt exist
                        'Id': sKey,
                        _http: 'remove'
                     }, sTable)
                        .then(function(data)
                        {
                           IndexedDb._db.close();
                           resolve();
                        })
                  }
                  else
                  {
                     reject();
                  }
               }
               else
               {
                  resolve(); //entry found + deleted
               }
            }
         };
      }
   })
},

 

Remove%20token%20for%20Data%20saved%20on%20SAP%20Backend

Remove token for Data saved on SAP Backend

 

7. Synchronization

The local data must be synchronized with the SAP system After the network is available again. There is now an entry for deleting and an entry for creating data.

Delete%20token%20and%20data%20to%20be%20created

Delete token and data to be created

 

On the IndexedDB a distinction which depends on the stored http verb is made as to whether data is to be created or deleted.

Synchronization%20process

Synchronization process

 

View1.controller.js

Process starts here

/**
 *@description Synchronize
 *@memberOf View1
 */
processLocalData: function(eEvent)
{
   var sPath = "/Table1Set";
   var that = this;

   this.getOwnerComponent()
      .getIndexDb()
      .processLocalData(sPath)
      .then(function(data)
      {
         that.onLoadTable();
      })

 

IndexedDb.js

Processing of locally stored data:

/**
 *@description After the system is back online, this will synchronize local storage with Backend data
 *@memberOf IndexedDb
 *@param {String} sTable - Name of table that data is add
 *@return {promise}
 */
processLocalData: function(sTable)
{
   var that = this;
   return new Promise(function(resolve, reject)
   {
      that.getTable(sTable, true)
         .then(function(data)
         {
            data.forEach(function(line)
            {
               var sKey = line.Id;
               if (line._http === 'create')
               {
                  that._oComponent.getModel()[line._http](sTable, line, {
                     success: function()
                     {
                        that._removeData(sTable, sKey, false)
                           .then(function()
                           {
                              resolve()
                           });
                     },
                     error: reject
                  })
               }
               else if (line._http === 'remove')
               {
                  that._oComponent.getModel()[line._http](line.Id, {
                     success: function()
                     {
                        that._removeData(sTable, sKey)
                           .then(function()
                           {
                              resolve()
                           });
                     },
                     error: reject
                  })
               }
            });
         });
   })
},

 

 

8. Outlook

Depending on application area, it is conceivable to build an app that after initial loading works independently of the network. All functions are therefore available to the user at any time. Field service technicians or mechanics in large production halls can load the required master data for the application upon opening the app. Full functionality is still guaranteed even though you might be e.g. in storage rooms without reception or at the customer without WIFI. New data is simply created in the IndexedDB and synchronized when the network is available.

 

 

This article was first published on inwerken.de.

About the author

7 Comments
You must be Logged on to comment or reply to a post.
  • Nice implementation.

    We used also used indexedDb and because of Internet Explorer and Edge implementation we have decided to:

    • Use a Worker to read/write into IndexedDb without freezing for large amount of data
    • As each entry is inserted into the DB, we encapsulate the results into an object to insert it at once or read it at once (only for “readOnly” entitySet => once again for large set of data
    • And last point we used a different DB store for each EntitySet because of IE/Edge that keeps the Db increasing even if you delete and could slow down after months of usage of the app

    We use such implementation in a hybrid Windows App.

     

    Thanks for the blog anyway!

     

  • Hi Tobias Gube,

    Your blog is very informative, thanks for posting a very interesting topic while following your blog I am getting

    “var networkModel = that.getModel(“network”);” is not defined error. After successful creation of Service Model, also I didn’t see network.js file code from your block, please let us know what is the use of network.js file.

     

    For more information please have a look at the below screenshot.

     

     

    Thank & Regards,

    Mallesh

     

     

    /
  • Hello Mallesh,

    the networkModel is part of a function, indicating whether the application has access to the internet. I found it to be useful in an app such as this. It is not covered here because i found the blog to be too lenghty.

    Regards

    Tobias