Vaadin 在具有活动编辑器的网格上双击按钮引发异常 ("java.lang.NullPointerException: Editor can't edit null")

Vaadin double clicking button on grid with active editor throws exception ("java.lang.NullPointerException: Editor can't edit null")

我有一个带有活动缓冲编辑器的 vaadin 网格。默认情况下,双击一行时会打开编辑器。一切正常,除了当我双击按钮时出现异常:(异常没有指向我的代码的任何地方)

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.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(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.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.onMessage(JSR356Endpoint.java:272)
at org.atmosphere.container.JSR356Endpoint.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)

这是我将按钮添加到网格的方式:

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

这是returns每行按钮的方法:

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

不幸的是,我对这个问题知之甚少...... 由于 vaadin 按钮不会停止点击事件传播,我尝试了这个:

  1. 通过从布局中移除点击侦听器,将按钮添加到布局以 "block" 点击事件。

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

    构建布局而不只是按钮:

    // 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. 单击时禁用按钮,直到它的任务完成 完成(禁止双击按钮)。

    // 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;
    }
    

两者都没有让我能够摆脱异常。有什么建议吗?在启用编辑器的情况下双击网格上的按钮时如何防止此类异常?

编辑:

编辑器正在尝试编辑按钮的值,显然不可能。我想阻止编辑这样做。 ("can't edit null "表示 vaadin 无法通过按钮创建有效的 bean)

编辑 2:我之前在第一次编辑中做出的假设似乎是错误的。我的按钮点击刷新了网格,我无法阻止编辑器尝试编辑一行,即使所有项目都被删除并重新加载。

这里有一个class和一个pom,可以用来重现异常(点击按钮速度太快时出现异常):


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>

免责声明:由于缺少setHandleWidgetEvents方法,此解决方案不适用于8.3之前的版本。否则,虽然复杂,但此解决方案可能适用于某些人:

  1. 从按钮中删除单击事件处理 (addClickListener)。那里无法获得有关双击的信息,BUT

  2. 有一种方法可以使用 addItemClickListener 及其 clickedItem.getMouseEventDetails().isDoubleClick()但是 再次

  3. 使用 grid.getEditor().setEnabled(true); 启用网格后,双击检查总是 returns false,所以

  4. 默认情况下您需要禁用编辑器grid.getEditor().setEnabled(false); (这里有一个很好的相关答案Couldn't capture double click event using vaadin 7

  5. 并且,相反,一旦双击事件发生(并打开一行)就重新启用它 grid.getEditor().setEnabled(true); grid.getEditor().editRow(item.getRowIndex());

  6. 如果按钮列被点击一次,执行你的addButtonClicked操作然后

  7. 为了从网格列的组件接收事件,单击按钮时,列应该能够处理事件。这是使用 grid.addComponentColumn(this::buildAddButton).setHandleWidgetEvents(true).setId("buttonClick");
  8. 实现的

init 方法的完整修改代码。所有其他保持不变(另外,请记住从按钮中删除 addClickListener):

        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);