Offline app

Beside offline assets which are served from cache by service worker, service worker can use IndexDB to serve and store data objects contents.

We are using network first strategy for data and if network is not available, it will try to serve data from IndexDB.

There are some things, which needs to be considered and implemented in order for an app to be available for offline usage.

Service worker works like a proxy and all request goes threw it. In this place we are taking care of requests manually. In the other words, by registering service worker we are taking care of a strategy how data is being served to the article.

IndexDB structure

Base structure is created

  • DataObjects - will store required dsObjects data
  • Settings - table to store service worker run time settings
  • Queue - changes which are made offline will be put to the queue
  • Error - stored offline errors
  • Post - other api calls cached here.
  • dsObject... - article dataobjects stored in separate tables.

Adding data to IndexDB

Article is marked as ready for offline usage and data is added to IndexDB. By default master dataObjects data is saved like this:

  • If dsObject.getData() returns more then 1 row it will take rows having af_Selected marked column.
  • If dsObject.getData() returns 1 row, it will take only 1.

Child sources will be queried to the server, by looping master objects data.

If dsObject row will have FileRef column, it will be treated as FileStore table and all related files will also be downloaded form server.

  1. By clicking button with markup - button with tag data-offline="someID" offline-dbversion = "2"
  2. Creating in javascript - someID = new af.dataBinding.bindOfflineHandler({dbVersion: 2}); . And calling method someID.saveToIndexDB();

Settings

  • Article settings

    Can be added with markup or by Javascript

    • *Required dbversion - when adding/removing data objects, changing keys in the article designer, dbVersion has to be increased. When version is increased ,it will remove all tables from IndexDB and create new ones with new settings. If no version is specified, every time it will recreate index DB. By this way IndexDB structure can be controlled on client browser, making sure that all updates will be in place.
    • retrieveAll - when triggering SaveToIndexDB() it will take all dsObject.getData(), instead of default behavior.
    • appenditems - By default before adding items with SaveToIndexDB() it will remove old items and add new ones. By setting this to true, it will not clean, just append new ones and update old ones which meets key criteria.
    • autosync - Default is true. When article is set as ready for offline, when loading data to page at the same time it will try to fetch that data into IndexDB as well. It will fetch, update only values which are already in IndexDB and meets key criteria. Also this allows for data changed in offline mode, to be synced automatically back to server. If this is false. Custom data synchronization has to be implemented.
    • showalerts - after SaveToIndexDB() completion it will show alert. By setting this to false, alerts will not be showed. It will show reload required and save to indexDB complete messages.
    • shownotifications - after auto syncing to server, browser notification will popup, informing user that sync was successful.
    • showProgress - When saving to IndexDB this would open popup with overal progress. Default is false;.
    var someID = new af.dataBinding.bindOfflineHandler({ dbVersion : 2, retrieveAll :false, appendItems :false, autoSync :true, showAlerts :true, showNotifications:true });
  • Data Object settings

    Should be passed by accessing object someID.setSettings({....})

    • filterObject - When passing filter object before SaveToIndexDB() instead of default getData() for master source it will query server to get data by give filter. Useful when only certain data is needed to be added to IndexDB. For example 'Checked out by me'.
    • key - if no key is specified, when creating table for dataObject in IndexDB it will use PrimKey if it is provided, else it will just add id with autoincrement value.
      Always try to add PrimKey or provide specific unique key, then it will make sure that data would be correctly synchronised. .
    • retrieveAll - when using SaveToIndexDB() data will be fetched from server, instead from dsObject.getData(). Do not mix with article setting save all. By adding this to dataobject it will override article setting
    • appendItems - if true, it will not delete data object in IndexDB before inserting.
    someID.setSettings({ 'dsObject':{ filterObject: someFilterObject }, 'dsObject2':{ retrieveAll: true, key: 'MyID' } });

