Standard UI5 event handling will usually go a long way. Yet sometimes, certain user actions can cause ui5 objects to generate a lot of similar events within a small period of time, and it is often not useful to handle each and all of them: only the last event needs handling.
A very common scenario is doing a search in response to the
liveChange
event: if you'd attach a handler to handle the
liveChange
event, and do the backend query from there, then a backend request would be sent for each keystroke while the user is typing in the search field. This causes a storm of requests that the backend must somehow handle. But most of these requests will be for naught, as the user is only interested in the result of the query that matches the last complete search term they typed.
So, rather than firing a query to the backend for each and every keystroke, it makes more sense to buffer these events, and react to only the last one. The bufferedEventHandler utility helps you to do just that in a generic and reusable way.
This ui5tip describes the
bufferedEventHandler utility. It is available on github under terms of the Apache 2.0 License. There's also a
sample application so you can try it out yourself.
The BufferedEventHandler sample app
The
bufferedEventHandler
sample application illustrates the scenario from the introduction. It consists of a single page showing mockup company data in a
sap.ui.table.Table
. A screenshot is shown below:
At the top left of the grid, there's a
sap.m.SearchField
labeled "Search in Name". The user can type some search term into the searchfield, and the grid will automatically refresh and show only the rows for which the CompanyName has a case-insensitive match with the entered search term.
While the search happens
automatically, it does not happen
immediately as the search term changes at every keystroke. Rather, about 1 second after the user stops typing, the data grid is filtered.
At the top right of the grid, there's a
sap.m.ProgressIndicator
labeled "Event buffer Timeout". The progress indicator reflects how much time has passed since the last keystroke. When the progress indicator reaches a 100%, the filter action is executed.
The bufferedEventHandler Utility
To buffer events we provide a
bufferedEventHandler
utility object with just one
bufferEvents
function. You can find this in the
bufferedEventHandler
file in the
utils
directory.
To use it, we need to import it into the source file where we want to use it. This will usually be in a ui5 controller and in the sample app we do this in
MainPage.controller.js
:
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/table/Column",
"sap/m/Text",
"sap/ui/model/Filter",
"sap/ui/model/FilterOperator",
"sap/ui/model/FilterType",
"ui5tips/utils/bufferedEventHandler"
],
function(
Controller,
Column,
Text,
Filter,
FilterOperator,
FilterType,
bufferedEventHandler
){
"use strict";
var controller = Controller.extend("ui5tips.components.mainpage.MainPage", {
...
});
return controller;
}
We can now refer to the
bufferedEventHandler
utility through the local variable that is also called
bufferedEventHandler
.
The controller uses the
bufferedEventHandler
utility in the
initSearchField()
method. This called from the controller's standard
onInit()
lifecycle method, which is called just once for the
Controller
instance:
...
onInit: function() {
this.initSearchField();
},
initSearchField: function(){
var searchField = this.byId('searchField');
bufferedEventHandler.bufferEvents(
// event provider
searchField,
// timeInterval
1000,
// eventId
'liveChange',
// data
null,
// handler
this.doSearch,
// listener
this,
// progressHandler
this.searchFieldProgress,
// progressUpdateInterval
50
);
},
...
The bufferEvents()
Method
The meat of the
initSearchField()
method is the call to the
bufferEvents
method of the
bufferedEventHandler
utility. This method has the following arguments:
eventProvider
: the 1st argument should be the object that emits the events - in our example this is the sap.m.SearchField
. This object should be a subclass of sap.ui.base.EventProvider
. (bufferEvents
will throw an error if it's not!)
timeInterval
: the 2nd argument is the timeout, in milliseconds. This is the amount of time that should pass between the occurrence of the last event and the call to the actual handler of the event. If a new event occurs during the wait period, the timeout is reset, and a new waiting period is started. In the example, we use a timeInterval
of 1000
- that is, we will wait 1000 milliseconds (1 second) before handling the last event.
Choosing the
timeInterval
is a balancing act. In the case of the example, where the events are generated in response to user actions, the
timeInterval
should not be too short, as the user should be given enough time to type a meaningful searchterm before the actual query kicks in. But if the
timeInterval
is too long, the application may appear unresponsive to the user. If the application appears unresponsive, the user may try to retype their search term, which will only postpone the reaction even more. (There's more about this in the section about the ProgressIndicator).
The next 4 arguments of
bufferEvents
correspond to
sap.ui.base.EventProvider
's attachEvent()
method:
eventId
: a string that identifies the event to listen to. In our example this is 'liveChange'
. Some ui5 objects, (for example, sap.ui.base.ManagedObject
s, which includes all sap.ui.core.Control
s) expose the events they expose through their metadata. In these cases, bufferEvents
will verify whether the passed eventId
is in fact exposed by the object, and it will throw an error in case it doesn't. EventProvider
s that do not expose their events through metadata can still be used with the bufferedEventHandler
, but you'll just need to make sure yourself the value for eventId
is valid, as bufferEvent
has no way of checking it.
data
: an optional argument to pass any "extra" data that the event handler might need. In the example, we pass null
as we have no need for any additional data.
handler
: this should be the callback-function that will be called upon to actually handle the event. The callback function will receive an instance of an sap.ui.base.Event
as single argument, which typically provides access to all relevant information pertaining to the event. In the example, we pass this.doSearch
, which is a method of the controller that will perform the actual filtering of the data grid.
listener
: this is an optional argument which you can use to specify the scope in which the handler will be called. Typically the handler will not be completely standalone, but it will refer to a this
object, one way or another. If the handler function is not already bound (for example, by using the function's bind()
method), then you should pass whatever object should act as this
for the handler function via the listener
argument. In the example, we simply use this
which refers to the controller instance itself. This makes sense as the handler function is also a method of the controller. (Remember: we passed this.doSearch
as handler.)
In the call to
bufferEvents
, these arguments will be used to create an actual handler for the event, and also automatically attach it to the
eventProvider
for the specified
eventId
. But rather then calling the passed
handler
, it will start a
javascript timeout for a duration of the passed
timeInterval
. If the timeout was already initiated, it is cleared, thus canceling the previous event, and initiating a new waiting period.
Monitoring wait progress
The final 2 arguments to
bufferEvents
are optional, and may be used by the application to monitor the waiting period between the occurrence of the last event and the time when the handler will actually be called:
progressHandler
: when passed, this should be a callback function which is to be called at the start and during the waiting period. If the progressHandler
callback is called, it will be called using the listener
as scope. The callback will be passed a floating point number between 0
and 1
, indicating the fraction of the time that has passed between the last event and now. If a progressHandler
is specified, it is always called at least once and passed 0
whenever a new waiting period is initiated. In this example we passed this.searchFieldProgress
, which is a method of the controller that updates the sap.m.ProgressIndicator
that sits in the right top of the data grid.
progressUpdateInterval
: this should be in integer, indicating the number of milliseconds between the calls to the progressHandler
. In our example it is 50, which means we will get 1000 / 50 = 20
updates during the waiting period, which ensures a smooth and regular update of the ProgressIndicator
control.
The ProgressIndicator
The sample application provides a
sap.m.ProgressIndicator
to indicate when the entered search term will be used to filter the data.
A progress indicator may not be necessary in case the
timeInterval
is so short that it will appear to the user as if the event is handled immediately. But when the
timeInterval
exceeds
200
or
250
milliseconds, most users will start to experience a noticeable lag.
Now, there is this strange psychological phenomenon happening here - as the user is still typing their search term, they will be happy that the backend query is not already fired. It would make them feel rushed if the grid was constantly being updated while they were typing. But once the user is done typing their search term, they want to have the result as quickly as possible. Obviously, the software cannot read the user's mind (yet!), so once the user stops typing, the application needs to let the user know they acknowledged their action, and that it is 'working on it'.
Hence the need for a progress indicator: by having a visual indicator that "something's happening", the user will be assured the application has acknowledged their input, and this will make the wait period before actually handling the event more acceptable.
If the wait is sufficiently short, a simple busyIndicator might do the trick, but since the
progressHandler
gets passed an exacte estimate of how much longer the user will need to wait, our progress indicator can communicate this to the user. This will make the application's behavior more predictable and hopefully more satisfying to use.
Of course, it is not absolutely necessary to use the
sap.m.ProgressIndicator
to give this kind of feedback to the user. It's just that for this sample, this was the easiest, most straighforward illustration of this principle. You can use the `progressHandler` callback to do anything you like to fit your need.
Detaching
The
bufferEvents
method will create and attach a handler to the eventProvider.
bufferEvents
will also return that generated handler so you can detach it explicitly from the
eventProvider
if you need to. As a convenience, the returned handler provides its own
detach()
method for this purpose:
var bufferedEventHandlerInstance = bufferedEventHandler.bufferEvents(...);
...
bufferedEventHandlerInstance.detach();
(Note that in a typical scenario, the eventHandler and the eventProvider will almost certainly be in the same scope and lifecycle, so there is rarely a need to explicitly do this.)
Other Use Cases
The
liveSearch
scenario may not always be a convincing use case. For example, if the query is done against a client model rather than a remote backend system then it might not actually be a problem to re-issue the query for every keystroke. But there are some other scenarios that benefit from event buffering. We will encounter one such case in the tip about
Persisting UI State.
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!
1 comment:
its cool
Post a Comment