I'm trying to use PrimeFaces 5.3 (with JSF 2.2) to implement a data table that contains items, and each item has a list of command buttons to perform server side actions. I use primefaces LazyDataModel because I have huge datasets and cannot afford to load all the items when the view loads. But after livescrolling happens, rows do not behave as expected.
I have setup a small project to illustrate my issue. (full sources at the end of the post)
The issue
When I press A0, "Executed A0" must be printed on the console (and so on).
Everything works fine for the first data set. Here's what happens when I press the first three buttons:
Fetching starting at 0 //expected result
Executed A0 //A0
Executed A1 //A1
Executed A2 //A2
But when I scroll, and 3 other items are loaded, the first three rows do not work correctly.
Here's what happens when I press all the buttons in order:
Fetching starting at 3 //expected result
Executed A3 //A0
Executed A4 //A1
Executed A5 //A2
Executed A3 //A3
Executed A4 //A4
Executed A5 //A5
The first three buttons execute the actions of the last three ones :(
A server-side issue
It is not a client-side issue, I have monitored the requests sent.
When I press A0, the request contains the following POST parameters:
j_idt8:resultsTable:0:j_idt12:0:j_idt13=j_idt8:resultsTable:0:j_idt12:0:j_idt13
javax.faces.source=j_idt8:resultsTable:0:j_idt12:0:j_idt13
And when I press A3:
j_idt8:resultsTable:3:j_idt12:0:j_idt13=j_idt8:resultsTable:3:j_idt12:0:j_idt13
javax.faces.source=j_idt8:resultsTable:3:j_idt12:0:j_idt13
The correct row is referenced after resultsTable:
Debug mode
I also messed with the debug mode. When I press A1, the UIData#brodacast(FacesEvent) is called with an event that contains a field rowIndex=1. So it works fine at this point.
Then, I don't really understand what happens. But at the end of the process, the target method is executed on the wrong instance...
Source code
Item.java
public class Item {
private int id;
private String name;
private List<ItemAction> actions;
public Item(int id, String name, List<ItemAction> actions) {
this.id = id;
this.name = name;
this.actions = actions;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public List<ItemAction> getActions() {
return actions;
}
}
ItemAction.java
public class ItemAction {
private String label;
public ItemAction(String label) {
this.label = label;
}
public void execute(){
System.out.println("Executed " + label);
}
public String getLabel() {
return label;
}
}
TheService.java
public class TheService {
private static final int ITEMS_COUNT = 8;
public int getItemsCount() {
return ITEMS_COUNT;
}
public List<Item> retrieveItems(int startIndex, int pageSize){
List<Item> result = new ArrayList<>(pageSize);
int endIndex = startIndex + pageSize;
if(endIndex > ITEMS_COUNT){
endIndex = ITEMS_COUNT;
}
for(int index=startIndex; index<endIndex; index++){
result.add(generateItem(index));
}
return result;
}
private Item generateItem(int id){
List<ItemAction> actions = new ArrayList<>(2);
actions.add(new ItemAction("A" + id));
actions.add(new ItemAction("B" + id));
return new Item(id, "Item " + id, actions);
}
}
DatatableLazyModel.java
#ManagedBean
#ViewScoped
public class DatatableLazyModel extends LazyDataModel<Item>{
private TheService service;
private List<Item> allResults;
public DatatableLazyModel() {
this.service = new TheService();
this.allResults = new ArrayList<>();
}
#Override
public int getRowCount() {
return service.getItemsCount();
}
#Override
public List<Item> load(int first, int pageSize, String sortField, SortOrder sortOrder,
Map<String, Object> filters) {
System.out.println("Fetching starting at " + first);
List<Item> pageResults = service.retrieveItems(first, pageSize);
allResults.addAll(pageResults);
return pageResults;
}
}
index.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:p="http://primefaces.org/ui" template="template/ui.xhtml">
<ui:define name="body">
<h3>Datatablelazy load</h3>
<h:form>
<p:dataTable id="resultsTable" var="item"
value="#{datatableLazyModel}" liveScroll="true" scrollRows="3"
scrollHeight="100" scrollable="true" lazy="true">
<p:column headerText="Name">
<h:outputText value="#{item.name}" />
</p:column>
<p:column headerText="Actions">
<ui:repeat var="action" value="#{item.actions}">
<p:commandButton action="#{action.execute()}" value="#{action.label}">
</p:commandButton>
</ui:repeat>
</p:column>
</p:dataTable>
</h:form>
</ui:define>
</ui:composition>
pom.xml dependencies
<dependency>
<groupId>com.sun.faces</groupId>
<artifactId>jsf-api</artifactId>
<version>2.2.4</version>
</dependency>
<dependency>
<groupId>com.sun.faces</groupId>
<artifactId>jsf-impl</artifactId>
<version>2.2.4</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>org.primefaces</groupId>
<artifactId>primefaces</artifactId>
<version>5.3</version>
</dependency>
Questions
I have tried this with more items loaded. Each time, only methods on the last loaded set are executed. Even from old components.
So basically, the question is : "How can I make all the buttons work as expected?"
Why do methods do not get executed on the correct instance? How is the target instance resolved?
Thank you for your help :)
The issue is with the LazyDataModel implementation that is meant to be used with pages and not live scroll, especially this snippet :
if(rowIndex == -1 || pageSize == 0)
this.rowIndex = -1;
else
this.rowIndex = (rowIndex % pageSize);
I subclassed LazyDataModel with my own implementation to solve the problem.
LiveScrollLazyDataModel.java
public abstract class LiveScrollLazyModel<T> extends LazyDataModel<T> {
private int rowIndex;
#Override
public void setRowIndex(int rowIndex) {
int oldIndex = getRowIndex();
if (rowIndex == -1 || getPageSize() == 0){
super.setRowIndex(-1);
this.rowIndex = -1;
}else{
super.setRowIndex(rowIndex);
this.rowIndex = rowIndex;
}
if (getResults() == null) {
return;
}
DataModelListener[] listeners = getDataModelListeners();
if (listeners != null && oldIndex != this.rowIndex) {
Object rowData = null;
if (isRowAvailable()) {
rowData = getRowData();
}
DataModelEvent dataModelEvent = new DataModelEvent(this, rowIndex, rowData);
for (int i = 0; i < listeners.length; i++) {
listeners[i].rowSelected(dataModelEvent);
}
}
}
#Override
public T getRowData() {
return getResults().get(this.rowIndex);
}
#Override
public boolean isRowAvailable() {
if(getResults() == null) {
return false;
}
return rowIndex >= 0 && rowIndex < getResults().size();
}
/**
* The complete list of results
* #return
*/
public abstract List<T> getResults();
}
The implementor needs to implement the getResults() method that provides the complete list of results.
Related
I am very new to JSF and was trying to implement a pagination using <ui:repeat>. I am using the #ViewScoped but currently my state is not saved. Every time the backing bean resets the page information and the next page is not displayed. Attaching the source code for reference.
WorkItemList.xhtml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:c="http://java.sun.com/jsp/jstl/core"
template="/WEB-INF/template/default.xhtml">
<ui:define name="content">
<h:form id="form">
<ui:repeat value="#{repeatPaginator.model}" var="listItem">
<div>
<h:outputText value="#{listItem}"/>
</div>
</ui:repeat>
<h:commandButton value="< prev" action="#{repeatPaginator.prev}"/>
<h:outputText value="#{repeatPaginator.pageIndex} / #{repeatPaginator.pages}"/>
<h:commandButton value="next >" action="#{repeatPaginator.next}"/>
<h:inputHidden value="#{repeatPaginator.pageIndex}"/>
</h:form>
</ui:define>
</ui:composition>
Backing Bean - RepeatPaginator.java
package test;
#ManagedBean
#ViewScoped
public class RepeatPaginator implements Serializable{
private static final long serialVersionUID = 1L;
private static final int DEFAULT_RECORDS_NUMBER = 2;
private static final int DEFAULT_PAGE_INDEX = 1;
private TestDelegate delegate;
private int records;
private int recordsTotal;
private int pageIndex;
private int pages;
private List<String> model;
public RepeatPaginator() {
delegate = new TestDelegate();
this.records = DEFAULT_RECORDS_NUMBER;
this.pageIndex = DEFAULT_PAGE_INDEX;
// Get Model
this.model = delegate.fetchCurrentList(getFirst(), getFirst()+records);
this.recordsTotal = delegate.getListSize();
if (records > 0) {
pages = records <= 0 ? 1 : recordsTotal / records;
if (recordsTotal % records > 0) {
pages++;
}
if (pages == 0) {
pages = 1;
}
} else {
records = 1;
pages = 1;
}
updateModel();
}
public void updateModel() {
int fromIndex = getFirst();
int toIndex = getFirst() + records;
if(toIndex > this.recordsTotal) {
toIndex = this.recordsTotal;
}
setModel(delegate.fetchCurrentList(fromIndex, toIndex));
}
public void next() {
if(this.pageIndex < pages) {
this.pageIndex++;
}
updateModel();
}
public void prev() {
if(this.pageIndex > 1) {
this.pageIndex--;
}
updateModel();
}
public int getRecords() {
return records;
}
public int getRecordsTotal() {
return recordsTotal;
}
public int getPageIndex() {
return pageIndex;
}
public int getPages() {
return pages;
}
public int getFirst() {
return (pageIndex * records) - records;
}
public List<String> getModel() {
if(model==null)
updateModel();
return model;
}
public void setModel(List<String> model) {
this.model = model;
}
public void setPageIndex(int pageIndex) {
this.pageIndex = pageIndex;
}
}
Delegate - TestDelegate.java
#ViewScoped
public class TestDelegate {
private List<String> list = new ArrayList<String>();
public TestDelegate()
{
this.list.add("Item 1");
this.list.add("Item 2");
this.list.add("Item 3");
this.list.add("Item 4");
this.list.add("Item 5");
this.list.add("Item 6");
this.list.add("Item 7");
this.list.add("Item 8");
this.list.add("Item 9");
this.list.add("Item 10");
this.list.add("Item 11");
}
public List<String> fetchCurrentList(int from, int to)
{
return list.subList(from, to);
}
public int getListSize()
{
return this.list.size();
}
}
The #ViewScoped bean is not "magically present all the time". At the end of the request, it's values will be serialized and the bean is destructed.
This causes a Re-Construct of the bean at the beginning of the next request (click on next). Your Container will take care to deserialize the stored values again, in order to return the actual State of the bean. BUT: This happens after Bean-Construction, meaning when you try to use the values within the Constructor of the bean, nothing is restored.
Move your logic into a method that you annotate with #PostConstruct - then all values will be loaded and you can fetch the correct result. The container will call the method annotated with #PostConstruct once it restored the prior state of the bean.
This becomes important when you start to use ManagedProperties or CDI-Injections.
public RepeatPaginator() {
//no serialized data available here, Good location to initialize some stuff
//that is independent.
delegate = new TestDelegate();
this.records = DEFAULT_RECORDS_NUMBER;
this.pageIndex = DEFAULT_PAGE_INDEX;
}
#PostConstruct
public void init() {
//Here the ViewScoped values are restored. We can start to use them.
// Get Model
this.model = delegate.fetchCurrentList(getFirst(), getFirst()+records);
this.recordsTotal = delegate.getListSize();
if (records > 0) {
pages = records <= 0 ? 1 : recordsTotal / records;
if (recordsTotal % records > 0) {
pages++;
}
if (pages == 0) {
pages = 1;
}
} else {
records = 1;
pages = 1;
}
updateModel();
}
This question already has answers here:
Primefaces DataTable, lazy loading and CommandButton per row
(2 answers)
Closed 5 years ago.
I'm newbie on JSF and I'm working with primefaces-2.2.RC2.
I have a commandButton and when I click I want that it fill a dataTable that use lazyLoading (my work is based on primefaces showcase).
When I click on commandButton, the data is not loaded.
Here piece of my code:
<p:commandButton value="Search"
action="#{tableBean.search()}"
update="carList"
/>
<p:dataTable id="carList"
var="car"
value="#{tableBean.lazyDataModel}"
paginator="true"
rows="10"
dynamic="true"
lazy="true"
paginatorPosition="bottom"
>
Ande on my bean:
#ViewScoped
public class TableBean {
LazyDataModel<Car> lazyDataModel;
public TableBean() {
lazyDataModel = new LazyDataModel<Car>() {
#Override
public List<Car> load(int i, int i1, String string, boolean bln, Map<String, String> map) {
return new ArrayList<Car>();
}
};
}
public LazyDataModel<Car> getLazyDataModel() {
return lazyDataModel;
}
public void search() {
lazyDataModel = new LazyDataModel<Car>() {
#Override
public List<Car> load(int first, int pageSize, String sortField,boolean sortOrder, Map<String, String> filters) {
return fetchLazyData(first, pageSize);
}
#Override
public void setRowIndex(int rowIndex) {
setPageSize(10);
if (rowIndex == -1 || getPageSize() == 0) {
super.setRowIndex(-1);
} else {
super.setRowIndex(rowIndex % getPageSize());
}
}
public List<Car> fetchLazyData(int first, int pageSize) {
System.out.println("Loading the lazy car data between " + first + " and " + (first + pageSize));
List<Car> lazyCars = new ArrayList<Car>();
for (int i = 0; i < pageSize; i++) {
int offset = i + first;
lazyCars.add(new Car("Model_" + offset, (int) (Math.random() * 60 + 1960), "Brand_" + offset, "Color_" + offset));
}
return lazyCars;
}
};
lazyDataModel.setRowCount(10000);
}
}
update: I descovered that I have to bind my DataTable and on my bean I have to call loadLazyData() function.
I descovered that I have to bind my DataTable with my bean and on my bean I have to call loadLazyData() function to inform to table to load it.
I have a problem with the view in JSP (Java EE)
Only the heading is shown.
My Code:
Entitiy Class (Konto);
#Entity
public class Konto implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#Column(nullable=false)
#NotNull(message="Kontonummer muss angegenben werden")
#Pattern(regexp="[0-9][0-9][0-9][0-9]")
private String kontonummer;
#Column(nullable=false)
#NotNull(message="Kontostand muss angegeben werden")
#DefaultValue(value="0.0")
private Double ktostd;
#Column(nullable=false)
#DecimalMin(value="0", message="Der Zins muss zw. 0 und 10 % liegen")
#DecimalMax(value="0.1", message="Der Zins muss zw. 0 und 10 % liegen")
private Double habenZins;
#ManyToOne
#JoinColumn(nullable=false)
#NotNull(message="Besitzer muss angegeben werden")
private Besitzer besitzer;
public Besitzer getBesitzer() {
return besitzer;
}
public void setBesitzer(Besitzer besitzer) {
this.besitzer = besitzer;
}
public Double getHabenZins() {
return habenZins;
}
public void setHabenZins(Double habenZins) {
this.habenZins = habenZins;
}
public String getKontonummer() {
return kontonummer;
}
public void setKontonummer(String kontonummer) {
this.kontonummer = kontonummer;
}
public Double getKtostd() {
return ktostd;
}
public void setKtostd(Double ktostd) {
this.ktostd = ktostd;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
#Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
#Override
public boolean equals(Object object) {
// TODO: Warning - this method won't work in the case the id fields are not set
if (!(object instanceof Konto)) {
return false;
}
Konto other = (Konto) object;
if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
return false;
}
return true;
}
#Override
public String toString() {
return "at.korn.entity.NewEntity[ id=" + id + " ]";
}
}
Kontolist.xhtml:
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">
<h:head>
<title>Facelet Title</title>
</h:head>
<h:body>
<h1>Kontoliste</h1>
<h:form>
<h:dataTable value="#{kontolist.kontos}" var="konto">
<h:column>
<f:facet name="header">
<h:outputText value="Kontonummer"></h:outputText>
</f:facet>
<h:outputText value="#{konto.kontonummer}"></h:outputText>
</h:column>
</h:dataTable>
</h:form>
</h:body>
</html>
KontoList Controller:
#ManagedBean
#SessionScoped
public class Kontolist {
#EJB
KontoFacadeLocal kontofacade;
private List<Konto> kontos;
/** Creates a new instance of kontolist */
public Kontolist() {
kontos = kontofacade.findAll();
}
public KontoFacadeLocal getKontofacade() {
return kontofacade;
}
public void setKontofacade(KontoFacadeLocal kontofacade) {
this.kontofacade = kontofacade;
}
public List<Konto> getKontos() {
setKontos(kontofacade.findAll());
return kontos;
}
public void setKontos(List<Konto> kontos) {
this.kontos = kontos;
}
}
Problem:
Only the header is shown. In the source from the browser is the same code without html injection (like value="#{konto.kontonummer}")
First of all, that is not a JSP file. That's a Facelets (XHTML) file. JSP is an ancient view technology. Facelets is the successor of JSP.
So, your concrete problem is that the JSF tags are not been parsed? That can happen when the request URL did not match the URL pattern of the FacesServlet as definied in web.xml. If it is for example *.jsf, then you'd need to change the request URL from
http://localhost:8080/contextname/kontolist.xhtml
to
http://localhost:8080/contextname/kontolist.jsf
However, much better is to just change the URL pattern of the FacesServlet to *.xhtml so that you do not need to fiddle with virtual URLs and introduce security constraints to prevent the enduser from accidently or awaringly viewing the raw *.xhtml pages.
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.xhtml</url-pattern>
</servlet-mapping>
See also:
What is the difference between creating JSF pages with .jsp or .xhtml or .jsf extension
Unrelated to the concrete problem, you've by the way a NullPointerException bug in your code. Replace
public Kontolist() {
kontos = kontofacade.findAll();
}
by
#PostConstruct
public void init() {
kontos = kontofacade.findAll();
}
Injected dependencies are namely not available during construction. The getter and setter for the kontofacate are also entirely superfluous, I'd remove them to prevent future confusion and abuse.
Do you know this feeling when every code you write works immedietly and you underrun your schedule :-P It's like 'oh yeah now I have time to make it perfect'. That's where I am at the moment^^
So I implemented a repeater with JSF (ui:repeat) and I thought about a paging for all the entities. Is there maybe an easy way to do that? What are the points I have to think about?
Would be nice if someone gives me some help. My googleskills haven't helped me so far :-P
Cheers...
Here is a simple example that should give you the idea on how to implement this.
RepeatPaginator:
public class RepeatPaginator {
private static final int DEFAULT_RECORDS_NUMBER = 2;
private static final int DEFAULT_PAGE_INDEX = 1;
private int records;
private int recordsTotal;
private int pageIndex;
private int pages;
private List<?> origModel;
private List<?> model;
public RepeatPaginator(List<?> model) {
this.origModel = model;
this.records = DEFAULT_RECORDS_NUMBER;
this.pageIndex = DEFAULT_PAGE_INDEX;
this.recordsTotal = model.size();
if (records > 0) {
pages = records <= 0 ? 1 : recordsTotal / records;
if (recordsTotal % records > 0) {
pages++;
}
if (pages == 0) {
pages = 1;
}
} else {
records = 1;
pages = 1;
}
updateModel();
}
public void updateModel() {
int fromIndex = getFirst();
int toIndex = getFirst() + records;
if(toIndex > this.recordsTotal) {
toIndex = this.recordsTotal;
}
this.model = origModel.subList(fromIndex, toIndex);
}
public void next() {
if(this.pageIndex < pages) {
this.pageIndex++;
}
updateModel();
}
public void prev() {
if(this.pageIndex > 1) {
this.pageIndex--;
}
updateModel();
}
public int getRecords() {
return records;
}
public int getRecordsTotal() {
return recordsTotal;
}
public int getPageIndex() {
return pageIndex;
}
public int getPages() {
return pages;
}
public int getFirst() {
return (pageIndex * records) - records;
}
public List<?> getModel() {
return model;
}
public void setPageIndex(int pageIndex) {
this.pageIndex = pageIndex;
}
}
Bean:
public class TestBean {
private List<String> list;
private RepeatPaginator paginator;
#PostConstruct
public void init() {
this.list = new ArrayList<String>();
this.list.add("Item 1");
this.list.add("Item 2");
this.list.add("Item 3");
this.list.add("Item 4");
this.list.add("Item 5");
this.list.add("Item 6");
this.list.add("Item 7");
this.list.add("Item 8");
this.list.add("Item 9");
this.list.add("Item 10");
this.list.add("Item 11");
paginator = new RepeatPaginator(this.list);
}
public RepeatPaginator getPaginator() {
return paginator;
}
}
XHTML:
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
template="/WEB-INF/template/default.xhtml">
<ui:define name="content">
<h:form>
<ui:repeat value="#{testBean.paginator.model}" var="listItem">
<div>
<h:outputText value="#{listItem}"/>
</div>
</ui:repeat>
<h:commandButton value="< prev" action="#{testBean.paginator.prev}"/>
<h:outputText value="#{testBean.paginator.pageIndex} / #{testBean.paginator.pages}"/>
<h:commandButton value="next >" action="#{testBean.paginator.next}"/>
<h:inputHidden value="#{testBean.paginator.pageIndex}"/>
</h:form>
</ui:define>
</ui:composition>
Pagination is in fact easy. You just have to keep passing one or two parameters around: firstrow and optionally rowcount (which can also be kept in the server side). When the enduser clicks Next, you just increment the value of firstrow with the value of rowcount. When the enduser clicks Back, you just decrement the value of firstrow with the value of rowcount. You only need to check if it doesn't exceed the borders of 0 and totalrows and alter accordingly.
Then, based on the desired firstrow and rowcount, you know exactly which data to display. If all the data is already in some List in Java's memory, then you just use List#subList() to obtain a sublist from it for display. It is however not efficient to duplicate the entire database table into Java's memory. It may not harm when it's only 100 rows, but when it's much more than that and/or you're duplicating it for every single user, then the application will run out of memory very soon.
In that case you'd rather like to paginate at DB level. In for example MySQL you can use LIMIT clause to obtain a subset of results from the DB. JPA/Hibernate even provides ways using setFirstResult() and setMaxResults() methods of Query and Criteria respectively. You can find examples in this and this answer.
You can find a basic JSF 1.2 targeted kickoff example of Google-like pagination (and sorting) in this article. It uses Tomahawk components, but in JSF 2.0 you can just leave them away by making the bean #ViewScoped (replaces t:saveState) and using ui:repeat (replaces t:dataList).
Last but not least, there are lot of component libraries which does all the works in a single component. For example RichFaces <rich:datascroller> and PrimeFaces <p:dataTable paginator="true"> (can also be done ajaxical).
In my application i need to add a row on a click of a button and this button will be in all the rows. Need help to do this?
Item Class
public class Item {
public Item()
{
}
private String value;
public Item(String value) { this.value = value; }
public void setValue(String value) { this.value = value; }
public String getValue() { return value; }
}
Manage Bean Class
public class MyMB
{
private List<Item> list;
public void addItem() { // JSF action method
list.add(new Item("Default"));
Iterator<Item> iterator = list.iterator();
while(iterator.hasNext())
{
Item item = (Item)iterator.next();
System.out.println(item.getValue());
}
System.out.println();
}
/**
* #return the list
*/
public List<Item> getList() {
if(list==null)
{
loadList();
}
return list;
}
private void loadList() {
list = new ArrayList<Item>();
list.add(new Item("Data"));
}
}
JSF code
<h:form>
<rich:dataTable value="#{myMB.list}" var="item" id="tabel">
<h:column><h:inputText value="#{item.value}" /></h:column>
<h:column><a4j:commandButton value="Add" actionListener="#{myMB.addItem}" reRender="tabel"/></h:column>
All you need to do is basically indeed just adding an empty object to the datamodel behind the value attribute of h:dataTable.
But the same empty row needs to be preserved in the subsequent request as well. If the backing bean is request scoped, then the datamodel get reloaded without the empty row. This all should work when the bean is session scoped.
Further there are several errors in your JSF code. The h:dataTable var attribute is missing and the column content needs to be inside a h:column.
<h:form>
<h:dataTable value="#{bean.list}" var="item">
<h:column><h:inputText value="#{item.value}" /></h:column>
</h:dataTable>
<h:commandButton value="Add" action="#{bean.add}"/>
</h:form>
A session or view scoped bean can look like this:
public class Bean {
private List<Item> list;
public Bean() {
list = new ArrayList<Item>();
}
public void add() {
list.add(new Item());
}
public List<Item> getList() {
return list;
}
}
The Item class should of course have a default no-arg constructor. Normally this is already implicitly available, but if you define your own constructor with arguments, then it is not available anymore. You'll need to explicitly define it, otherwise you cannot do Item item = new Item(); anymore.
public class Item {
private String value;
public Item() {
// Keep default constructor alive.
}
public Item(String value) {
this.value = value;
}
// ...
}
If you prefer to keep the bean in the request scope, then you'll need to maintain the amount of newly added items, so that the bean can preserve the same amount on load.
public class Bean {
private List<Item> list;
private HtmlInputHidden count = new HtmlInputHidden();
public Bean() {
count.setValue(0);
}
public void add() {
list.add(new Item());
}
public List<Item> getList() {
if (list == null) loadList();
return list;
}
public HtmlInputHidden getCount() {
return count;
}
public void setCount(HtmlInputHidden count) {
this.count = count;
}
private void loadList() {
list = new ArrayList<Item>();
// Preserve list with newly added items.
for (int i = 0; i < (Integer) count.getValue(); i++) {
list.add(new Item());
}
}
}
You'll only need to add the following to the <h:form> of the JSF page:
<h:inputHidden binding="#{bean.count}" converter="javax.faces.Integer" />
For more insights about using datatables in any way you may find this article useful: Using Datatables. It also contains a WAR file with lot of examples in both request and session scope.
Take this table as an example:
<h:datatable value="#{myBean.list}" ...>
...
<h:column>
<h:commandButton value="Add a row" action="#{myBean.addRow}"/>
</h:column>
</h:datatable>
The method myBean.addRow will simply add a new element in your list:
public class MyBean {
private List<SomeClass> list;
...
public List<SomeClass> getList() {
return list;
}
public void addRow() {
list.add(new SomeClass());
}
}
When you will click on the button, the method addRow will add a new element in the list. The page will refresh and display the table with a new row.
Edit:
Regarding your post edition, three things:
Point 1: Could you please attach the stacktrace of your error?
Point 2: Your method addRow return a String which is an ID used by JSF for the navigation. As this action does not involve any navigation (i.e. the user stay on the same page), simply return null or "":
public String addRow() {
list.add(new Item("new data"));
return null;
}
Point 3: I suggest that your class Item provide an empty constructor (in addition of your current constructor):
public Item() {
}