Refreshing other rows in Grid while Column.setEditorComponent lambda

Looks like I’m running into issues with my inline Grid editing because of this:

  1. I need to dynamically generate the editor field on each row
  2. I need to update values in other rows

For (2) I got some help back in October, but it now seems to be incomplete. Possibly because of (1).
Ref: While editing a grid row, how can I update other rows?

In the example I have a Grid with two columns (Start Location and End Location) and two rows. When you edit Start location on one of the rows, it should update End Location in the other row, and vice versa.

When I test it out, it works the 1st time, and then it stops. It looks like the bindings are fcked.

I’m using the lambda version of Column.setEditorComponent.
What I want is to recreate the EditorComponents each time we change line.

I learned earlier today that he lambda is called every time the Grid wants a reference to the editorComponent. To solve that I added caching.
Ref: “Uncaught TypeError: $0 is null” when setting focus on TextField in Grid

But, it still doesn’t work, because Vaadin calls the lambda for any row that I call grid.refreshItem(row) on, not only the edited row (in EditorRenderer.refreshData)

This is a problem, because the lambda is also responsible for setting up the binding, but the binder points to the row that is actually edited, so what should the lambda do?

I’m not sure if I’ve found a bug in vaadin, or if I’m using it wrong.
“Bug” is probably a bit strong, since I’m trying to use a workaround. The real issue is that Vaadin lacks an official way of refreshing rows and cells without closing the editor.

@Route(value="/testGridEdit")
public class TestGridEdit extends VerticalLayout {

    public static class Row {
        
        int id;
        
        String startLocation;
        String endLocation;
        
        public String getStartLocation() {
            return startLocation;
        }
        public void setStartLocation(String startLocation) {
            this.startLocation = startLocation;
        }
        public String getEndLocation() {
            return endLocation;
        }
        public void setEndLocation(String endLocation) {
            this.endLocation = endLocation;
        }
        
        public Row(int id) {
            this.id = id;
        }
        
        @Override
        public String toString() {
            return "Row " + id;
        }
        
    }
    
    public static class EditorFieldCache {

        private HashMap<Row, HashMap<Column, TextField>> cache = new HashMap<>();
        
        public TextField get(Row row, Column column) {
            var rowCache = cache.get(row);
            return rowCache==null ? null : rowCache.get(column);
        }
        
        public void put(Row row, Column column, TextField field) {
            var rowCache = cache.get(row);
            if(rowCache==null) {
                rowCache = new HashMap<>();
                cache.put(row, rowCache);
            }
            rowCache.put(column,  field);
        }
        
    }

    /**
     * Override Grid, so that I can avoid editor-row being closed when any row is refreshed
     */
    public static class MyGrid<T> extends Grid<T> {
        
        @Override
        protected void onDataProviderChange() {
            // Do not close the editor by default
        }

        public void refreshItem(T item) {
            getDataProvider().refreshItem(item);

            // Cancel / close the editor if the item is being edited
            if (item.equals(getEditor().getItem())) {
                super.onDataProviderChange();
            }
        }

        public void refreshAll() {
            getDataProvider().refreshAll();

            // Always cancel / close the editor
            super.onDataProviderChange();
        }
    
    }
    
    public TestGridEdit() {
    
        var grid = new MyGrid<Row>();
        
        var row1 = new Row(1);
        var row2 = new Row(2);
        var rows = List.of(row1, row2);
        var listDataProvider = new ListDataProvider<Row>(rows);

        var editorFieldCache = new EditorFieldCache();
        
        Binder<Row> binder = new Binder(Row.class);
        Editor<Row> editor = grid.getEditor();
        editor.setBinder(binder);

        // Start Location
        
        var startLocationColumn = grid.addColumn(row -> row.getStartLocation()).setHeader("Start Location");

        startLocationColumn.setEditorComponent(row -> {

            var cached = editorFieldCache.get(row, startLocationColumn);
            if(cached==null) {
                System.out.println("Create startLocationColumn editor");
                cached = new TextField(); 
                binder.forField(cached).bind(Row::getStartLocation, Row::setStartLocation);
                editorFieldCache.put(row, startLocationColumn, cached);
            }
                
            return cached;
        });
    
        // End Location

        var endLocationColumn = grid.addColumn(row -> row.getEndLocation()).setHeader("End Location");

        endLocationColumn.setEditorComponent(row -> {
            
            var cached = editorFieldCache.get(row, endLocationColumn);
            if(cached==null) {
                System.out.println("Create endLocationColumn editor");
                cached = new TextField(); 
                binder.forField(cached).bind(Row::getEndLocation, Row::setEndLocation);
                editorFieldCache.put(row, endLocationColumn, cached);
            }
                
            return cached;
        });

        //
        grid.setItems(listDataProvider);
        
        add(grid);
        
        grid.getEditor().editItem(row1);
        
        binder.addValueChangeListener(event -> {
           if(binder.getBean()==row1) {
              row2.setStartLocation(row1.getEndLocation());
              row2.setEndLocation(row1.getStartLocation());
              grid.refreshItem(row2);
           }
           else if(binder.getBean()==row2) {
               row1.setStartLocation(row2.getEndLocation());
               row1.setEndLocation(row2.getStartLocation());
               grid.refreshItem(row1);
            }
        });

        grid.addItemDoubleClickListener(e -> {
            if(e.getItem()!=editor.getItem()) {
                editor.editItem(e.getItem());
                Component editorComponent = e.getColumn().getEditorComponent();
                if (editorComponent instanceof Focusable) {
                    ((Focusable) editorComponent).focus();
                }
            }
        });
        
    }

}

