Editable Grid backed by BeanItemContainer

Hi,

I have a table backed by a BeanItemContainer. I’ve added a custom TableFieldFactory to the table; so that when a field in the table is clicked; the field factory renders a form UI component that allows users to edit the data:

Table in “read-only” mode:

Table in “edit” mode:

What I’m struggling a bit with is how the actual changes to the underlying beans in the BeanItemContainer are supposed to be handled.

While the form fields for editing the underlying beans in the container render fine and are correctly populated with the bean properties; something weird happens when I edit the data.

First of all; there seems to be no way to really force a “commit” on the change: hitting enter wile a field has focus does nothing.

Secondly: if I edit a field (in the example; I changed the last name for “Dannielle Wester” to “Dannielle Westeros”) and then click in another field in another row; the row I was editing actually disappears from the table; and instead I get a empty row at the bottom of the table:

So, obviously, I’m doing something wrong; but I’m not sure what. Any ideas?

Here is the code for the container:

public class ExcelUserContainer extends BeanItemContainer<UserInfo> implements Serializable, UIComponent{

	protected final transient Logger LOGGER = Logger.getLogger(ExcelUserContainer.class);
	
	public ExcelUserContainer()
	{
		super(UserInfo.class);
		
	
	}
	
	

	public void init() throws InitializationException {
		try
		{
			bind();
		}
		catch(BindException exc)
		{
			throw new InitializationException("Caught a BindException while initializing", exc);
		}
		
	}

	public void bind() throws BindException {
		// TODO Auto-generated method stub
		
	}

	public void unbind() throws UnbindException {
		// TODO Auto-generated method stub
		
	}
	
	
}

Code for the table:

public class ExcelUserTable extends Table implements UIComponent {
	
	private static final Logger LOGGER = Logger.getLogger(ExcelUserTable.class);
	private static final String WIDTH="100%";
	private static final String HEIGHT="220px";
	
	protected List<UserField> userTableColumns;
	protected Object[] visibleColumns;

	protected void layout()
	{
		this.addStyleName(TPNTheme.TABLE_STRIPED);
		this.setWidth(WIDTH);
		this.setHeight(HEIGHT);
		this.setColumnHeaders(this.getUserTableColumnHeaders());
	}

	public void init() throws InitializationException {
		
		visibleColumns = new Object[userTableColumns.size()]
;
		
		for(int i=0; i<userTableColumns.size(); i++)
		{
			visibleColumns[i]
= userTableColumns.get(i).getPropertyName();
		}
		
		
		this.setVisibleColumns(visibleColumns);
		layout();
		this.setImmediate(true);
		this.setWriteThrough(true);
		try
		{
			bind();
		}
		catch(BindException exc)
		{
			throw new InitializationException("Caught a BindException while initializing", exc);
		}
	}

	public void bind() throws BindException {
		
		
		this.addListener(new ItemClickListener()
		{

			public void itemClick(ItemClickEvent event) {
				LOGGER.debug(event.getItemId()  + "." + event.getPropertyId() + " clicked!");
				LOGGER.debug("Property clicked : " + event.getPropertyId());
				if(event.getItemId()!= null)
				{	
					((UserFormFieldFactory)getTableFieldFactory()).setEditableTableRowId(event.getItemId());
					((UserFormFieldFactory)getTableFieldFactory()).setEditablePropertyId(event.getPropertyId());
					setEditable(true);
					
				}
				
			}
			
		});
		
	}

	public void unbind() throws UnbindException {
		// TODO Auto-generated method stub
		
	}
	
	public String[] getUserTableColumnHeaders()
	{
		List<String> headers = new ArrayList<String>();
		for(UserField field : userTableColumns)
		{
			headers.add(field.getCaption());
		}
		
		return headers.toArray(new String[headers.size()]
);
		
	}
	
	public List<UserField> getUserTableColumns() {
		return userTableColumns;
	}

	public void setUserTableColumns(List<UserField> userTableColumns) {
		this.userTableColumns = userTableColumns;
	}
	
