custom database paging listbox

From Documentation
Revision as of 09:53, 17 November 2015 by Chillworld (talk | contribs)
DocumentationSmall Talks2015Decembercustom database paging listbox
custom database paging listbox

Author
Cossaer Filip
Date
December 01, 2015
Version
ZK 7 and above

Under Construction-100x100.png This paragraph is under construction.


Foreword

This small talk is how you could create your own custom component for real database pagination with a listbox and what's simple to use.
As all code, it's a working progress and I'm open to suggestions/refactoring's/improvements.

You can leave a comment below or create a discussion on ZK forum.


Introduction: Database paging listbox

While ZK has a lot of good components easy to use, I missed out some important one for me, and that's an easy to use database paging listbox.
You can do automatically paging but then your whole list is actually in your model.
If you wanted to do database paging you needed to set a <paging> and bind it to your viewmodel.
The viewmodel needed then methods for activePage, totalSize, pageSize and when I work with Spring this is already contained in mine Page I get from the repository.
I did some attempts on creating this with custom implementations of ListModel and the use of the autopaging of the listbox.
I failed here because the listbox inner paging doesn't make a difference between totalSize and pageSize.
If I used pageSize, I wasn't able to page to other pages.
If I used totalSize, everything worked BUT underlying the listbox generated totalsize minus pagesize empty objects.
For small queries it isn't so bad, but I had queries what have results of over 1000000 results while page was size 20.
The query was fast enough, but still the listbox takes a long time to load because it was creating the empty objects.
It's here where I decided to go for a custom listbox, where I add mine own paging above and handle it by myself.

Requirements

Now it was time to think about the requirements of the listbox.
What should it do and how to make it easy for usage.
The first thing what popped in mine head is MVVM. We all like it and let's be honest, it's ZK strongest point.
Then I was looking at the listbox and paging component.
The following attributes where important for me that I could set in the zul :

  • pageSize
  • model
  • selectedItem
  • selectedItems
  • pagingPosition
  • checkmark
  • multiple
  • detailed

Then it's up to how do I want to code it in the zul.
The first idea was to use it like this :

<paginglistbox>
      <auxhead>
        <auxheader/>
      </auxhead>
      <listhead>
        <listheader  />
        <listheader  />
      </listhead>
      <template name="model">
        <listitem>
          <listcell>
            <label value="${each.id}" />
          </listcell>
         </listitem>
      </template>
  </paginglistbox>

But then we have the problem because paginglistbox isn't a listbox but it's mine own component who has a listbox inside.
In a zul page, the paginglistbox should look like this :

  <paging/>
  <listbox/>
  <paging/>

Luckily there are templates in ZK. With the templates I could retrieve it and inject it in the listbox.
The usage of the paginglistbox would be changing to this :

<paginglistbox>
    <template>
      <auxhead>
       ...
      </template>
    </template>
  </paginglistbox>

Now we have 2 possible solutions what we could do :

  • Define a constant name to the template like you use <template name="model"> for your model.
  • Don't define a name but add the name of the template in the attribute of paginglistbox.

As I'm pretty lazy, I don't want to add the name of the template in the attributes.
But let's think ahead, if I add the name in attribute I can even use @load for that.
This means I can change the template name in runtime, witch means that if I create multiple templates I can easily switch between them.
This behavior does attract me more then mine laziness of not writing an extra attribute.

Creating the PagingListbox class

Here I am writing mine PagingListbox and everything is going great.
At that point I realize I'm forgetting 1 big thing namely sorting.
If we use sort="auto(property)" in the listheader we don't get database sorting but we sort only the page.
After some investigation of the Listheader class, I saw that if we use sort="client(property)" the sorting is enabled on the columns.
The next problem was getting that value back from the listheader. There is a setter but no suitable getter for it.
At this point I'm left with one very dangerous solution called reflection.
I don't like to use reflection because if ZK decide to change the variable name the effect would be in best case that sorting don't work anymore, in worst case we are getting errors.

PagingListbox.java :

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Components;
import org.zkoss.zk.ui.annotation.ComponentAnnotation;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.event.SelectEvent;
import org.zkoss.zk.ui.event.SortEvent;
import org.zkoss.zk.ui.ext.AfterCompose;
import org.zkoss.zk.ui.select.Selectors;
import org.zkoss.zk.ui.select.annotation.Listen;
import org.zkoss.zk.ui.util.Template;
import org.zkoss.zul.Idspace;
import org.zkoss.zul.ListModelList;
import org.zkoss.zul.Listbox;
import org.zkoss.zul.Listheader;
import org.zkoss.zul.Listitem;
import org.zkoss.zul.Paging;
import org.zkoss.zul.event.PagingEvent;

/**
 *
 * @author cossaer.f
 */
@ComponentAnnotation({"selectedItems:@ZKBIND(ACCESS=both,SAVE_EVENT=onSelect)",
    "selectedItem:@ZKBIND(ACCESS=both,SAVE_EVENT=onSelect)"})
public class PagingListbox extends Idspace implements AfterCompose {

