This Vaadin project intends to demonstrate some capabilities of the Vaadin web framework.
The material can be presented in different ways:
- As a full-fledged workshop
- As live-coding in front of an audience
- As a tutorial
The project is licensed under the terms of GPL v3, although if you use it - as you're encouraged to do, I would appreciate if you would:
- mention this Github project
- give me credit in your slides
- ping me so that I know who uses it
Feedback and pull requests are also much appreciated!
There are only a few prerequisites to this workshop:
- Java: the minim Java version Vaadin is compatible with is 6. However, version 8 provides lambda expressions.
- Maven: this workshop provides a POM to retrieve dependencies and package the WAR
- Your IDE of choice: get your favorite IDE, be it IntelliJ IDEA, Eclipse or Oracle NetBeans
If you're interested to go further into Vaadin, I would suggest some of my personal resources:
- morevaadin.com contains a number of articles on a single dedicated subject
- Learning Vaadin 7 is a whole book dedicated to learning Vaadin 7 from the ground up
Features are presented in a step-by-step way. A tag demoes a specific feature (or a tight group thereof):
Tag | Feature |
---|---|
v7.2-1 | Starting point |
v7.2-2 | Basic UI stuff |
v7.2-3 | Architecture |
v7.2-4 | More UI stuff |
v7.2-5 | Tabular data |
v7.2-6 | Push |
If at any point, you're lagging behind in the workshop, you can easily get to the next step byt checking out the desired tag:
git checkout v7.2-2
Vaadin provides a Maven archetype, which can be used with the following command-line:
mvn -B archetype:generate -DarchetypeGroupId=com.vaadin -DarchetypeArtifactId=vaadin-archetype-application -DarchetypeVersion=7.2.5 -DgroupId=ch.frankel.vaadin.workshop -DartifactId=vaadin-workshop -Dversion=1.0-SNAPSHOT -Dpackaging=war
This will generate a not-so-empty Vaadin application, with more than what is strictly necessary in this workshop. At this step, the project has been cleaned up to remove widgetset-specific code and POM configuration.
Now, let's get to work:
- Create the WAR package with Maven:
mvn package
- Look at the generated package
- Deploy the WAR in a servlet container (e.g. Tomcat or Jetty)
- Play with the application
- Look at the raw source
- Look at the generated HTML (with Chrome's Developer Tools or Firebug for Firefox)
- Finally, look at the code
- Try to put everything together
In the Vaadin framework, components can be added to the GUI to be displayed. Most of them are very similar to those in Swing (e.g. TextField
, PasswordField
, etc.). They are located under the com.vaadin.ui
package.
- Note 1: some Vaadin components have no visual display, but this is the exception, not the rule.
- Note 2: most of Vaadin component constructors require a
String
argument. This is the label associated with the component. - Note 3:
UI
is a container, not a layout - it can contain only a single child.
Containers are specialized components that can have other components (including containers) as children. In turn, layouts are specialized containers that lay out their children in a specific way.
Try the following, be sure to check the display after each step:
- Try adding a few components to the
VerticalLayout
- Change the
VerticalLayout
to aHorizontalLayout
- Change the
HorizontalLayout
to aFormLayout
Note: it is possible to nest layouts inside layouts. However, as they translate as div
, client computers may run into performance problems rendering too many nested levels.
In case of need, to debug layout issues, use the debug=true
query parameter.
Though complete theming is available through Cascading Style Sheets, it's possible to use just Java to customize appearance. As previously, check the display after each code change:
- There's no space between the window border and the password label. Use the
setMargin()
method on the layout. - Use the
setHeight()
orsetWidht()
on different components. Their parameter is a CSS size (e.g50px
or100%
). There's a shortcut method forsetHeight("100%")
andsetWidht("100%")
namedsetSizeFull()
Note: aligning components inside the layout is of course possible, but require references to the former and the later as well as defined sizes for each of them
Check the page title in the browser: it is set to the page's location. Having a relevant title would go a long way toward a better user-experience. To achieve that, Vaadin provide the Page
object:
Page.getCurrent().setTitle("Vaadin Workshop");
Note: the static getCurrent()
method is available on many Vaadin objects. It uses the ThreadLocal
pattern, so that you don't have to pass references around.
Alternatively, if the title is static, setting the title can be achieved through the @Title
annotation on the UI
.
@Title("Vaadin Workshop")
public class MyVaadinUI extends UI {
...
}
OOP is about componentization and reusability. At this point, we only have a single class with everything coded inside it, which defeats its purpose.
The following steps consist of architectural improvements, so make sure the displayed result should stay the same throughout of all them.
Servlet 3.0 and associated annotations is a good way to replace the legacy web.xml
, despite the overhead to subclass the VaadinServlet
class. However, this doesn't mean the latter should be an inner class of the UI
.
Extract the configuration servlet into its own class in the ch.frankel.vaadin.workshop.web
package.
Java 8 allows to use lambda expressions in place of single method interfaces. Replace the Button.ClickListener
with a lambda.
As a general rule, anonymous inner classes (or lambdas) should be avoided and top-level abstractions should be promote instead. Refactor the code to do this. Passing the layout reference as a constructor arguments is necessary (but please do not create accessors, think immutability). Put the newly created class into a dedicated ch.frankel.vaadin.workshop.behavior
package and name it DummyListener
. We'll create more useful listeners in this package later.
Also, move the UI
class into a ch.frankel.vaadin.workshop.ui
package.
The UI
represents the inner content of the browser window. To make parts of it reusable, they must be made top-level components.
Extract the FormLayout
into its own dedicated class. This works but binds the component irremediably to the layout.
To make more configurable components, create components that extends CustomComponent
and add a constructor that accepts a layout. In our case, name the component LoginScreen
and the layout parameter should be of type AbstractOrderedLayout
.
A CustomComponent
is set its content either through its super constructor or through the setCompositionRoot()
method.
public class LoginScreen extends CustomComponent {
public LoginScreen(AbstractOrderedLayout layout) {
super(layout);
}
}
Instantiate the FormLayout
in the UI
and pass it to the constructor.
@Override
protected void init(VaadinRequest request) {
setContent(new LoginScreen(new FormLayout()));
}
Vaadin brings unique and powerful features within your reach. This section is dedicated to a few of them.
Vaadin implements the Single Page Interface paradigm.
An application which always displays the same components is not interesting. This section is dedicated to switch screens.
We will use a chat application mockup as an illustration. As the LoginScreen
has already been developed previously, we just need a new screen ChatScreen
. Create this component and for now, just add a simple label as the composition root to have something displayed.
Switching screens is achieved by using the UI.getCurrent().setContent(...)
. Put it in a dedicated LoginListener
and set this LoginListener
as the button's new behavior. Don't implement anything for the login logic yet. Change the button's label accordingly (from "Click me!" to "Login"). Verify that when the button is clicked, the displayed screen changes.
@Override
public void buttonClick(Button.ClickEvent event) {
UI.getCurrent().setContent(new MainScreen());
}
Refresh the browser window (press F5): the display is reset to the first login screen. The reason is that the init()
method of the UI
is called. This is not so good when one has to develop screens that are set after a long sequence of screen changes. In order for the server state to be saved across browser refreshes, annotate the UI
with @PreserveOnRefresh
.
Note: to force init()
to be called, add restartApplication
to the query parameters
Notifications are the Vaadin way to notify the user about events of interest happening. They are accessible through the Notification
class. Use it to let the user he has been successfully logged in.
Notification.show("You've been successfully logged in");
Simple notifications simply disappear when the user moves the mouse, which is not adapted to error. Implement a simple login logic in the listener and display an error notification when the user fails authentication: this is possible through the Type.ERROR_MESSAGE
parameter in the show()
method.
Notification.show("Wrong credentials", ERROR_MESSAGE);
Notice Vaadin cleverly hides the JavaEE API, so that there's no straightforward access to many objects, such as HttpSession
, HttpServletRequest
and HttpServletResponse
objects. This doesn't mean that their features are not accessible: in particular, storing data into the user session is provided via the VaadinSession.getCurrent().setAttribute()
method. Store the login field value into the session and display it as a label on the main screen.
Attributes can be stored by key as usual, but also by class. This makes storing full-fledged objects such as User
a breeze.
VaadinSession.getCurrent().setAttribute(User.class, user);
For a chat application, the ChatScreen
needs a few components:
- One
Table
component to display sent messages - One
TextArea
component to type messages - One
Button
to effectively send messages
Use what you've learned previously to add those components to the screen as in the following mockup:
Also, create a Listener
to move the typed message from the message input to a row in the table. This first requires configuring the table to have a single column of type String
.
table.addContainerProperty("Message", String.class, null);
For the moment, simply use the Table.addItem(Object[] cells, Object itemId)
method to add a new message.
table.addItem(new Object[] { message }, message.hashCode());
Of course, using a table to add a single piece of data is useless. Let's try to add more data:
- Author (i.e. the logged-in user)
- Date and time
- And keep the message, of course
Using Object
arrays to structure the table model is extremely fragile. Fortunately, Vaadin provides an extremely rich structured model. In particular, there're 3 levels of abstraction available:
Property
to wrap a simple value (e.g. a date)Item
to wrap objects - a collection of properties (e.g. a person)Container
to wrap a collection objects
Container
is extremely well-suited to be displayed in a table component. Each Item
in a container (i.e. row) is identified by a unique identifier dependent on the container's implementation.
BeanContainer
has to provide a custom identifier generator- In a
BeanItemContainer
, theItem
itself is the identifier - In a
SQLContainer
, the identifier is the primary key of the underlying database row
Leaving SQLContainer
aside for now, choose the right Container
implementation. Then, create a class to hold author, message and date. Finally, binding the container to the table is extremely simple with the table.setContainerDataSource()
method.
Note that by using a container data source, items are to be added on the container instead of the table. Vaadin out-of-the-box components that are set a data source register it so they will reflect changes to the underlying model.
Message message = new Message(author, text, date);
BeanItemContainer<Message> container = new BeanItemContainer<>(Message.class);
container.addBean(message);
Table components can be heavily customized in a variety of ways.
-
Headers
-
By default, column headers take the value of the underlying property. However, they can be customized via the
table.setColumnHeaders(String...)
. Use it to change the column header labels. -
By using a container datasource, the initial order of the columns is hard to guess. Besides, underlying wrapped class could change, so the previous method is quite fragile. An alternate way to customize header labels is throught the
table.setColumnHeader(String, String)
method. The firstString
parameter is the property's name, the second one the column header's label to set. Use this alternate way to change column labels. -
Column headers can also be hidden. In fact, the default header display is governed by the
columnHeaderMode
property of the table. Check its different possible values and hide headers altogether.
-
-
Columns
-
Columns can be hidden or ordered in a different way, both thanks to the
table.setVisibleColumns(String[])
. The array parameter is the ordered list of all container properties to be displayed on the table. -
As any components, columns can be set a width with
table.setColumnWidth(String, int)
. The first parameter is the property name, the second the width in pixel. Use this to resize the author and timestamp columns. -
Column cells rendering can also be customized. By default, the property's value is formatted into a
String
and displayed in aLabel
. Most types are transformed usingtoString()
, but some (e.g.java.util.Date
) are handled in a specific way. To change the rendering of a column, implement a newColumnGenerator
and add it to the table withtable.addGeneratedColumn(String, ColumnGenerator)
.@Override public Object generateCell(Table source, Object itemId, Object columnId) { Item item = source.getItem(itemId); Property property = item.getItemProperty(columnId); Object value = property.getValue(); return MyComponent(value);
The returned type can be either
Component
orString
. In the latter case, it will be wrapped into a label to be displayed.Even better, column generators can be used to add entirely new columns. Write such a generator to display a column with a "Delete me" button. Then add a behavior so that when the button is pressed, the associated message is removed from the container. Hint: store the
Item
's id and the reference to the container with theButton.setData(Object)
and remove the item with theBeanItemContainer.removeItem(Object)
.
-
Extract all table-related code into its own decicated MessageTable
class.
At this point, migrating to a SQL database instead of a in-memory container is straightforward: just change the BeanItemContainer<Message>
type to a SQLContainer
type.
Of course, some adaptations are in order:
-
The main changes has to be made in the
MessageTable
. Replace the previousBeanItemContainer
and along, replaceMessage
property names by the names of the database columns (TIME_STAMP
,AUTHOR
andTEXT
). Note some of the above objects have to be used, choose wisely!JDBCConnectionPool connectionPool = ... QueryDelegate queryDelegate = ... Container container = new SQLContainer(queryDelegate);
For connection parameters, use the available
Parameters
class. -
In the click listener, replace usage of the
addBean()
method and theMessage
class with theaddItem()
method and getting the desired property to set its value:Object rowId = output.addItem(); RowItem rowItem = (RowItem) output.getItem(rowId); rowItem.getItemProperty("DB_COLUMN").setValue(property);
-
By default, the SQL container doesn't commit changes automatically to the underlying database. Either set it auto-commit or commit each change (add and delete) explicitly.
Try the application wit these changes. Notice that the line added to the table will be empty. In order to sync changes made to the container, call its refresh()
method.
Icing on the cake, the "add items" feature can be extracted from the listener to the table, so that as to promote encapsulate.
A messaging application with only one user is not really usable. This is however what happens with the current state of our app.
To fix it, we need messages sent from one client to be broadcasted to other logged in client. This translates into the code with a Broadcaster
singleton that will take care of:
- Registering clients. At startup, each client will register itself to the broadcaster.
- Un-registering them. Clients must be made to unregister so as to prevent memory leaks.
- Broadcasting messages. Messages will be sent to all registered clients.
Vaadin doesn't offer such a class out-of-the-box but it's provided in the project.
Do the following:
-
Make the
UI
implementBroadcastListener
and override the method to refresh the table.Note: at present, tables don't refresh the display either throught their container's
refresh()
method or their ownrefreshRowCache()
method. Either replace the table with a new instance or the screen with a new screen - the latter is simpler. -
In the
UI.init()
method, callBroadcaster.register()
to register the current instance. -
In the
UI.detach()
method, call the oppositeBroadcaster.unregister()