In this ui5tip, we'll take a look at integrating ui5's
sap.ui.model.json.JSONModel
with
sap.ui.util.Storage
utility.
Our immediate use case for this was to allow easy and transparent persistence of UI State, which
has its own dedicated tip. In this tip, we describe how the actual persistence is implemented.
Sample application: a Shopping List
To illustrate just the storage model, we developed a tiny Shopping List application. You can run it yourself by downloading the contents of the
localstoragemodel
folder and exposing them with your webserver.
This is what the application looks like:
Sample Application Features
- A Products list (left), and a Shopping list (right). Users can browse the Products list and see name and price. The Products list has a row action button with a shopping cart icon. If the product is already on the shopping list, the shopping cart appears full. Hitting the row action button will add the product to the Shopping List.
- In the Shopping List, users see the product name, item price, quantity, and item total. The items in the shopping list also have a row action to remove the item from the shopping list.
The Shopping List also has a toolbar with some buttons that control the Shopping List Data:
- The Save button will save the current contents of the shopping list to the local storage
- The Undo button will restore the current contents of the shopping list with whatever data was stored in the local storage
- The Submit button represents the action of actually placing an order for the shopping list. It will also clear the shopping list and save.
- The Clear button will empty the current contents of the shopping list, but without saving the state to the local storage.
Sample Application Demo
To test the application, try the following sequence of actions:
- Open the application. Initially, the Shopping list should be empty.
- In the Products list, add a Product to the shopping list by hitting the shopping cart button. The item should be added to the Shopping list.
- Refresh the browser window. When the application reloads, you'll notice that the shopping list is empty - that's expected, since you didn't save the shopping list.
- Now, repeat step 2 and add some products to the shopping list. Hit the Save button.
- Refresh the Browser again. Now, when the application reloads, the products you added in step 4. should re-appear automatically in the list.
This demonstrates that the application is capable of persisting the saved shopping list data. Instead of refreshing the window, you can also try to completely close the browser, or even reboot your machine. But when you revisit the application - with the same browser - then you'll notice that the data will still appear.
In addition to the persistence the application also provides a simple, one level undo action. Whenever you make a modification to the list, either by adding a new item, removing an item, or modifying the quantity of an item, both the Save and the Undo button will become enabled. We already demonstrated the Save button action. Hitting the Undo action button will restore the contents of the shopping list with whatever was available in the Storage, restoring the contents of the list to the previously saved state.
You can use your browser to inspect the local storage. It might look something like this:
In the remainder of this tip we will discuss how these features were built by combinding two classes in the ui5 framework - the
sap.ui.util.Storage
utility and the
sap.ui.model.json.JSONModel
.
sap.ui.util.Storage
utility
The
sap.ui.util.Storage
utility offers a UI5 APIto access the Browser's standard HTML5
Web Storage API. It's a pretty basic, no-nonsense wrapper for managing modest amounts of data based on key/value access.
Of course, you can use the
sap.ui.util.Storage
utility directly and code your own logic to control exactly when you want to retrieve and store some data. While there is nothing against that approach, we envisioned something that also works when using
models and data binding.
This may need a little bit of explanation.
UI5 models
Models are a way to achieve managed data access. A model manages a particular collection of data, and can be shared across multiple elements of the application, or even be accessible to all elements of the application.
For example, both the Product List and the Shopping List are each managed by their own model, which are declared in the application's
manifest.json
:
...,
"models": {
"products": {
"type": "sap.ui.model.json.JSONModel",
"dataSource": "products"
},
"shoppingList": {
"type": "ui5tips.utils.LocalStorageJSONModel",
"dataSource": "shoppingListTemplate"
}
},
...
The model can be observed by listening to its events, and this allows different parts of the application to react whenever something interesting happens to the state of the model - i.e. when its data is manipulated. For example, in
MainPage.controller.js
, an event handler is attached to listen to the Shopping list's
dirtyStateChange
and
propertyChange
events, which in turn control certain aspects of the screen logic, such as enabling and disabling the Save and Undo buttons:
...,
initShoppingListModelHandlers: function(){
var shoppingListModel = this.getShoppingListModel();
shoppingListModel.attachDirtyStateChange(function(event){
this.dirtyStateChanged(event.getParameters());
}, this);
shoppingListModel.attachPropertyChange(function(event){
var path = event.getParameter('path');
var context = event.getParameter('context');
var itemsPath = '/items';
if (path === itemsPath || context && context.getPath() === itemsPath) {
this.itemsChanged(shoppingListModel.getProperty(itemsPath));
}
}, this);
},
...
Data Binding
In Ui5, databinding is a mechanism that lets you declaratively construct objects and change their properties based on the state of a model. The declarative aspect means that no explicit coding is involved. For example, rather than setting up an event handler that contains explicit code to respond to changes to the state of the model, you can use a special syntax in designtime property assignments that ensures the runtime property value will be assigned directly from some part of the data in the model.
Some examples include:
- The actual data in both the Product List and Shopping List. Both these are implemented using a
sap.ui.table.Table
control, which only support adding rows through data binding. For example, take a look at ShoppingList.fragment.xml
to see how it gets its rows from the Shopping List model:
...
<table:Table
id="shoppingList"
title="Shopping List"
editable="true"
selectionMode="None"
enableBusyIndicator="true"
visibleRowCountMode="Auto"
rowActionCount="1"
rows="{
path: 'shoppingList>/items'
}"
>
...
(This example basically says: create a row in the shopping list for each item in the shopping list model)
- The Product List's row action shows a full or empty shopping cart, depending upon whether the product is already in the shopping list. This is achieved in
Products.fragment.xml
with databinding, which passes the current product and the items from the shopping list to the controller's getShoppingCartRowActionIconSource
formatter function:
...
<table:RowActionItem
binding="{shoppingList>/items}"
icon="{
parts: [
{path: 'products>'},
{path: 'shoppingList>'}
],
formatter: '.getShoppingCartRowActionIconSource'
}"
text="Add to Cart"
press="onCartButtonPressed"
/>
...
(This example says: call the
getShoppingCartRowActionIconSource
method to obtain an icon, depending on the current product from the product model and all the items in the shopping list model.)
- In
ShoppingList.fragment.xml
, the state of the Submit and Clear buttons is enabled depending upon whether the shopping list has any items:
...
<m:contentMiddle>
<m:Button
id="approvalButton"
icon="sap-icon://cart-approval"
tooltip="Send Order"
enabled="{= ${shoppingList>/items}.length > 0 && ${shoppingList>/items/0} !== undefined }"
press="onApproveButtonPressed"
/>
</m:contentMiddle>
<m:contentRight>
<m:Button
id="clearAllButton"
icon="sap-icon://clear-all"
tooltip="Clear Shoppinglist"
enabled="{= ${shoppingList>/items}.length > 0 && ${shoppingList>/items/0} !== undefined}"
press="onClearButtonPressed"
/>
</m:contentRight>
...
(This example says uses a slightly different binding syntax called
expression binding to enable the button if there is at least one item in the shopping list.)
While all these features could also have been implemented by explicit coding, data binding allows a lot of this to be defined completely declaratively in the view, with much less code, and denoted in a way that transparently and unambiguously ties the data to the relevant item in the UI.
Now - there is no shame (I think) in not immediately embracing ui5's data binding. There are a number of areas that can be somewhat complex and unintuitive at first. But by using it more and more often, you start experiencing the benefits, and - just as important -, learn about the limitations. Now, this post is not an in-depth article on ui5 databinding. It's just that, at some point, you learn to use it in such a way that it becomes one of the most important factors in how you design ui5 applications, as well as the way different parts of the application communicate with each other.
So, we consider using ui5 models and data binding as a given. And if you find you have a need for the kind of client-side persistence capabilities offered by the Web Storage API, then you are probably not interested in that as an isolated way of storing some bits of data. Instead, you're going to want to have a normal, regular ui5 model that incorporates these persistence features.
A sap.ui.model.json.JSONModel
backed by sap.ui.util.Storage
We decided to take the
sap.ui.model.json.JSONModel
as a base, and extend it to add a few methods that allow the model's data to be stored and retrieved from
sap.ui.util.Storage
.
The reason for this approach is to allow our model exactly the same as the standard ui5
sap.ui.model.json.JSONModel
. This means that in particular, all behavior with regards to databinding will be exactly as with the
sap.ui.model.json.JSONModel
.
In theory, it would also be possible to extend the abstract
sap.ui.model.ClientModel
, but it turns out that implementing reliable databinding is not as easy as it seems. Or I should say, I took a naive shot at doing that, and failed. While it might be very instructive to try it in earnest, I decided that at this point I am more interested in having a working solution than to learn all the ui5 internals required to succesfully implement databinding.
The result is the
LocalStorageJSONModel
.
Instantiating the LocalStorageJSONModel
from the manifest.json
The sample application creates the
LocalStorageJSONModel
implicitly by declaring it in the
manifest.json
:
"shoppingList": {
"type": "ui5tips.utils.LocalStorageJSONModel",
"dataSource": "shoppingListTemplate"
}
It gets initialized with a
datasource
called
shoppingListTemplate
which is also declared in the
manifest.json
:
"shoppingListTemplate": {
"uri": "data/shoppingListTemplate.json",
"type": "JSON"
}
The
datasource
refers to some configuration data stored in
data/shoppingListTemplate.json
and its contents are:
{
"autoSaveTimeout": -1,
"storagePrefix": "shoppingList",
"template": {
"items": [
]
}
}
This data is passed as first argument to the
LocalStorageJSONModel
constructor. Its properties are:
int autoSaveTimeout
: (optional) an integer specifying the number of milliseconds to wait after the last change to the model before automatically saving the model's data to the persistent storage. If this is 0
or less, data is not automatically persisted.
string storagePrefix
: (optional) a string that is used to prefix the key under which the models data will be stored in storage. The
sap.ui.util.Storage
constructor takes a storagePrefix, and the LocalStorageJSONModel
takes its own class name for that. But if you have several of these models in one application, you can keep them apart by specifying a specific storagePrefix
here.
object template
: (optional) an object that will be used as template data for the model.
Instantiating the LocalStorageJSONModel
directly
Of course, you can also import the class into your in your ui5 classes (for example, in a controller) and call its constructor to create an instance:
sap.ui.define([
"sap/ui/core/mvc/Controller",
"ui5tips/utils/LocalStorageJSONModel"
],
function(
Controller,
LocalStorageJSONModel
){
"use strict";
var controller = Controller.extend("ui5tips.components.app.App", {
onInit: function(){
var localStorageModel = new LocalStorageJSONModel({
"autoSaveTimeout": -1,
"storagePrefix": "myApp",
"template": {
...data...
}
});
this.getView().setModel(localStorageModel, 'localStorageModel');
}
});
return controller;
});
Key Methods
The most important methods provided by
LocalStorageJSONModel
are:
loadFromStorage(template)
: populates the model with the data persisted in the storage. If the template
argument is specified, then the data from the storage is patched with the data in the template. (For more details, see the next section about model initialization and the template). In the sample application, the Undo button action is implemented by calling loadFromStorage()
:
onUndoButtonPressed: function(){
var shoppingListModel = this.getShoppingListModel();
shoppingListModel.loadFromStorage();
}
saveToStorage()
: stores the model data to the browser storage. In the sample application, the Save button action is implemented by calling the saveToStorage()
method.
onSaveButtonPressed: function(){
var shoppingListModel = this.getShoppingListModel();
shoppingListModel.saveToStorage();
}
deleteFromStorage()
: permanently removes the data from the local storage. Use this if you're sure the application will not need any of the currently stored data anymore.
isDirty()
: returns a boolean that indicates whether the current model state is different from what is stored. If it returns true, it means the current state of the model is different from the stored state. Note that you can also use the dirtyStateChanged
event to get notified of a change in the dirty state.
Template and Model initialization
As part of model initialization, whatever data the browser had associated with the
storagePrefix
is retrieved.
If a
template
is specified, then the data retrieved from the storage is patched with the template and the resulting data structure is immediately saved to the storage. This provide a basic method to evolve the structure of the model and pre-populate it with any defaults.
The patching of the data occurs non-destructively: only those paths in the template that do not exist already in the stored data structure will be added.
If you need it, you can always apply more advanced patching schemes after instantiation, but in many cases, this built-in behavior will suffice to update and upgrade the model structure as your application grows and gets more features.
You can use the following methods to work with the template:
getTemplateData()
: retrieve the template passed to the constructor.
resetToTemplate()
: repopulates the model with the template. Any data stored in the model will be lost.
updateDataFromTemplate(data, template)
: utility method that is used to patch the data
argument with the template
argument. It returns a object that represents the merge of the data
argument and the template
argument.
Events
The
LocalStorageJSONModel
provides these events:
dirtyStateChange
: this event has two parameters, isDirty
to indicate whether the model is now dirty and wasDirty
, indicating whether the model was dirty prior to the latest change.
The sample application uses this event to determine whether to enable or disable the Save and Undo buttons:
shoppingListModel.attachDirtyStateChange(function(event){
this.dirtyStateChanged(event.getParameters());
}, this);
and
dirtyStateChanged: function(parameters){
var isDirty = parameters.isDirty;
this.byId('saveButton').setEnabled(isDirty);
this.byId('undoButton').setEnabled(isDirty);
},
The following events can be used to keep track of the model state:
attachDirtyStateChange(data, handler, listener)
: attach a handler
function to get notifications of a change in the dirty state. If a some change is made that causes a difference between the stored data and the model data, this event is fired and the handler
is called in the scope of the listener
, and gets passed the application specific payload data
.
Autosave
The sample application controls when the model data will be persisted to local storage by calling
saveToStorage()
explicitly. But there are also use cases where you simply want the storage to always reflect the state of the model, or at least, track it as closely as possible. The
persistence of UI state is such a case, and for these scenarios the
LocalStorageJSONModel
supports an automatic save feature.
Autosave works by monitoring the state of the model, and then saving to the storage whenever a change is detected. While saving to storage should generally be pretty fast, it is a blocking operation. So rather than always explicitly persisting after a change occurs, we simply buffer the change events with the
bufferedEventHandler
and persist the data to storage some time after the occurrence of the last change event.
To use the autosave feature, simply pass a positive value for the
autoSaveTimeout
property when you instantiate the model. Alternatively, can also get or set the value of the autoSaveTimeout property after model construction by calling the getAutoSaveTimeout() and setAutoSaveTimeout() methods respectively. To disable autosave, simply set the property to a zero or negative value.
Finally
Did you like this tip? Do you have a better tip? Feel free to post a comment and share your approach to the same or similar problem.
Want more tips? Find other posts with the
ui5tips tag!
No comments:
Post a Comment