Blog

Building a local file manager with Vaadin Flow

By  
Lawrence Lockhart
Lawrence Lockhart
·
On Dec 13, 2024 10:58:44 PM
·

Handling file uploads and managing stored files is a common requirement in web applications. However, creating a robust and user-friendly interface can be challenging. This post demonstrates how to build a file management application using Vaadin Flow, showcasing how Vaadin's components, data binding, and backend integration simplify the process of uploading, storing, and displaying files.

filemanagerCore components: Upload and FileGrid

The user interacts with the application primarily through the UploadView. This view incorporates an Upload component, allowing users to select and upload files. The uploaded data is temporarily stored in a MemoryBuffer for further processing. The SucceededListener reads this data, creates a new FileEntity object, and triggers the editFile method in the FileUploadForm to handle the uploaded file.

       Upload upload = new Upload();
        MemoryBuffer buffer = new MemoryBuffer();
        upload.setReceiver(buffer);
        upload.addSucceededListener(event -> {
            upload.clearFileList();
            FileEntity fileEntity = new FileEntity();
            fileEntity.setFileName(event.getFileName());
            try {
                fileEntity.setFileData(buffer.getInputStream().readAllBytes());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            uploadForm.editFile(fileEntity);
        });

The FileGrid component, extending the Vaadin Grid component, displays a tabular view of uploaded files. This versatile component is popular for its ability to:

  • Efficiently handle large datasets with minimal code.
  • Offer built-in support for selection, drag-and-drop, and other common table interactions.
  • Allow customization of columns, including defining custom renderers and editors.
   public FileGrid(FileService service, FileUploadForm fileUploadForm) {
        this.service = service;
        this.uploadForm = fileUploadForm;
        addColumn(FileEntity::getFileName).setHeader("File Name").setAutoWidth(true);
        addColumn(FileEntity::getDescription).setHeader("Description").setAutoWidth(true);
        addComponentColumn(file -> {
            Button editButton = new Button("Edit");
            editButton.addClickListener(e -> uploadForm.editFile(file));
            Button deleteButton = new Button("Delete");
            deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR);
            deleteButton.addClickListener(e -> {
                service.deleteFile(file);
                refreshGrid();
                Notification.show("File deleted!");
            });
            HorizontalLayout actionsLayout = new HorizontalLayout(new FileDownload(file), editButton, deleteButton);
            actionsLayout.setAlignItems(FlexComponent.Alignment.BASELINE);
            actionsLayout.setSpacing(true);
            return actionsLayout;
        }).setHeader("Actions");

The addComponentColumn method seen above empowers us to add custom actions for each file within the grid, specifically downloading, editing, and deleting. The edit button triggers the editFile method in the FileUploadForm to allow modification of file details.

The delete button both removes the file from the system and refreshes the grid. Finally, the download button utilizes the StreamResource class from the Vaadin Server package. This functionality is encapsulated in a separate FileDownload class for improved readability, reusability, and testability. It is pictured below.

   private static class FileDownload extends Anchor {
        public FileDownload(FileEntity fileEntity) {
            super();
            add(VaadinIcon.DOWNLOAD.create());
            StreamResource resource = new StreamResource(fileEntity.getFileName(),
                    () -> new ByteArrayInputStream(fileEntity.getFileData()));
            resource.setCacheTime(0);
            resource.setContentType("application/octet-stream");
            setHref(resource);
            getElement().setAttribute("download", true);
            setTitle("Download the file");
        }
    }

Data binding with Binder and BeanValidationBinder

The Vaadin Binder class simplifies the process of binding data between UI fields and data properties. In this project, the FileUploadForm utilizes the Binder class and its subclass, BeanValidationBinder, to manage data binding between the description TextArea and the FileEntity object.

The BeanValidationBinder leverages JSR-303 annotations to automatically validate user input against the defined constraints found in the FileEntity class. For example, if we add the @NotNull annotation to the description field, the BeanValidationBinder ensures the user provides a description before submitting the form.

Here's a code snippet demonstrating the data binding setup:

   Binder<FileEntity> binder = new BeanValidationBinder<>(FileEntity.class);
    private final FileService service;
    public FileUploadForm(FileService service) {
        this.service = service;
        add(new VerticalLayout(
                description,
                new HorizontalLayout(save, cancel)
        ));
        binder.bindInstanceFields(this);
 

This simplifies our data handling and ensures data integrity within the application.

Conclusion

This demonstrates how Vaadin Flow facilitates the development of user-friendly web applications. Developers can create robust solutions with minimal effort by utilizing core components like Upload and Grid, leveraging data binding with Binder, and integrating with backend services.

Ready to start building your own applications with Vaadin Flow? Explore the latest Vaadin tutorial. You can also find the code for this example on GitHub.

Lawrence Lockhart
Lawrence Lockhart
Lawrence is a Developer Advocate with Vaadin. He works with the Hilla and Flow products and enjoys tech conversations that lead to developer wins.
Other posts by Lawrence Lockhart