    private String pagingPosition = "top";
    private String emptyMessage = "";
    private String template = null;
    private boolean multiple = false;
    private boolean checkmark = false;
    private boolean detailed = true;
    private PagingModel pagingModel;
    private int pageSize = 20;
    private int activePage;
    private Component[] templateComponents;
    private Paging topPaging;
    private Paging bottomPaging;
    private final List<Paging> pagers = new ArrayList<>();
    private Listbox listbox;
    private Set selectedItems = new HashSet();
    private final EventListener onSelectListener = new EventListener<SelectEvent>() {

        @Override
        public void onEvent(SelectEvent event) throws Exception {
            selectedItems.clear();
            for (Object item : event.getSelectedItems()) {
                selectedItems.add(((Listitem) item).getValue());
            }
            Events.postEvent("onSelect", PagingListbox.this, selectedItems);
        }
    };

    private final EventListener onPagingListener = new EventListener<PagingEvent>() {

        @Override
        public void onEvent(PagingEvent event) throws Exception {
            if (pagingModel != null) {
                activePage = event.getPageable().getActivePage();
                refreshModel();
            }
        }
    };

    @Override
    public void afterCompose() {
        initComponents();
        changeTemplate();
        listbox.setModel(new ListModelList());
        refreshModel();
    }

    private void initComponents() {
        topPaging = new Paging();
        this.appendChild(topPaging);
        listbox = new Listbox();
        this.appendChild(listbox);
        listbox.addEventListener("onSelect", onSelectListener);
        bottomPaging = new Paging();
        this.appendChild(bottomPaging);
        pagers.add(topPaging);
        pagers.add(bottomPaging);
        for (Paging paging : pagers) {
            paging.addEventListener("onPaging", onPagingListener);
        }
    }

    private void changeTemplate() {
        if (listbox != null && template != null) {
            if (templateComponents != null) {
                for (Component comp : templateComponents) {
                    listbox.removeChild(comp);
                }
            }
            Template currentTemplate = this.getTemplate(template);
            if (currentTemplate != null) {
                templateComponents = currentTemplate.create(listbox, null, null, Components.getComposer(this));
            }
            Selectors.wireEventListeners(this, this);

            refreshModel();//because otherwise the template="model" will not change but selection is removed.
        }
    }

    public PagingModel getModel() {
        return pagingModel;
    }

    public void setModel(PagingModel pagingModel) {
        this.pagingModel = pagingModel;
        refreshModel();
    }

    private void refreshModel() {
        if (pagers != null && listbox != null && pagingModel != null) {
            setPagersVisible();
            listbox.setCheckmark(checkmark);
            List page = pagingModel.getPage(activePage, pageSize);
            for (Paging paging : pagers) {
                paging.setDetailed(detailed);
                paging.setActivePage(activePage);
                paging.setTotalSize((int) pagingModel.getTotalSize());
                paging.setPageSize(pageSize);
            }
            ListModelList model = (ListModelList) listbox.getModel();
            model.setMultiple(multiple);
            model.clear();
            model.addAll(page);
            createSelection();
        }
    }

    @Listen("onSort = listheader")
    public void onSorting(SortEvent sortEvent) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
        Listheader header = (Listheader) sortEvent.getTarget();
        Field f = header.getClass().getDeclaredField("_sortAscNm");
        f.setAccessible(true);
        String sortAttribute = (String) f.get(header);
        if (sortAttribute != null && sortAttribute.startsWith("client(")) {
            sortAttribute = sortAttribute.substring(7);
            sortAttribute = sortAttribute.substring(0, sortAttribute.length() - 1);
            pagingModel.setSortField(sortAttribute);
        }
        String headerDirection = header.getSortDirection();
        SortDirection sortDirection = null;
        switch (headerDirection.toUpperCase()) {
            case "DESCENDING":
            case "NATURAL":
                sortDirection = SortDirection.ASCENDING;
                setSortDirection(header, sortDirection);
                break;
            case "ASCENDING":
                sortDirection = SortDirection.DESCENDING;
                setSortDirection(header, sortDirection);
                break;

        }
        pagingModel.setSortDirection(sortDirection);
        refreshModel();
    }

    private void setSortDirection(Listheader header, SortDirection direction) {
        for (Component comp : header.getParent().getChildren()) {
            if (comp instanceof Listheader) {
                ((Listheader) comp).setSortDirection("natural");
            }
        }
        header.setSortDirection(direction.getLongName().toLowerCase());
    }

    private void setPagersVisible() {
        topPaging.setVisible("top".equals(pagingPosition) || "both".equals(pagingPosition));
        bottomPaging.setVisible("bottom".equals(pagingPosition) || "both".equals(pagingPosition));
    }

    public int getPageSize() {
        return pageSize;
    }

    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
        refreshModel();
    }

    public Object getSelectedItem() {
        return selectedItems.isEmpty() ? null : selectedItems.iterator().next();
    }

    public void setSelectedItem(Object selectedItem) {
        this.selectedItems.clear();
        if (selectedItems != null) {
            selectedItems.add(selectedItem);
        }
        createSelection();
    }

    public Set getSelectedItems() {
        return new HashSet(selectedItems);// new Hashset to break the reference to internal object.
    }

    public void setSelectedItems(Set selected) {
        selectedItems.clear();
        if (selected != null) {
            selectedItems.addAll(selected);
        }
        createSelection();
    }

    private void createSelection() {
        if (listbox != null) {
            ((ListModelList) listbox.getModel()).clearSelection();
            for (Object selection : selectedItems) {
                ((ListModelList) listbox.getModel()).addToSelection(selection);
            }
        }
    }

    public boolean isMultiple() {
        return multiple;
    }

    public void setMultiple(boolean multiple) {
        this.multiple = multiple;
        refreshModel();
    }

    public boolean isCheckmark() {
        return checkmark;
    }

    public void setCheckmark(boolean checkmark) {
        this.checkmark = checkmark;
        refreshModel();
    }

    public String getPagingPosition() {
        return pagingPosition;
    }

    public void setPagingPosition(String pagingPosition) {
        this.pagingPosition = pagingPosition;
    }

    public String getEmptyMessage() {
        return emptyMessage;
    }

    public void setEmptyMessage(String emptyMessage) {
        this.emptyMessage = emptyMessage;
    }

    public String getTemplate() {
        return template;
    }

    public void setTemplate(String template) {
        this.template = template;
        changeTemplate();
    }

    public boolean isDetailed() {
        return detailed;
    }

    public void setDetailed(boolean detailed) {
        this.detailed = detailed;
    }

}

