Tuesday, June 05, 2007

Changing Wicket's default form processing behaviour

If you have any validated fields in your Wicket form (e.g. required fields), the default behaviour upon a validation error is to not update the form component models. In my case, I had checkboxes on the form and I needed to have them prefilled with their state even after a validation error. The solution was to provide my own Form subclass and change the default form processing a bit. This is mentioned briefly in the javadoc for the Form class, but I think it's worth spelling out with an example. The key is the process() method of the MyForm inner class below:

HTML


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:wicket="http://wicket.sourceforge.net/" xml:lang="en"
lang="en">
<body>
<span wicket:id="feedback" id="feedback">feedback goes
here</span>

<form id="jsform" wicket:id="jsform">
<input type="text" wicket:id="textfield" />

<br />

<span wicket:id="group">
<input type="checkbox" wicket:id="groupselector" />

Select All

<table
style="border: 1px solid black;border-collapse: collapse;margin: 10px;">

<tbody>
<tr wicket:id="datatable">
<td
style="border: 1px solid black;border-collapse: collapse;">

<input type="checkbox" wicket:id="selected" />
</td>

<td
style="border: 1px solid black;border-collapse: collapse;">

<span wicket:id="item">Item</span>
</td>
</tr>
</tbody>
</table>
</span>

<input type="submit" wicket:id="okbutton" />
</form>
</body>
</html>


Java


package wicket.quickstart;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import wicket.markup.html.WebPage;
import wicket.markup.html.basic.Label;
import wicket.markup.html.form.Button;
import wicket.markup.html.form.Check;
import wicket.markup.html.form.CheckGroup;
import wicket.markup.html.form.CheckGroupSelector;
import wicket.markup.html.form.Form;
import wicket.markup.html.form.TextField;
import wicket.markup.html.list.ListItem;
import wicket.markup.html.list.ListView;
import wicket.markup.html.panel.FeedbackPanel;
import wicket.model.CompoundPropertyModel;
import wicket.model.Model;
import wicket.model.PropertyModel;

/**
* @author jsinai
*/
public class TestCheckGroup extends WebPage
{
private List checkGroupModel = new ArrayList();
private List items = new ArrayList();
private String text;
private Form form;

public TestCheckGroup()
{
items.add(new RowBean(true, "hi"));
items.add(new RowBean(false, "there"));
for (RowBean rb : items)
{
if (rb.isSelected())
{
checkGroupModel.add(rb);
}
}
add(new FeedbackPanel("feedback"));
form = new MyForm("jsform");
form.add(new TextField("textfield", new PropertyModel(this, "text"))
.setRequired(true));
final CheckGroup checkGroup = new CheckGroup("group", checkGroupModel);
form.add(checkGroup);
checkGroup.add(new CheckGroupSelector("groupselector"));
checkGroup.add(new ListView("datatable", items)
{
@Override
protected void populateItem(ListItem item)
{
final RowBean row = (RowBean) item.getModelObject();
item.setModel(new CompoundPropertyModel(row));
item.add(new Check("selected", item.getModel()));
item.add(new Label("item", row.getItem().toString()));
}
});
Button ok = new Button("okbutton", new Model("ok"))
{
@Override
protected void onSubmit()
{
// Do your submit processing here
}
};
form.add(ok);
add(form);
}

public static class RowBean implements Serializable
{
private boolean selected;
private String item;

public RowBean(boolean selected, String item)
{
this(item);
this.selected = selected;
}

public RowBean(String item)
{
if (item == null)
{
throw new IllegalArgumentException(
"RowBean: item cannot be null");
}
this.item = item;
}

public String getItem()
{
return item;
}

public void setItem(String item)
{
this.item = item;
}

public boolean isSelected()
{
return selected;
}

public void setSelected(boolean selected)
{
this.selected = selected;
}

}

public List getCheckGroupModel()
{
return checkGroupModel;
}

public void setCheckGroupModel(List checkGroupModel)
{
this.checkGroupModel = checkGroupModel;
}

public List getItems()
{
return items;
}

public void setItems(List items)
{
this.items = items;
}

public String getText()
{
return text;
}

public void setText(String text)
{
this.text = text;
}

class MyForm extends Form
{

public MyForm(String id)
{
super(id);
}

/**
* Changed default form processing to enable fields to be refilled with
* default values even when an error occurred.
*/
@Override
public boolean process()
{
// run validation
validate();

// sinai: moved up
// before updating, call the interception method for clients
beforeUpdateFormComponentModels();

// sinai: moved up
// Update model using form data
updateFormComponentModels();

// If a validation error occurred
if (hasError())
{
// mark all children as invalid
markFormComponentsInvalid();

// let subclass handle error
onError();

// sinai: changed to true
// Form has an error
return true;
}
else
{
// mark all childeren as valid
markFormComponentsValid();

// Form has no error
return true;
}
}
}
}

2 comments:

Delli said...

Hi,

good thought, but still i cannot retain the the values which are incorrect, could please also explain the process() of this and process() Form.. there are some extra code in Form which is missing here.

thanks in advance!!

regards,
Delli

Julian Sinai said...

Delli, this technique still works for me. The key thing is that updateFormComponentModels() is called regardless of whether there is a validation error or not. But there is another technique you can use instead: FormComponent.getConvertedInput() does not rely on the model, it pulls the value out of the form component.