One of two events is lost

Hello,

There is a strange problem I’m facing. I have several forms on several tabs and a single save button for all. The functionality I’m trying to implement when one of the forms changes is:

  • show a ‘*’ in the corresponding tab’s name
  • set a style to show if the form has an error or not
  • enable a save button

The problem is that when I change a text field in a form and try directly (without moving the focus from the field) to select another tab that event is lost. The three operations mentioned above are performed (with the value change event) but the selected tab doesn’t change. Only when I click the second time on a new tab the tab gets selected. So every time I’m editing a text field and want to change the tab I have to click twice on that tab, or first move the focus somewhere else and then click the tab.

I made a small test case but the weird thing is that the test case works! I cannot reproduce the problem outside my project. Even more, running the project in debug mode also works, so I’m guessing there is a race condition somewhere and the tab click event gets lost or overridden by the text field change event. I tested that theory putting the thread to sleep for 500ms in the valueChange event of the text field and it worked - the tab was changed as expected.

In debug mode I found that the tab change gets treated precisely after exiting the synchronized block in com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java from lines 741 to 790. I don’t exactly know the internals of vaadin but I suppose there is a main application thread and multiple threads for each request which all must stay in sync. The browser cannot, of course, join the onChange event (text field) and the onClick event (tab click) in a single server request and somehow the click event gets lost. Am I correct?

I attached the test case so you can get an idea about what I’m trying to do. It will work but maybe someone can spot something. The only difference between the real project and the test case is the volume of forms, in rest they both do the same things: put the asterisk, set the style and enable the button.


import java.util.Arrays;

import com.vaadin.Application;
import com.vaadin.data.Item;
import com.vaadin.data.Property.ValueChangeEvent;
import com.vaadin.data.Property.ValueChangeListener;
import com.vaadin.data.util.BeanItem;
import com.vaadin.ui.Button;
import com.vaadin.ui.Component;
import com.vaadin.ui.DefaultFieldFactory;
import com.vaadin.ui.Field;
import com.vaadin.ui.Form;
import com.vaadin.ui.Label;
import com.vaadin.ui.Panel;
import com.vaadin.ui.TabSheet;
import com.vaadin.ui.TabSheet.SelectedTabChangeEvent;
import com.vaadin.ui.TabSheet.SelectedTabChangeListener;
import com.vaadin.ui.TabSheet.Tab;
import com.vaadin.ui.TextArea;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.Window;

@SuppressWarnings({ "javadoc", "serial" })
public class ModifiedTabBug extends Application implements ValueChangeListener,
    SelectedTabChangeListener {

  private Button save = new Button("save");

  private TabSheet tabSheet = new TabSheet();

  private Form form = new Form();

  @Override
  public void init() {
    final Window mainWindow = new Window("testing");
    mainWindow.getContent().setHeight("100%");

    Person person = new Person(); // a person POJO
    BeanItem<Person> personItem = new BeanItem<Person>(person);

    this.form.setItemDataSource(personItem);
    this.form.setFormFieldFactory(new PersonFieldFactory());
    this.form.setVisibleItemProperties(Arrays.asList(
         new String[] { "firstName", "lastName" }));
    VerticalLayout layout = new VerticalLayout();

    this.tabSheet.addTab(this.form, "testTab"); // a test tab
    for (int i = 0; i < 15; i++) {
      this.tabSheet.addTab(this.buildStuff(i), "tab" + i);// and some dummy tabs
    }

    this.tabSheet.addListener(this); // just for debug
    layout.addComponent(this.tabSheet);
    this.save.setEnabled(false);
    layout.addComponent(this.save);
    mainWindow.addComponent(layout);
    this.setMainWindow(mainWindow);
  }

  private VerticalLayout buildStuff(int i) {// put some content on each dummy tab
    VerticalLayout vl = new VerticalLayout();
    vl.addComponent(new Label("label " + i));
    vl.addComponent(new TextArea());
    Panel p = new Panel();
    p.addComponent(new Label("another label " + i));
    vl.addComponent(p);
    return vl;
  }

  // we need this to listen to text field changes
  private class PersonFieldFactory extends DefaultFieldFactory {

    public PersonFieldFactory() {
      //
    }

    @Override
    public Field createField(Item item, Object propertyId, Component uiContext) {
      Field f = super.createField(item, propertyId, uiContext);
      f.addListener(ModifiedTabBug.this);
      return f;
    }
  }

  public class Person {// a bean for the form

    private String firstName = "";

    private String lastName = "";

    public String getFirstName() {
      return this.firstName;
    }

    public void setFirstName(String firstName) {
      this.firstName = firstName;
    }

    public String getLastName() {
      return this.lastName;
    }

    public void setLastName(String lastName) {
      this.lastName = lastName;
    }

  }

  // problems appear here. in my project only the first event is treated
  public void valueChange(ValueChangeEvent event) {
    this.save.setEnabled(true);
    Tab tab = this.tabSheet.getTab(this.form);
    if (tab != null) {
      tab.setCaption("* testTab");
      tab.setStyleName("modified");
    }
    /*
     * try { //this prevents the event below to disappear Thread.sleep(500); } catch
     * (InterruptedException e) { e.printStackTrace(); }
     */
  }

  public void selectedTabChange(SelectedTabChangeEvent event) {
    System.out.println("this event does not occur in my project, only on the second click");
  }
}

I found that setting the field as immediate causes the test case also not to work.
If you modify the above example with this snippet the problem finally manifests.


public Field createField(Item item, Object propertyId, Component uiContext) {
      TextField f = new TextField();//.createField(item, propertyId, uiContext);
      f.setImmediate(true);
      f.addListener(ModifiedTabBug.this);
      return f;
    }

The problem is that I need setImmediate to be true or else no validation is performed when the field loses the focus. Ideas?

created ticket
10985