	@Override
	public String formatPropertyValue(Object rowId, Object colId, Property property)
	{
		if(property.getType() == Account.class)
		{
			if(property.getValue()!= null)
			{
				return ((Account)property.getValue()).getAccountCode();
			}
			else
			{
				return "";
			}
		}
		else if(property.getType() == ServiceLocation.class)
		{
			if(property.getValue()!= null)
			{
				return ((ServiceLocation)property.getValue()).getName();
			}
			else
			{
				return "";
			}
		}
		else
		{
			return super.formatPropertyValue(rowId, colId, property);
		}
	}
	
	
	

}

Code for the Field factory:

public class UserFormFieldFactory implements FormFieldFactory, TableFieldFactory {
	
	protected static final Logger LOGGER = Logger.getLogger(UserFormFieldFactory.class);
	protected static final String FIELD_WIDTH="120px";
	protected Container accountContainer;
	protected Container locationContainer;
	protected Object editableTableRowId;
	protected Object editablePropertyId;
	
	
	
	public Field createField(Item item, Object propertyId, Component uiContext) {
		
		Field field = null;
		
		if(propertyId.equals(UserField.USER_NAME.getPropertyName()))
		{
			field = new TextField(UserField.USER_NAME.getCaption());
			((AbstractTextField)field).setNullRepresentation("");
			field.setWidth(FIELD_WIDTH);
			field.setRequired(true);
		}
		if(propertyId.equals(UserField.FIRST_NAME.getPropertyName()))
		{
			field = new TextField(UserField.FIRST_NAME.getCaption());
			((AbstractTextField)field).setNullRepresentation("");
			field.setWidth(FIELD_WIDTH);
			field.setRequired(true);
		}
		else if(propertyId.equals(UserField.LAST_NAME.getPropertyName()))
		{
			field = new TextField(UserField.LAST_NAME.getCaption());
			((AbstractTextField)field).setNullRepresentation("");
			field.setWidth(FIELD_WIDTH);
			field.setRequired(true);
		}
		else if(propertyId.equals(UserField.EMAIL.getPropertyName()))
		{
			field = new TextField(UserField.EMAIL.getCaption());
			((AbstractTextField)field).setNullRepresentation("");
			field.setWidth(FIELD_WIDTH);
			field.setRequired(true);
			field.addValidator( new EmailValidator("Not a valid email address."));
		}
		else if(propertyId.equals(UserField.ACCOUNT.getPropertyName()))
		{
			ComboBox accountField = new ComboBox(UserField.ACCOUNT.getCaption());
			accountField.setContainerDataSource(accountContainer);
			accountField.setItemCaptionPropertyId("accountCode");
			accountField.setMultiSelect(false);
			accountField.setNullSelectionAllowed(false);
			accountField.setNewItemsAllowed(false);
			accountField.setWidth(FIELD_WIDTH);
			accountField.setRequired(true);
			field=accountField;
		}
		else if(propertyId.equals(UserField.LOCATION.getPropertyName()))
		{
			ComboBox locationField = new ComboBox(UserField.LOCATION.getCaption());
			locationField.setContainerDataSource(locationContainer);
			locationField.setItemCaptionPropertyId("name");
			locationField.setMultiSelect(false);
			locationField.setNullSelectionAllowed(true);
			locationField.setNewItemsAllowed(false);
			locationField.setWidth(FIELD_WIDTH);
			locationField.setRequired(false);
			field=locationField;
		}
		else if(propertyId.equals(UserField.CREATED.getPropertyName()))
		{
			field = new DateField(UserField.CREATED.getCaption());
			field.setWidth(FIELD_WIDTH);
			field.setReadOnly(true);
			((DateField)field).setResolution(DateField.RESOLUTION_SEC);
		}
		else if(propertyId.equals(UserField.LAST_MODIFIED.getPropertyName()))
		{
			field = new DateField(UserField.LAST_MODIFIED.getCaption());
			field.setWidth(FIELD_WIDTH);
			field.setReadOnly(true);
			((DateField)field).setResolution(DateField.RESOLUTION_SEC);
		}
		
		
		return field;
	}

	public Container getAccountContainer() {
		return accountContainer;
	}

	public void setAccountContainer(Container accountContainer) {
		this.accountContainer = accountContainer;
	}

