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
andVisitRepository
to extendJpaRepository
(rather than simplyRepository
)This will provide additional finders "for free".
-
modify
HomePageViewModel
to show the currentPetOwner
s,Pet
s andVisit
s in three separate columns:@DomainObject( nature = Nature.VIEW_MODEL, (1) logicalTypeName = "petclinic.HomePageViewModel" ) @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 PetOwner
s.4 new derived collection returning all Pet
s.5 new derived collection returning all Visits
s. -
update the
HomePageViewModel.layout.xml
:HomePageViewModel.layout.xml<!-- ... --> <bs3:row> <bs3:col span="12" unreferencedCollections="true"> <bs3:row> <bs3:col span="4"> <collection id="petOwners" defaultView="table"/> </bs3:col> <bs3:col span="4"> <collection id="pets" defaultView="table"/> </bs3:col> <bs3:col span="4"> <collection id="visits" defaultView="table"/> </bs3:col> </bs3:row> </bs3:col> </bs3: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 PetOwner
s based upon their name.6 Finds the Pet
s owned by thePetOwner
, 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<!-- ... --> <bs3:row> <bs3:col span="12" unreferencedActions="true"> <domainObject/> <action id="bookVisit"/> <!-- ... --> </bs3:col> </bs3: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:
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:
@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 theVisit
entity:VisitPlusPetOwner.java@DomainObject(nature=Nature.VIEW_MODEL, logicalTypeName = "petclinic.VisitPlusPetOwner") @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
entity5 add in additional derived properties, in this case the Pet
's owner. -
Refactor the
getVisits
collection ofHomePageViewModel
to use the new view model:VisitPlusPetOwner.javapublic 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.txtpetOwner 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.