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 an 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/08-01-home-page-additional-collections
mvn clean install
mvn -pl spring-boot:run

Tasks

  • update PetRepository and VisitRepository to extend JpaRepository (rather than simply Repository)

    This will provide additional finders "for free".

  • modify HomePageViewModel to show the current PetOwners, Pets and Visits in three separate columns:

    @Named("petclinic.HomePageViewModel")
    @DomainObject( nature = Nature.VIEW_MODEL )                     (1)
    @HomePage                                                       (2)
    @DomainObjectLayout()
    public class HomePageViewModel {
    
        public String title() {
            return getPetOwners().size() + " owners";
        }
    
        public List<PetOwner> getPetOwners() {                      (3)
            return petOwnerRepository.findAll();
        }
        public List<Pet> getPets() {                                (4)
            return petRepository.findAll();
        }
        public List<Visit> getVisits() {                            (5)
            return visitRepository.findAll();
        }
    
        @Inject PetOwnerRepository petOwnerRepository;
        @Inject PetRepository petRepository;
        @Inject VisitRepository visitRepository;
    }
    1 indicates that this is a view model.
    2 exactly one view model can be annotated as the @HomePage
    3 renamed derived collection, returns PetOwners.
    4 new derived collection returning all Pets.
    5 new derived collection returning all Visitss.
  • update the HomePageViewModel.layout.xml:

    HomePageViewModel.layout.xml
    <!-- ... -->
        <bs:row>
            <bs:col span="12" unreferencedCollections="true">
                <bs:row>
                    <bs:col span="4">
                        <collection id="petOwners" defaultView="table"/>
                    </bs:col>
                    <bs:col span="4">
                        <collection id="pets" defaultView="table"/>
                    </bs:col>
                    <bs:col span="4">
                        <collection id="visits" defaultView="table"/>
                    </bs:col>
                </bs:row>
            </bs:col>
        </bs:row>
    <!-- ... -->
  • update or add columnOrder.txt files for the 3 collections.

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/08-02-home-page-bookVisit-convenience-action
mvn clean install
mvn -pl spring-boot:run

Tasks

  • create a bookVisit action for HomePageViewModel, as a mixin:

    HomePageViewModel_bookVisit.java
    @Action                                                                                 (1)
    @RequiredArgsConstructor
    public class HomePageViewModel_bookVisit {                                              (2)
    
        final HomePageViewModel homePageViewModel;
    
        public Object act(
                PetOwner petOwner, Pet pet, LocalDateTime visitAt, String reason,
                boolean showVisit) {                                                               (3)
            Visit visit = wrapperFactory.wrapMixin(Pet_bookVisit.class, pet).act(visitAt, reason); (4)
            return showVisit ? visit : homePageViewModel;
        }
        public List<PetOwner> autoComplete0Act(final String lastName) {                     (5)
            return petOwnerRepository.findByLastNameContaining(lastName);
        }
        public List<Pet> choices1Act(PetOwner petOwner) {                                   (6)
            if(petOwner == null) return Collections.emptyList();
            return petRepository.findByPetOwner(petOwner);
        }
        public LocalDateTime default2Act(PetOwner petOwner, Pet pet) {                      (7)
            if(pet == null) return null;
            return factoryService.mixin(Pet_bookVisit.class, pet).default0Act();
        }
        public String validate2Act(PetOwner petOwner, Pet pet, LocalDateTime visitAt) {     (8)
             return factoryService.mixin(Pet_bookVisit.class, pet).validate0Act(visitAt);
        }
    
        @Inject PetRepository petRepository;
        @Inject PetOwnerRepository petOwnerRepository;
        @Inject WrapperFactory wrapperFactory;
        @Inject FactoryService factoryService;
    }
    1 declares this class as a mixin action.
    2 The action name is derived from the mixin’s class ("bookVisit").
    3 cosmetic flag to control the UI; either remain at the home page or navigate to the newly created `Visit
    4 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.
    5 Uses an autoComplete supporting method to look up matching PetOwners based upon their name.
    6 Finds the Pets owned by the PetOwner, once selected.
    7 Computes a default for the 2nd parameter, once the first two are selected.
    8 surfaces (some of) the business rules of the delegate mixin.
  • update the layout file to position:

    HomePageViewModel.layout.xml
    <!-- ... -->
        <bs:row>
            <bs:col span="12" unreferencedActions="true">
                <domainObject/>
                <action id="bookVisit"/>
                <!-- ... -->
            </bs:col>
        </bs:row>
    <!-- ... -->

Ex 8.3: Using a view model as a projection of an entity

In the home page, the Visit instances show the Pet but they do not show the PetOwner. One option (probably the correct one in this case) would be to extend Visit itself and show this derived information:

Visit.java
public PetOwner getPetOwner() {
    return getPet().getOwner();
}

Alternatively, if we didn’t want to "pollute" the entity with this derived property, we could use a mixin:

Visit_petOwner.java
@Property
@RequiredArgsConstructor
public class Visit_petOwner {

    final Visit visit;

    public PetOwner prop() {
        return visit.getPet().getOwner();
    }
}

Even so, this would still make the "petOwner" property visible everywhere that a Visit is displayed.

If we instead want to be more targetted and only show this "petOwner" property when displayed on the HomePage, yet another option is to implement the TableColumnVisibilityService SPI. This provides the context for where an object is being rendered, so this could be used to suppress the collection everywhere except the home page.

A final option though, which we’ll use in this exercise, is to display not the entity itself but instead a view model that "wraps" the entity and supplements with the additional data required.

Solution

git checkout tags/08-03-view-model-projecting-an-entity
mvn clean install
mvn -pl spring-boot:run

Tasks

  • create a JAXB style view model VisitPlusPetOwner, wrapping the Visit entity:

    VisitPlusPetOwner.java
    @Named("petclinic.VisitPlusPetOwner")
    @DomainObject(nature=Nature.VIEW_MODEL)
    @DomainObjectLayout(named = "Visit")
    @XmlRootElement                                                     (1)
    @XmlType                                                            (1)
    @XmlAccessorType(XmlAccessType.FIELD)                               (1)
    @NoArgsConstructor
    public class VisitPlusPetOwner {
    
        @Property(
                projecting = Projecting.PROJECTED,                      (2)
                hidden = Where.EVERYWHERE                               (3)
        )
        @Getter
        private Visit visit;
    
        VisitPlusPetOwner(Visit visit) {this.visit = visit;}
    
        public Pet getPet() {return visit.getPet();}                    (4)
        public String getReason() {return visit.getReason();}           (4)
        public LocalDateTime getVisitAt() {return visit.getVisitAt();}  (4)
    
        public PetOwner getPetOwner() {                                 (5)
            return getPet().getPetOwner();
        }
    }
    1 Boilerplate for JAXB view models
    2 if the icon/title is clicked, then traverse to this object rather than the view model. (The view model is a "projection" of the underlying Visit).
    3 Nevertheless, hide this property from the UI.
    4 expose properties from the underlying Visit entity
    5 add in additional derived properties, in this case the Pet's owner.
  • Refactor the getVisits collection of HomePageViewModel to use the new view model:

    VisitPlusPetOwner.java
    public List<VisitPlusPetOwner> getVisits() {
        return visitRepository.findAll()
                .stream()
                .map(VisitPlusPetOwner::new)
                .collect(Collectors.toList());
    }
  • update the columnOrder file for this collection to display the new property:

    HomePageViewModel#visits.columnOrder.txt
    petOwner
    pet
    visitAt

Run the application; the visits collection on the home page should now show the PetOwner as an additional column, but otherwise behaves the same as previously.