Skip to Content
Technical Articles

Trees in SAPUI5 – build a UI5 file browser with ODataModel on client side, but pure Node.js (no OData) as server

Read this, if you want to:

  • know how Tree controls work in SAPUI5/OpenUI5
  • know how they dynamically load data on-demand, as the user scrolls and drills through the Tree
  • get working code which you can easily debug and analyze on client and server side

 

Introduction

Trees are quite a niche topic: as witnessed by myself, one can work on the UI5 framework for many years without ever using a Tree and binding it to a data model. The limited amount of documentation also indicates that Trees and their way of being bound are not in the main focus of app developer interest. But there are times when you just need to display hierarchical data. And lots of it. What are the options?

Let’s explore this topic, understand the ODataTreeBinding and OData structures for trees – and create a UI5 file browser based on a plain Node.js server (without any OData helper library) along the way.

sap.m.Tree vs sap.ui.table.TreeTable

First of all, there are two different Tree controls in OpenUI5. They compare quite like the respective Table controls: the sap.m.Tree shares a common base with the sap.m.Table and hence it has no support for virtual scrolling. This means that all its rows are present in the HTML displayed by the browser (in “growing” mode: all up to the position to which the user has already scrolled down). So it is a good solution for a limited amount of tree nodes being displayed at the same time, like up to a few hundred. Beyond that, performance will be affected.

The sap.ui.table.TreeTable, on the other hand, inherits from the powerful sap.ui.table.Table, which can handle huge amounts of data because it only creates the HTML for the currently visible rows – rarely more than one or two dozen. As the user scrolls, the HTML of the rows is not exchanged, only the data displayed in each row is modified. So the Table (and hence also the TreeTable) does not care whether there are a hundred or a billion rows in the data.

JSONModel vs ODataModel

The JSONModel is a very versatile and popular one, as it has no other requirements than the data being available in the popular JSON format. The drawback is that all the data needs to be present on the client side (at least usually – we will come to this point soon below). So it is not a good choice for something like a file browser where only the actually needed data should be collected on-demand and sent to the browser. Traversing ALL directories and sending ALL data would take way too much time.

The ODataModel on the other hand is extremely easy to use – basically just the service URL is needed to bring tables to the UI which support features like sorting, filtering, and of course paging. All the related logic is happening behind the scenes in the ODataModel on UI5 side and the respective server implementation. The downside is that server-side libraries implementing OData are not available on all platforms. Implementing a fully OData-compatible server from scratch is a huge task, as the protocol is quite complex/powerful.

Luckily, there are actually two ways out of this dilemma:

  1. At least with the sap.m.Tree, there is a pretty simple way to achieve lazy-loading of the needed data even with the JSONModel. It uses the toggleOpenState event of the Tree control; a sample can be found here (press the top right button to view the code). But note: this only covers lazy-loading of the complete content of a directory; this does not cover loading the content of a big directory with thousands of files in smaller chunks.
  2. With batch support disabled (which anyway is of no big use in a file browser scenario) and pure read-only access, the actually used subset of the OData protocol in a Tree is quite minimal – basically only the $skip and $top query parameters are used to specify the needed chunk of data and the $filter parameter to specify the parent node of the requested data. This subset can be implemented from scratch with very limited effort, compared to the full OData spec.

As it sounds pretty easy to support these parameters and the sap.ui.table.TreeTable should be used for its paging capabilities, let’s go this second route.

Another reason for this choice is that I was curious to understand how the paging works with tree structures. The UI5 Trees work quite differently with the JSONModel than with the ODataModel (the differences are probably encapsulated in the respective *TreeBinding classes):

The former requires a tree structure in the JSON data and the knowledge under which property name the child list is available (“categories” in this example). From there it’s pretty trivial to understand how this structure is mapped to the UI and how dynamic loading can populate subtrees on-demand. But as OData only knows flat lists of data, how does it handle trees?

Tree Structures in OData

For its UI5 client maturity, I focused on OData v2 (as implemented in the sap.ui.model.odata.v2.ODataModel). There, the tree structure is basically encoded as part of the data. Most importantly, each data element has to know the ID of its parent node in the tree. While this information is sufficient to define a tree structure, there is more information needed, probably for more efficient handling: each node knows its hierarchy level (how deep in the tree it resides) and whether it is a leaf or has children. And of course each node needs an ID to which its child nodes can refer. An example can be seen here.

The names for these attributes in the data are not predefined, they can be configured when the binding of the Tree control is created (in a map called treeAnnotationProperties) as can be seen in lines 17-20 of my sample app.

While this looks like some overhead, you can imagine that it’s pretty easy to populate this information on-the-fly. Also note that the flat data structure used by OData is only the (virtual) data transfer structure. It’s not required that the original data has a flat structure.

