Page tree
Skip to end of metadata
Go to start of metadata

On this page we explain the building blocks of the content app framework and provide some guidelines.

Building blocks

This diagram shows the building blocks you need to create a custom content app:

  1. Data source contains the data such as products.
  2. ContentConnector is a bridge to the data source. It helps you read content from data sources outside the JCR repository.
  3. Container represents the data source in a Vaadin context. It provides methods to get, add and remove items and properties. 
  4. ContentPresenter uses the data in the Container to draw a user interface.

Container, ItemIds and Items

We start with some Vaadin basics which you should know. We recommend to read Vaadin documentation for general information about  Container  and binding components to Data

A Container represents a data source in a Vaadin context. The Container provides methods to get, add and remove items and properties in the data source. The Container can hold 0 to n items. An Item can hold 0 to n Properties. The Presenter uses the data of a Container to render a Content view.

  1. Container wraps the data of the data source. It provides methods to get items by their ID, to get all Items, to get all itemIds and so on. Every Container must implement at least the Container interface.
  2. ItemId is the key to get an item. Vaadin doesn't type the itemId. It is just an object. A simple itemId could be a string. In some cases an itemId is a POJO with its own properties, for example  JcrItemId . itemIds are sent by events.

  3. Item is comparable to a row in a database table; it must implement the Vaadin Item interface. An item contains a set of properties and offers methods to add, modify and remove the properties.
    Every property is identified by a property ID. The property ID is not typed either, it is just an object, usually a string. Use the property ID to configure columns in the content view.
  4. Property is the Vaadin interface for a item property. A property has a value and a type.

You must configure the Container before adding items: The Container must know the properties (IDs and types) which it will display. You can add an item only, if all of its properties are known to the Container. It is valid to add an item which has not all properties as the configured Container.

Guidelines for creating Containers

  • Write methods of the Container interface are not required when running in the content app. 

  • If you want to manipulate data - add, edit, delete - do not manipulate Container items. Instead, create actions that interact directly with the data source. After an action is executed, the view and its underlying Container are refreshed automatically if the Container implements Refreshable .
  • Create a fully customized Container or extend a concrete implementation provided by Vaadin or Magnolia.
  • When creating a fully custom Container, implement "empty" write methods, they are not required.

     Click to see the example of an "empty" method
    @Override
    public Object addItem() throws UnsupportedOperationException {
        throw new UnsupportedOperationException("addItem is not supported!");
    }

Mandatory and optional Container interfaces

In addition to the base interface Container there are a number of subinterfaces. Which of them should a custom Container implement?

  • The Container interface is mandatory.
  • A Container of a tree view additionally requires at least Container.Hierarchical. Implementing Collapsible is not mandatory but helpful in a custom tree Container.
  • For lazy loading behavior implement also Container.Indexed.
  • Implementing  Refreshable  is optional but recommended.

Concrete Vaadin Containers

Vaadin offers some concrete Containers. Use IndexedContainer for a list and HierarchicalContainer for a tree. The hierarchical Container extends the list. Both extend AbstractInMemoryContainer.

To prepare a concrete Vaadin Container within a content subapp, create the data structure using the Container's write methods to add the data which goes into memory.

Concrete Vaadin Containers are difficult to use for lazy loading. If you find a nice lazy loading solution in a subclass of HierarchicalContainer, please leave a comment!

Completely custom Containers

For a completely custom Container, implement the required interfaces. Depending on the data source and your requirements concerning speed in a production environment your Container probably should have these abilities:

  • Lazy loading
  • Caching data 

Guidelines for ItemIds

The Vaadin API leaves it open how you want to implement ItemIds. In the Vaadin API an ItemId is just an Object. Think carefully what ItemId to use.

  • An ItemId must be unique within a Container.
  • The itemId must carry enough information to fetch data from the data source to instantiate its corresponding Item. This is required in the context of ContentConnector. For example, JcrItemId is a plain Java object that carries two properties: uuid and workspace. These two properties are sufficient to get data from a JCR repository given that there is access to a Session.
  • It must be possible to get an ItemId by a given Item in the context of ContentConnector.

What is a good ItemId? String or POJO?

A String is a very simple and easy implementation for an ItemId. If your data source provides a unique String which is sufficient to build the corresponding item by querying the data source using only the String, then a String as ItemId is fine. However, if you must concatenate a String to build a unique itemId you should rather implement a plain Java object (POJO) for the ItemId.

When using a POJO you will require a method which creates a String representation of your ItemId. Furthermore it must be possible to create an ItemId (of your type, the POJO) by the String representation.

Creating Items

When using a concrete Container from Vaadin, you won't need a specific Item implementation, instead you have to add items by calling:

Item item = addItem(Object itemId);

which returns an Item on which you have to set properties.

Tip: When using a Container which extends IndexedContainer , Items returned by #addItem already have set properties with default values. You cannot add the properties again. Instead, you have to set the values on the already existing properties.

Object itemId = createItemId(/* some arguments here */)

Item item = indexedContainer.addItem(itemId);

// Adding a property will fail. (Property has already been set by container with default value.)
// item.addItemProperty("property-id", new ObjectProperty("value"));

// Instead, change the value of the properties that are already set.
item.getItemProperty("property-id").setValue("new value");

When you implement a fully customized Container you most probably require a custom Item. Extend a Vaadin implementation. BeanItem and PropertysetItem are both adequate for many use cases.

Content view Presenter knows columns and provides a Container

