VAADIN

Vaadin TreeGrid con carga diferida, filtrado y ordenamiento.

13 mayo, 2021

Estoy dando mis primeros pasos en Vaadin 14 después de mucho, mucho tiempo de trabajar en proyectos de Vaadin 8 y quiero compartir mi experiencia cuando me tocó trabajar con el componente TreeGrid para mostrar datos en una estructura jerárquica. Me encontré con esta situación en la que necesitaba agregar filtrado y ordenamiento a un TreeGrid de Vaadin que estaba utilizando carga diferida. Como base, tenía este artículo de similar implementación utilizando el componente Grid, pero algunas cuestiones son diferentes si hablamos de TreeGrid, así que pensé que sería una buena idea compartir todos los pasos necesarios y algunos tips a tener en cuenta para este tipo de implementación.

Para la siguiente explicación, voy a asumir que tienes algo de conocimiento sobre los componentes de Vaadin y sobre la carga diferida.

Como punto de partida, definamos una clase entidad para representar los datos que queremos mostrar jerárquicamente. En este caso, la entidad Department:

public class Department {

    private int id;
    private String name;
    private String manager;
    private Department parent;

    public Department(int id, String name, Department parent, String manager) {
        this.id = id;
        this.name = name;
        this.manager = manager;
        this.parent = parent;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public String getManager() {
        return manager;
    }

    public void setManager(String manager) {
        this.manager = manager;
    }

    public Department getParent() {
        return parent;
    }

    public void setParent(Department parent) {
        this.parent = parent;
    }

}

Luego, podemos definir el TreeGrid y su data provider (proveedor de datos) con carga diferida. A diferencia de Grid, los fetch y count callbacks se basan en HierarchicalQuery. Así que ahora, para implementar el data provider jerárquico, necesitamos definir los siguientes tres métodos:

protected Stream<Department> fetchChildrenFromBackEnd(HierarchicalQuery<Department, String> query) {...}

public int getChildCount(HierarchicalQuery<Department, String> query) {...}

public boolean hasChildren(Department item) {...}

Como resultado, la implementación del TreeGrid con carga diferida, se verá así:

public MainView() {

    TreeGrid<Department> treeGrid = new TreeGrid<>();
    treeGrid.addHierarchyColumn(Department::getName)
    .setHeader("Department Name").setKey("name");
    treeGrid.addColumn(Department::getManager)
    .setHeader("Manager").setKey("manager");
	 
    DepartmentService departmentService = new DepartmentService();	
	
    HierarchicalDataProvider<Department, Void> dataProvider =
	new AbstractBackEndHierarchicalDataProvider<Department, Void>() {

	// returns the number of immediate child items
	@Override
	public int getChildCount(HierarchicalQuery<Department, Void> query) {
	    return (int) departmentService.getChildCount(query.getParent());
	}

	// checks if a given item should be expandable
	@Override
	public boolean hasChildren(Department item) {
	    return departmentService.hasChildren(item);
	}
		
	// returns the immediate child items based on offset and limit
	@Override
	protected Stream<Department> fetchChildrenFromBackEnd(
		HierarchicalQuery<Department, Void> query) {
		return departmentService.fetchChildren(query.getParent(), 
                  query.getLimit(), query.getOffset()).stream();
	}
    };

    treeGrid.setDataProvider(dataProvider);	
    add(treeGrid);
}

Por supuesto, tenemos que definir una clase servicio que será la encargada de obtener los datos desde el backend. Este servicio deberá implementar los siguientes métodos para poder setear el data provider del TreeGrid:

public List<Department> fetchChildren(Department parent) {...}

public int getChildCount(Department parent) {...}

public boolean hasChildren(Department parent, int limit, int offset) {...}

Pero ahora, ¿qué pasa si queremos agregarle filtrado y ordenamiento a esta implementación? Veamos…

Filtrado

Primero, definamos una clase de tipo filtro que pueda ser aplicada a la colección de elementos que queremos filtrar:

public class DepartmentFilter {

	private String nameFilter = null;	
	private String managerFilter = null;
	