Let’s use the file browser as example: of course the original data is a tree of directories and files, which is only flattened virtually for data transfer. When the user expands the node for directory “A” in the browser, then the client asks for all files with parent node ID “A” and the server side just needs to access the file system to get the content of directory “A”. For each contained file it is trivial to check whether it is a leaf in the tree or another directory. The hierarchy level is just the number of folders in the complete path. Done. Very easy.

Ok, then we also have the paging-related OData parameters… $skip tells the server the position of the first file it is interested in, $top tells how many files should be sent, starting from that position. Assuming we have two files “B” and “C” inside directory “A”, with $skip=1 and $top=1, the server would have to ignore one file (“B”) and only send one file (“C”). Usually $top has a value around 100 in UI5 and $skip keeps being increased by this value, as the user scrolls through the list.

So all in all there will be requests like this one:

http://localhost:2019/filebrowser/Files?$filter=ParentFileID%20eq%20%27some_folder%27&$skip=0&$top=110&$inlinecount=allpages

There is one last thing, the “count”. This information from server side is required by the TreeBinding and lets the client know how much data there is in total (needed e.g. for correctly-sized virtual scrollbars). Again, providing this information is trivial for a file browser back-end: despite only sending the requested subset of files to the client, also set the __count value in the response to the total number of files within the requested parent directory.

The Implementation

As promised, this blog post comes with complete code for a runnable sample app containing a Node.js server which handles the OData requests triggered by the UI5 TreeTable. The logic is described above and the actual implementation is simple, with less than 100 lines of code, empty lines and comments included.

Starting from line 21, the OData parameters are extracted, in line 54, the file system is asked for the list of files and in the subsequent loop, the data (just file names) is enriched with the tree information as (mostly) described above. There is also a __metadata section, which, well, has to be there.

The structure created for one single file looks like this:

{
	"Name": "New folder",
	"FileID": "some_folder%2fNew folder",
	"HierarchyLevel": 1,
	"Description": "some file",
	"ParentFileID": null,
	"DrillState": "collapsed",
	"__metadata": {
		"id": "some_folder%2fNew folder",
		"type": "DemoModel.File",
		"uri": "Files(some_folder%2fNew folder)"
	}
}

The server-side entry point in server.js also handles the delivery of the metadata document. This is required for all OData services, so the client knows the data structure. Information which properties contain the tree structure information could also be added there as annotations, but I wanted to keep the file as simple as possible.

The UI5 client side is also kept extremely simple, loading OpenUI5 from the public CDN and just configuring one TreeTable control bound to the “Files” collection in our fake OData server. This server listens at the URL “/filebrowser”. I didn’t bother setting up a view and a controller – the ~30 lines of code are in index.html. It’s important to turn off batch mode, otherwise our fake OData server would have to understand those as well.

Try It

All the code is available in a GitHub repository. To run the app on your computer (assuming Node.js is installed) just do the following – it will not take longer than a minute or so:

git clone https://github.com/akudev/ODataFileBrowserAgainstNodeJS.git

cd ODataFileBrowserAgainstNodeJS

npm install

npm start

Then open http://localhost:2019 in your browser.

As the directory used by this app as root for browsing files is almost empty, you’ll want to put some more files there – or point the file browser to a different root directory in line 6 of myApp.js.

In the server console you can see the OData requests being handled (I made the app browse the C:\ root directory of a Windows PC):

After the metadata document, the top-level data is requested. Then the content of the “Windows” directory: initially the first 110 files, then – on scrolling down – the remaining 31 files.

 

 

Hope this helps with understanding how Trees work and can be used in UI5 – with and without OData.

Andreas

 

Disclaimer

  • I am not an expert for trees in UI5 or for OData, so please don’t expect me to answer all kinds of questions around those topics. However, I can explain parts of the sample project, where I wasn’t as clear as needed.
  • …therefore the code in the sample project should not be considered good or robust enough for productive use. It’s meant to give a minimal client and server setup which can be used to understand better how trees in UI5 work with OData. That said, a tree expert has had a short look and confirmed that the code is not total crap.
  • Everything comes without warranty (Apache 2 Open Source license) and is not an official SAP software/documentation delivery or meant to live up to the UI5 documentation standards. It’s just an experiment I wanted to share. I also probably won’t keep it updated. But pull requests to the app and other improvement comments are welcome.
  • The code is kept very simple. For a real file browser you would add more information, like file size, creation date, etc., (easy) and sorting/filtering (a bit more complex, as the parent node filter and the user filter need to be merged, and both be applied while creating the file list in the server-side part).
Be the first to leave a comment
You must be Logged on to comment or reply to a post.