UI Layout & Hints

In implementing the naked objects pattern, Apache Isis infers as much information from the domain classes as possible. Nevertheless, some metadata relating solely to the UI is inevitably required.

The Apache Isis programming model includes several mechanisms to influence the way in which the domain objects are rendered in the UI.

  • the layout of application menu items (inferred from domain services)

  • the layout of domain objects (domain entities andview models ) allow the positioning of the object members into columns and tabs

  • UI hints provided by the domain object itself returning:

    • the title so the end-user can distinguish one object from another

    • an icon to indicate the type of object (and perhaps its state)

    • CSS styles for additional adhoc styling

    • an alternate layout, for example changed according to the roles of the end-user that is viewing it.

  • in collections, how to customise which properties of the associated object appear as columns.

This chapter discusses these topics.

Names and Descriptions

The name of classes and class members are usually inferred from the Java source code directly. For example, an action method called placeOrder will be rendered as "Place Order", and a collection called orderItems is rendered as "Order Items". The same is true for action parameter names also, though note that the code must be compiled with the --parameters flag (to javac).

Occasionally though the desired name is not possible; either the name is a Java reserved word (eg "class"), or might require characters that are not valid, for example abbreviations.

In such cases the name can be specified declaratively. It is also possible to specify a description declaratively; this is used as a tooltip in the UI.

The table below summarizes the annotations available:

Table 1. Names and descriptions
Feature Named Description

Class

Property

Collection

Action

Action Parameters

The framework also supports i18n: locale-specific names and descriptions. For more information, see the beyond-the-basics guide.

Titles, Icons etc.

In Apache Isis every object is identified to the user by a title (label) and an icon. This is shown in several places: as the main heading for an object; as a link text for an object referencing another object, and also in tables representing collections of objects.

The icon is often the same for all instances of a particular class, but it’s also possible for an individual instance to return a custom icon. This could represent the state of that object (eg a shipped order, say, or overdue library book).

It is also possible for an object to provide a CSS class hint. In conjunction with customized CSS this can be used to apply arbitrary styling; for example each object could be rendered in a page with a different background colour.

Finally, a domain object can even indicate the layout that should be used to render it. For example, a domain object may be rendered differently depending upon the role of the user viewing it.

Object Title

The object title is a label to identify an object to the end-user. Generally the object title is a label to identify an object to the end-user. There is no requirement for it to be absolutely unique, but it should be "unique enough" to distinguish the object from other object’s likely to be rendered on the same page.

The title is always shown with an icon, so there is generally no need for the title to include information about the object’s type. For example the title of a customer object shouldn’t include the literal string "Customer"; it can just have the customer’s name, reference or some other meaningful business identifier.

Declarative style

The @Title annotation can be used build up the title of an object from its constituent parts.

For example:

import lombok.Getter;

public class Customer {

    @Title(sequence="1", append=" ")    (1)
    @Getter
    private String firstName;

    @Title(sequence="2")                (2)
    @Getter
    private String lastName;

    // ...
}
1 First component of the title, with a space afterwards
2 Second component of the title

might return "Arthur Clarke", while:

import lombok.Getter;

public class CustomerAlt {

    @Title(sequence="2", prepend=", ")
    @Getter
    private String firstName;

    @Title(sequence="1")
    @Getter
    private String lastName;

    // ...
}

could return "Clarke, Arthur".

Note that the sequence is in Dewey Decimal Format, which allows a subclass to intersperse information within the title. For example:

import lombok.Getter;

public class Author extends Customer {

    @Title(sequence="1.5", append=". ") (1)
    @Getter
    private String middleInitial;

    // ...
}
1 "Slots" between the components defined by the superclass

could return "Arthur C. Clarke".

Imperative style

Alternatively, the title can be provided simply by implementing the title() reserved method.

For example:

public class Author extends Customer {