Handlers

  • DataObject handler - will handle CRUD and stored proc requests to database. When app is marked for offline usage it will also try to copy all CRUD requests and update indexDB on the fly if AutoSync is not disabled.
  • Api handler - will handle '/api/*', '/gridlayouts/*', '/filterbuilder/*' requests. Is working similar to DataObject handler, except when offline, it is only returning saved requests by path (GET) only. Does not store POST parameters.
  • File upload handler - will be activated only in offline mode. Will handle file/upload/* requests.

Syncing changes to server

Syncing is triggered by changing navigator status from offline to online, on page load or after some changed data is added to the queue then background sync is registered. On mobile devices, if tab is closed, background sync will make sure that data is synced back to server, when connection will be available.

When users makes changes in offline mode, these changes reflects in IndexDB and also are added to Queue table. When sync is triggered, it will try to send all changes to the server. After syncing is complete, browser notification should appear.

There might not be any visible errors after syncing if such appeared. Database logic should appear in database as well.

Make sure you have added PrimKey's to you modifiable s data objects. It will use same PrimKey's for synchronization. For created rows, it will generate new PrimKey client side and will try to create value with generated PrimKey.

Data syncing on the fly

If article is prepeared for offline usage, by this point service worker will take over all CRUD requests. It will check retrieved data and update indexDB with new values if possible.

For dataObjects with no master child records defined, it will check if such record with defined key exists in database. If it finds such, it will update it.

For dataObjects with master child records defined, it will check if master record is available in indexDB. If it finds such, then it will try to check all retrieved records in indexDB and update/insert if needed. Delete wll be performed if only dynamic loading is disabled.

Files which are like attachments to records in IndexDB, service worker also should add to IndexDB.

af.dataBinding.bindOfflineHandler methods

  • SaveToIndexDB() - will trigger data is saved to indexDB and article is marked as ready for offline usage. If you use default settings, make sure data required data is available with dsObject.getData().
  • checkIfOfflineAvailable() - will return promise which resolves to true/false if article is marked as ready for offline usage
  • attachEvent('onComplete') - will fire after SaveToIndexDB()
  • attachEvent('indexDbReady') - will fire when service worker finished prepearing index DB for usage.
  • attachEvent('onNewVersion') - will fire when new service worker versions is waiting.
  • attachEvent('onDataSyncStarted') - will fire before checking if there is offline changes
  • attachEvent('onDataSyncComplete') - will fire after page load, going online or on page load, if there was some offline made changes done and Apffame tried to sync them to server.
  • attachEvent('onNoDataSyncComplete') - will fire after page load, going online or on page load, if there was no offline changes made.
  • updateIndexDB(articleID, datasource, data, pCallback) - can update indexDB.
    Useful when you need to update parent article, from child article, when you make some changes in offline mode, and these changes should reflect in parent.
    Make sure provided keys are identical in both article datasources
    dsObject.attachEvent("onBeforeSave", function(res){ saveToIndexDBHandler.updateIndexDB(parentArticleID, parentDataObject, res, function(return){ console.log(return); }) });
  • setSettings(settings) - used to set settings for data objects.
  • addArticleToCache(articleURL) - will open provided article in Iframe.
    Useful when there is a detail article and you want that it would be avalaible offline.
    Make sure you will remove that article from DOM
  • addArticlesToCache(articlesURLArray) - will open given articles in a loop, after each has received SaveToIndexDB() onComplete event.
    Used in parent article, when details article is setup to call SaveToIndexDB() on load. Make sure it is defined before SaveToIndexDB()
  • checkIfOfflineAvailable() - Will return promise, resolved value will show if article data is addaded to IndexDB and article is marked as Offline ready.
  • skipWaiting() - ServiceWorker method. Calling this, will trigger service worker to update if new service worker version is available. Usefull if alerts are disabled.

Considerations

To prepare app for offline usage, app should be prepared

  • Razor

    Avoid accessing database or do other UI changes in razor. Your page is cached and served without checking search parameters. Razor runs on server side and in offline mode, server is not accessible and you might get wrong cached version.

  • Files

    Data objects having files and for them to be available offline add FileRef column. Make sure to pass all required field values with onBeforeUpload event.

    uploadContext.attach('onBeforeUpload', function(pParam){ pParam.append('Domain',Value); });
    UploadContext.attach('onBeforeUpload', function(pParam){pParam.appne});
  • WhereClause and FilterString

    Using WhereClause and FilterString should be avoided. There are no methods to parse SQL clauses in offline mode. Use FilterObject or WherObject insted.

  • Do not use
    /* Razor start*/ /* Razor end*/ /* Javascript start*/ var gMyParam = ""; dsObject.setWhereClaue('MyParam = '+ gMyParam); /* Javascript end*/
    Change to
    dsObject.setParameter('filterObject', { type: 'group', mode: 'and', items: [ { type: 'expression', column: 'MyParam', operator: 'equals', valueType: 'number', value: parseInt(af.common.getUrlParameters().MyParam) } ] });
  • Stored procedures

    Stored procedures in the app will be executed successfully in offline mode. But they will return empty array. Stored proc execution request will be added to the queue.

  • Other api calls

    Requests '/api/*', '/gridlayouts/*', '/filterbuilder/*' will be handled by api handler results will be stored in posts table and will be provided in offline mode. These request are saved automatically when requested.

  • Compatabilty

    We are using ServiceWorker, Notifications, BackgroundSync, ServiceWorker Messages. Check browser compatability before launching to production and targeting certain device/browser.

  • Master details articles

    When creating articles it is recommended to have master and details articles in the same scope.
    Master - https://someURL/Myarticle
    Master - https://someURL/Myarticle-details
    This makes sure that service worker for details article will always be ready.

  • Sorting

    Sorting is also available when offline. It is done witth arrays so might differ when it is retrieved from SQL, especialy with fields which are equal. Test and pick up correct sorting criterias, for data order to be the same as retrieving from SQL database.