Table of Contents
Part One – The Application
Part Two – XPages Advanced 1
Part Three: XPages Advanced Database Separation
Part Four: XPages Advanced Document Wrapper
Part Five: XPages Advanced MVC Controller
Part Six: Use of OpenNTF Domino API
Part Seven: OSGi Application Development Introduction
Part Eight: OSGi JAX-RS REST Access with ODA
Part Nine: OsgiWorlds
Part Ten: Vaadin
Part Eleven: OsgiWorlds Document Wrapper
Part Twelve: Key Date Page
Part Thirteen: Application Structure
Part Fourteen: Calendar View
Part Fifteen: Key Dates Filterable View
Finally, to complete the OSGi application, I will cover the Key Dates filterable view itself. This is a fairly complex page and will probably also make XPages developers appreciate more what XPages brings to the table out-of-the-box. That’s because we need to manually create a view wrapper that performs optimally and loads the relevant data for each entry, as well as pagers to allow navigation. In addition, I’m also creating a multi-dimensional filter to attempt to minimise the amount of data being displayed. This is an approach that takes some thinking about to identify the entries to return, but an approach that I believe is better than the “Key Dates” and “By Customer” views used in the XPages application or a complex search frame. It’s worth emphasising that there is no reason this user experience can’t be reproduced in an XPages application (and I have). Stepping beyond a particular framework (any framework) is a good time to re-evaluate the design patterns chosen. (This is an aspect I also consider very important when moving from Notes Client development to XPages, to reconsider the user experiences and the data structures typically used because only multiple documents could not easily be edited in a single page or a view of documents restricted to a specific category could not easily be done in Notes Client.) You can see the final layout in this screenshot:
First I’ll cover the pager. This is a combination of the Pager Sizes and Pager components familiar from XPages. I since found out there is a Paging Component add on for Vaadin, but I’ve not tried that. The Pager class is a separate Vaadin component that can be bound to a ViewWrapper (after all, it needs to act on the view data), but could easily be extended to provide different functionality. The page sizes are managed via an enum, but the actual sizes displayed can be passed as a List to the constructor, along with the ViewWrapper. The
loadPagerSizesButtons() method creates the button links for selecting the number of entries per page,
updatePagerSizeButtonStyles() method handles setting whether or not each is marked as selected, by comparing the current value to the
count property of the ViewWrapper. So if the
count on the ViewWrapper is set to display 5 rows at a time, 5 shows highlighted. Each button has a
buttonClick() method that sets the
count property on the ViewWrapper, redraws the view contents, updates the button styles and reloads the buttons. Vaadin handles passing any UI changes back to the browser, not as raw HTML, like XPages, but JSON data to be processed client-side.
The Page buttons themselves are similarly handled, with the
loadPagerPagesButtons() method crreating the button links and the
updatePagerPagesButtonStyles setting whether or not each is marked selected. The “Previous” and “Next” buttons are pretty straightforward – we just need to check there is more than one page. They then increment or decrement the
currentPage property on the ViewWrapper, update the
start property based on the current start and the
count property of the ViewWrapper and redraw the view contents. The individual page numbers get more complicated, because they needs to check the
availablePages property and
currentPage property of the ViewWrapper, but ensure the correct five pages are shown. The button’s
buttonClick event does the same kind of thing as the Next and Previous buttons, setting the
currentPage properties of the ViewWrapper and redrawing the contents.
It sounds complicated, but it’s really just a case of breaking down what the pager buttons actually need to do, in terms of granular steps.
The filter is, effectively, allows multi-faceted slicing and dicing of the data. As you see from the screenshot, it allows filtering on customer, starting at a specific date plus the option to restrict to only entries for that date. This has been the approach I’ve taken in many applications more recently and is based on my experience of using Google: if I don’t find the document I want in the first couple of pages, I usually try a different search. Performance of scrolling or jumping to entries in the Notes Client is reasonable once the view has initially loaded but on the web, I now want to get a small subset of documents matching specific criteria rather than scrolling through a huge view or navigating multiple pages, just to get to the one document I want to act upon. I’m also not a huge fan of infinite scroll approach delivered in XPages via the Pager Add Rows control, which I think is only relevant for applications where the user lives 90% of the time in the application, so rarely needs to scroll far. Otherwise, I find it painful to use, particularly if an entry opens in the same tab, replacing the contents and sending you back to the start of the view.
The first component is a
ComboBox although, as will be seen from the method
setInvalidAllowed(boolean), it is more analagous to the DojoFilteringSelect rather than the ComboBox in XPages, because it can allow valules not in the list. Of course with this filter, it makes no sense to accept values not in the list. This is bound to a Vaadin IndexedContainer. The IndexedContainer needs to load all the entries when first initialised, so it’s important to consider the amount of data required and to optimise accordingly. This calls
KeyDateDatabaseUtils.loadCustomersToContainer(AbstractContainer container, String property), which navigates the “(luCustomers)” view of Key Date documents using a
ViewNavigator and using
getNextSibling to only go from one category to another. This means it should be pretty performant. When an entry is selected, it calls
MainView.loadRowData() to reload the data from
KeyDateViewWrapper.getEntriesAsMap(), passing in the customer.
The second component is the
PopupDateField to allow selection of a date. This sets the start date for the entries to load and defaults to today’s date. If the date is changed, it also calls
MainView.loadRowData() to reload the data from
KeyDateViewWrapper.getEntriesAsMap(), passing in the date.
Finally, there is a
CheckBox to restrict entries to the specified date. When the checkbox is clicked, it also calls
MainView.loadRowData() to reload the data from
KeyDateViewWrapper.getEntriesAsMap(), passing in whether the checkbox is selected or deselected.
The KeyDateViewWrapper extends the AbstractViewWrapper class which, in many ways, simulates the dominoView datasource. One of the common misconceptions in XPages development is that the dominoView datasource is the same as the backend Domino View object. But in actual fact it is a filter that provides access to all or a subset of view entry wrappers (again, not Domino ViewEntry objects). Because the Domino objects are not serializable, it needs to store things like the server name, database name, view name as well as properties for the count (number of entries to return for each page), start (where through the total entries available the current page should start), category, search string, exact match setting and single category restriction. This then means that when the wrapper is called to display a set of entries or work out the current page, how many pages are available and whether we’re on the last page, it can interact with the reloaded Domino View object to subset all entries and get to the correct position based on the previously stored state. This is what the AbstractViewWrapper reproduces. (REST service calls just store all those properties in cookies client-side, modify accordingly before passing them in the querystring, because there is no server-side state.)
The key method here is
a href="https://stash.openntf.org/projects/KEYDATES/repos/keydatesapps/browse/osgiworlds/src/uk/co/intec/keyDatesApp/model/KeyDateViewWrapper.java#138" target="_blank">getEntriesAsMap(String custName, Date startDate, boolean single, int rows). This loads the relevant entry wrappers depending on the criteria passed from the filter. There is an internal variable to determine whether or not to reset the wrapper – i.e. go back to page 1, if any settings have changed for customer name, start date, or whether or not to restrict to that date. The same method is used if the user is just navigating to a new page, in which case it should not reset to page one. But if criteria have changed and the user was on, for example, page 2, then there may no longer be a page 2 and the user will expect to go to the first entry matching the new criteria. The other thing that may have changed is the number of entries to display per page. All of these settings are passed to variables in the KeyDateViewWrapper itself. Then the code needs to get the relevant entries. If there is no customer name defined, the code passes to
getEntriesByDate(), otherwise it passes to
By using a wrapper, we don’t necessarily have to go to one specific view. We can change the view from which entries are retrieved depending on the criteria. This means a single page can be used instead of two different pages (which is what was used for the XPages application). To my mind, this produces a better user experience.
getEntriesByDate() goes to the keyDatesCal view and finds the first entry on or after the start date (providing it’s not set to show only that date’s entries). It then creates a
ViewNavigator from that entry (so the wrapper in reality only has access to entries after the start date – the same way the
dominoView datasource only has access to entries after a
startKey, if provided). It then jumps to the nth entry based on the start point, so if it’s page 2 where it’s set to show 30 rows per page, the start point is entry 31. It then wraps each view entry until the number of rows per page is reached or until the end of the view is reached. But because I’m creating a wrapper from scratch, I can make an enhancement to how it works in XPages. In XPages, the number of rows is based on the actual ViewEntries, so if there are categories in the view, those category rows also count as rows in the view on the XPage. But here it’s only picking actual document-level view entries. Those are put into a
Map where the key is the date that will be displayed and the contents of each
Map entry is a
KeyDateEntryWrappers for that date. The relevant column values are loaded into each
KeyDateEntryWrapper, so it can be fully serializable but the underlying
Document can be accessed via its note ID. The code is also only loading the entries to show on that page.
getEntriesByCustomer() goes to the KeyDateByCust view to only get entries for the selected customer. Because the entries are sorted on date with most recent first, a
TreeMap is used this time to auto-sort by earliest first. Note also that this view only has one level of categorisation, but because the entries are being loaded into a
Map in order to provide categorisation by date, that categorisation can be built dynamically. Again, that’s another benefit of building a wrapper. Again, the actual ViewEntries are loaded into
In both cases, the code then calculates the available page numbers to display in the pager.
The Map, with entries grouped by date, is then used by the
MainView class. Because the view only shows one level of categorisation, it’s a
KeyDateEntryWrappers, but if a second level of categorisation were needed, it could be a
KeyDateEntryWrappers etc. If you wanted to simplify it, you could build the top level first, then have a for loop within it to basically create a nested repeat, in XPages terms. But then it would be harder to restrict to a certain number of bottom-level Document entries.
Label is added for each date category and, under that, a link-style
Button is added to open the relevant Key Date document (using a generic
DocLinkListener to load the KeyDate page with the relevant note ID). Some of the text in that Button varies depending on whether it’s filtered on customer or not – obviously there is no need to display the customer if the user has filtered on a specific customer. Below that a
Label is added with summary data.
This may sound like a lot of work, but there are some generic abstract classes available that can just be extended to add functionality. As mentioned already, the techniques here could similarly be used in XPages to roll your own and extend functionality. Hopefully it also shows the approaches I took to break down fuctionality from XPages into smaller chunks that allowed me to build what may, on the surface, seem very complex and daunting constructs.
Next up, I’ll cover the differences between this application and a CrossWorlds version, that is a traditional web application rather than an OSGi-wrapped we application, running on Websphere Liberty rather than Domino.