View models

So far the application consists only of domain entities and domain services. However, the framework also supports view models.

A classic use case is to provide a home page or dashboard, but they are also used to represent certain specific business processes when there isn’t necessarily a domain entity required to track the state of the process itself. Some real-world examples include importing/exporting spreadsheets periodically (eg changes to indexation rates), or generating extracts such as a payment file or summary PDF for a quarterly invoice run.

Ex 8.1: Extend the Home Page.

In this exercise we’ll extend the home page by displaying additional data in new collections.

Solution

git checkout tags/2.1.0/08-01-home-page-additional-collections
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"

Tasks

  • update VisitRepository to list all visits after a certain time:

    public interface VisitRepository extends Repository<Visit, Integer> {
    
        List<Visit> findByVisitAtAfter(LocalDateTime visitAt);
    
        // ...
    }
  • modify HomePageViewModel to show the current PetOwners and any Visits in the future:

    @Named(SimpleModule.NAMESPACE + ".HomePageViewModel")
    @DomainObject(nature = Nature.VIEW_MODEL)                                   (1)
    @HomePage                                                                   (2)
    @DomainObjectLayout()
    public class HomePageViewModel {
    
        // ...
    
        @Collection
        @CollectionLayout(tableDecorator = TableDecorator.DatatablesNet.class)
        public List<Visit> getFutureVisits() {                                  (3)
            LocalDateTime now = clockService.getClock().nowAsLocalDateTime();
            return visitRepository.findByVisitAtAfter(now);
        }
    
        @Inject ClockService clockService;
        @Inject VisitRepository visitRepository;
    
    }
    1 indicates that this is a view model. Causeway provides several ways of implementing view models; this is the most straightforward.
    2 exactly one view model can be annotated as the @HomePage
    3 new collection returning future Visitss.
  • update the HomePageViewModel.layout.xml.

    Here it is in its entirety:

    HomePageViewModel.layout.xml
    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <bs3:grid
            xsi:schemaLocation="https://causeway.apache.org/applib/layout/component https://causeway.apache.org/applib/layout/component/component.xsd https://causeway.apache.org/applib/layout/grid/bootstrap3 https://causeway.apache.org/applib/layout/grid/bootstrap3/bootstrap3.xsd"
            xmlns:cpt="https://causeway.apache.org/applib/layout/component"
            xmlns:bs3="https://causeway.apache.org/applib/layout/grid/bootstrap3"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
        <bs3:row>
            <bs3:col span="12">
                <bs3:row>
                    <bs3:col span="12" unreferencedActions="true">
                        <cpt:domainObject/>
                        <cpt:action id="clearHints" hidden="EVERYWHERE"/>
                        <cpt:action id="impersonate" hidden="EVERYWHERE"/>
                        <cpt:action id="impersonateWithRoles" hidden="EVERYWHERE"/>
                        <cpt:action id="stopImpersonating" hidden="EVERYWHERE"/>
                        <cpt:action id="downloadLayoutXml" hidden="EVERYWHERE"/>
                        <cpt:action id="inspectMetamodel" hidden="EVERYWHERE"/>
                        <cpt:action id="rebuildMetamodel" hidden="EVERYWHERE"/>
                        <cpt:action id="downloadMetamodelXml" hidden="EVERYWHERE"/>
                        <cpt:action id="openRestApi" hidden="EVERYWHERE"/>
                    </bs3:col>
                </bs3:row>
            </bs3:col>
            <bs3:col span="6" unreferencedCollections="true">
                <bs3:row>
                    <bs3:col span="12">
                        <cpt:collection id="petOwners" defaultView="table"/>
                    </bs3:col>
                </bs3:row>
            </bs3:col>
            <bs3:col span="6">
                <bs3:row>
                    <bs3:col span="12">
                        <cpt:collection id="futureVisits" defaultView="table"/>
                    </bs3:col>
                </bs3:row>
            </bs3:col>
        </bs3:row>
        <bs3:row>
            <bs3:col span="0">
                <cpt:fieldSet name="General" id="general" unreferencedProperties="true"/>
            </bs3:col>
        </bs3:row>
    </bs3:grid>
  • add columnOrder.txt files for the new collection:

    HomePageViewModel#futureVisits.columnOrder.txt
    pet
    visitAt
    #version

Ex 8.2: Add a convenience action

View models can have behaviour (actions), the same as entities. In this exercise we’ll extend the home page by providing a convenience action to book a Visit for any Pet of any PetOwner.

Solution

git checkout tags/2.1.0/08-02-home-page-bookVisit-convenience-action
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"

Tasks

  • add a new finder to VisitRepository:

    VisitRepository.java
    Visit findByPetAndVisitAt(Pet pet, LocalDateTime visitAt);
  • create a bookVisit action for HomePageViewModel, as a mixin:

    HomePageViewModel_bookVisit.java
    @Action                                                                                 (1)
    @ActionLayout(associateWith = "futureVisits")
    @RequiredArgsConstructor
    public class HomePageViewModel_bookVisit {
    
        final HomePageViewModel homePageViewModel;
    
        @MemberSupport
        public Object act(
                PetOwner petOwner, Pet pet, LocalDateTime visitAt,
                boolean showVisit) {                                                        (2)
            wrapperFactory.wrapMixin(PetOwner_bookVisit.class, petOwner).act(pet, visitAt); (3)
            if (showVisit) {
                return visitRepository.findByPetAndVisitAt(pet, visitAt);
            }
            return homePageViewModel;
        }
        @MemberSupport
        public List<PetOwner> autoComplete0Act(final String lastName) {                     (4)
            return petOwnerRepository.findByNameContaining(lastName);
        }
        @MemberSupport
        public Set<Pet> choices1Act(PetOwner petOwner) {                                   (5)
            if(petOwner == null) {
                return Collections.emptySet();
            }
            return petOwner.getPets();
        }
        @MemberSupport
        public LocalDateTime default2Act(PetOwner petOwner, Pet pet) {                      (6)
            if(petOwner == null || pet == null) {
                return null;
            }
            return factoryService.mixin(PetOwner_bookVisit.class, petOwner).default1Act();
        }
        @MemberSupport
        public String validate2Act(PetOwner petOwner, Pet pet, LocalDateTime visitAt) {     (7)
            return factoryService.mixin(PetOwner_bookVisit.class, petOwner).validate1Act(visitAt);
        }
    
        @Inject VisitRepository visitRepository;
        @Inject PetOwnerRepository petOwnerRepository;
        @Inject WrapperFactory wrapperFactory;
        @Inject FactoryService factoryService;
    }
    1 declares this class as a mixin action.
    2 cosmetic flag to control the UI; either remain at the home page or navigate to the newly created Visit
    3 use the WrapperFactory to delegate to the original behaviour "as if" through the UI. If additional business rules were added to that delegate, then the mistake would be detected.
    4 Uses an autoComplete supporting method to look up matching PetOwners based upon their name.
    5 Finds the Pets owned by the PetOwner, once selected.
    6 Computes a default for the 2nd parameter, once the first two are selected.
    7 surfaces (some of) the business rules of the delegate mixin.
  • update the title of HomePageViewModel:

    HomePageViewModel.layout.xml
    @ObjectSupport public String title() {
        return getPetOwners().size() + " pet owners, " +
               getFutureVisits() + " future visits";
    }