    public String title() {
        final StringBuilder buf = new StringBuilder();
        buf.append(getFirstName());
        if(getMiddleInitial() != null) {
            buf.append(getMiddleInitial()).append(". ");
        }
        buf.append(getLastName();
        return buf.toString();
    }

    ...
}

A variation on this approach also supports localized names; see beyond-the-basics guide for further details.

Using a UI subscriber

A third alternative is to move the responsibility for deriving the title into a separate subscriber object.

In the target object, we define an appropriate event type and use the @DomainObjectLayout#titleUiEvent() attribute to specify:

@DomainObjectLayout(
    titleUiEvent = Author.TitleUiEvent.class
)
public class Author extends Customer {
    public static class TitleUiEvent
            extends org.apache.isis.applib.events.ui.TitleUiEvent<Author> {}

    //...
}

The subscriber can then populate this event:

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import lombok.val;

@Service
public class AuthorSubscriptions {

    @EventListener(Author.TitleUiEvent.class)
    public void on(Author.TitleUiEvent ev) {
        val author = ev.getSource();
        ev.setTitle(titleOf(author));
    }

    private String titleOf(Author author) {
        val buf = new StringBuilder();
        buf.append(author.getFirstName());
        if(author.getMiddleInitial() != null) {
            buf.append(author.getMiddleInitial()).append(". ");
        }
        buf.append(author.getLastName());
        return buf.toString();
    }
}

UI listeners are useful when using third-party libraries or extensions.

Object Icon

The icon is often the same for all instances of a particular class, and is picked up by convention.

It’s also possible for an individual instance to return a custom icon, typically so that some significant state of that domain object is represented. For example, a custom icon could be used to represent a shipped order, say, or an overdue library loan.

Declarative style

If there is no requirement to customize the icon (the normal case), then the icon is usually picked up as the .png file in the same package as the class. For example, the icon for a class org.mydomain.myapp.Customer will be org/mydomain/myapp/Customer.png (if it exists).

Alternatively, a font-awesome icon can be used. This is specified using the @DomainObjectLayout#cssClassFa() attribute or in the layout.xml file.

For example:

@DomainObjectLayout( cssClassFa="play" )    (1)
public class InvoiceRun {
    ...
}
1 will use the "fa-play" icon.

Imperative style

To customise the icon on an instance-by-instance basis, we implement the reserved iconName() method.

For example:

public class Order {
    public String iconName() {
        return isShipped() ? "shipped": null;
    }
    // ..
}

In this case, if the Order has shipped then the framework will look for an icon image named "Order-shipped.png" (in the same package as the class). Otherwise it will just use "Order.png", as normal.

Using a UI subscriber

As for title, the determination of which image file to use for the icon can be externalized into a UI event subscriber.

In the target object, we define an appropriate event type and use the @DomainObjectLayout#iconUiEvent() attribute to specify.

For example:

@DomainObjectLayout(
    iconUiEvent = Order.IconUiEvent.class
)
public class Order {
    public static class IconUiEvent
            extends org.apache.isis.applib.events.ui.IconUiEvent<Order> {}
    // ..
}

The subscriber can then populate this event:

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import lombok.val;

@Service
public class OrderSubscriptions {