	public DepartmentFilter() {	}

	public DepartmentFilter(String nameFilter, String managerFilter) {
		this.nameFilter = nameFilter;
		this.managerFilter = managerFilter;
	}

	public String getNameFilter() {
		return nameFilter;
	}

	public void setNameFilter(String nameFilter) {
		this.nameFilter = nameFilter;
	}

	public String getManagerFilter() {
		return managerFilter;
	}

	public void setManagerFilter(String managerFilter) {
		this.managerFilter = managerFilter;
	}
	
}

En lugar de utilizar un data provider de tipo HierarchicalDataProvider, necesitamos configurar uno de tipo HierarchicalConfigurableFilterDataProvider que nos permitirá establecer un filtro para aplicar a las consultas o queries. Aparte, necesitamos hacer un wrapper del data provider para convertirlo en un ConfigurableDataProvider. Esto lo logramos llamando al método withConfigurableFilter(), el cual le indicará al data provider que va a ser filtrable.

HierarchicalConfigurableFilterDataProvider<Department, Void, DepartmentFilter> dataProvider =
     new AbstractBackEndHierarchicalDataProvider<Department, DepartmentFilter>() {

   // returns the number of immediate child items based on query filter
   @Override
   public int getChildCount(HierarchicalQuery<Department, DepartmentFilter> query) {
      return (int) departmentService.getChildCount(query.getParent(), query.getFilter().orElse(null));
   }

   // checks if a given item should be expandable
   @Override
   public boolean hasChildren(Department item) {
      return departmentService.hasChildren(item);
   }

   // returns the immediate child items based on offset, limit and filter
   @Override
   protected Stream<Department> fetchChildrenFromBackEnd(
	    HierarchicalQuery<Department, DepartmentFilter> query) {
      return departmentService.fetchChildren(query.getParent(), 
            query.getLimit(), query.getOffset(), 
            query.getFilter().orElse(null)).stream();
   }

}.withConfigurableFilter();

// set data provider to tree
treeGrid.setDataProvider(dataProvider);
	
// define filter
DepartmentFilter treeFilter = new DepartmentFilter();
	
// set filter to data provider
dataProvider.setFilter(treeFilter);

Tenemos que actualizar nuestra UI, agregando campos para filtrado para cada una de las columnas que queremos que sean filtrables. Estos campos deben agregarse a la fila cabecera (o header row) del TreeGrid entonces, los mismos, se mostrarán en la parte superior de cada columna:

HeaderRow filterRow = treeGrid.prependHeaderRow();

TextField nameFilterTF = new TextField();
nameFilterTF.setClearButtonVisible(true);
nameFilterTF.addValueChangeListener(e -> {
	treeFilter.setNameFilter(e.getValue());
	dataProvider.refreshAll(); 
});
filterRow.getCell(treeGrid.getColumnByKey("name")).setComponent(nameFilterTF);

TextField managerFilterTF = new TextField();
managerFilterTF.setClearButtonVisible(true);
managerFilterTF.addValueChangeListener(e -> {
	treeFilter.setManagerFilter(e.getValue());
	dataProvider.refreshAll();
});
filterRow.getCell(treeGrid.getColumnByKey("manager")).setComponent(managerFilterTF);  

Finalmente, no olvidemos actualizar la clase servicio para poder obtener los resultados usando DepartmentFilter para el filtrado. Los métodos que se deben actualizar en este ejemplo son:

public List<Department> fetchChildren(Department parent, int limit, int offset, DepartmentFilter filter) {...}

public int getChildCount(Department parent, DepartmentFilter filter) {...}

Ordenamiento

De manera similar al filtrado, necesitamos definir una clase para almacenar la información de ordenamiento:

public class DepartmentSort {

        public static final String NAME = "name";	
        public static final String MANAGER = "manager";

        private String propertyName;	
        private boolean descending;
	
