How to select multiple rows of <h:dataTable> with <h:selectBooleanCheckbox> - jsf

I use <h:dataTable> to list data from database. We have many records in page, now I would like to select multiple records with a checkbox in each row. How can I achieve this?

I assume that your entity is that well-designed that it has an unique technical identifier, for example the auto increment sequence from the DB.
public class Entity {
private Long id;
// ...
}
If not, you'll need to add it.
Then, add a Map<Long, Boolean> property to the bean which is tied to the table.
private Map<Long, Boolean> checked = new HashMap<Long, Boolean>();
(preinitialization can also happen in (post)constructor, take your pick, at least JSF won't do it for you; oh, give it a getter as well, a setter is not necessary)
Then, add a column with a checkbox which maps to the boolean value by entity ID as key.
<h:dataTable value="#{bean.entities}" var="entity">
<h:column>
<h:selectBooleanCheckbox value="#{bean.checked[entity.id]}" />
</h:column>
...
</h:dataTable>
<h:commandButton value="Delete" action="#{bean.delete}" />
Now, in the action method associated with the delete button, you can collect and delete the checked items as follows:
public void delete() {
List<Entity> entitiesToDelete = new ArrayList<Entity>();
for (Entity entity : entities) {
if (checked.get(entity.getId())) {
entitiesToDelete.add(entity);
}
}
entityService.delete(entitiesToDelete);
checked.clear();
loadEntities();
}

Related

How to add ManyCheckbox items to seperate rows in database using JSF [duplicate]

I have a Facelets page with a <h:dataTable>. In each row there is a <h:selectBooleanCheckbox>. If the checkbox is selected the object behind the corresponding row should be set in the bean.
How do I do this?
How to get the selected rows or their data in a backing bean?
Or would it be better to do it with <h:selectManyCheckbox>?
Your best bet is to bind the <h:selectBooleanCheckbox> value with a Map<Item, Boolean> property where Item represents the object behind the corresponding row.
<h:dataTable value="#{bean.items}" var="item">
<h:column>
<h:selectBooleanCheckbox value="#{bean.checked[item]}" />
</h:column>
...
</h:dataTable>
<h:commandButton value="submit" action="#{bean.submit}" />
public class Bean {
private Map<Item, Boolean> checked = new HashMap<Item, Boolean>();
private List<Item> items;
public void submit() {
List<Item> selectedItems = checked.entrySet().stream()
.filter(Entry::getValue)
.map(Entry::getKey)
.collect(Collectors.toList());
checked.clear(); // If necessary.
// Now do your thing with selectedItems.
}
// ...
}
You see, the map is automatically filled with all table items as key and the checkbox value is automatically set as map value associated with the item as key.
This only requires that the Item#equals() and Item#hashCode() is properly implemented as per their contracts.
If you can't guarantee that, then you'd better use a Map<RowId, Boolean> instead where RowId represents the type of the row identifier. Let's take an example that you've a Item object whose identifier property id is a Long:
public class Item {
private Long id;
// ...
}
<h:dataTable value="#{bean.items}" var="item">
<h:column>
<h:selectBooleanCheckbox value="#{bean.checked[item.id]}" />
</h:column>
...
</h:dataTable>
<h:commandButton value="submit" action="#{bean.submit}" />
public class Bean {
private Map<Long, Boolean> checked = new HashMap<Long, Boolean>();
private List<Item> items;
public void submit() {
List<Item> selectedItems = items.stream()
.filter(item -> checked.get(item.getId()))
.collect(Collectors.toList());
checked.clear(); // If necessary.
// Now do your thing with selectedItems.
}
// ...
}
In the following example I am using checkboxes to select two or more products to allow the user to compare product specifications on a new web page using JSF 2.0.
It took me a good while to find the following problem (totally obvious now of course) so thought it worth a mention for those trying to use pagination with BalusC's code above (nice answer BalusC, much simpler than I ever imagined it would be).
If you are using pagination you will get nullpointers at the line:
if (checked.get(item.getId()))
-in BalusC's code above.
This is because only displayed check boxes are added to the Map (doh; slap forehead).
For those products whose check boxes are never displayed, due to pagination, this line will result in a null pointer error and a check needs to be added to ignore these null pointers (assuming that all check boxes are unchecked on page load). In order for the user to tick a check box then they need to display the pagination page so all works well there after.
If some or all of the check boxes are required to be ticked on first page load then this will be of no help to you...you will have to manually add those to the Map in order for them to be displayed correctly on page load.
Note: because I am using a JPA 'Entity class from database' object I also needed to use #Transient for the id in my ProductTbl Entity Class as all variables are considered columns in the database by JPA, by default, unless prefixed with #Transient. Also I am using a second link to reset the check boxes, which calls clearSelections(), and my 'submit' is a link calling compareSelectedProducts() rather than a Submit button.
The full code is as follows:
In the 'ProductTbl' Entity class derived from the database :
#Transient
private Long id;
public Long getId()
{
return id;
}
public void setId(Long id)
{
this.id = id;
}
In the backing bean 'ProductSelection':
private Map<Long, Boolean> checked = new HashMap<Long, Boolean>();
private String errorMessage = "";
// List of all products.
private List<ProductTbl> products;
// List of products to compare.
private List<ProductTbl> compareProducts;
// Setters and getters for above...
public String compareSelectedProducts()
{
// Reset selected products store.
compareProducts = new ArrayList();
for (ProductTbl item: products)
{
// If there is a checkbox mapping for the current product then...
if(checked.get(item.getId()) != null)
{
// If checkbox is ticked then...
if (checked.get(item.getId()))
{
// Add product to list of products to be compared.
compareProducts.add(item);
}
}
}
if(compareProducts.isEmpty())
{
// Error message that is displayed in the 'ErrorPage.xhtml' file.
errorMessage = "No Products selected to compare specifications. Select two or more products by ticking the check box in the second column 'Cmpr'";
return "process_ErrorPage";
}
// Rest of code to get product specification data ready to be displayed.
return "process_CompareSelected";
}
public String clearSelections()
{
// Untick all checkbox selections.
checked.clear();
return "process_MainSearchResult";
}
In the JSF Web page 'MainSearchResult.xhtml':
<h:commandLink action="#{productSelection.compareSelectedProducts()}" value="Cmpr Specification Comparison Table" />
<h:commandLink action="#{productSelection.clearSelections()}" value="Clear Selected" />
<h:dataTable value="#{productSelection.products}" rows="#{productSelection.numberRowsToDisplay}" first="#{productSelection.rowStart}" var="item" headerClass="table-header" >
<h:column>
<f:facet name="header">
<h:outputText style="font-size:12px" value="Cmpr" />
</f:facet>
<div style="text-align:center;" >
<h:selectBooleanCheckbox value="#{productSelection.checked[item.id]}" />
</div>
</h:column>
</h:dataTable>
In the 'faces-config.xml' file:
<navigation-rule>
<navigation-case>
<from-outcome>process_MainSearchResult</from-outcome>
<to-view-id>/MainSearchResult.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
<navigation-rule>
<navigation-case>
<from-outcome>process_CompareSelected</from-outcome>
<to-view-id>/CompareSelected.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
<navigation-rule>
<navigation-case>
<from-outcome>process_ErrorPage</from-outcome>
<to-view-id>/ErrorPage.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
One way to send in a parameter via <h:selectBooleanCheckbox> is to send it in via the title of the Checkbox. In the ValueChangeListener, you can get it from the component using a getAttributes().get("title"). This helps in cases where you want to send an id value as a parameter (as opposed to the selected row index).

Resetting child p:selectOneMenus inside a p:dataTable to empty, when a labelled item in their parent list is selected

There are three tables in MySQL database, category, sub_category and brand (manufacturer) where category is a parent of the rest i.e. sub_category and brand. I hope, the relationship between menus can be clearer based on the table relationships.
All of three <p:selectOneMenu>s are placed inside a <p:dataTable> in three respective columns as identified by <p:column>. I am ignoring <p:column>, <p:cellEditor>, <f:facet name="output">, <f:facet name="input">, <p:rowEditor> and all such nuisances for brevity.
row corresponds to a JPA managed entity which is product in this case as specified by var="row" in the <p:dataTable> associated.
This is the actual question mark : When an item (the first one) with a null value in the categoryList (parent) is selected, its child lists subCategoryList and brandList should be rest to empty.
Category List:
<p:selectOneMenu id="categoryList"
value="#{row.category}"
required="#{param['javax.faces.source'] ne component.clientId}">
<f:selectItem itemLabel="Select"
itemValue="#{null}"/>
<!-- When this item is selected, its children below should be reset to empty. -->
<f:selectItems var="category"
value="#{productManagedBean.categories}"
itemLabel="Select"
itemValue="#{category}"/>
<p:ajax update="subCategoryList brandList"/>
<!-- The listener functionality is left incomplete here. -->
</p:selectOneMenu>
Subcategory List :
<p:selectOneMenu id="subCategoryList"
value="#{row.subCategory}">
<f:selectItem itemLabel="Select"
itemValue="#{null}"/>
<f:selectItems var="subCategory"
value="#{productManagedBean.getSubCategories(row.category)}"
itemLabel="#{subCategory.subCatName}"
itemValue="#{subCategory}"
rendered="true"/>
</p:selectOneMenu>
Brand (manufacturer) List :
<p:selectOneMenu id="brandList"
value="#{row.brand}">
<f:selectItem itemLabel="Select"
itemValue="#{null}"/>
<f:selectItems var="brand"
value="#{productManagedBean.getBrands(row.category)}"
itemLabel="#{brand.brandName}"
itemValue="#{brand}"
rendered="true"/>
</p:selectOneMenu>
The managed bean (lazy data model can be ignored in the context of this question) :
#Named
#ViewScoped
public class ProductManagedBean extends LazyDataModel<Product> implements Serializable {
#Inject
private Service service;
// Associated with <p:selectOneMenu id="categoryList">.
private List<Category> categories; // Getter & setter.
// These are merely helper maps to reduce possible database calls.
private Map<Category, List<SubCategory>> subCategoriesByCategory;
private Map<Category, List<Brand>> brandByCategory;
public ProductManagedBean() {}
#PostConstruct
private void init() {
// This can be application scoped somewhere else as per business requirement.
categories = service.getCatgeoryList();
subCategoriesByCategory = new HashMap<Category, List<SubCategory>>();
brandByCategory = new HashMap<Category, List<Brand>>();
}
// This method populates <f:selectItems> associated with <p:selectOneMenu id="brandList">.
public List<SubCategory> getSubCategories(Category category) {
// category is never null here unless something is broken deliberately.
if (category == null) {
return null;
}
List<SubCategory> subCategories = subCategoriesByCategory.get(category);
if (subCategories == null) {
subCategories = service.findSubCategoriesByCategoryId(category.getCatId());
subCategoriesByCategory.put(category, subCategories);
}
return subCategories;
}
// This method populates <f:selectItems> associated with <p:selectOneMenu id="brandList">.
public List<Brand> getBrands(Category category) {
// category is never null here unless something is broken deliberately.
if (category == null) {
return null;
}
List<Brand> brands = brandByCategory.get(category);
if (brands == null) {
brands = service.findBrandsByCategoryId(category.getCatId());
brandByCategory.put(category, brands);
}
return brands;
}
}
In any case, the selected value in any of these menus is not supplied to the corresponding backing bean. It is only available in the model backed by JPA (value="#{row.category}", value="#{row.subCategory}" and value="#{row.brand}" respectively).
► How to signal the backing bean that the first item with a nullvalue (labelled "Select") in the parent menu is selected so as to resetting its child lists to empty? This should happen in any feasible way, if this is not feasible.
I am using PrimeFaces 5.2 final (community release) and Mojarra 2.2.12.
This is not needed unless there is a null foreign key in the underlying database table specifically using the vendor specific ON DELETE SET NULL option allowing an optional parent in each (or some) corresponding child row.
To the point, you need to make sure that the <f:selectItem> getter is called with a null argument. In other words, the #{row.category} must be null. Given that you're for #{row.category} using the model als shown in this answer, Populate p:selectOneMenu based on another p:selectOneMenu in each row of a p:dataTable, most likely as below,
#Transient
private Category category;
public Category getCategory() {
return (category == null && subCategory != null) ? subCategory.getCategory() : category;
}
then the #{row.category} will actually never be null when there's a subCategory. This will be the case when an existing data entry is presented in view.
You basically need to explicitly null out the subCategory (and brand) when the transient category property is explicitly set to null. This oversight has in the meanwhile been fixed in the mentioned answer. Here's how your new setCategory() method should look like:
public void setCategory(Category category) {
this.category = category;
if (category == null) {
subCategory = null;
brand = null;
}
}
This way, the getCategory() will properly return null and thus the passed-in #{row.category} also.

Reusing of Beans

On the Dashboard each User can see some Basic Stats. Lets take for example the "last login" Date. (But there are many more stats / values / settings to display)
The XHTML Files looks simplidfied like this:
<h:outputText value="statisticController.lastLoginDate()" />
The Bean itself uses #Inject to get the Session and therefore the current user:
#Named
#RequestScoped
public StatisticController{
#Inject
private mySessionBean mySession;
#PostConstruct
private void init(){
//load stats for mySession.currentUser;
}
}
Now, i want to generate a List where for example a certain role can view the values for ALL users. Therefore i can't use the Session Inject anymore, because the StatisticController now needs to be generated for multiple Users.
Having regular Classes this would not be a big problem - add the userEntity to the constructor. What is the "best practice" to solve this in JSF?
If i modify the StatisticController to something like this:
#Named
#RequestScoped
public StatisticController{
public void init(User user){
//load stats for user;
}
}
i would need to call init(user) manually of course. How can this be achieved from within a Iteration in the XHTML file?
I could refactor it so the valueLoading happens in the actual getter method, and iterate like this:
<ui:repeat var="user" value="#{userDataService.getAllUsers()}">
<h:outputText value="statisticController.lastLoginDate(user)" />
...
</ui:repeat>
But then i would need to load "every" value seperate, which is bad.
So a way like this would be "better":
<ui:repeat var="user" value="#{userDataService.getAllUsers()}">
statisticController.init(user);
<h:outputText value="statisticController.lastLoginDate()" />
...
</ui:repeat>
However this doesnt look very comfortable either. Further more doing things like this, will move nearly "all" Backend Stuff into the Render Response Phase, which is feeling wrong.
Any Ideas / Tipps how to solve this in a way that's not feeling "like a workaround"?
Create a new model wrapping those models.
public class UserStatistics {
private User user;
private Statistics statistics;
// ...
}
So that you can just use e.g.
public class UserStatisticsBacking {
private List<UserStatistics> list;
#EJB
private UserService userService;
#EJB
private StatisticsService statisticsService;
#PostConstruct
public void init() {
list = new ArrayList<UserStatistics>();
for (User user : userService.list()) {
list.add(new UserStatistics(user, statisticsService.get(user)));
}
}
// ...
}
(better would be to perform it in a new UserStatisticsService though)
with
<ui:repeat value="#{userStatisticsBacking.list}" var="userStatistics">
<h:outputText value="#{userStatistics.user.name}" />
<h:outputText value="#{userStatistics.statistics.lastLoginDate}" />
...
</ui:repeat>
An alternative to using a wrapped model proposed by BalusC is to store two separate lists with model data. With this approach you don't need to introduce modifications.
Following this route you'll be iterating over one list with <ui:repeat> and, ensuring equality of sizes of both lists, get second list element by index, which in turn is available via varStatus attribute that exports the iteration status variable.
<ui:param name="stats" value="#{bean.stats}/>
<ui:repeat value="#{bean.users}" var="user" varStatus="status">
<h:outputText value="#{user}/>
<h:outputText value="#{stats[status.index]}/>
</ui:include>
Population of lists may be done beforehand in PostConstruct method:
private List<User> users;
private List<Statistic> stats;
#PostConstruct
public void init() {
users = userService.list();
stats = statService.list();
}

How to use <h:selectBooleanCheckbox> in <h:dataTable> or <ui:repeat> to select multiple items?

I have a Facelets page with a <h:dataTable>. In each row there is a <h:selectBooleanCheckbox>. If the checkbox is selected the object behind the corresponding row should be set in the bean.
How do I do this?
How to get the selected rows or their data in a backing bean?
Or would it be better to do it with <h:selectManyCheckbox>?
Your best bet is to bind the <h:selectBooleanCheckbox> value with a Map<Item, Boolean> property where Item represents the object behind the corresponding row.
<h:dataTable value="#{bean.items}" var="item">
<h:column>
<h:selectBooleanCheckbox value="#{bean.checked[item]}" />
</h:column>
...
</h:dataTable>
<h:commandButton value="submit" action="#{bean.submit}" />
public class Bean {
private Map<Item, Boolean> checked = new HashMap<Item, Boolean>();
private List<Item> items;
public void submit() {
List<Item> selectedItems = checked.entrySet().stream()
.filter(Entry::getValue)
.map(Entry::getKey)
.collect(Collectors.toList());
checked.clear(); // If necessary.
// Now do your thing with selectedItems.
}
// ...
}
You see, the map is automatically filled with all table items as key and the checkbox value is automatically set as map value associated with the item as key.
This only requires that the Item#equals() and Item#hashCode() is properly implemented as per their contracts.
If you can't guarantee that, then you'd better use a Map<RowId, Boolean> instead where RowId represents the type of the row identifier. Let's take an example that you've a Item object whose identifier property id is a Long:
public class Item {
private Long id;
// ...
}
<h:dataTable value="#{bean.items}" var="item">
<h:column>
<h:selectBooleanCheckbox value="#{bean.checked[item.id]}" />
</h:column>
...
</h:dataTable>
<h:commandButton value="submit" action="#{bean.submit}" />
public class Bean {
private Map<Long, Boolean> checked = new HashMap<Long, Boolean>();
private List<Item> items;
public void submit() {
List<Item> selectedItems = items.stream()
.filter(item -> checked.get(item.getId()))
.collect(Collectors.toList());
checked.clear(); // If necessary.
// Now do your thing with selectedItems.
}
// ...
}
In the following example I am using checkboxes to select two or more products to allow the user to compare product specifications on a new web page using JSF 2.0.
It took me a good while to find the following problem (totally obvious now of course) so thought it worth a mention for those trying to use pagination with BalusC's code above (nice answer BalusC, much simpler than I ever imagined it would be).
If you are using pagination you will get nullpointers at the line:
if (checked.get(item.getId()))
-in BalusC's code above.
This is because only displayed check boxes are added to the Map (doh; slap forehead).
For those products whose check boxes are never displayed, due to pagination, this line will result in a null pointer error and a check needs to be added to ignore these null pointers (assuming that all check boxes are unchecked on page load). In order for the user to tick a check box then they need to display the pagination page so all works well there after.
If some or all of the check boxes are required to be ticked on first page load then this will be of no help to you...you will have to manually add those to the Map in order for them to be displayed correctly on page load.
Note: because I am using a JPA 'Entity class from database' object I also needed to use #Transient for the id in my ProductTbl Entity Class as all variables are considered columns in the database by JPA, by default, unless prefixed with #Transient. Also I am using a second link to reset the check boxes, which calls clearSelections(), and my 'submit' is a link calling compareSelectedProducts() rather than a Submit button.
The full code is as follows:
In the 'ProductTbl' Entity class derived from the database :
#Transient
private Long id;
public Long getId()
{
return id;
}
public void setId(Long id)
{
this.id = id;
}
In the backing bean 'ProductSelection':
private Map<Long, Boolean> checked = new HashMap<Long, Boolean>();
private String errorMessage = "";
// List of all products.
private List<ProductTbl> products;
// List of products to compare.
private List<ProductTbl> compareProducts;
// Setters and getters for above...
public String compareSelectedProducts()
{
// Reset selected products store.
compareProducts = new ArrayList();
for (ProductTbl item: products)
{
// If there is a checkbox mapping for the current product then...
if(checked.get(item.getId()) != null)
{
// If checkbox is ticked then...
if (checked.get(item.getId()))
{
// Add product to list of products to be compared.
compareProducts.add(item);
}
}
}
if(compareProducts.isEmpty())
{
// Error message that is displayed in the 'ErrorPage.xhtml' file.
errorMessage = "No Products selected to compare specifications. Select two or more products by ticking the check box in the second column 'Cmpr'";
return "process_ErrorPage";
}
// Rest of code to get product specification data ready to be displayed.
return "process_CompareSelected";
}
public String clearSelections()
{
// Untick all checkbox selections.
checked.clear();
return "process_MainSearchResult";
}
In the JSF Web page 'MainSearchResult.xhtml':
<h:commandLink action="#{productSelection.compareSelectedProducts()}" value="Cmpr Specification Comparison Table" />
<h:commandLink action="#{productSelection.clearSelections()}" value="Clear Selected" />
<h:dataTable value="#{productSelection.products}" rows="#{productSelection.numberRowsToDisplay}" first="#{productSelection.rowStart}" var="item" headerClass="table-header" >
<h:column>
<f:facet name="header">
<h:outputText style="font-size:12px" value="Cmpr" />
</f:facet>
<div style="text-align:center;" >
<h:selectBooleanCheckbox value="#{productSelection.checked[item.id]}" />
</div>
</h:column>
</h:dataTable>
In the 'faces-config.xml' file:
<navigation-rule>
<navigation-case>
<from-outcome>process_MainSearchResult</from-outcome>
<to-view-id>/MainSearchResult.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
<navigation-rule>
<navigation-case>
<from-outcome>process_CompareSelected</from-outcome>
<to-view-id>/CompareSelected.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
<navigation-rule>
<navigation-case>
<from-outcome>process_ErrorPage</from-outcome>
<to-view-id>/ErrorPage.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
One way to send in a parameter via <h:selectBooleanCheckbox> is to send it in via the title of the Checkbox. In the ValueChangeListener, you can get it from the component using a getAttributes().get("title"). This helps in cases where you want to send an id value as a parameter (as opposed to the selected row index).

JSF dataTable query

can some one help me with the following JSF dataTable? here I am getting data from database table and I used dataTable binding, but I don't know why it displays the rows 3 times in the screen, but if I remove the binding then it displays only one time.
<h:dataTable binding="#{threadController.dataTable}" var="category" value="#{threadController.queryCategories}" border="1" cellpadding="2" cellspacing="0">
<h:column>
<img src="../../images/directory.jpg" alt="Forum Icon" />
</h:column>
<h:column>
<h:form>
<h:commandLink value="#{category.cname}" action="#{threadController.categoryDateItem}" />
</h:form>
</h:column>
// defined globally
private HtmlDataTable dataTable;
private HtmlInputHidden dataItemId = new HtmlInputHidden();
public String categoryDateItem() {
category = (Category) dataTable.getRowData();
System.out.println("category action by select: "+category.getCname());
dataItemId.setValue(category.getId());
return "editItem"; // Navigation case.
}
#SuppressWarnings("unchecked")
public ArrayList<Category> getQueryCategories(){
return (ArrayList<Category>)HibernateUtil.getSession().createCriteria(Category.class).list();
}
output:
myText myText myText
The binding expression to bind this component to the bean value="#{threadController.queryCategories}".So value attribute is sufficient to retrieve data using dataTable tag.
Binding = component backing bean
Value= data model backing bean
So, you have the Value and Binding set correctly (at least, as far as I can see). Your problem may result from the fact that you're not caching the list you're getting back from the database in getQueryCategories().
You really can't have any idea how often getQueryCategories() will be called in the process of rendering that dataTable, so it's a good idea to do something like this:
// Somewhere near the top of the handler class.. create a cache variable:
private ArrayList<Category> qCategories = null;
// now for getQueryCategories
public ArrayList<Category> getQueryCategories(){
if ( qCategories == null ) { // qCategories should be a member of the handler
qCategories = (ArrayList<Category>)HibernateUtil.getSession().createCriteria(Category.class).list();
}
return qCategories
}
This kind of cache-ing is very helpful in JSF apps with handlers that are session of even request scoped, as again you can't really know how often JSF will evaluate your "value" expression in the dataTable.

Resources