    @EventListener(Order.IconUiEvent.class)
    public void on(Order.IconUiEvent ev) {
        val order = ev.getSource();
        ev.setIconName(iconNameOf(order);
    }

    private String iconNameOf(Order order) {
        return order.isShipped() ? "shipped": null;
    }
}

Object CSS Styling

It is also possible for an object to return a CSS class. In conjunction with a viewer-specific customisation of CSS (eg for the Web UI (Wicket viewer), seehere) this can be used to apply arbitrary styling; for example each object could be rendered in a page with a different background colour.

Declarative style

To render an object with a particular CSS, use @DomainObjectLayout#cssClass() or in the layout.xml file.

The usage of this CSS class is viewer-specific. In the case of the Web UI (Wicket viewer), when the domain object is rendered on its own page, this CSS class will appear on a top-level <div>. Or, when the domain object is rendered as a row in a collection, then the CSS class will appear in a <div> wrapped by the <tr> of the row.

One possible use case would be to render the most important object types with a subtle background colour: Customers shown in light green, or Orders shown in a light pink, for example.

Imperative style

To specify a CSS class on an instance-by-instance basis, we implement the reserved cssClass() method.

For example:

public class Order {
    public String cssClass() {
        return isShipped() ? "shipped": null;       (1)
    }
    ...
}
1 the implementation might well be the same as the iconName().

If non-null value is returned then the CSS class will be rendered in addition to any declarative CSS class also specified.

Using a UI subscriber

As for title and icon, the determination of which CSS class to render can be externalized into a UI event subscriber.

In the target object, we define an appropriate event type and use the @DomainObjectLayout#cssClassUiEvent() attribute to specify.

For example

@DomainObjectLayout( cssClassUiEvent = Order.CssClassUiEvent.class )
public class Order {
    public static class CssClassUiEvent
            extends org.apache.isis.applib.events.ui.CssClassUiEvent<Order> {}
    // ..
}

The subscriber can then populate this event:

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import lombok.val;

@Service
public class OrderSubscriptions {

    @EventListener(Order.CssClassUiEvent.class)
    public void on(Order.CssClassUiEvent ev) {
        val order = ev.getSource();
        ev.setCssClass(cssClassOf(order));
    }

    private static String cssClassOf(Order order) {
        return order.isShipped() ? "shipped": null;
    }
}

Switching Layouts

A domain object may also have multiple layouts. One reason might be based on the role of the user viewing the object; the object members most relevant to a data entry clerk could be quite different to an manager that is viewing, eg to approve it. The layout could be used to hide some object members, show others.

If an alternative layout is indicated (we’ll look at the mechanics of this below), then this is used to locate an alternative layout file. For example, if the "edit" layout is specified, then the Xxx.edit.layout.xml file is used (if it exists).

More generally, for a given domain object Xxx, if it has specified a layout yyy, then the framework will search for a file Xxx.yyy.layout.xml on the classpath.

Imperative style

To specify the layout on an instance-by-instance basis, we implement the reserved layout() method.

For example:

public class IncomingInvoice {
    public String layout() {
        return isUserInDataEntryRole() ? "edit": null;
    }
    ...
}

Using a UI subscriber

As for title, icon and CSS, the determination of which layout class to render can be externalized into a UI event subscriber.

In the target object, we define an appropriate event type and use the @DomainObjectLayout#layoutUiEvent() attribute to specify.

For example

@DomainObjectLayout( layoutUiEvent = Order.LayoutUiEvent.class )
public class IncomingInvoice {
    public static class LayoutUiEvent
            extends org.apache.isis.applib.events.ui.LayoutUiEvent<Order> {}
    // ..
}

The subscriber can then populate this event:

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import lombok.val;

@Service
public class IncomingInvoiceSubscriptions {

    @EventListener(IncomingInvoice.LayoutUiEvent.class)
    public void on(IncomingInvoice.LayoutUiEvent ev) {
        val incomingInvoice = ev.getSource();
        ev.setLayout(layoutOf(incomingInvoice));
    }

    private static String layoutOf(IncomingInvoice incomingInvoice) {
        return isUserInDataEntryRole() ? "edit": null;
    }
}

Fallback layouts

In addition to searching for alternate layouts and then the default layout, in the absence of either the framework will also search for a "fallback" layouts.

The use case is to allow libraries that provide domain objects (for example, the SecMan extension) to define the UI of these objects using a layout file, while still allowing the consuming application to override that layout if it so requires.

Thus, for a domain object "Xxx", the framework searches:

  1. Xxx.yyy.layout.xml

    for layout "yyy" (if a non-null layout "yyy" is specified)

  2. Xxx.layout.xml

    the default layout

  3. Xxx.layout.fallback.xml