	public DepartmentSort(String propertyName, boolean descending) {
		this.propertyName = propertyName;
		this.descending = descending;
	}
	public String getPropertyName() {
		return propertyName;
	}
	public void setPropertyName(String propertyName) {
		this.propertyName = propertyName;
	}
	public boolean isDescending() {
		return descending;
	}
	public void setDescending(boolean descending) {
		this.descending = descending;
	}
	
}

Luego, hay que actualizar la definición del data provider para enviar las propiedades de ordenamiento al backend encargado de realizar el ordenamiento:

HierarchicalConfigurableFilterDataProvider<Department, Void, DepartmentFilter> dataProvider =
	new AbstractBackEndHierarchicalDataProvider<Department, DepartmentFilter>() {

	// returns the number of immediate child items based on query filter
	@Override
	public int getChildCount(HierarchicalQuery<Department, DepartmentFilter> query) {
		return (int) departmentService.getChildCount(query.getParent(), 
                  query.getFilter().orElse(null));
	}

	// checks if a given item should be expandable
	@Override
	public boolean hasChildren(Department item) {
		return departmentService.hasChildren(item);
	}

	// returns the immediate child items based on offset, limit, filter and sorting
	@Override
	protected Stream<Department> fetchChildrenFromBackEnd(
		HierarchicalQuery<Department, DepartmentFilter> query) {
		List<DepartmentSort> sortOrders = query.getSortOrders().stream()
			.map(sortOrder -> new DepartmentSort(sortOrder.getSorted(), 
                         sortOrder.getDirection().equals(SortDirection.ASCENDING)))
			.collect(Collectors.toList());
		return departmentService.fetchChildren(query.getParent(), query.getLimit(), 
                   query.getOffset(), query.getFilter().orElse(null), sortOrders).stream();
	}

}.withConfigurableFilter();

Como paso final, debemos volver a actualizar la clase servicio para obtener los resultados ordenados de acuedo a las propiedades seleccionadas. En este caso, solo debemos actualizar el siguiente método:

public Collection<Department> fetchChildren(Department parent, int limit, 
         int offset, DepartmentFilter filter, List<DepartmentSort> sortOrders) { ... }	

Y así, tenemos un Tree Grid con carga diferida, filtrado y ordenamiento.

En la siguiente animación puede observarse un TreeGrid en acción:

Además, algunos tips para tener en cuenta:

#1: No olvidar especificar qué columnas se pueden ordenar utilizando los métodos setSortable(true) or setSortProperty(…)

treeGrid.addHierarchyColumn(Department::getName)
    .setHeader("Department Name").setKey("name")
    .setSortProperty(DepartmentSort.NAME);	  
treeGrid.addColumn(Department::getManager)
    .setHeader("Manager").setKey("manager")
    .setSortProperty(DepartmentSort.MANAGER);

Si la columna es ordenable (setSortable(true)) y no se definió ninguna propiedad de ordenamiento con el método setSortProperty(…), se utilizará como propiedad la key definida para la columna.

#2: Se puede utilizar el método setMultisort(true) para habilitar el ordenamiento de múltiples columnas en el client-side

treeGrid.setMultiSort(true);

#3: Al agregar los campos para el filtrado en la cabecera, recordar llamar al método refreshAll(). De esta manera, el data provider activa la carga de los datos basados en los valores de filtro especificados

TextField managerFilterTF = new TextField();
	managerFilterTF.setClearButtonVisible(true);
	managerFilterTF.addValueChangeListener(e -> {
		treeFilter.setManagerFilter(e.getValue());
		dataProvider.refreshAll();    // -> don't forget about this!
	});    

Puedes encontrar el ejemplo completo visitando nuestra organización de GitHub. Haz click aquí para hacer checkout del código fuente y testear la implementación.

Espero que esta explicación resulte útil y no dudes en escribirnos si surge alguna inquietud.

Gracias por leernos y keep the code flowing!

Paola De Bartolo
By Paola De Bartolo

Ingeniera en Sistemas. Java Developer. Entusiasta de Vaadin desde el momento en que escuché "puedes implementar todo el UI con Java". Parte del #FlowingCodeTeam desde 2017.

¡Únete a la conversación!
Profile Picture

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.