A Magnolia content app contains at least one subapp, displayed to users as a tab. A browser subapp is a special type of subapp that contains a workbench for operating on content items.

The workbench must have at least one content view. Magnolia provides three view types out of the box: tree, list and thumbnail. You can also define other customized views.

To add a content view you need at least:

  • ContentPresenter   implementation
  • ConfiguredContentPresenterDefinition  implementation 

A content view is rendered by the content presenter which must be configured separately for each view. The data rendered on the view is provided by the Container. The presenter instantiates a suitable Container. 

A tree view requires a Container which implements the  Container.Hierarchical  interface. For list and thumbnail views it is sufficient to have a Container which implements only the base interface  Container.

This means that you will probably need different Containers for different views.

The subapp has exactly one ContentConnector  which must work for all used Containers within the sub-app.

Content view configuration

A content view configuration requires the following parts:

  1. ContentPresenter for every content view in a content app. 
    1. class property
    2. columns node for configuring columns
  2. ContentPresenterDefinition  to set the implementation class of the custom content presenter.
  3. ContentPresenter that extends AbstractContentPresenterBase or one of its subclasses. Connected to a Container. Must implement the AbstractContentPresenterBase#initializeContainer method.
  4. Container that is bound to the content presenter.

Column configuration

Every column in the view must be configured. Example:

Node nameValue

 contentViews

 

 columns

 

 title

 

 class

info.magnolia.flickr.app.contentview.FlickrBrowserItemColumnDefinition

 formatterClass

info.magnolia.flickr.app.contentview.FlickrBrowserItemColumnFormatter

 propertyName

title
contentViews:
  columns:
    - name: title
      class: info.magnolia.flickr.app.contentview.FlickrBrowserItemColumnDefinition
      formatterClass: info.magnolia.flickr.app.contentview.FlickrBrowserItemColumnFormatter
      propertyName: title

Properties:

<column name>

required

The name of a column is arbitrary, however, it makes sense to use the same or similar name as the propertyName.

class

required

A definition class which must extend  AbstractColumnDefinition .

formatterClass

required

A formatter class which must extend  AbstractColumnFormatter .

propertyName

required

Must be equal to a Container property ID.

For all properties see Column definition.

ContentConnector knows the relation between item, itemId and URL fragment

The ContentConnector  is a core piece for a content app. It knows which item belongs to an itemId and vice versa. The connector can create a URL fragment from an itemId and vice versa.

URL fragments and events

When the user selects an item in a content app a  SelectionChangedEvent  is fired. The event carries the ID of the selected item. The event leads to a change in the  BrowserLocation  object of the subapp. That's why the URL fragment methods are required.

ContentConnector methods

If you look at the  Container  interface, its methods are similar to methods in the ContentConnector:  

info.magnolia.ui.vaadin.integration.contentconnector.ContentConnector
public interface ContentConnector {
    String getItemUrlFragment(Object itemId);
 	Object getItemIdByUrlFragment(String urlFragment);
    Object getDefaultItemId();
    Item getItem(Object itemId);
    Object getItemId(Item item);
    boolean canHandleItem(Object itemId);
}

ContentConnector's relation to Container

A ContentConnector must implement methods that are very similar to methods in the Container. That said, it can be handy to have an instance of the Container in your ContentConnector. However, as soon as you have more than one Container this pattern no longer works.

The presenter responsible for a view must initialize the Container. If you can use one Container for all presenters, it is fine to "couple" the ContentConnector with the Container. For example, if a subapp uses a list view and a thumbnail view it should be possible to use the same Container.

If you need more than one Container within a sub app, do not use Container in the ContentConnector implementation but get data directly from your data source the same way you retrieve data when preparing data for the Container.

You may implement a utility class which provides access to the data source and creates ItemIds and Items to use both in Container and contentConnector.

Best practice

Make the Container instance available in your ContentConnector. This makes it easier to implement item and itemId related methods. This works only if you have only one Container per subapp. If you need more Containers you must decouple the Container and the ContentConnector.

Configure and register a ContentConnector

Every subapp must provide and configure and register its own content connector . Once registered, you can get the ContentConnector via injection. 

  1. Implement  ContentConnector .
  2. Create an interface which extends ContentConnectorDefinition , for example FlickrBrowserContentConnectorDefinition.
  3. Create a class which implements your interface and extends ConfiguredContentConnectorDefinition . Set the implementation class.
  4. Configure the connector in the subapp:

    Node nameValue

     <subapp>

     

     contentConnector

     

     class

     info.magnolia.flickr.app.contentconnector.FlickrBrowserContentConnectorDefinition
    <subapp>:
      contentConnector:
        class:  info.magnolia.flickr.app.contentconnector.FlickrBrowserContentConnectorDefinition

Injecting the ContentConnector

ContentConnectorProvider  creates one instance of the type of connector you set in ConfiguredContentConnectorDefinition. You can inject your ContentConnector into different classes wherever it is required. You will always get the same instance.

Example:

public class FlickrBrowserTreePresenter extends TreePresenter {
    private FlickrBrowserContentConnector contentConnector;
    @Inject
    public FlickrBrowserTreePresenter(TreeView view, ComponentProvider componentProvider, ContentConnector contentConnector) {
        super(view, componentProvider);
        this.contentConnector = (FlickrBrowserContentConnector) contentConnector;
    }
}

Since your subapp implementation class is either  BrowserSubApp or a subclass, your subapp already injects the ContentConnector. 

  • No labels