4

I have a vaadin grid with active, buffered editor. By default, the editor is opened when double clicking a row. Everything works fine, except when I double click the button I get an exception: (The exception doesn't point anywhere at my code)

java.lang.NullPointerException: Editor can't edit null
at java.base/java.util.Objects.requireNonNull(Objects.java:246)
at com.vaadin.ui.components.grid.EditorImpl.doEdit(EditorImpl.java:216)
at com.vaadin.ui.components.grid.EditorImpl$1.bind(EditorImpl.java:151)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:564)
at com.vaadin.server.ServerRpcManager.applyInvocation(ServerRpcManager.java:155)
at com.vaadin.server.ServerRpcManager.applyInvocation(ServerRpcManager.java:116)
at com.vaadin.server.communication.ServerRpcHandler.handleInvocation(ServerRpcHandler.java:445)
at com.vaadin.server.communication.ServerRpcHandler.handleInvocations(ServerRpcHandler.java:410)
at com.vaadin.server.communication.ServerRpcHandler.handleRpc(ServerRpcHandler.java:274)
at com.vaadin.server.communication.PushHandler.lambda$new$1(PushHandler.java:145)
at com.vaadin.server.communication.PushHandler.callWithUi(PushHandler.java:235)
at com.vaadin.server.communication.PushHandler.onMessage(PushHandler.java:520)
at com.vaadin.server.communication.PushAtmosphereHandler.onMessage(PushAtmosphereHandler.java:87)
at com.vaadin.server.communication.PushAtmosphereHandler.onRequest(PushAtmosphereHandler.java:77)
at org.atmosphere.cpr.AsynchronousProcessor.action(AsynchronousProcessor.java:223)
at org.atmosphere.cpr.AsynchronousProcessor.suspended(AsynchronousProcessor.java:115)
at org.atmosphere.container.Servlet30CometSupport.service(Servlet30CometSupport.java:67)
at org.atmosphere.cpr.AtmosphereFramework.doCometSupport(AtmosphereFramework.java:2284)
at org.atmosphere.websocket.DefaultWebSocketProcessor.dispatch(DefaultWebSocketProcessor.java:593)
at org.atmosphere.websocket.DefaultWebSocketProcessor$3.run(DefaultWebSocketProcessor.java:345)
at org.atmosphere.util.VoidExecutorService.execute(VoidExecutorService.java:101)
at org.atmosphere.websocket.DefaultWebSocketProcessor.dispatch(DefaultWebSocketProcessor.java:340)
at org.atmosphere.websocket.DefaultWebSocketProcessor.invokeWebSocketProtocol(DefaultWebSocketProcessor.java:447)
at org.atmosphere.container.JSR356Endpoint$3.onMessage(JSR356Endpoint.java:272)
at org.atmosphere.container.JSR356Endpoint$3.onMessage(JSR356Endpoint.java:269)
at org.apache.tomcat.websocket.WsFrameBase.sendMessageText(WsFrameBase.java:395)
at org.apache.tomcat.websocket.server.WsFrameServer.sendMessageText(WsFrameServer.java:119)
at org.apache.tomcat.websocket.WsFrameBase.processDataText(WsFrameBase.java:495)
at org.apache.tomcat.websocket.WsFrameBase.processData(WsFrameBase.java:294)
at org.apache.tomcat.websocket.WsFrameBase.processInputBuffer(WsFrameBase.java:133)
at org.apache.tomcat.websocket.server.WsFrameServer.onDataAvailable(WsFrameServer.java:82)
at org.apache.tomcat.websocket.server.WsFrameServer.doOnDataAvailable(WsFrameServer.java:171)
at org.apache.tomcat.websocket.server.WsFrameServer.notifyDataAvailable(WsFrameServer.java:151)
at org.apache.tomcat.websocket.server.WsHttpUpgradeHandler.upgradeDispatch(WsHttpUpgradeHandler.java:148)
at org.apache.coyote.http11.upgrade.UpgradeProcessorInternal.dispatch(UpgradeProcessorInternal.java:54)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:53)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1459)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.base/java.lang.Thread.run(Thread.java:844)

This is how I add the Button to the grid:

// Adding the column
    grid.addComponentColumn(this::buildAddButton);

This is the method that returns the buttons for each row:

// Building the button
private Button buildAddButton(ProductTemplate p) {
    Button button = new Button("add");
    // Configurate the button
    ...
    return button;
}