	public Container getLocationContainer() {
		return locationContainer;
	}

	public void setLocationContainer(Container locationContainer) {
		this.locationContainer = locationContainer;
	}

	public Field createField(Container container, Object itemId,
			Object propertyId, Component uiContext) {
		if(itemId.equals(editableTableRowId) && propertyId.equals(editablePropertyId))
		{
			Field field =  this.createField(container.getItem(itemId), propertyId, uiContext);
			field.setWriteThrough(true);
			field.setWidth(null);
			field.addListener(new Field.ValueChangeListener(){

				public void valueChange(ValueChangeEvent event) {
					System.out.println("Value changed to " + event.getProperty().getValue());
					
				}
				
				
			});
			return field;
		}
		else
		{
			return null;
		}
	}

	public Object getEditableTableRowId() {
		return editableTableRowId;
	}

	public void setEditableTableRowId(Object editableTableRowId) {
		this.editableTableRowId = editableTableRowId;
	}

	public Object getEditablePropertyId() {
		return editablePropertyId;
	}

	public void setEditablePropertyId(Object editablePropertyId) {
		this.editablePropertyId = editablePropertyId;
	}
	
	

}

I discovered the solution to my second problem. The reason the row was disappearing was because I had overwritten the equals() and hashCode() methods on the UserInfo Bean to be:


@Override
	public int hashCode() {
		return HashCodeBuilder.reflectionHashCode(this);
	}
	
	@Override
	public boolean equals(Object obj) {
		return EqualsBuilder.reflectionEquals(this, obj);
	}
	

This ensured that as values were changed on the User; the hashCode() and equals() methods were indicating this was a different object.

