custom database paging listbox"

From Documentation
 
(19 intermediate revisions by 2 users not shown)
Line 2: Line 2:
 
|author=Cossaer Filip
 
|author=Cossaer Filip
 
|date=December 01, 2015
 
|date=December 01, 2015
|version=ZK 7 and above}}
+
|version=ZK 6.5 and above}}
{{Template:UnderConstructionParagraph}} 
 
  
  
 
=Foreword=
 
=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.<br/>
+
This small talk shows how you can create your own custom component for real database pagination with a listbox and what is simple to use.<br/>
As I'm writing this small talk I will write about the problems and choices I encountered and explaining why I did it this way.<br/>  
+
As I'm writing this small talk I will write about the problems and choices I encountered and explain why I did it this way.<br/>  
As all code, it's a working progress and I'm open to suggestions/refactoring's/improvements.<br/>
+
Like all code, it is still a work in progress and I'm open to suggestions/refactoring's/improvements.<br/>
  
 
You can leave a comment below or create a discussion on [http://forum.zkoss.org/questions/ ZK forum].
 
You can leave a comment below or create a discussion on [http://forum.zkoss.org/questions/ ZK forum].
  
 +
=Introduction: Database Paging Listbox=
  
=Introduction: Database paging listbox=
+
While ZK has a lot of good components that are easy to use, one of the most important and simplest ones is the database paging listbox.<br/>
 
+
You can do automatic paging but then your whole list will be in your model.<br/>
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.<br/>
+
If you wanted to do database paging you need to set a <paging> and bind it to your viewmodel.<br/>
You can do automatically paging but then your whole list is actually in your model.<br/>
+
The viewmodel is needed, as well as methods for activePage, totalSize, pageSize. When I work with Spring, this is already contained in the page I get from the repository.<br/>
If you wanted to do database paging you needed to set a <paging> and bind it to your viewmodel.<br/>
+
I made some attempts in creating this with custom implementations of ListModel and the use of the autopaging of the listbox.<br/>
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.<br/>
 
I did some attempts on creating this with custom implementations of ListModel and the use of the autopaging of the listbox.<br/>
 
 
I failed here because the listbox inner paging doesn't make a difference between totalSize and pageSize.<br/>
 
I failed here because the listbox inner paging doesn't make a difference between totalSize and pageSize.<br/>
 
If I used pageSize, I wasn't able to page to other pages.<br/>
 
If I used pageSize, I wasn't able to page to other pages.<br/>
 
If I used totalSize, everything worked BUT underlying the listbox generated totalsize minus pagesize empty objects.<br/>
 
If I used totalSize, everything worked BUT underlying the listbox generated totalsize minus pagesize empty objects.<br/>
For small queries it isn't so bad, but I had queries what have results of over 1000000 results while page was size 20.<br/>
+
For small queries, it isn't so bad, but I had queries which had results of over 1000000 while page was size 20.<br/>
 
The query was fast enough, but still the listbox takes a long time to load because it was creating the empty objects.<br/>
 
The query was fast enough, but still the listbox takes a long time to load because it was creating the empty objects.<br/>
It's here where I decided to go for a custom listbox, where I add mine own paging above and handle it by myself.<br/>
+
It's here that I decided to go for a custom listbox, where I added my own paging and handled it myself.<br/>
  
 
=Requirements=
 
=Requirements=
  
Now it was time to think about the requirements of the listbox.<br/>
+
Now it is time to think about the requirements of the listbox.<br/>
 
What should it do and how to make it easy for usage.<br/>
 
What should it do and how to make it easy for usage.<br/>
The first thing what popped in mine head is MVVM.  We all like it and let's be honest, it's ZK strongest point.<br/>
+
The first thing that popped in my head is MVVM.  We all like it and let's be honest, it's ZK's strongest point.<br/>
 
Then I was looking at the listbox and paging component.<br/>
 
Then I was looking at the listbox and paging component.<br/>
The following attributes where important for me that I could set in the zul :<br/>
+
The following attributes are important for me when I work with zul :<br/>
  
 
* pageSize
 
* pageSize
Line 45: Line 43:
 
* detailed
 
* detailed
  
Then it's up to how do I want to code it in the zul.<br/>
+
Then it's up to me how I want to code it in the zul.<br/>
 
The first idea was to use it like this :
 
The first idea was to use it like this :
  
Line 67: Line 65:
 
</source>
 
</source>
  
But then we have the problem because paginglistbox isn't a listbox but it's mine own component who has a listbox inside.<br/>
+
But then we have a problem because paginglistbox isn't a listbox, but it's my own component who has a listbox inside.<br/>
 
In a zul page, the paginglistbox should look like this :
 
In a zul page, the paginglistbox should look like this :
  
Line 77: Line 75:
  
 
Luckily there are [http://books.zkoss.org/wiki/ZK_Developer's_Reference/MVC/View/Template templates] in ZK.
 
Luckily there are [http://books.zkoss.org/wiki/ZK_Developer's_Reference/MVC/View/Template templates] in ZK.
With the templates I could retrieve it and inject it in the listbox.<br/>
+
With the templates, I could retrieve them and inject them in the listbox.<br/>
The usage of the paginglistbox would be changing to this :
+
The usage of the paginglistbox would change to this :
  
 
<source lang="xml">
 
<source lang="xml">
Line 90: Line 88:
 
</source>
 
</source>
  
Now we have 2 possible solutions what we could do :
+
Now we have 2 possible solutions of what we could do :
* Define a constant name to the template like you use <template name="model"> for your model.
+
* Define a constant name to the template you would like to use <template name="model"> for your model.
 
* Don't define a name but add the name of the template in the attribute of paginglistbox.
 
* 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.<br/>
 
As I'm pretty lazy, I don't want to add the name of the template in the attributes.<br/>
But let's think ahead, if I add the name in attribute I can even use @load for that.<br/>
+
But let's think ahead: if I add the name in attribute I can even use @load for that.<br/>
 
This means I can change the template name in runtime, witch means that if I create multiple templates I can easily switch between them.<br/>
 
This means I can change the template name in runtime, witch means that if I create multiple templates I can easily switch between them.<br/>
This behavior does attract me more then mine laziness of not writing an extra attribute.
+
This behavior is more attractive to me more than my laziness in not writing an extra attribute.
  
=Creating the PagingListbox class=
+
=Creating the PagingListbox Class=
  
Here I am writing mine PagingListbox and everything is going great.<br/>
+
Here I am writing my PagingListbox and everything is going great.<br/>
At that point I realize I'm forgetting 1 big thing namely sorting.<br/>
+
At this point I realize I'm forgetting one big thing- namely sorting.<br/>
 
If we use sort="auto(property)" in the listheader we don't get database sorting but we sort only the page.<br/>
 
If we use sort="auto(property)" in the listheader we don't get database sorting but we sort only the page.<br/>
After some investigation of the Listheader class, I saw that if we use sort="client(property)" the sorting is enabled on the columns.<br/>
+
After some investigation of the Listheader class, I saw that if we use sort="client(property)" the sorting is enabled in the columns.<br/>
 
The next problem was getting that value back from the listheader.  There is a setter but no suitable getter for it.<br/>
 
The next problem was getting that value back from the listheader.  There is a setter but no suitable getter for it.<br/>
 
At this point I'm left with one '''very dangerous solution''' called reflection.<br/>
 
At this point I'm left with one '''very dangerous solution''' called reflection.<br/>
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.<br/>
+
I don't like to use reflection because if ZK decided to change the variable name the effect would be, at best, sorting doesn't work anymore and in worst case, we will get errors.<br/>
 
The solution provides that with sort="client(id;name)" the pagerequest will receive the sortField as id;name
 
The solution provides that with sort="client(id;name)" the pagerequest will receive the sortField as id;name
  
Line 113: Line 111:
  
 
<source lang="java">
 
<source lang="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)",
 
@ComponentAnnotation({"selectedItems:@ZKBIND(ACCESS=both,SAVE_EVENT=onSelect)",
 
     "selectedItem:@ZKBIND(ACCESS=both,SAVE_EVENT=onSelect)"})
 
     "selectedItem:@ZKBIND(ACCESS=both,SAVE_EVENT=onSelect)"})
 
public class PagingListbox extends Idspace implements AfterCompose {
 
public class PagingListbox extends Idspace implements AfterCompose {
 
+
         // implementation found on github link below.
    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;
 
    }
 
 
 
}
 
 
</source>
 
</source>
  
One of the things you will notice is the extending of IdSpace.<br/>
+
One of the things you will notice is the extension of IdSpace.<br/>
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 [http://books.zkoss.org/wiki/ZK_Developer's_Reference/UI_Composing/ID_Space idspace] so they wouldn't clash.<br/>
+
This is done so you can use in the paginglistbox scope some ids and if you set a second one, the ids are in a different [http://books.zkoss.org/wiki/ZK_Developer's_Reference/UI_Composing/ID_Space idspace] so they wouldn't clash.<br/>
  
  
 
=The PagingModel=
 
=The PagingModel=
  
What's the role of the model.<br/>
+
What's the role of the model?<br/>
Well, it should actually contains the page request to the database, but a request contains active page, pagesize, sortfield(s) and sortdirection<br/>
+
Well, it should actually contain the page request to the database, but the request actually contains active page, pagesize, sortfield(s) and sortdirection<br/>
I'm again to a crossroad :
+
I'm again at 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 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.
+
* Do I make a model, which needs 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 [https://www.youtube.com/watch?v=llGgO74uXMI Venkat Subramaniam talk of Core Design Principles for Software Developers].<br/>
+
At first I'm leaning towards first solution but then I attended [https://www.youtube.com/watch?v=llGgO74uXMI Venkat Subramaniam talk of Core Design Principles for Software Developers].<br/>
 
So I'm like yes you are right '''but it's so easy to make and use the abstract class'''.<br/>
 
So I'm like yes you are right '''but it's so easy to make and use the abstract class'''.<br/>
But like a good student, I'm thinking of the advantages of the other way.<br/>
+
But like a good student, I'm thinking of the advantages of going the other way.<br/>
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.<br/>
+
And I noticed that 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.<br/>
Reasons of doing this is maybe multiple databases, query with different filters when you use multiple templates,...<br/>
+
Reasons of doing this include multiple databases and query with different filters when you use multiple templates,...<br/>
  
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, [http://books.zkoss.org/wiki/Small_Talks/2009/July/Handling_huge_data_using_ZK#AbstractPagingListModel similar to this small talk].<br/>
+
The interface is actually quite simple; we only need 2 methods. The first one is getting the data and the second one is getting the totalsize, [http://books.zkoss.org/wiki/Small_Talks/2009/July/Handling_huge_data_using_ZK#AbstractPagingListModel similar to this small talk].<br/>
  
 
PagingModelRequest.java
 
PagingModelRequest.java
  
 
<source lang="java">
 
<source lang="java">
import java.util.List;
 
 
/**
 
*
 
* @author cossaer.f
 
*/
 
 
public interface PagingModelRequest<T> {
 
public interface PagingModelRequest<T> {
     List<T> getPage(int activePage, int pageSize, String sortField, SortDirection sortDirection);
+
     Collection<T> getContent(int activePage, int pageSize, String sortField, SortDirection sortDirection);
 
     long getTotalSize();
 
     long getTotalSize();
 
}
 
}
Line 430: Line 152:
  
 
<source lang="java">
 
<source lang="java">
 
import java.util.List;
 
import java.util.Objects;
 
 
/**
 
*
 
* @author cossaer.f
 
*/
 
 
public class PagingModel<T> {
 
public class PagingModel<T> {
  
Line 459: Line 173:
 
     }
 
     }
  
     public List<T> getPage(int activePage, int pageSize) {
+
     public Collection<T> getContent(int activePage, int pageSize) {
         return pagingModelRequest.getPage(activePage, pageSize, sortField, sortDirection);
+
         return pagingModelRequest.getContent(activePage, pageSize, sortField, sortDirection);
 
     }
 
     }
  
Line 466: Line 180:
 
         return pagingModelRequest.getTotalSize();
 
         return pagingModelRequest.getTotalSize();
 
     }
 
     }
 
+
      
    public String getSortField() {
+
     //getters and setters
        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) {
 
     public void setPagingModelRequest(PagingModelRequest request) {
Line 494: Line 190:
 
</source>
 
</source>
  
You notice that you can't instantiate the model without a real instance of PagingModelRequest or use the setter with null object.
+
You will notice that you can't instantiate the model without a real instance of PagingModelRequest or use the setter with null object.
There was also the problem with sortDirection.  Because I wanted to create a component what could be used by any implementation on the backend, I needed to create a sortDirection with no reference to other frameworks, but still easy to change to your needs.
+
There was also the problem with sortDirection.  Because I wanted to create a component what could be used by any implementation on the back-end, I needed to create a sortDirection with no reference to other frameworks, but make sure that it is still easy to change according to your needs.
  
I came up with the following enum, you can change the enum yourself by adding more properties to it.<br/>
+
I came up with the following enum: you can change the enum yourself by adding more properties to it.<br/>
It's already configured for usage with Spring Data.
+
It is already configured for usage with Spring Data.
  
 
SortDirection.java
 
SortDirection.java
  
 
<source lang="java">
 
<source lang="java">
/**
+
 
*
 
* @author cossaer.f
 
*/
 
 
public enum SortDirection {
 
public enum SortDirection {
  
Line 520: Line 213:
 
     }
 
     }
 
      
 
      
    public String getShortName() {
+
  // getters
        return shortName;
 
    }
 
 
 
    public String getLongName() {
 
        return longName;
 
    }
 
 
}
 
}
 
</source>
 
</source>
Line 545: Line 232:
 
</source>
 
</source>
  
This makes that we can use our component in the zul like this '''without declaring any other stuff''':
+
We can use this in our component in the zul like this '''without declaring any other stuff''':
  
 
<source lang="xml">
 
<source lang="xml">
Line 551: Line 238:
 
</source>
 
</source>
  
=Spring data paging=
+
=Spring Data Paging=
  
Because a lot of people use spring paging, I'll show you how you can make an abstract implementation of the PageModelRequest.<br/>
+
Because a lot of people use spring paging, I have created an abstract class for Spring usage : SpringPagingModelRequest.java<br/>
One of the things is if you want to sort on multiple fields, example id and name or on ignoring case.<br/>
+
You can find the implementation in the github repository below this small talk.
I created '''an example class''' on how you could implement it.<br/>
 
The example will take care of ignoring case and multiple fields.<br/>
 
As you will see, you can adjust the implementation to whatever you want, and even make it more gereric by detecting sort="client(lower(id;name))" as the sortField will be in the interface : lower(id;name).
 
  
SpringPagingModelRequest.java
+
Advantages of this class :
  
<source lang="java">
+
* Implementation of sorting multiple fields, separated by semicolon, comma or space.
/**
+
* Implementation of ignore case if desired.
* Abstract implementation for Spring paging. Required to implement the method
+
* Implementation of fix direction or opposite direction sorting.
* "public Page getPage(PageRequest request);"
 
*
 
* If you want to have multiple sortfields, you have to seperate them in zul
 
* page with ; example for ZUL :  <listheader sort="client(id;name)/>
 
* In this example the sortage is first on id, second on name.
 
* Sorts are ignorecase!!!!
 
*
 
* @author cossaer.f
 
*/
 
public abstract class SpringPagingModelRequest implements PagingModelRequest {
 
  
    private long totalSize;
+
Example :
  
    @Override
+
<source lang="xml">
    public List getPage(int activePage, int pageSize, String sortField, SortDirection sortDirection) {
+
    <listheader sort="client(id;lower(name);asc(birthdate))/>
        PageRequest request;
+
</source>
        Sort.Direction direction;
 
        if (sortDirection != null) {
 
            direction = Sort.Direction.fromString(sortDirection.getShortName());
 
        } else {
 
            direction = Sort.Direction.ASC;
 
        }
 
        if (!StringUtils.isEmpty(sortField)) {
 
            List<Order> sortOrders = new ArrayList<>();
 
            for (String field : sortField.split(";")) {
 
                sortOrders.add(new Sort.Order(direction, field).ignoreCase());
 
            }
 
            request = new PageRequest(activePage, pageSize, new Sort(sortOrders));
 
        } else {
 
            request = new PageRequest(activePage, pageSize);
 
        }
 
        Page page = getPage(request);
 
        totalSize = page.getTotalElements();
 
        return page.getContent();
 
    }
 
  
    @Override
+
Explication : <br/>
    public long getTotalSize() {
 
        return totalSize;
 
    }
 
  
    public abstract Page getPage(PageRequest request);
+
* First sort field is 'id'.
}
+
* Second sort field is 'name' with ignoring case.
</source>
+
* Third sort field is 'birthdate' but always sorted in ascending order.
  
 
=Usage=
 
=Usage=
  
 
I have explained everything about coming to this component but still the most important thing is how to use it.<br/>
 
I have explained everything about coming to this component but still the most important thing is how to use it.<br/>
There are some attributes required and some are optional.
+
There are both required and optional attributes:
  
 
* template : '''required'''
 
* template : '''required'''
Line 666: Line 318:
 
</source>
 
</source>
  
==Important note==
+
The viewmodel does require these methods :
 +
 
 +
<source lang="java">
 +
public PagingModel getPageModel() {
 +
        return new PagingModel(new PagingModelRequest(){
 +
            private long totalSize;
 +
 
 +
            @Override
 +
            public Collection getContent(int activePage, int pageSize, String sortField, SortDirection sortDirection) {
 +
                Sort.Direction direction = Sort.Direction.fromString(sortDirection.getShortName());
 +
                Page page = mySpringDataRepo.findAll(new PageRequest(activePage, pageSize, direction, sortField));
 +
                totalSize = page.getTotalElements();
 +
                return page.getContent();
 +
            }
 +
 
 +
            @Override
 +
            public long getTotalSize() {
 +
                return totalSize;
 +
            }
 +
        }, "id");
 +
    }
 +
   
 +
    public String getTemplateName() {
 +
        return readOnly?"readOnly":"edit";
 +
    }
 +
}
 +
</source>
 +
 
 +
=Important note=
  
 
<source lang="xml">
 
<source lang="xml">
Line 673: Line 353:
 
</source>
 
</source>
  
As you can see under the model template I fill the listitem's value with each, and I need to set a reference.<br/>
+
As you can see, under the model template, I have filled the listitem's value with each, and I need to set a reference.<br/>
 
The binding doesn't work directly to the each object cause it's in a different lifecycle and by some strange behavior the each object is already gone before we can do the binding.<br/>
 
The binding doesn't work directly to the each object cause it's in a different lifecycle and by some strange behavior the each object is already gone before we can do the binding.<br/>
 
So a (hopefully temporarily) workaround is adding a reference : xxx="@ref(self.value)".<br/>
 
So a (hopefully temporarily) workaround is adding a reference : xxx="@ref(self.value)".<br/>
Like this we can do the binding to the xxx.  You choose self the name for xxx.
+
Like this we can do the binding to the xxx.  You can choose the name for xxx.
 +
 
 +
=Github Link=
 +
 
 +
[https://github.com/chillworld/custom_database_paging_listbox/tree/master Github repository]
  
=Donation=
+
This article is contributed by Cossaer Filip for the community. Please feel free to leave him a comment below or create a discussion on [http://forum.zkoss.org/questions/ ZK forum].
  
Donations are always welcome, but certainly not obligated.<br/>
+
{{Template:CommentedSmalltalk_Footer|
I like to help people where I can and share code.
+
|name=Potix Corporation
<html>
+
}}
<form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top">
 
<input type="hidden" name="cmd" value="_s-xclick">
 
<input type="hidden" name="hosted_button_id" value="ZJJ4ZNCKK2ADL">
 
<input type="image" src="https://www.paypalobjects.com/en_US/BE/i/btn/btn_donateCC_LG.gif" border="0" name="submit" alt="PayPal - The safer, easier way to pay online!">
 
<img alt="" border="0" src="https://www.paypalobjects.com/en_US/i/scr/pixel.gif" width="1" height="1">
 
</form>
 
</html>
 

Latest revision as of 08:25, 2 December 2015

DocumentationSmall Talks2015Decembercustom database paging listbox
custom database paging listbox

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


Foreword

This small talk shows how you can create your own custom component for real database pagination with a listbox and what is simple to use.
As I'm writing this small talk I will write about the problems and choices I encountered and explain why I did it this way.
Like all code, it is still a work in 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 that are easy to use, one of the most important and simplest ones is the database paging listbox.
You can do automatic paging but then your whole list will be in your model.
If you wanted to do database paging you need to set a <paging> and bind it to your viewmodel.
The viewmodel is needed, as well as methods for activePage, totalSize, pageSize. When I work with Spring, this is already contained in the page I get from the repository.
I made some attempts in 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 which had results of over 1000000 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 that I decided to go for a custom listbox, where I added my own paging and handled it myself.

Requirements

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

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

Then it's up to me how 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 a problem because paginglistbox isn't a listbox, but it's my 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 them and inject them in the listbox.
The usage of the paginglistbox would change to this :

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

Now we have 2 possible solutions of what we could do :

  • Define a constant name to the template you would like to 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 is more attractive to me more than my laziness in not writing an extra attribute.

Creating the PagingListbox Class

Here I am writing my PagingListbox and everything is going great.
At this point I realize I'm forgetting one 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 in 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 decided to change the variable name the effect would be, at best, sorting doesn't work anymore and in worst case, we will get errors.
The solution provides that with sort="client(id;name)" the pagerequest will receive the sortField as id;name

PagingListbox.java :

@ComponentAnnotation({"selectedItems:@ZKBIND(ACCESS=both,SAVE_EVENT=onSelect)",
    "selectedItem:@ZKBIND(ACCESS=both,SAVE_EVENT=onSelect)"})
public class PagingListbox extends Idspace implements AfterCompose {
        // implementation found on github link below.
   }

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


The PagingModel

What's the role of the model?
Well, it should actually contain the page request to the database, but the request actually contains active page, pagesize, sortfield(s) and sortdirection
I'm again at 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, which needs an implementation of an interface for implementing the query to the database.

At first I'm leaning towards first solution but then I attended 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 going the other way.
And I noticed that 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 include multiple databases and query with different filters when you use multiple templates,...

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

PagingModelRequest.java

public interface PagingModelRequest<T> {
    Collection<T> getContent(int activePage, int pageSize, String sortField, SortDirection sortDirection);
    long getTotalSize();
}

And the code of the model

PagingModel.java

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 Collection<T> getContent(int activePage, int pageSize) {
        return pagingModelRequest.getContent(activePage, pageSize, sortField, sortDirection);
    }

    public long getTotalSize() {
        return pagingModelRequest.getTotalSize();
    }
    
    //getters and setters

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

You will notice that you can't instantiate the model without a real instance of PagingModelRequest or use the setter with null object. There was also the problem with sortDirection. Because I wanted to create a component what could be used by any implementation on the back-end, I needed to create a sortDirection with no reference to other frameworks, but make sure that it is still easy to change according to your needs.

I came up with the following enum: you can change the enum yourself by adding more properties to it.
It is already configured for usage with Spring Data.

SortDirection.java

public enum SortDirection {

    ASCENDING("ASC","ASCENDING"),
    DESCENDING("DESC","DESCENDING");
    
    private final String shortName;
    private final String longName;

    private SortDirection(String shortName, String longName) {
        this.shortName = shortName;
        this.longName = longName;
    }
    
   // getters 
}

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>

We can use this in our component in the zul like this without declaring any other stuff:

  <paginglistbox/>

Spring Data Paging

Because a lot of people use spring paging, I have created an abstract class for Spring usage : SpringPagingModelRequest.java
You can find the implementation in the github repository below this small talk.

Advantages of this class :

  • Implementation of sorting multiple fields, separated by semicolon, comma or space.
  • Implementation of ignore case if desired.
  • Implementation of fix direction or opposite direction sorting.

Example :

    <listheader sort="client(id;lower(name);asc(birthdate))/>

Explication :

  • First sort field is 'id'.
  • Second sort field is 'name' with ignoring case.
  • Third sort field is 'birthdate' but always sorted in ascending order.

Usage

I have explained everything about coming to this component but still the most important thing is how to use it.
There are both required and optional attributes:

  • template : required
  • model : required
  • pageSize : optional, default 20.
  • selectedItem : optional
  • selectedItems : optional
  • pagingPosition : optional, default "top" and possible values : "top","bottom","both". Any other value will shown no paging element.
  • checkmark : optional default false
  • multiple : optional default false
  • detailed : optional default true
  • emptyMessage : optional default empty string.


I'm going to show an example of the zul usage with multiple templates, one for edit and one for readOnly.

<paginglistbox model="@load(vm.pageModel)" pagingPosition="bottom" template="@load(vm.templateName)">
    <template name="readOnly">
      <listhead>
        <listheader label="id" sort="client(id)" />
        <listheader label="name" vflex="min" />
      </listhead>
      <template name="model">
        <listitem value="${each}" item="@ref(self.value)">
          <listcell>
            <label value="${each.id}" />
          </listcell>
          <listcell>
            <label value="@load(item.name)" />
          </listcell>
        </listitem>
      </template>
    </template>
    <template name="edit">
      <listhead>
        <listheader label="id" sort="client(id)" />
        <listheader label="name" vflex="min" />
      </listhead>
      <template name="model">
        <listitem value="${each}" xxx="@ref(self.value)">
          <listcell>
            <label value="@load(xxx.id)" />
          </listcell>
          <listcell>
            <textbox value="@bind(xxx.name)" />
          </listcell>
        </listitem>
      </template>
    </template>
    </template>
  </paginglistbox>

The viewmodel does require these methods :

public PagingModel getPageModel() {
        return new PagingModel(new PagingModelRequest(){
            private long totalSize;

            @Override
            public Collection getContent(int activePage, int pageSize, String sortField, SortDirection sortDirection) {
                Sort.Direction direction = Sort.Direction.fromString(sortDirection.getShortName());
                Page page = mySpringDataRepo.findAll(new PageRequest(activePage, pageSize, direction, sortField));
                totalSize = page.getTotalElements();
                return page.getContent();
            }

            @Override
            public long getTotalSize() {
                return totalSize;
            }
        }, "id");
    }
    
    public String getTemplateName() {
        return readOnly?"readOnly":"edit";
    }
}

Important note

 <template name="model">
        <listitem value="${each}" item="@ref(self.value)">

As you can see, under the model template, I have filled the listitem's value with each, and I need to set a reference.
The binding doesn't work directly to the each object cause it's in a different lifecycle and by some strange behavior the each object is already gone before we can do the binding.
So a (hopefully temporarily) workaround is adding a reference : xxx="@ref(self.value)".
Like this we can do the binding to the xxx. You can choose the name for xxx.

Github Link

Github repository

This article is contributed by Cossaer Filip for the community. Please feel free to leave him a comment below or create a discussion on ZK forum.


Comments



Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License.