如何使用 vaadin 网格导出到 csv/excel?

how to export to csv/excel using vaadin grids?

在 Vaadin 14+ 中,我正在创建网格并希望用户有一种 stable/easy 方法将网格的内容导出到 csv,或者最好是 Excel。为此,令我感到惊讶的是 Vaadin 似乎没有提供此功能,因此必须使用第 3 方开发人员插件(例如 https://vaadin.com/directory/component/exporter/overview)。但是,这些插件有许多错误(例如,无法将具有日期值的网格正确导出到 Excel 等)。 Vaadin 14 中是否有推荐的方法来支持我认为是任何基于 Web 的网格小部件的高度要求的功能?

无需插件(在 Vaadin 中称为 add-ons)。

DataProvider

您需要了解 Grid 小部件用于 演示 ,而不是数据存储。

每个 Grid 对象都由 DataProvider, which is responsible for accessing the data store. The data to be displayed in a Grid might come from some objects in memory, or from a data feed, or from the results of a database query, or from some other source. Following the design principle of separation of concerns 支持,Grid class 只关心显示数据,不管理数据访问。 DataProvider 界面关注的是管理数据访问,而不是显示数据。所以 GridDataProvider 一起工作。

对于基于内存的有限数量的数据对象,我们可以使用 DataProviderListDataProvider 实现。当我们传递数据对象的集合时,可以自动为我们构建此列表数据提供程序。

因此您不从 Grid 对象中导出数据。相反,您想监听 DataProvider 的更改,然后提供通过该数据提供程序获取的导出数据。

DataProvider 中没有内置导出功能。您可以在利用 DataProvider 实现中提供的数据的同时编写自己的导出功能。您可以在许多基于 Java 的库中进行选择,以协助为导出的数据编写数据文件。在下面显示的代码中,我们使用 Apache Commons CSV 库来编写制表符分隔或逗号分隔的值。

这是一个完整的示例应用程序。

我们有一个简单的 Person class 来保存姓名和 phone 号码。

package work.basil.example;

import java.util.Objects;

public class Person
{
    //---------------|  Member vars  |--------------------------------
    private String name, phone;


    //---------------|  Constructors  |--------------------------------

    public Person ( String name , String phone )
    {
        this.name = name;
        this.phone = phone;
    }


    //---------------|  Accessors  |--------------------------------

    public String getName ( ) { return this.name; }

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

    public String getPhone ( ) { return this.phone; }

    public void setPhone ( String phone ) { this.phone = phone; }


    //---------------|  Object  |--------------------------------


    @Override
    public boolean equals ( Object o )
    {
        if ( this == o ) return true;
        if ( o == null || getClass() != o.getClass() ) return false;
        Person person = ( Person ) o;
        return getName().equals( person.getName() );
    }

    @Override
    public int hashCode ( )
    {
        return Objects.hash( getName() );
    }
}

这是一个完整的 Vaadin 14.1.18 应用程序,它生成 4 个 Person 对象作为示例数据集。这些对象被馈送到 Grid,为了我们的方便,它会生成 ListDataProvider

我们有一个文本字段,用于编辑网格中显示的所选 Person 对象的 phone 编号。

我们还有一个导出按钮,它使用 Apache Commons CSV library to write out a CSV file. Notice the key line where we access the data items from the ListDataProvider. First we cast the data provider to ListDataProvider, then we extract a Collection of all the Person objects stored within. Java Generics 提供类型安全,并使编译器能够知道数据提供者包含 Person 个对象。

Collection < Person > persons = ( ( ListDataProvider < Person > ) grid.getDataProvider() ).getItems();

完整的 Vaadin 14.1 应用程序代码如下。

package work.basil.example;

import com.vaadin.flow.component.AbstractField;
import com.vaadin.flow.component.ClickEvent;
import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.dependency.CssImport;
import com.vaadin.flow.component.dialog.Dialog;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.grid.GridSingleSelectionModel;
import com.vaadin.flow.component.html.Input;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.provider.ListDataProvider;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.PWA;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;

import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;

/**
 * The main view contains a button and a click listener.
 */
@Route ( "" )
//@PWA ( name = "Project Base for Vaadin", shortName = "Project Base" )
@CssImport ( "./styles/shared-styles.css" )
@CssImport ( value = "./styles/vaadin-text-field-styles.css", themeFor = "vaadin-text-field" )
public class MainView extends VerticalLayout
{

    Grid < Person > grid;
    TextField phoneField;
    Button phoneSaveButton, exportButton;

    public MainView ( )
    {
        // Widgets
        List < Person > personList = new ArrayList <>( 4 );
        personList.add( new Person( "Alice" , "555.123.1234" ) );
        personList.add( new Person( "Bob" , "555.688.4787" ) );
        personList.add( new Person( "Carol" , "555.632.2664" ) );
        personList.add( new Person( "David" , "555.543.2323" ) );

        // Create a grid bound to the list
        grid = new Grid <>();
        grid.setItems( personList );
        grid.addColumn( Person :: getName ).setHeader( "Name" );
        grid.addColumn( Person :: getPhone ).setHeader( "Phone" );
        GridSingleSelectionModel < Person > singleSelect = ( GridSingleSelectionModel < Person > ) grid.getSelectionModel();
        singleSelect.setDeselectAllowed( false );
        singleSelect.addSingleSelectionListener( singleSelectionEvent -> {
                    Optional < Person > personOptional = singleSelectionEvent.getSelectedItem();
                    if ( personOptional.isPresent() )
                    {
                        this.phoneField.setValue( personOptional.get().getPhone() );
                    }
                }
        );

        phoneField = new TextField( "Phone:" );

        phoneSaveButton = new Button( "Update phone on person " );
        phoneSaveButton.addClickListener(
                ( ClickEvent < Button > clickEvent ) -> {
                    Optional < Person > personOptional = ( ( GridSingleSelectionModel < Person > ) grid.getSelectionModel() ).getSelectedItem();
                    if ( personOptional.isEmpty() )
                    {
                        Notification.show( "First, select a person in list." );
                    } else
                    {
                        Person person = personOptional.get();
                        person.setPhone( phoneField.getValue() );
                        grid.getDataProvider().refreshItem( person );
                    }
                }
        );

        exportButton = new Button( "Export" );
        exportButton.setEnabled( false );
        exportButton.addClickListener(
                ( ClickEvent < Button > clickEvent ) -> {
                    String fileName = "Persons_" + Instant.now().toString() + ".csv";
                    final String fileNamePath = "/Users/basilbourque/" + fileName;
                    try (
                            BufferedWriter writer = Files.newBufferedWriter( Paths.get( fileNamePath ) ) ;
                            CSVPrinter csvPrinter = new CSVPrinter( writer , CSVFormat.RFC4180.withHeader( "Name" , "Phone" ) ) ;
                    )
                    {
                        Collection < Person > persons = ( ( ListDataProvider < Person > ) grid.getDataProvider() ).getItems();
                        for ( Person person : persons )
                        {
                            csvPrinter.printRecord( person.getName() , person.getPhone() );
                        }
                    }
                    catch ( IOException e )
                    {
                        e.printStackTrace();
                    }

                    // Tell user.
                    Notification.show( "Exported to file in your home folder: " + fileName );
                }
        );
        grid.getDataProvider().addDataProviderListener( dataChangeEvent -> {
            exportButton.setEnabled( true );
        } );


        // Arrange
        this.add( grid , phoneField , phoneSaveButton , exportButton );
    }
}

顺便说一句,Apache Commons CSV 提供了多种文件格式。通常,最好的格式是 RFC 4180 中定义的标准格式。但是您提到了 Microsoft Excel,该库支持该变体。见 CSVFormat class.