    the fallback layout

If none of these exist, then the framework will use a layout based on any available annotations. The page will be split 4:8, with the first column for properties and the second column for collections.

Action Icons and CSS

Apache Isis allows font awesome icons to be associated with each action, and for Bootstrap CSS to be applied to action rendered as buttons. These UI hints can be applied either to individual actions, or can be applied en-masse using pattern matching.

It is also possible to specify additional CSS for an object’s members (not just actions).

Icons

Action icons can be specified in several ways.

One option is to use the @ActionLayout#cssClassFa. For example:

@ActionLayout(cssClassFa="refresh")
public void renew() {
    ...
}

Alternatively, you can specify these hints dynamically in the layout file for the entity:

<cpt:action id="renew" cssClassFa="refresh"/>

Rather than annotating every action with @ActionLayout#cssClassFa and @ActionLayout#cssClass you can instead specify the UI hint globally using regular expressions. Not only does this save a lot of boilerplate/editing, it helps ensure consistency across all actions.

To declare fa classes globally, use the isis.applib.annotation.action-layout.css-class-fa.patterns configuration property (a comma separated list of key:value pairs).

For example:

isis.applib.annotation.action-layout.css-class-fa.patterns=\
                        new.*:fa-plus,\
                        add.*:fa-plus-square,\
                        create.*:fa-plus,\
                        renew.*:fa-refresh,\
                        list.*:fa-list, \
                        all.*:fa-list, \
                        download.*:fa-download, \
                        upload.*:fa-upload, \
                        execute.*:fa-bolt, \
                        run.*:fa-bolt

Here:

  • the key is a regex matching action names (eg create.*), and

  • the value is a font-awesome icon name

For example, "fa-plus" is applied to all action members called "newXxx"

CSS

Similarly, a CSS class can be specified for object members:

Again, this CSS class will be attached to an appropriate containing <div> or <span> on the rendered page.

Possible use cases for this is to highlight the most important properties of a domain object.

It is also possible to specify CSS classes globally, using the isis.applib.annotation.action-layout.css-class.patterns configuration property.

For example:

isis.applib.annotation.action-layout.css-class.patterns=\
                        delete.*:btn-warning

where (again):

  • the key is a regex matching action names (eg delete.*), and

