Displaying Data Using DataTable in Apache Wicket

April 30, 2011

Java

«»

Making cells clickable

A common requirement, when presenting tabular data, is to put links into the cells so that the user can interact with the rows in the table. In this recipe, we will create a column that, instead of simply displaying a property of the row object, will allow the user to click the property. When we are done, we will have a table where one column consists of links:

 

Getting ready

To get started see the Getting Ready section of the fi rst recipe in this chapter.

How to do it…

  1. Implement a column that will allow cells to be clicked:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    
    ClickablePropertyColumn$LinkPanel.html
    	<wicket:panel>
    		<a wicket:id="link"><span wicket:id="label"></span></a>
    	</wicket:panel>
    	ClickablePropertyColumn.java
    	public abstract class ClickablePropertyColumn<T> extends
    	AbstractColumn<T> {
    		private final String property;
    		public ClickablePropertyColumn(IModel<String> displayModel,
    			String property) {
    		this(displayModel, property, null);
    	}
    	public ClickablePropertyColumn(IModel<String> displayModel,
    	String property, String sort) {
    		super(displayModel, sort);
    		this.property = property;
    	}
    	public void populateItem(Item<ICellPopulator<T>> cellItem,
    	String componentId, IModel<T> rowModel) {
    		cellItem.add(new LinkPanel(componentId, rowModel,
    		new PropertyModel<Object>(rowModel, property)));
    	}
    	protected abstract void onClick(IModel<T> clicked);
    	private class LinkPanel extends Panel {
    		public LinkPanel(String id, IModel<T> rowModel, IModel<?>
    		labelModel) {
    			super(id);
    			Link<T> link = new Link<T>("link", rowModel) {
    				@Override
    				public void onClick() {
    					ClickablePropertyColumn.this.onClick(getModel());
    				}
    			};
    			add(link);
    			link.add(new Label("label", labelModel));
    		}
    	}
  2. Replace the standard name column with the clickable one:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    HomePage.java
    	List<IColumn<Contact>> columns = new
    	ArrayList<IColumn<Contact>>();
    		columns.add(new ClickablePropertyColumn<Contact>(Model.
    			of("Name"),
    				"name") {
    			@Override
    			protected void onClick(IModel<Contact> clicked) {
    				info("You clicked: " + clicked.getObject().getName());
    			}
    		});
    		columns.add(new PropertyColumn<Contact>(Model.of("Email"),
    			"email"));
    		columns.add(new PropertyColumn<Contact>(Model.of("Phone"),
    			"phone"));

How it works…

What we want to do is to wrap the property string that is used to populate the cell with an anchor tag and react to the click on the anchor tag. We are going to achieve this by implementing a custom DataTable column. We begin by extending AbstractColumn, which is the base class for most column implementations:

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class ClickablePropertyColumn<T> extends
	AbstractColumn<T> {
		private final String property;
		public ClickablePropertyColumn(IModel<String> displayModel, String
		property) {
			super(displayModel, null);
			this.property=property;
		}
		public void populateItem(Item<ICellPopulator<T>> cellItem,
		String componentId, IModel<T> rowModel) {
		}
	}

Now it is time to populate each cell inside the populateItem() method. We want to populate the cell with markup that looks like this:

1
<a href="..."><span>property value</span></a>

To accomplish this we will create a panel that contains the anchor and the span, and populate the cell with this panel. Because this panel will only really be useful inside our custom column we will create it as an inner class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ClickablePropertyColumn$LinkPanel.html
		<wicket:panel>
			<a wicket:id="link"><span wicket:id="label"></span></a>
		</wicket:panel>
	ClickablePropertyColumn.java
		public abstract class ClickablePropertyColumn<T> extends
		AbstractColumn<T> {
			protected abstract void onClick(IModel<T> clicked);
			private class LinkPanel extends Panel {
				public LinkPanel(String id, IModel<T> rowModel, IModel<?>
				labelModel) {
					super(id);
					Link<T> link = new Link<T>("link", rowModel) {
					@Override
					public void onClick() {
					}
				};
				add(link);
				link.add(new Label("label", labelModel));
			}
		}
 
		The markup fi le for the panel is named: ClickablePropertyColumn$Li
		nkPanel.html, this is because ClickablePropertyColumn$LinkPa
		nel is the qualifi ed name of the LinkPanel class. This can be observed by
		printing out the value of LinkPanel.class.getName()

The markup file for the panel is named: ClickablePropertyColumn$LinkPanel.html, this is because ClickablePropertyColumn$LinkPanel is the qualified name of the LinkPanel class. This can be observed by printing out the value of LinkPanel.class.getName()

Now that we have the panel, let’s populate the cell with it:

1
2
3
4
5
6
7
8
public abstract class ClickablePropertyColumn<T> extends
	AbstractColumn<T> {
		public void populateItem(Item<ICellPopulator<T>> cellItem,
		String componentId, IModel<T> rowModel) {
			cellItem.add(new LinkPanel(componentId, rowModel,
			new PropertyModel<Object>(rowModel, property)));
		}
	}

Our custom column is almost functional. All that is left is to forward the click event from the Link component inside the LinkPanel to the column so we can react to it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class ClickablePropertyColumn<T> extends
	AbstractColumn<T> {
		protected abstract void onClick(IModel<T> clicked);
		private class LinkPanel extends Panel {
			public LinkPanel(String id, IModel<T> rowModel, IModel<?>
			labelModel) {
				super(id);
				Link<T> link = new Link<T>("link", rowModel) {
					@Override
					public void onClick() {
						ClickablePropertyColumn.this.onClick(getModel());
					}
				};
			}
		}
 
		Notice that we pass in the rowModel into the onClick() method of the
		column; this is so that the user knows which row was clicked.

Notice that we pass in the rowModel into the onClick() method of the column; this is so that the user knows which row was clicked.

With our custom column now fully functional let’s see how we can use it to display the name of the contact that was clicked:

1
2
3
4
5
6
7
8
9
List<IColumn<Contact>> columns = new ArrayList<IColumn<Contact>>();
		columns.add(new ClickablePropertyColumn<Contact>(Model.
		of("Name"),
		"name") {
		@Override
		protected void onClick(IModel<Contact> clicked) {
			info("You clicked: " + clicked.getObject().getName());
		}
	});

Making rows selectable with checkboxes

A common requirement, when working with tables, is to allow the user to select one or more rows by clicking on checkboxes located in a column. In this recipe, we will build such a column:

 

Getting ready

Let’s get started by creating the page that lists contacts without any selectable rows.

Create the Contact bean:

1
2
3
4
5
Contact.java
	public class Contact implements Serializable {
		public String name, email, phone;
		// getters, setters, constructors
	}

Create the page to display the list of contacts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
HomePage.html
	<html>
		<body>
			<div wicket:id="feedback"></div>
				<table wicket:id="contacts" class="contacts"></table>
		</body>
	</html>
 
		HomePage.java
	public class HomePage extends WebPage {
		private static List<Contact> contacts = Arrays.asList(new Contact[]
	{
		new Contact("Homer Simpson", "homer@fox.com", "555-1211"),
		new Contact("Charles Montgomery Burns", "cmb@fox.com", "555-
			5322"),
		new Contact("Ned Flanders", "green@fox.com", "555-9732") });
		private Set<Contact> selected = new HashSet<Contact>();
		public HomePage(final PageParameters parameters) {
			add(new FeedbackPanel("feedback"));
			List<IColumn<Contact>> columns = new
			ArrayList<IColumn<Contact>>();
			columns.add(new PropertyColumn<Contact>(Model.of("Name"),
				"name"));
			columns.add(new PropertyColumn<Contact, String>(Model.
				of("Email"), "email"));
			columns.add(new PropertyColumn<Contact>(Model.of("Phone"),
				"phone"));
			DefaultDataTable<Contact> table = new DefaultDataTable<Contact>(
				"contacts", columns, new ContactsProvider(), 10);
			add(table);
		}
		private static class ContactsProvider extends
		SortableDataProvider<Contact> {
			public Iterator<? extends Contact> iterator(int first, int
			count) {
				return contacts.subList(first,
				Math.min(first + count, contacts.size())).iterator();
			}
			public int size() {
				return contacts.size();
			}
			public IModel<Contact> model(Contact object) {
				return Model.of(object);
			}
		}
	}

How to do it…

  1. Implement a custom column that will contain the checkboxes:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    
    CheckBoxColumn$CheckPanel.html
    	<wicket:panel>
    		<input wicket:id="check" type="checkbox"/>
    	</wicket:panel>
    	CheckBoxColumn.java
    	public abstract class CheckBoxColumn<T> extends AbstractColumn<T>
    	{
    		public CheckBoxColumn(IModel<String> displayModel) {
    			super(displayModel);
    		}
    		public void populateItem(Item<ICellPopulator<T>> cellItem,
    		String componentId, IModel<T> rowModel) {
    			cellItem.add(new CheckPanel(componentId,
    			newCheckBoxModel(rowModel)));
    		}
    		protected CheckBox newCheckBox(String id, IModel<Boolean>
    		checkModel) {
    			return new CheckBox("check", checkModel);
    		}
    		protected abstract IModel<Boolean> newCheckBoxModel(IModel<T>
    		rowModel);
    		private class CheckPanel extends Panel {
    			public CheckPanel(String id, IModel<Boolean> checkModel) {
    				super(id);
    				add(newCheckBox("check", checkModel));
    			}
    		}
    	}
  2. Add the custom column to the DataTable:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    HomePage.java
          List<IColumn<Contact>> columns = new
                          ArrayList<IColumn<Contact>>();
          columns.add(new CheckBoxColumn<Contact>(Model.of("")) {
                @Override
                 protected IModel&lt;Boolean&gt; newCheckBoxModel(
                    final IModel&lt;Contact&gt; rowModel) {
                       return new AbstractCheckBoxModel() {
                           @Override
                           public void unselect() {
                                 selected.remove(rowModel.getObject());
                           }
                           @Override
                           public void select() {
                                selected.add(rowModel.getObject());
                           }
                           @Override
                           public boolean isSelected() {
                               return selected.contains(rowModel.getObject());
                           }
                           @Override
                           public void detach() {
                               rowModel.detach();
                           }
                       };
                    }
                });
                columns.add(new PropertyColumn&lt;Contact&gt;(Model.of("Name"),
                    "name"));
  3. Put the DataTable into a form:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    HomePage.html
    	<body>
    		<div wicket:id="feedback"></div>
    		<form wicket:id="form">
    			<table wicket:id="contacts" class="contacts"></table>
    			<input type="submit" value="Submit"/>
    		</form>
    	</body>
     
    	HomePage.java
    	Form<?> form = new Form<Void>("form") {
    		@Override
    		protected void onSubmit() {
    			for (Contact contact : selected) {
    				info("Selected " + contact.getName());
    			}
    		}
    	};
    	add(form);
    	form.add(new DefaultDataTable<Contact>("contacts", columns,
    		new ContactsProvider(), 10));

How it works…

The first thing we have to do is to create a custom column for our table that will contain CheckBoxes. We start by subclassing AbstractColumn, which is a common base class for DataTable columns:

1
2
3
4
5
6
7
8
public abstract class CheckBoxColumn<T> extends AbstractColumn<T> {
		public CheckBoxColumn(IModel<String> displayModel) {
			super(displayModel);
		}
		public void populateItem(Item<ICellPopulator<T>> cellItem,
		String componentId, IModel<T> rowModel) {
		}
	}

Next, we create a panel that will contain the CheckBox which will be inserted into the cell. We have to create a panel because whatever component we use to populate the cell will be a tached to a span tag, and we cannot attach a CheckBox directly to a span. So, we will put the CheckBox into a panel and attach that to the span instead, which is perfectly valid.

1
2
3
4
5
6
When allowing the user to populate predefi ned placeholders, it is common to
	use a <div> or a <span> as the markup tag and allow the user to create a
	panel to populate the placeholder. Using a <span> or a <div> allows the
	most fl exibility, and the user may always remove it from generated markup by
	calling setRenderBodyOnly(true) on the instance of the panel they
	attach to the placeholder tag.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CheckBoxColumn$CheckColumn.html
	<wicket:panel>
		<input wicket:id="check" type="checkbox"/>
	</wicket:panel>
 
	CheckBoxColumn.java
		public abstract class CheckBoxColumn<T> extends AbstractColumn<T> {
			protected abstract IModel<Boolean> newCheckBoxModel(IModel<T>
			rowModel);
			protected CheckBox newCheckBox(String id, IModel<Boolean>
			checkModel) {
				return new CheckBox("check", checkModel);
			}
			private class CheckPanel extends Panel {
				public CheckPanel(String id, IModel<Boolean> checkModel) {
					super(id);
					add(newCheckBox("check", checkModel));
				}
			}
		}

Notice that we delegate the creation of the checkbox to the newCheckBox() method, and doing so will allow the user to override the creation of the CheckBox and either replace it with a custom instance or modify the instance created by the default implementation.

The CheckBox we create is set up with a model that the user will have to specify by
implementing the abstract newCheckBoxModel() method . This will allow us total control over how we keep track of which rows are selected.

Next, we wire in the panel into the cell:

1
2
3
4
5
6
7
public abstract class CheckBoxColumn<T> extends AbstractColumn<T> {
		public void populateItem(Item<ICellPopulator<T>> cellItem,
		String componentId, IModel<T> rowModel) {
			cellItem.add(new CheckPanel(componentId,
			newCheckBoxModel(rowModel)));
		}
	}

The CheckBox column is now complete. Let’s see how we can use it to keep track of the selected contacts. If we look at step 1 where we created the page we will notice a set field:

1
2
HomePage.java
		private Set<Contact> selected = new HashSet<Contact>();

We will use this set to keep track of the contacts that are selected. If we look at the
CheckBoxColumn’s newCheckBoxModel() method we will notice that it returns a model of type Boolean; this is because the CheckBox component only works with this model. When the model contains true, the check box is selected, and when it contains false the check box is unselected. In order to keep track of which contacts are selected we will somehow have to map a Boolean of each contact to our Set. Wicket provides a helper model that makes implementing this mapping easier called an AbstractCheckBoxModel , and this model requires us to implement three abstract methods:

1
2
3
4
5
6
public abstract class AbstractCheckBoxModel implements IModel
	{
		public abstract boolean isSelected();
		public abstract void select();
		public abstract void unselect();
	}

When the CheckBox renders it will call getObject() on the model, which will pass the call onto isSelected() . When the CheckBox is submitted, AbstractCheckBoxModel will either call select() or unselect() based on whether or not the CheckBox was selected.
Let’s see how we can use this to implement a column to track which contacts are selected:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
HomePage.java
	columns.add(new CheckBoxColumn<Contact>(Model.of("")) {
		@Override
		protected IModel<Boolean> newCheckBoxModel(
		final IModel<Contact> rowModel) {
			return new AbstractCheckBoxModel() {
				@Override
				public boolean isSelected() {
					return selected.contains(rowModel.getObject());
				}
				@Override
				public void unselect() {
					selected.remove(rowModel.getObject());
				}
				@Override
				public void select() {
					selected.add(rowModel.getObject());
				}
				@Override
				public void detach() {
					rowModel.detach();
				}
			};
		}
	});

Notice that we chain the detach call on our anonymous implementation of AbstractCheckBoxModel to rowModel.detach() because we access it directly. This is an important practice when creating models that use other models because it ensures all models in the chain are detached at the end of the request.

Now, when the table is submitted, all selected contacts will be placed into the selected Set, but before we can submit the check boxes inside the table we need to make sure they are in a form. What we do is modify our page and put the DataTable inside a form we create:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
HomePage.html
 
	<body>
		<div wicket:id="feedback"></div>
		<form wicket:id="form">
			<table wicket:id="contacts" class="contacts"></table>
			<input type="submit" value="Submit"/>
		</form>
	</body>
 
	HomePage.java
 
	Form<?> form = new Form<Void>("form") {
		@Override
		protected void onSubmit() {
			for (Contact contact : selected) {
				info("Selected " + contact.getName());
			}
		}
	};
	add(form);
	form.add(new DefaultDataTable<Contact>("contacts", columns,
		new ContactsProvider(), 10));

There’s more…

In the next section, we will see how to add more usability to our checkbox column.

Adding select/deselect all checkbox

Let’s take a look at how to modify CheckBoxColumn to implement select/deselect all
checkboxes in the header of the column. As this will be a client-side behavior we will use jQuery to implement the necessary JavaScript.

The complete code listing for the new CheckBoxColumn follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
CheckBoxColumn.java
 
	public abstract class CheckBoxColumn<T> extends AbstractColumn<T> {
		private final String uuid = UUID.randomUUID().toString().
			replace("-", "");
		public CheckBoxColumn() {
			super(null);
		}
		public void populateItem(Item<ICellPopulator<T>> cellItem,
			String componentId, IModel<T> rowModel) {
			cellItem.add(new CheckPanel(componentId,
			newCheckBoxModel(rowModel)));
		}
		protected CheckBox newCheckBox(String id, IModel<Boolean>
			checkModel) {
			return new CheckBox("check", checkModel) {
				@Override
				protected void onComponentTag(ComponentTag tag) {
					super.onComponentTag(tag);
					tag.append("class", uuid, " ");
				}
			};
		}
		protected abstract IModel<Boolean> newCheckBoxModel(IModel<T>
			rowModel);
		@Override
		public Component getHeader(String componentId) {
			CheckPanel panel = new CheckPanel(componentId, new
			Model<Boolean>());
			panel.get("check").add(new AbstractBehavior() {
				@Override
				public void onComponentTag(Component component, ComponentTag
				tag) {
					tag.put("onclick", "var val=$(this).attr('checked'); $('."
					+ uuid
					+ "').each(function() { $(this).attr('checked',
					val); });");
				}
			});
			return panel;
		}
		private class CheckPanel extends Panel {
			public CheckPanel(String id, IModel<Boolean> checkModel) {
				super(id);
				add(newCheckBox("check", checkModel));
			}
		}
	}

The first change we make is to add a uuid fi eld to the column:

1
2
private final String uuid = UUID.randomUUID().toString().replace("-",
	"");

We will need this fi eld to uniquely identify check boxes generated by this column on the client side so we can select/deselect them all. We will do this by appending a unique CSS class to all of them:

1
2
3
4
5
6
7
8
9
10
protected CheckBox newCheckBox(String id, IModel checkModel)
	{
		return new CheckBox("check", checkModel) {
			@Override
			protected void onComponentTag(ComponentTag tag) {
				super.onComponentTag(tag);
				tag.append("class", uuid, " ");
			}
		};
	}

Lastly, we override the column’s header component and replace it with a checkbox which contains the onclick jQuery trigger that selects/deselects the checkboxes in the column:

1
2
3
4
5
6
7
8
9
10
11
12
13
private final String js="var val=$(this).attr('checked'); $('." + uuid
		+ "').each(function() { $(this).attr('checked', val); });";
		public Component getHeader(String componentId) {
			CheckPanel panel = new CheckPanel(componentId, new
			Model<Boolean>());
			panel.get("check").add(new AbstractBehavior() {
				public void onComponentTag(Component component, ComponentTag
				tag) {
				tag.put("onclick", js);
			}
		});
		return panel;
	}
email

«»

Comments

comments