One of the things you will notice is the extending of IdSpace.
This is done so you could use in the paginglistbox scope some id's and if you set a second one, the id's are in a different idspace so they wouldn't clash.


The PagingModel

What's the role of the model.
Well, it should actually contains the page request to the database, but a request contains active page, pagesize, sortfield(s) and sortdirection
I'm again to a crossroad :

  • Do I make an abstract class where you could just need to implement 1 method, the query to the database.
  • Do I make a model, what's need an implementation of an interface for implementing the query to the database.

At first I'm tending to go for the first solution but then I was attending Venkat Subramaniam talk of Core Design Principles for Software Developers.
So I'm like yes you are right but it's so easy to make and use the abstract class.
But like a good student, I'm thinking of the advantages of the other way.
And I notice, if I work with the interface, I can create 2 implementations of the interface and switch them easy in the model without making a new instance of the model.
Reasons of doing this is maybe multiple databases, query with different filters when you use multiple templates,...

The interface is actually real simple, we only need 2 methods. The first on is getting the data and the second one is getting the totalsize, similar to this small talk.

PagingModelRequest.java

import java.util.List;

/**
 *
 * @author cossaer.f
 */
public interface PagingModelRequest<T> {
    List<T> getPage(int activePage, int pageSize, String sortField, SortDirection sortDirection);
    long getTotalSize();
}

And the code of the model

PagingModel.java

import java.util.List;
import java.util.Objects;

/**
 *
 * @author cossaer.f
 */
public class PagingModel<T> {

    private String sortField;
    private PagingModelRequest pagingModelRequest;
    private SortDirection sortDirection;

    public PagingModel(PagingModelRequest request) {
        this(request, null, null);
    }

    public PagingModel(PagingModelRequest request, String sortField) {
        this(request, sortField, null);
    }

    public PagingModel(PagingModelRequest request, String sortField, SortDirection sortDirection) {
        Objects.requireNonNull(request, "PageModelRequest can't be null.");
        this.pagingModelRequest = request;
        this.sortField = sortField;
        this.sortDirection = sortDirection == null ? SortDirection.ASCENDING : sortDirection;
    }

    public List<T> getPage(int activePage, int pageSize) {
        return pagingModelRequest.getPage(activePage, pageSize, sortField, sortDirection);
    }

    public long getTotalSize() {
        return pagingModelRequest.getTotalSize();
    }

    public String getSortField() {
        return sortField;
    }

    public void setSortField(String sortField) {
        this.sortField = sortField;
    }

    public SortDirection getSortDirection() {
        return sortDirection;
    }

    public void setSortDirection(SortDirection sortDirection) {
        this.sortDirection = sortDirection;
    }

    public PagingModelRequest getPagingModelRequest() {
        return pagingModelRequest;
    }

    public void setPagingModelRequest(PagingModelRequest request) {
        Objects.requireNonNull(request, "PageModelRequest can't be null.");
        this.pagingModelRequest = request;
    }
}

You notice that you can't instantiate the model without a real instance of PagingModelRequest or use the setter with null object.

Declare your component

Under the folder WEB-INF there is the lang-addon.xml file.
Now it's time to add our component to this file.

<language-addon>
     <component>
        <component-name>paginglistbox</component-name>
        <extends>idspace</extends>
        <component-class>my.choosen.path.PagingListbox</component-class>
    </component>
</language-addon>

This makes that we can use our component in the zul like this without declaring any other stuff:

  <paginglistbox/>