Table: Veto row change

Hello forum,

how do I implement a Master - Detail Dialog which asks the user for confirmation, if there are unsaved modifications in the form?

Imagine the following scenario:
The user selects an item in the table, it gets loaded into the form.
The item is edited through the form, but not committed.
Now the user selects a different item in the table.

To prevent loosing the modifications made to the first item, the application displays a confirmation dialog:
“Would you like to save your changes? (YES, NO, CANCEL)”

YES => easy, save and load the 2nd item into the form
NO => easy, do not save and load the 2nd item into the form
CANCEL => not that easy

  • the form part: easy, do not save and keep the 1st item in the form
  • the list part: not that easy (?) veto the selection/value change

Now how do I veto the selection change?
Currently, the confirmation dialog is displayed by a ValueChangeListener (which may not be the best idea). Now I’d like to throw a VetoException or something like that which causes the table to keep the old value.

Any ideas?

Keep track of the item id of currently edited item and then when “canceling row change”, just select back the previous row.

We also had that idea, but this would cause two value changed events which are not necessary. I’d prefer a solution which would not involve the execution of ValueChangeListeners which are not under my control, because I’d like to avoid the (possible) side effects. Selecting the old item id is only possible if all (other) value change listeners are “revertible”.

My Desktop applications typically implement this scenario:

  • User selects a row
  • System detects modification on the current row, so it
    keeps the old row selected
    and asks for confirmation
  • when the user confirms using YES/NO (as described above), the newly selected row will be selected; in case of YES that will happen
    after
    saving the changes
  • when the user confirms using CANCEL nothing will happen (Cancel causes a VetoException)

Is it technically possible to implement a “vetoable value change”? Or is there a (possibly html or gwt) limitation?

[s]
Everything’s possible with unlimited time to hack :slight_smile:

Hacking (copy-pasting the code) VScrollTable you could handle clicking and keyboard navigation to disallow selecting a row so that rowkey of selected row is not sent in UIDL to server. Then you could select the value from server side after approval and allow normal selection again. Very horrible idea.
[/s]

Edit: See John’s answer below

Well actually you can do it purely on the server side as well. Took a coffee break and wrote this, it should get you started:



public class TestApplication extends Application {
	@Override
	public void init() {
		Window mainWindow = new Window("Test Application");
		Label label = new Label("Hello Vaadin user");
		mainWindow.addComponent(label);
		setMainWindow(mainWindow);

		VetoTable table = new VetoTable();
		table.addContainerProperty("Column1", String.class, "cell");
		table.addContainerProperty("Column2", String.class, "cell");
		for (int i = 0; i < 50; i++) {
			table.addItem();
		}

		table.addListener(new Property.ValueChangeListener() {
			public void valueChange(ValueChangeEvent event) {
				getMainWindow().showNotification("New selection!");
			}
		});

		mainWindow.addComponent(table);
	}

	/**
	 * Confirmation window
	 */
	private class VetoTableConfirmation extends Window implements ClickListener {

		private Button yes = new Button("Yes", this);
		private Button no = new Button("No", this);
		private boolean confirmed = false;

		public VetoTableConfirmation() {
			setModal(true);
			setResizable(false);
			setClosable(false);
			addComponent(yes);
			addComponent(no);
		}

		public void buttonClick(ClickEvent event) {
			if (event.getButton() == no) {
				confirmed = false;
			} else if (event.getButton() == yes) {
				confirmed = true;
			}
			close();
		}

		public boolean confirmed() {
			return confirmed;
		}
	}

	/**
	 * Vetoable table
	 */
	private class VetoTable extends Table {

		public VetoTable() {
			setSelectable(true);
			setImmediate(true);
		}

		@Override
		public void changeVariables(final Object source,
				final Map<String, Object> variables) {
			if (variables.containsKey("selected")) {
				final VetoTableConfirmation confirm = new VetoTableConfirmation();
				confirm.addListener(new Window.CloseListener() {
					public void windowClose(CloseEvent e) {
						if (confirm.confirmed()) {
							VetoTable.super.changeVariables(source, variables);
						} else {
							requestRepaint();
							// Throw VetoException if you want etc..
						}
					}
				});
				getMainWindow().addWindow(confirm);
				return;
			}
			super.changeVariables(source, variables);
		}
	}
}

thanks a lot, that really helps!

It’s me again …

I’ve made it 90% using a modified version of the example provided by John Ahlroos (thanks again!). There is just one thing I cannot figure out:

If the user confirms that he just forgot to save by himself and confirms the “would you like to save your changes” question, my application sends the modified object to the business layer which in turn persists the changes and returns the persisted version.

Now I need to update the returned (persisted) version of the bean in my beanItemContainer. This essentially boils down to

final BeanItemContainer<User> beanItemContainer = (BeanItemContainer<User>)table.getContainerDataSource();
final User item = (User)table.getValue();
final int itemIdx = beanItemContainer.indexOfId(item);
//real update: final User updatedItem = dao.updateItem(item);
//simulated update:
final User updatedItem = new User(item.getId());
updatedItem.setShortName(item.getShortName());
beanItemContainer.removeItem(item);
beanItemContainer.addItemAt(itemIdx, updatedItem);
VetoTable.super.changeVariables(source, variables);

This code is executed between line 75 and 76 in John Ahlroos’ example.

As a result, the table selection(i.e. table.getValue()) does NOT change to the newly selected line although super.changeVariables has been executed. It seems that “addItemAt” steals the selection back to the added item (even though super.changeVariables is executed after addItemAt), but after hours of debugging, I did not see when and where …

Could someone please give me a hint?

One more remark:

beanItemContainer.removeItem(item);
beanItemContainer.addItemAt(itemIdx, updatedItem);

Is a workaround required to “update” an existing row in the table. Without remove, AbstractInMemoryContainer#internalAddAt would NOT update the item:

// Make sure that the item has not been added previously
if (getAllItemIds().contains(itemId)) {
    return null;
}

So if I’d just call addItemAt, the bean item container would keep the old version of the item, since item.equals(updatedItem) is true.

I would prefer having an “update” method in the container, but I did not see one until now.

No idea about the previous point? Do I need to be more precise?

Thanks

Similar scenario but with Tree instead. Resolved by:

... //tree selectable only by code tree.setSelectable(false); tree.addItemClickListener(treeItemLister); .. then in the itemClickListener:

public void itemClick(ItemClickEvent event) {
   if (validateAndCommitLogic){
       //select new item only if current is ok
       tree.select(event.getItemId());
   }
   else{
       ...
   }