Apparently, BeanItemContainer doesn’t handle that all too well (which is actually mentioned in the API documentation: http://vaadin.com/api/com/vaadin/data/util/BeanItemContainer.html); so what was happening was that if you modified UserInfo instanceA; afterwards ExcelUserContainer.getBean(instanceA) would return null.

I do still have the other problem however. I’m now setting the fields in the table to Immediate and I’m also turning on writeThrough. So now when I hit enter after making a modification; the change takes effect immediately. I also added a field valueChangeListener on the field so that when the value is changed; it clears the current editableTableRowId and editablePropertyId from the FieldFactory. This should, theoretically, get the table to now go back returning non-editable fields; but this isn’t happening. A sequencing problem maybe?


public class UserFormFieldFactory implements FormFieldFactory, TableFieldFactory {
	
	protected static final Logger LOGGER = Logger.getLogger(UserFormFieldFactory.class);
	protected static final String FIELD_WIDTH="120px";
	protected Container accountContainer;
	protected Container locationContainer;
	protected Object editableTableRowId;
	protected Object editablePropertyId;
	
	
	
	public Field createField(Item item, Object propertyId, Component uiContext) {
		
		Field field = null;
		
		if(propertyId.equals(UserField.USER_NAME.getPropertyName()))
		{
			field = new TextField(UserField.USER_NAME.getCaption());
			((AbstractTextField)field).setNullRepresentation("");
			[b]
((AbstractTextField)field).setImmediate(true);
[/b]
			field.setWidth(FIELD_WIDTH);
			field.setRequired(true);
		}
		if(propertyId.equals(UserField.FIRST_NAME.getPropertyName()))
		{
			field = new TextField(UserField.FIRST_NAME.getCaption());
			((AbstractTextField)field).setNullRepresentation("");
			[b]
((AbstractTextField)field).setImmediate(true);
[/b]
			field.setWidth(FIELD_WIDTH);
			field.setRequired(true);
		}
		else if(propertyId.equals(UserField.LAST_NAME.getPropertyName()))
		{
			field = new TextField(UserField.LAST_NAME.getCaption());
			((AbstractTextField)field).setNullRepresentation("");
			[b]
((AbstractTextField)field).setImmediate(true);
[/b]
			field.setWidth(FIELD_WIDTH);
			field.setRequired(true);
		}
		else if(propertyId.equals(UserField.EMAIL.getPropertyName()))
		{
			field = new TextField(UserField.EMAIL.getCaption());
			((AbstractTextField)field).setNullRepresentation("");
			[b]
((AbstractTextField)field).setImmediate(true);
[/b]
			field.setWidth(FIELD_WIDTH);
			field.setRequired(true);
			field.addValidator( new EmailValidator("Not a valid email address."));
		}
		else if(propertyId.equals(UserField.ACCOUNT.getPropertyName()))
		{
			ComboBox accountField = new ComboBox(UserField.ACCOUNT.getCaption());
			[b]
((ComboBox)accountField).setImmediate(true);
[/b]
			accountField.setContainerDataSource(accountContainer);
			accountField.setItemCaptionPropertyId("accountCode");
			accountField.setMultiSelect(false);
			accountField.setNullSelectionAllowed(false);
			accountField.setNewItemsAllowed(false);
			accountField.setWidth(FIELD_WIDTH);
			accountField.setRequired(true);
			field=accountField;
		}
		else if(propertyId.equals(UserField.LOCATION.getPropertyName()))
		{
			ComboBox locationField = new ComboBox(UserField.LOCATION.getCaption());
			[b]
((ComboBox)locationField).setImmediate(true);
[/b]
			locationField.setContainerDataSource(locationContainer);
			locationField.setItemCaptionPropertyId("name");
			locationField.setMultiSelect(false);
			locationField.setNullSelectionAllowed(true);
			locationField.setNewItemsAllowed(false);
			locationField.setWidth(FIELD_WIDTH);
			locationField.setRequired(false);
			field=locationField;
		}
		else if(propertyId.equals(UserField.CREATED.getPropertyName()))
		{
			field = new DateField(UserField.CREATED.getCaption());
			field.setWidth(FIELD_WIDTH);
			field.setReadOnly(true);
			((DateField)field).setResolution(DateField.RESOLUTION_SEC);
		}
		else if(propertyId.equals(UserField.LAST_MODIFIED.getPropertyName()))
		{
			field = new DateField(UserField.LAST_MODIFIED.getCaption());
			field.setWidth(FIELD_WIDTH);
			field.setReadOnly(true);
			((DateField)field).setResolution(DateField.RESOLUTION_SEC);
		}
		
		
		return field;
	}

	public Container getAccountContainer() {
		return accountContainer;
	}

	public void setAccountContainer(Container accountContainer) {
		this.accountContainer = accountContainer;
	}

	public Container getLocationContainer() {
		return locationContainer;
	}

	public void setLocationContainer(Container locationContainer) {
		this.locationContainer = locationContainer;
	}

	public Field createField(Container container, Object itemId,
			Object propertyId, Component uiContext) {
		if(itemId.equals(editableTableRowId) && propertyId.equals(editablePropertyId))
		{
			Field field =  this.createField(container.getItem(itemId), propertyId, uiContext);
			[b]
field.setWriteThrough(true);
[/b]
			field.setWidth(null);
			[b]
field.addListener(new Field.ValueChangeListener(){

				public void valueChange(ValueChangeEvent event) {
					System.out.println("Value changed to " + event.getProperty().getValue());
					editableTableRowId = null;
					editablePropertyId = null; 
				}
[/b]
				
				
			});
			return field;
		}
		else
		{
			return null;
		}
	}

	public Object getEditableTableRowId() {
		return editableTableRowId;
	}

	public void setEditableTableRowId(Object editableTableRowId) {
		this.editableTableRowId = editableTableRowId;
	}

	public Object getEditablePropertyId() {
		return editablePropertyId;
	}

	public void setEditablePropertyId(Object editablePropertyId) {
		this.editablePropertyId = editablePropertyId;
	}
	
	

}

Did some more testing… the problem actually seems to be that after I hit enter in a field inside the table; the data is committed to the underlying datamodel (so that’s good); but the table itself isn’t redrawn (prompting no further calls to the custom field factory; and so the field that was just edited remains a text field). How do I trigger the table to be repainted?

Regards Sicco

Please could help me with this.
I have a table. It’s in editable mode. One column is represented by Comboboxes.
When I click in the header to sort or insert a new row in the table. The data selected in the combo in the previous rows disappear.
My problem is similar to the second problem you had, please could you provide the code of the classes UserInfo, UserField

Thanks for your help
Jorge