  • the value is a Bootstrap CSS button class (eg `btn-warning) to be applied

The actions of domain services (annotated using @DomainService with a nature of VIEW) are made available as menu items on menus.

For example:

@DomainService(
        nature = NatureOfService.VIEW,                              (1)
        objectType = "simple.SimpleObjects"
        )
public class SimpleObjects {
    // ...
}
1 Domain service with actions visible in the UI

By default each domain service corresponds to a single menu on this menu bar, with its actions as the drop-down menu items.

By annotating the domain service class and its actions, it’s possible to have a little more control over the placement of the menu items; this is discussed below.

For more fine-grained control, though, the menubars.layout.xml file can be supplied, as discussed after. This file is also read dynamically at runtime, so layouts can be changed during prototyping and the page reloaded to check.

Annotation-based

If using annotations, services can be added to either the primary, secondary or tertiary menu bar, as shown in this screenshot shows:

layout menus

This is done using the @DomainServiceLayout#menuBar() annotation.

For example:

@DomainService(
        nature = NatureOfService.VIEW,                              (1)
        objectType = "simple.SimpleObjects"
        )
@DomainServiceLayout(menuBar = DomainServiceLayout.MenuBar.PRIMARY) (2)
public class SimpleObjects {
    // ...
}
1 Domain service with actions visible in the UI
2 Menu for the service added to the primary menu bar.

The tertiary menu bar consists of a single unnamed menu, rendered underneath the user’s login, top right. This is intended primarily for actions pertaining to the user themselves, eg their account, profile or settings:

tertiary

In addition, the @MemberOrder annotation can be used to group and order domain service actions:

  • @MemberOrder#name() is used to define the name of the menu

    in which the menu items of the domain service action will appear

  • @MemberOrder#sequence() is used to order menu items within that menu.

Thus, using name() it’s possible to group menu items from actions that are from different domain services.

There are some significant limitations, however:

  • domain services are added to the menu bar alphabetically. This cannot be controlled using annotations.

  • there is no way to order or group menu items from multiple domain services that appear on the same menu.

The annotation based approach is therefore useful during very early prototyping, but in real-world applications you should use file based menu layouts.

menubars.layout.xml

Rather than use annotations to specify the location of menu items corresponding to the domain services' actions, the framework instead allow domain service actions to be arranged using the menubars.layout.xml file.

This offers a number of benefits:

  • Probably most significantly, the layout can be updated without requiring a recompile of the code and redeploy of the app; fine-tuning the layout with your end users is easy to do

  • You’ll probably find it easier to reason about menu bars layout when all the hints are collated together in a single place (rather than scattered across the domain service classes as annotations).

There are some disadvantages to using file-based layouts:

  • file-based layouts are not typesafe: a typo will result in the metadata not being picked up for the element.

  • they also suffer from syntactic fragility: an invalid XML document could result in no metadata for the entire class.

The menubars.layout.xml file is just the serialized form of a MenuBars layout class defined within Apache Isis' applib. These are JAXB-annotated classes with corresponding XSD schemas; the upshot of that is that IDEs such as IntelliJ and Eclipse can provide "intellisense", making it easy to author such layout files.

For example, here’s a fragment of that provided by the SimpleApp starter app:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<mb3:menuBars
    xsi:schemaLocation="..."
    xmlns:cpt="http://isis.apache.org/applib/layout/component"
    xmlns:lnk="http://isis.apache.org/applib/layout/links"
    xmlns:mb3="http://isis.apache.org/applib/layout/menubars/bootstrap3"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <mb3:primary>                                               (1)
        <mb3:menu>                                              (2)
            <mb3:named>Simple Objects</mb3:named>
            <mb3:section>                                       (3)
                <mb3:serviceAction                              (4)
                        objectType="simple.SimpleObjects"       (5)
                        id="create">
                    <cpt:named>Create</cpt:named>               (6)
                </mb3:serviceAction>
                <mb3:serviceAction
                        objectType="simple.SimpleObjects"
                        id="findByName">
                    <cpt:named>Find By Name</cpt:named>
                </mb3:serviceAction>
                <mb3:serviceAction
                        objectType="simple.SimpleObjects"
                        id="listAll">
                    <cpt:named>List All</cpt:named>
                </mb3:serviceAction>
            </mb3:section>
        </mb3:menu>
        <mb3:menu unreferencedActions="true">                   (7)
            <mb3:named>Other</mb3:named>
        </mb3:menu>
    </mb3:primary>
    <mb3:secondary>                                             (8)
        <mb3:menu>
            <mb3:named>Prototyping</mb3:named>
            ...
    </mb3:secondary>
    <mb3:tertiary>                                              (9)
        <mb3:menu>
            ...
        </mb3:menu>
    </mb3:tertiary>
</mb3:menuBars>
1 Primary menu bar.
2 Menu on the menu bar
3 References an action of a domain service
4 Divider is placed between each section
5 Identifies the service through its object type
6 Optionally override the name inferred from the action
7 Domain service actions not specified elsewhere are displayed on the "Other" menu (with unreferencedActions attribute set to true).
For a layout file to be valid there must be exactly one <menu> with the unreferencedActions attribute set.
8 Secondary menu bar.
9 Tertiary menu bar.

Any domain service actions that are not explicitly listed will be placed under this menu.

The downloaded menubars.layout.xml file can be adjusted as necessary, creating new menus and menu sections. Once done, it can be saved and the project rebuilt in the IDE. If running in prototype mode, the file will be dynamically reloaded from the classpath.

Once the application has bootstrapped with a layout file, downloading the "Default" layout (from the prototyping menu) in essence just returns this file.

Downloading the layout file

The current menubars.layout.xml can be downloaded from the MenuBarsService (exposed on the prototyping menu):

010 download

If there are unknown/unreferenced actions in the "Other" menu (which you would like to place elsewhere), then these will be listed in the downloaded layout, so they can easily be moved elsewhere.

Object Layout

As with menubars, although the layout of objects can be specified using just annotations, in real-world applications you will almost certainly use a companion layout file, Xxx.layout.xml (where Xxx is the entity or view model to be rendered).

File-based layouts offer a number of benefits:

  • Probably most significantly, the layout can be updated without requiring a recompile of the code and redeploy of the app; fine-tuning the layout with your end users is easy to do

  • Many developers also find it easier to rationalize about layout when all the hints are collated together in a single place (rather than scattered across the class members as annotations).

  • UI hints can be provided for mixin contributions that are synthesised at runtime.

It is also possible to download an initial .layout.xml - capturing any existing layout metadata - using the LayoutService (exposed on the prototyping menu) or using a mixin action contributed to every domain object.

There are some downsides, though:

  • file-based layouts are not typesafe: a typo will result in the metadata not being picked up for the element.

  • they suffer from syntactic fragility: an invalid XML document could result in no metadata for the entire class.

  • there is no notion of inheritance, so a .layout.xml is required for all concrete classes and also for any abstract classes (if used as a collection type).

The Xxx.layout.xml file is just the serialized form of a Grid layout class defined within Apache Isis' applib. These are JAXB-annotated classes with corresponding XSD schemas; the upshot of that is that IDEs such as IntelliJ and Eclipse can provide "intellisense", making iteasy to author such layout files.

Grids vs Components

The layout file distinguishes between two types of element:

  • those that define a grid structure, of: rows, columns, tab groups and tabs.

    The rows and columns are closely modelled on Bootstrap 3 (used in the implementation of the Web UI (Wicket viewer)).

  • those that define common components, of: fieldsets (previously called member groups or property groups), properties, collections, actions and also the title/icon of the domain object itself.

More information about these classes can be found in the reference guide. More information on Bootstrap 3’s grid system can be found here.

By Example

Probably the easiest way to understand dynamic XML layouts is by example, in this case of a "todo item":

ToDoItem

Namespaces

Every .layout.xml file must properly declare the XSD namespaces and schemas. There are two: one for the grid classes, and one for the common component classes:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!-- Licensed to the Apache Software Foundation (ASF) under one or more contributor
	license agreements. See the NOTICE file distributed with this work for additional
	information regarding copyright ownership. The ASF licenses this file to
	you under the Apache License, Version 2.0 (the "License"); you may not use
	this file except in compliance with the License. You may obtain a copy of
	the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required
	by applicable law or agreed to in writing, software distributed under the
	License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
	OF ANY KIND, either express or implied. See the License for the specific
	language governing permissions and limitations under the License. -->
<bs3:grid
  xsi:schemaLocation="http://isis.apache.org/applib/layout/component
                      http://isis.apache.org/applib/layout/component/component.xsd
                      http://isis.apache.org/applib/layout/grid/bootstrap3
                      http://isis.apache.org/applib/layout/grid/bootstrap3/bootstrap3.xsd"
  xmlns:bs3="http://isis.apache.org/applib/layout/grid/bootstrap3"
  xmlns:c="http://isis.apache.org/applib/layout/component"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    ...
</bs3:grid>

Most IDEs will automatically download the XSD schemas from the specified schema locations, thereby providing "intellisense" help as you edit the file.

Rows, full-width cols, and tabs

The example layout consists of three rows: a row for the object/icon, a row containing a properties, and a row containing collections. In all three cases the row contains a single column spanning the full width of the page. For the property and collection rows, the column contains a tab group.

This corresponds to the following XML:

<bs3:row>
    <bs3:col span="12" unreferencedActions="true">
        <c:domainObject bookmarking="AS_ROOT"/>
    </bs3:col>
</bs3:row>
<bs3:row>
    <bs3:col span="12">
        <bs3:tabGroup>
            <bs3:tab name="Properties">...</bs3:tab>
            <bs3:tab name="Other">...</bs3:tab>
            <bs3:tab name="Metadata">...</bs3:tab>
        </bs3:tabGroup>
    </bs3:col>
</bs3:row>
<bs3:row>
    <bs3:col span="12">
        <bs3:tabGroup unreferencedCollections="true">
            <bs3:tab name="Similar to">...</bs3:tab>
            <bs3:tab name="Dependencies">...</bs3:tab>
        </bs3:tabGroup>
    </bs3:col>
</bs3:row>

You will notice that one of the columns has an unreferencedActions attribute, while one of the tabGroups has a similar unreferencedCollections attribute. This topic is discussed in more detail below.

Fieldsets

The first tab containing properties is divided into two columns, each of which holds a single fieldset of multiple properties. Those properties in turn can have associated actions.

This corresponds to the following XML:

<bs3:tab name="Properties">
    <bs3:row>
        <bs3:col span="6">
            <c:fieldSet name="General" id="general" unreferencedProperties="true">
                <c:action id="duplicate" position="PANEL_DROPDOWN"/>
                <c:action id="delete"/>
                <c:property id="description"/>
                <c:property id="category"/>
                <c:property id="subcategory">
                    <c:action id="updateCategory"/>
                    <c:action id="analyseCategory" position="RIGHT"/>
                </c:property>
                <c:property id="complete">
                    <c:action id="completed" cssClassFa="fa-thumbs-up"/>
                    <c:action id="notYetCompleted" cssClassFa="fa-thumbs-down"/>
                </c:property>
            </c:fieldSet>
        </bs3:col>
        <bs3:col span="6">
            ...
        </bs3:col>
    </bs3:row>
</bs3:tab>

The tab defines two columns, each span of 6 (meaning half the width of the page).

In the first column there is a single fieldset. Notice how actions - such as duplicate and delete - can be associated with this fieldset directly, meaning that they should be rendered on the fieldset’s top panel.

Thereafter the fieldset lists the properties in order. Actions can be associated with properties too; here they are rendered underneath or to the right of the field.

Note also the unreferencedProperties attribute for the fieldset; this topic is discussed in more detail below.

The <fieldset>'s "name" attribute is optional. If omitted, then the title panel is suppressed, freeing more real estate.

Do be aware though that if there are any actions that have been placed on the fieldset’s panel, then these will not be displayed.

Collections

In the final row the collections are placed in tabs, simply one collection per tab. This corresponds to the following XML:

<bs3:tab name="Similar to">
    <bs3:row>
        <bs3:col span="12">
            <c:collection defaultView="table" id="similarTo"/>
        </bs3:col>
    </bs3:row>
</bs3:tab>
<bs3:tab name="Dependencies">
    <bs3:row>
        <bs3:col span="12">
            <c:collection defaultView="table" id="dependencies">
                <c:action id="add"/>
                <c:action id="remove"/>
            </c:collection>
        </bs3:col>
    </bs3:row>
</bs3:tab>

As with properties, actions can be associated with collections; this indicates that they should be rendered in the collection’s header.

Unreferenced Members

As noted in the preceding discussion, several of the grid’s regions have either an unreferencedActions, unreferencedCollections or unreferencedProperties attribute.

The rules are:

  • unreferencedActions attribute can be specified either on a column or on a fieldset.

    It would normally be typical to use the column holding the <domainObject/> icon/title, that is as shown in the example. The unreferenced actions then appear as top-level actions for the domain object.

  • unreferencedCollections attribute can be specified either on a column or on a tabgroup.

    If specified on a column, then that column will contain each of the unreferenced collections, stacked one on top of the other. If specified on a tab group, then a separate tab will be created for each collection, with that tab containing only that single collection.

  • unreferencedProperties attribute can be specified only on a fieldset.

The purpose of these attributes is to indicate where in the layout any unreferenced members should be rendered. Every grid must nominate one region for each of these three member types, the reason being that to ensure that the layout can be used even if it is incomplete with respect to the object members inferred from the Java source code. This might be because the developer forgot to update the layout, or it might be because of a new mixin (property, collection or action) contributed to many objects.

The framework ensures that in any given grid exactly one region is specified for each of the three unreferenced…​ attributes. If the grid fails this validation, then a warning message will be displayed, and the invalid XML logged. The layout XML will then be ignored.

Combining with Annotations

Rather than specify every UI semantic in the layout file, you can optionally combine with a number of annotations. The idea is that the layout.xml is used primarily for the coarse-grained grid layout, with annotations used for the stuff that changes less often, such as associating actions with properties or collections, or the order of properties or actions within a fieldset.

The annotations most relevant here is @MemberOrder:

  • for properties:

    • @MemberOrder#name() can be used to identify associate a property with a fieldset.

      With this approach all of the fieldsets in the layout.xml file are left empty. The properties "slot into" the relevant field set to associate = "…​", sequence = "…​")`.

    • @MemberOrder#sequence() specifies the order of properties within their fieldset

  • for actions:

    • @MemberOrder#name() is used to associate an action with a property.

      The @Action#associateWith() annotation can be used instead as a direct replacement that "reads" better.

    • @MemberOrder#sequence() specifies the order of actions (if there are multiple actions for a property)

      The @Action#associateWithSequence() annotation can be used as a direct replacement.

There are a number of other "layout" annotations, specifically @PropertyLayout, @CollectionLayout and @ActionLayout. All of the semantics in these layout annotations can also be specified in the .layout.xml files; which is used is a matter of taste.

In addition, @ParameterLayout provides layout hints for action parameters. There is no way to specify these semantics in the .layout.xml file (action parameters are not enumerated in the file).

Layout file styles

If you want to make your usage of layout files consistent, then the framework can help because it allows the layout XML files to be downloaded using the LayoutService. This is exposed on the prototyping menu to allow you to download a ZIP file of layout XML files for all domain entities and view models.

When downloading the layout files, there are four "styles" available:

  • current

  • complete

  • normalized

  • minimal

Ignoring the "current" style (which merely downloads the currently cached layout), the other three styles allow the developer to choose how much metadata is to be specified in the XML, and how much (if any) will be obtained from annotations in the metamodel.

As a developer, you can therefore select the layout file which maps to your preferred usage:

  • if you want all layout metadata to be read from the .layout.xml file, then download the "complete" version, and copy the file alongside the domain class. You can then remove all @MemberOrder, @ActionLayout, @PropertyLayout and @CollectionLayout annotations from the source code of the domain class.

  • if you want to use layout XML file to describe the grid (columns, tabs etc) and specify which object members are associated with those regions of the grid, then download the "normalized" version. You can then remove the @MemberOrder and @Action#associateWith annotations from the source code of the domain class, but retain the @ActionLayout, @PropertyLayout and @CollectionLayout annotations.

  • if you want to use layout XML file ONLY to describe the grid, then download the "minimal" version. The grid regions will be empty in this version, and the framework will use the @MemberOrder and @Action#associateWith annotations to bind object members to those regions.

If you want to adjust the layout XML for a single domain object, then you can use the mixin action (contributed to every domain object).

Table Columns

The optional TableColumnOrderService SPI service can be used to reorder columns in a table, either for a parented collection (owned by parent domain object) or a standalone collection (returned from an action invocation).

For example, suppose there is a Customer and an Order:

Diagram

The order of these properties of Order, when rendered in the context of its owning Customer, can be controlled using this implementation of TableColumnOrderService:

@Service
@Order(100)                                             (1)
public class TableColumnOrderServiceForCustomerOrders
                 implements TableColumnOrderService {
    public List<String> orderParented(
            final Object parent,
            final String collectionId,
            final Class<?> collectionType,
            final List<String> propertyIds) {
        return parent instanceof Customer &&            (2)
               "orders".equals(collectionId)
               ? Arrays.asList("num", "placedOn", "state", "shippedOn")
               : null;
    }
    public List<String> orderStandalone(
            final Class<?> collectionType,
            final List<String> propertyIds) {
        return null;                                    (3)
    }
}
1 specifies the order in which the TableColumnOrderService implementations are called.
2 represents the collection that this service can advise upon
3 provides no advice