datenhahn / componentrenderer

A ComponentRenderer for the Vaadin Grid
Apache License 2.0
6 stars 8 forks source link

ComponentRenderer

CAUTION!!! Does NOT support Vaadin 8 Grid. Only Vaadin 7 compatibility Grid. Version 2.0.0 runs with Vaadin 8 but is only compatible with Vaadin 8 V7 compatibility layer components. For Vaadin 7 you can still use the latest 1.X.X Version which is stable.

Upcoming Vaadin 8.1 supports components out-of-the box: https://demo.vaadin.com/sampler/#ui/grids-and-trees/grid/component-renderer

Renders standard Vaadin components in the grid.

Features

Limitations

Important info

Bugs & Features

Please report bugs or feature-wishes in the github issue-tracker to further improve this renderer.

Use the renderer

Add this dependency to your pom

<dependency>
    <groupId>de.datenhahn.vaadin</groupId>
    <artifactId>componentrenderer</artifactId>
    <version>ENTER VERSION HERE</version>
</dependency>

then recompile the widgetset.

Demo Application

Have a look at the demo app, you can start it with:

cd componentrenderer-demo
mvn jetty:run

Use with standard grid

Example using the renderer with the standard Vaadin grid, see Demo Source-Code for complete example.

// don't forget to activate all extensions (ComponentGrid automatically adds these)
ComponentCellKeyExtension.extend(grid);
focusPreserveExtension = FocusPreserveExtension.extend(grid);
DetailsKeysExtension.extend(grid);

myGrid.addColumn(COL_DELETE, Component.class).setRenderer(new ComponentRenderer());
//...
generatedPropertyContainer.addGeneratedProperty(COL_DELETE, new DeleteButtonValueGenerator(myGrid));

//...
// You can use the FocusPreserveExtension to refresh the grid withoud losing the current
// cell focus
focusPreserveExtension.saveFocus();
grid.setCellStyleGenerator(grid.getCellStyleGenerator());
focusPreserveExtension.restoreFocus();

Use with the ComponentGrid supplied with the renderer

Example using the renderer with the ComponentGrid, see Demo Source-Code for complete example.

grid.addComponentColumn(FOOD, cust -> createFoodSelector(grid, cust));
grid.addComponentColumn(FOOD_ICON, cust -> createFoodIcon(cust));

Using the componentrenderers addon stylesheet

To use the componentrenderers automatic addon stylesheet which centers components correctly include the addons.scss in your theme, have a look at the demotheme:

@import "../valo/valo.scss";
@import "addons.scss";