My understanding is that you need to close the editor, then refresh item and then re-open it programmatically.

Yes, but I shouldn’t have to. That is still a half baked workaround.

if I do that the users would lose the input focus.
Maybe I could track it and put it back, but I assume if they have started writing in the new field they will lose that content, or the cursor might end up in the wrong place.
Doesn’t sound ideal.

A better workaround could perhaps be to subclass and override, but Vaadin makes this hard.
For instance, it looks like this is a dumb issue in EditorRenderer:

    @Override
    public void refreshData(T item) {
        if (editor.isOpen()) {
            buildComponent(item);
        }
    }

If it had checked if editor is open on the given item I assume it would work.

EditorRenderer is public and this method is public, so maybe I can override?
Well, no, EditorRenderer is created by the private Column.setupColumnEditor

Perhaps I can fake editor.isOpen()? It would be really ugly, but I’ll take what I can get.
Grid.createEditor is overridable by design:

    /**
     * Creates a new Editor instance. Can be overridden to create a custom
     * Editor. If the Editor is a {@link AbstractGridExtension}, it will be
     * automatically added to {@link DataCommunicator}.
     *
     * @return editor
     */
    protected Editor<T> createEditor() {
        return new EditorImpl<>(this, propertySet);
    }

It is a shame that propertySet is private, so I can’t pass it to my replacement. I don’t think I need it but others might.

So, in MyGrid I override createEditor and return MyEditor:

    public static class MyEditor<T> extends EditorImpl<T> {
        
        boolean fakeEditorIsClosed;

        public MyEditor(Grid<T> grid, PropertySet<T> propertySet) {
            super(grid, propertySet);
        }

        @Override
        public boolean isOpen() {
            return !fakeEditorIsClosed && super.isOpen();
        }
        
        public void setFakeEditorIsClosed(boolean fakeEditorIsClosed) {
            this.fakeEditorIsClosed = fakeEditorIsClosed;
        }
        
    }

And in MyGrid:

        public void refreshItem(T item) {
            ((MyEditor)getEditor()).setFakeEditorIsClosed(true);
            try {
                getDataProvider().refreshItem(item);
            }
            finally {
                ((MyEditor)getEditor()).setFakeEditorIsClosed(false);
            }
    
            // Cancel / close the editor if the item is being edited
            if (item.equals(getEditor().getItem())) {
                super.onDataProviderChange();
            }
        }

That actually works for my simple test-case. I’ll try it in my full app and see what happens.
But, to reiterate, none of this should’ve been neccessary.

On the other hand there is nice utility to produce property set when you need it

Btw, check that AutoGrid in general. It is one study of mine of dynamic creation of Grid editor based on property types.

Well, that is not the only thing. There will be plenty of other corner cases to solve, and Grid’s integrated editor does not have any stance on that.

For better collaborative editing I would not use Grid’s integrated editor, but I would create CRUD view using a proper form with the CollaborativeBinder from CollaborationKit, which is our best shot on multiuser concurrent editing.

I do something similar, but instead of starting with an (annotated) pojo, I start with a set of columns. These have rich metadata from the db, including table, name, type, required, foreign keys etc. Since we generate stuff and write our own framework, we can also bake in conventions, like any column called xxx_kg should get a converter to lbs if the customer desires that.

From a list of columns, I can generate the select to run against the db, the field in a form, columns in a grid and editor fields on the grid column.

There is almost no Vaadin code in our actual screens. It is all generated by our framework.

Generating stuff is awesome :smiley:

I didn’t intend to imply collaborative editing when I wrote “users would lose the input focus”.

I have tried to think of where the Collaboration Engine might fit in in our application.
I’m not sure it would make sense for several persons to edit the same record, I think our users would be more interested in locking the row they’re editing… but it could be helpful to see that other users are editing other rows.

For us, I don’t see the point of Vaadin’s CRUD. If we want something like that I’d just generate it myself. Since a Form and a List is almost the same thing in our code, the framework could easily generate an editor popover, sidebar, whatever instead of the grid editor fields.
I have considered this while fighting with Grid, and I’ve considered it for mobile

Locking mechanism is somewhat easier to implement than full collaborative editing. But it also has some caveats to take into account. I have another project where I have a proof of concept of that (Vaadin 8 project though).

But the concept is pretty version independent.

I did not mean using CRUD component, but just building a custom CRUD view.

To make building custom CRUD view easier there is Mater - Detail layout coming now, which does just have placeholders for components you want to use there. I.e. Form and e.g. in most cases Grid, but could be something else.