Technical Articles
UI5 Tips: Buffering Events to avoid a request-storm
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 ); }, ...
bufferEvents()
Method
The 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 thesap.m.SearchField
. This object should be a subclass ofsap.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 atimeInterval
of1000
– 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 allsap.ui.core.Control
s) expose the events they expose through their metadata. In these cases,bufferEvents
will verify whether the passedeventId
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 thebufferedEventHandler
, but you’ll just need to make sure yourself the value foreventId
is valid, asbufferEvent
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 passnull
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 ansap.ui.base.Event
as single argument, which typically provides access to all relevant information pertaining to the event. In the example, we passthis.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 athis
object, one way or another. If the handler function is not already bound (for example, by using the function’sbind()
method), then you should pass whatever object should act asthis
for the handler function via thelistener
argument. In the example, we simply usethis
which refers to the controller instance itself. This makes sense as the handler function is also a method of the controller. (Remember: we passedthis.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 theprogressHandler
callback is called, it will be called using thelistener
as scope. The callback will be passed a floating point number between0
and1
, indicating the fraction of the time that has passed between the last event and now. If aprogressHandler
is specified, it is always called at least once and passed0
whenever a new waiting period is initiated. In this example we passedthis.searchFieldProgress
, which is a method of the controller that updates thesap.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 theprogressHandler
. In our example it is 50, which means we will get1000 / 50 = 20
updates during the waiting period, which ensures a smooth and regular update of theProgressIndicator
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!
cool, i didn't know there was a standard way for this.
generally i would go for debounce event from lodash
https://lodash.com/docs/#debounce
Bilen Cekic Hi Bilen, thanks for chiming in!
the bufferedEventHandler utility is not "standard" - we built it for some of our projects.
We did build it in a way that seemed natural for ui5 by using ui5 event primitives (Event, EventBuffer)
I suppose that integration could be a bit tighter but the presented solution is what was born from a practical need and so far we're ok with it.
sorry Roland my bad, i wrongly saw namespace as standard but it is your custom namespce actually. Thanks for the article. We had similar issues and used underscore initially and later on moved to lodash.
No worries and thanks for sharing your approach 🙂
Hi Roland,
thanks for this great blog post!
For moving inside the Fiori Launchpad, the sap/ushell/EventHub might be an option, since it supports waiting for events or asking for the last published event.
All the best!
Christian
Hi Christian Graff , thanks for chiming in!
Hey, that's interesting - this may be a silly question but where can I find API documentation for sap/ushell/EventHub ? I cannot seeem to locate it in https://sapui5.hana.ondemand.com/#/api/sap.ushell%23overview
It is not documented officially. You have to download an SDK from tools.hana.ondemand.com and dive into the sources 🙂
The path <SDK/test-resources/sap/ushell/test/EventHub.qunit.js> might be a good starting point for you.
Understood. Thanks man!