.demotheme {
  @include valo;
  @include addons;

OSGI

If you want to add the serverside parts of the renderer as OSGI bundle, you can use the karaf feature (if you use karaf) which installs automatically all needed dependencies (including vaadin).

feature:repo-add mvn:de.datenhahn.vaadin/componentrenderer/0.3.4/xml/features
feature:install vaadin-componentrenderer

Or you use the classic way only installing the componentrenderer bundle:

bundle:install -s mvn:de.datenhahn.vaadin/componentrenderer/0.3.4

To see all dependencies have a look at the features.xml, most come from vaadin itself:

features.xml

Keyboard-Navigation

There are two extensions improving on keyboard navigation.

ComponentCellKeyExtension

With the arrow keys you can navigate through the grid, but if you wanted to press a component-button in a cell or if you wanted to change a dropdown you had to use the mouse. This extensions adds two new keyboard actions:

DetailsKeysExtension

You also want to open/close the details-rows with the keyboard. So this extension adds the following to keyboard actions:

Focus Preserving refresh

When rerendering the grid looses its focus. I fixed that by suppling functions for saving and restoring the current focus. The ComponentGrid's refresh() function automatically makes use of that.

Standard Grid

focusPreserveExtension.saveFocus();
grid.setCellStyleGenerator(grid.getCellStyleGenerator());
focusPreserveExtension.restoreFocus();

ComponentGrid

grid.refresh()

In case you wonder grid.setCellStyleGenerator(grid.getCellStyleGenerator()); is a nasty hack to force the grid to rerender.

HeaderGenerator

The header generators make it very easy to generate headers for all bean properties and generated properties. See the ResourceBundleComponentHeaderGenerator for an example.

componentGridDecorator.generateHeaders(new ResourceBundleTextHeaderGenerator(ViewComponents.getLabels()));

The method generateHeaders calls the HeaderGenerators getHeader method for every property and sets its header-caption. You should use that method once you have set the columns you want to use.

There are the following types of HeaderGenerators:

Server-Side Memory Use

I used jvisualvm to check if the components were released correctly. Below you find the screenshots.

Later on I added an integration test, see: de.datenhahn.vaadin.componentrenderer.testbench.MemoryIT

run e.g. MemoryIT#profileFirefox(), you can also play around with different setups if you modifiy the grid loaded for testbench test (see method de.datenhahn.vaadin.componentrenderer.demo.ComponentRendererDemoUI.startTestBenchApp()).

Initial Load

The grids initially displayed rows are loaded, memory use about 50MB.

Scroll to row 1000 (ARROW_DOWN key) before GC

During scrolling we create a lot of objects, memory use before GC about 400MB.

Scroll to row 1000 (ARROW_DOWN key) after GC

But the generated objects do not stay in heap, but can be garbage collected which we can observe here. After GC again only about 50MB of memory is used. The number of instances left (100) is the amount the grid's RpcDataSource holds in its row-cache.

Internals of the renderer explained

Here you find a description how the renderer works. You won't need that to use the renderer, but it may be useful if you want to extend it or when writing your own renderers.

Grid basics for writing renderers

Before starting to extend the grid I recommend watching this video. Leif explain very detailed the mechanics of the grid.

GWT.create 2015 - Improving the HTML table (Leif Åstrand)

https://www.youtube.com/watch?v=Yi8ioIHTnZw

Other resources

ItemId, RowIndex and RowKey explained

When programming for the grid you will encounter these different keys for identifying rows. Be sure to convert correctly when you do not receive the type of key you need.

Conversions

Lifecycle of rendering a cell

Now you should have a basic understanding how the grid's internals work and we will look into the lifecycle of rendering a cell.

  1. client-side: the grid is in need for data (first load, or scrolling)
  2. the grid requests the needed range of rows from its DataSource, which in the default implementation is the RpcDataSource.
  3. the RpcDataSource makes a request to its server-side counterpart
  4. server-side: for each column the associated renderer is asked to encode the data from the container
  5. the renderer uses getValue() to retrieve the serverside java value object which it will encode to json
  6. before receiving the value converters are applied to convert from data model to presentation form
  7. the renderer's serverside encode(T value) function is called. T is the presentation type this renderer supports.
  8. (since 7.6) if any DataGenerators are registered with the grid the DataGenerator its generateData(Object itemId, Item item, JsonObject jsonObject) function is called. the fully json encoded row is passed to this function and additional modifications can be made by the DataGenerator (e.g. add additional information)
  9. client-side: the renderer's client-side connector retrieves the json value it should display and uses the decode(JsonValue jsonConnectorId) function of its connector.
  10. now the client-side renderer's render(...) function is called. It gets passed the cell-reference and the value it is supposed to display as returned from the decode(...) function. When you are using a WidgetRenderer you also have access to the widget contained in the cell.
  11. the row scrolls out of sight and out of the row-cache
  12. the row's data which is cached on the client-side is destroyed
  13. server-side(since 7.6) if DataGenerators are registered with the grid the destroyData(Object itemId) function of the DataGenerator is called.

The complete rendering lifecycle of the component renderer

Problems I encountered (with vaadin 7.5)

  1. although the grid implements HasComponents it does keep track of components in its hierarchy in its own way, meaning if you do setParent(grid) on a component it will not be added to the grid's child components and therefore it will have no effect (not added to the hierarchy) :( I circumvented that in my first trys by keeping of the renderer-components myself and overwriting the iterator() method of the grid (either by extending from Grid, or by a cglib-decorator) adding my renderer components to the grid's own tracked components
  2. Adding components to the hierarchy slows down the component. So a grid with 1000 rows worked, but with 10000 rows it got barely useable, that was in chrome. Firefox seems not to do some optimisations or is in general much slower in measuring/preventing reflow. It was killed already by several hundreds of rows. Conclusion: you want to keep as little components in the hierarchy as possible
  3. When vaadin renders a component with relative size it determines all sizes of the components involved and does some calculations to translate the percent values into absolute values. That is part of the vaadin magic. Unfortunately measuring an element causes the browser to reflow/relayout, doing it a lot of time results in a severe slowing-down effect called "layout thrashing" (https://developers.google.com/web/fundamentals/performance/rendering/avoid-large-complex-layouts-and-layout-thrashing). The vaadin core is already highly optimized to minimize reflow, but some browsers are better (chrome) than others (firefox) in measuring elements and optimizing reflow.
  4. Whereas you have a clear entrypoint when components have to be available to the renderer and therefore added to the hierarchy (encode()) there was no possibility to determine when it is not needed anymore. I tried to circumvent that by keeping track of the components widgets (if a widget in a cell was replaced with a different one I could throw the component away). The destruction of the component had to be made on the server-side so a server-side-rpc was necessary leading to rpc-spamming. Caching of the list of disposable components did leverage that a bit with the downside of having a bit more components in the hierarchy than necessary.
  5. The renderer's encode() function only gets passed the value but has no information about its context (e.g. the rowId). I used some homemade component tracking using ItemSet and PropertySet listeners. Let's just say, it was quite painful.

Beside all these problems, the 7.5 version of the renderer worked already extremly well in chrome and was useable in firefox if not too many rows were used (up to 1000).

Vaadin 7.6: The brave new world

Luckily these problems also were noticed at vaadin, so Teemu added some pretty neat features to the grid, which made most of my workarounds unnecessary. Joining the Vaadin Boattrip to JFokus I had the chance to meet him in person and during the conference whenever there was time we fixed the renderer to use the new features.

  1. HasComponents-Workaround. 7.6 grid contains the functions addComponentToGrid(Component component) and removeComponentFromGrid(Component component) which allow to add own components to the grids hierarchy.
  2. Big Hierarchy Problem Remember the conclusion: you want to keep as little components in the hierarchy as possible. With the introduction of DataGenerator this is quite easy (see below).
  3. Measurement/Reflow-Problem not solved, but if you set fixed sizes on your component the browser does not need to measure.
  4. Component-Destruction Workaround DataGenerator again saves the day. Its destroy(...) function is the hook I was waiting for.
  5. Renderer Context Workaround DataGenerator again it is. The generateData(...) function is aware of its context.