Unfortunatly I have found very little about this problem... Since the vaadin button doesn't stop click event propagation, I tried this:

  1. Adding the button to a layout to "block" the click event by removing the click listeners from the layout.

    // Adding the column 
        grid.addComponentColumn(this::buildAddLayout);
    

    Build a layout instead of just a button:

    // Building the layout with the button
    private VerticalLayout buildAddLayout(ProductTemplate p) {
        Button button = new Button("add");
    
        // Configurate the button
        ...
    
        VerticalLayout layout = new VerticalLayout();
        layout.addComponent(button);
        layout.getListeners(Event.class).clear();
    
        return layout;
    }
    
  2. Disabling the button when clicked until it's task is finished (Prohibiting double clicking the button).

    // Building the button
    private Button buildAddButton(ProductTemplate p) {
        Button button = new Button("add");
        // Configurate the button
            ...
        button.addClickListener(e -> {
            button.setEnabled(false);
            buttonClicked(p);
            button.setEnabled(true);
        });        
        return button;
    }
    

Both didn't make me able to get rid of the exception. Any suggestions, how I can prevent this type of exception, when double clicking a button on a grid with enabled editor?

Edit:

The editor is trying to edit the value of the button, what obviously is not possible. I want to stop the editor from doing this. ("can't edit null "means that vaadin wasn't able to create a valid bean out of the button)

Edit 2: My previous assumption, made in the first edit, seemed to be wrong. My button click is refreshing the grid and I am not able to stop the editor from trying to edit a row, even though all items are getting removed and reloaded.

Here are a class and a pom, you can use to reproduce the exception (The exception occurs when clicking the button very fast):


MyUI.java

package com.example.sample;

import java.util.ArrayList;
import java.util.List;

import javax.servlet.annotation.WebServlet;

import com.vaadin.annotations.Theme;
import com.vaadin.annotations.VaadinServletConfiguration;
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinServlet;
import com.vaadin.ui.Button;
import com.vaadin.ui.Grid;
import com.vaadin.ui.Notification;
import com.vaadin.ui.TextField;
import com.vaadin.ui.UI;
import com.vaadin.ui.VerticalLayout;

@Theme("mytheme")
public class MyUI extends UI {
    List<Product> items = new ArrayList<Product>();
    Grid<Product> grid;

    @Override
    protected void init(VaadinRequest vaadinRequest) {
        items.add(new Product("test", "test test"));

        final VerticalLayout layout = new VerticalLayout();

        grid = new Grid<Product>();
        layout.addComponent(grid);

        grid.getEditor().setBuffered(true);
        grid.getEditor().setEnabled(true);

        grid.removeAllColumns();

        grid.addComponentColumn(this::buildAddButton);

        TextField nameField = new TextField();
        TextField descriptionField = new TextField();

        grid.addColumn(Product::getName).setCaption("Name").setEditorComponent(nameField, Product::setName)
                .setExpandRatio(1);
        grid.addColumn(Product::getDescription).setCaption("Description")
                .setEditorComponent(descriptionField, Product::setDescription).setExpandRatio(1);

        grid.getEditor().addSaveListener(event -> {

            Notification.show((event.getBean() + "saved"));
        });

        grid.setItems(items);
        setContent(layout);
    }

    private Button buildAddButton(Product p) {

        Button button = new Button();

        button.addClickListener(event -> addButtonClicked(p));

        return button;
    }

    private void addButtonClicked(Product p) {
        refreshGrid();
    }

    private void refreshGrid() {
        grid.setItems(items);
    }

    @WebServlet(urlPatterns = "/*", name = "MyUIServlet", asyncSupported = true)
    @VaadinServletConfiguration(ui = MyUI.class, productionMode = false)
    public static class MyUIServlet extends VaadinServlet {
    }

    public class Product {
        String name;
        String description;

        public Product(String name, String description) {
            super();
            this.name = name;
            this.description = description;
        }

        public Product() {
            super();

        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getDescription() {
            return description;
        }

        public void setDescription(String description) {
            this.description = description;
        }

        @Override
        public String toString() {
            return name + " " + description;
        }

    }
}

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>sample</artifactId>
    <packaging>war</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>sample</name>

    <prerequisites>
        <maven>3</maven>
    </prerequisites>

    <properties>
        <vaadin.version>8.3.1</vaadin.version>
        <vaadin.plugin.version>8.3.1</vaadin.plugin.version>
        <jetty.plugin.version>9.3.9.v20160517</jetty.plugin.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <!-- If there are no local customizations, this can also be "fetch" or "cdn" -->
        <vaadin.widgetset.mode>local</vaadin.widgetset.mode>
    </properties>

    <repositories>
        <repository>
            <id>vaadin-addons</id>
            <url>http://maven.vaadin.com/vaadin-addons</url>
        </repository>
    </repositories>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.vaadin</groupId>
                <artifactId>vaadin-bom</artifactId>
                <version>${vaadin.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.0.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.vaadin</groupId>
            <artifactId>vaadin-server</artifactId>
        </dependency>
        <dependency>
            <groupId>com.vaadin</groupId>
            <artifactId>vaadin-push</artifactId>
        </dependency>
        <dependency>
            <groupId>com.vaadin</groupId>
            <artifactId>vaadin-client-compiled</artifactId>
        </dependency>
        <dependency>
            <groupId>com.vaadin</groupId>
            <artifactId>vaadin-themes</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.0.0</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                    <!-- Exclude an unnecessary file generated by the GWT compiler. -->
                    <packagingExcludes>WEB-INF/classes/VAADIN/widgetsets/WEB-INF/**</packagingExcludes>
                </configuration>
            </plugin>
            <plugin>
                <groupId>com.vaadin</groupId>
                <artifactId>vaadin-maven-plugin</artifactId>
                <version>${vaadin.plugin.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>update-theme</goal>
                            <goal>update-widgetset</goal>
                            <goal>compile</goal>
                            <!-- Comment out compile-theme goal to use on-the-fly theme compilation -->
                            <goal>compile-theme</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-clean-plugin</artifactId>
                <version>3.0.0</version>
                <!-- Clean up also any pre-compiled themes -->
                <configuration>
                    <filesets>
                        <fileset>
                            <directory>src/main/webapp/VAADIN/themes</directory>
                            <includes>
                                <include>**/styles.css</include>
                                <include>**/styles.scss.cache</include>
                            </includes>
                        </fileset>
                    </filesets>
                </configuration>
            </plugin>

            <!-- The Jetty plugin allows us to easily test the development build by
                running jetty:run on the command line. -->
            <plugin>
                <groupId>org.eclipse.jetty</groupId>
                <artifactId>jetty-maven-plugin</artifactId>
                <version>${jetty.plugin.version}</version>
                <configuration>
                    <scanIntervalSeconds>2</scanIntervalSeconds>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <profiles>
        <profile>
            <!-- Vaadin pre-release repositories -->
            <id>vaadin-prerelease</id>
            <activation>
                <activeByDefault>false</activeByDefault>
            </activation>

            <repositories>
                <repository>
                    <id>vaadin-prereleases</id>
                    <url>http://maven.vaadin.com/vaadin-prereleases</url>
                </repository>
                <repository>
                    <id>vaadin-snapshots</id>
                    <url>https://oss.sonatype.org/content/repositories/vaadin-snapshots/</url>
                    <releases>
                        <enabled>false</enabled>
                    </releases>
                    <snapshots>
                        <enabled>true</enabled>
                    </snapshots>
                </repository>
            </repositories>
            <pluginRepositories>
                <pluginRepository>
                    <id>vaadin-prereleases</id>
                    <url>http://maven.vaadin.com/vaadin-prereleases</url>
                </pluginRepository>
                <pluginRepository>
                    <id>vaadin-snapshots</id>
                    <url>https://oss.sonatype.org/content/repositories/vaadin-snapshots/</url>
                    <releases>
                        <enabled>false</enabled>
                    </releases>
                    <snapshots>
                        <enabled>true</enabled>
                    </snapshots>
                </pluginRepository>
            </pluginRepositories>
        </profile>
    </profiles>

</project>
Julius Hörger
  • 383
  • 3
  • 11
  • 5
    @Zoe I really don't have an idea, how this could be helpfull to me nor why this should be a duplicate. – Julius Hörger Feb 16 '18 at 11:12
  • 2
    I did not manage to reproduce your issue with v 8.3.1 using a simple grid and buttons that show notifications. I just get 2 notifications displayed and the grid switches to editor mode with a blank cell where the button was. Would you be able to provide a [sscce](http://sscce.org) that reproduces the problem? A pom and a basic UI class with the grid and button should suffice, so we can make sure we're using the same version and not missing out any important code pieces. – Morfic Feb 16 '18 at 14:18
  • 1
    P.S. _can't edit null "means that vaadin wasn't able to create a valid bean out of the button_. It actually means that the bean which is being edited is null, in your case the `ProductTemplate` associated with the row where the button is double-clicked. This makes me wonder if the button listener may be doing something behind the curtain to that specific bean... – Morfic Feb 16 '18 at 14:48
  • Hello @Morfic, you have already helped me a lot with your hints. I figured out, that refreshing my grids after adding/removing an item to/from the grid is the reason, I get the nullpointer later on (The button click is triggering a refresh). I guess what happening right now is, that the grid reloads the items, while the editor is trying to grab an item. Even though I tried to cancel and/or disable the editor before refreshing the grid, the editor is trying to edit. – Julius Hörger Feb 16 '18 at 15:39
  • @Morfic I have edited the post and added a class and pom to reproduce the exception. – Julius Hörger Feb 16 '18 at 15:53
  • Sorry for the late reply, I've been travelling the last few days. Indeed, it makes sense for the grid to complain if you're changing its items while it tries to edit the selected one. But my question is, why do you need to do that? What's your use-case? Perhaps we can think of a different way to achieve the same goal without triggering this issue. We can work on the code sample you provided if you can update it to portray what the actual app is doing. – Morfic Feb 19 '18 at 12:36
  • @Morfic I want to remove the items from the grid by clicking a remove button. I have a database connected to my app and don't want the user to edit and persist a non existing item. That's why I want to instantly remove the item from the grid, after clicking the button. I solved this problem for me by using a confirm dialog instead of simply deleting the item (The dialog disables the ui components and denies the editor). It perfectly works for me like that but might not help anyone with the same/related problem. – Julius Hörger Feb 19 '18 at 17:07
  • You could have a dedicated "Delete" button, below the grid, that removes the currently selected row(s), and/or react to the del key press, or even a right click menu. – Morfic Feb 19 '18 at 18:50
  • 1
    You can use Delete button from this aadon https://vaadin.com/directory/component/grid-renderers-collection-for-vaadin7 – Alex78191 Apr 01 '18 at 23:21
  • Ran into the same issue today - same use case, I put a "delete" button into my table. Now when I rapid-fire on my delete button, rows get deleted (and other rows fill the space), but I get double-click events which then try to edit the non-existing row (the first click deleted it). Seems like a proper race condition which could be triggered by anything where a click modifies a table's content. The delete-button use case just seems to be the most likely case where this would happen. – double-m Jun 25 '18 at 16:20

1 Answers1

1

Disclaimer: this solution is not applicable for the versions prior 8.3 due to a missing setHandleWidgetEvents method. Otherwise, while complicated, this solution might work for someone:

  1. Remove a click event handling from a button (addClickListener). There is no way to get an information about double click there, BUT

  2. There is a way using addItemClickListener and its clickedItem.getMouseEventDetails().isDoubleClick(), BUT again

  3. Once you enable grid using grid.getEditor().setEnabled(true); a double-click check always returns false,so

  4. You would need to disable editor by default grid.getEditor().setEnabled(false); (A good related answer here Couldn't capture double click event using vaadin 7)

  5. And , instead, re-enable it once a double click event has occurred(and open a row) grid.getEditor().setEnabled(true); grid.getEditor().editRow(item.getRowIndex());

  6. If, otherwise, a button column is clicked once, execute your addButtonClicked action then

  7. In order to receive an event from a Grid column's component, when a button is clicked, column should be able to handle events. This is achieved using grid.addComponentColumn(this::buildAddButton).setHandleWidgetEvents(true).setId("buttonClick");

A complete modified code from the init method. All others remain the same (also, remember to remove a addClickListener from button):

        items.add(new Product("test", "test test"));

        grid = new Grid<Product>();

        grid.getEditor().setBuffered(true);
        grid.getEditor().setEnabled(false);

        grid.removeAllColumns();

        // Important! Propagate events from components to Grid
        grid.addComponentColumn(this::buildAddButton).setHandleWidgetEvents(true).setId("buttonClick");

        TextField nameField = new TextField();
        TextField descriptionField = new TextField();

        grid.addColumn(Product::getName).setCaption("Name").setEditorComponent(nameField, Product::setName)
                .setExpandRatio(1);
        grid.addColumn(Product::getDescription).setCaption("Description")
                .setEditorComponent(descriptionField, Product::setDescription).setExpandRatio(1);

        //Once close editor--> Disable it
        grid.getEditor().addSaveListener(event -> {
            grid.getEditor().setEnabled(false);

        });
        grid.getEditor().addCancelListener(e->{
            grid.getEditor().setEnabled(false);
        });
        //THIS IS WHERE ALL THE LOGIC IS HAPPENING
        grid.addItemClickListener(item->{
            //If the button column is clicked
           if("buttonClick".equals(item.getColumn().getId())){
               //Regual click--> update content; also fired twice before editor is opened
               if(!item.getMouseEventDetails().isDoubleClick()){
                  addButtonClicked(item.getItem());
               }
               //If Double click is detected, just opened editor. The data is already updated
               else{
                   grid.getEditor().setEnabled(true);
                   grid.getEditor().editRow(item.getRowIndex());
               }
            }
           //In all the other cases, when double click is detected--> open editor
           else if(item.getMouseEventDetails().isDoubleClick()){
               grid.getEditor().setEnabled(true);
               grid.getEditor().editRow(item.getRowIndex());
           }
        });

        grid.setItems(items);
        addComponent(grid);
anasmi
  • 2,562
  • 1
  • 13
  • 25