You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by da...@apache.org on 2024/05/26 23:02:25 UTC
(causeway) 03/03: CAUSEWAY-2873: 08-02
This is an automated email from the ASF dual-hosted git repository.
danhaywood pushed a commit to branch CAUSEWAY-2873
in repository https://gitbox.apache.org/repos/asf/causeway.git
commit 09f6f2bf2bde2f2c4ebdbbb0355c31bc3465431a
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Mon May 27 00:02:16 2024 +0100
CAUSEWAY-2873: 08-02
---
.../modules/petclinic/pages/080-view-models.adoc | 309 ++++++++-------------
1 file changed, 123 insertions(+), 186 deletions(-)
diff --git a/antora/components/tutorials/modules/petclinic/pages/080-view-models.adoc b/antora/components/tutorials/modules/petclinic/pages/080-view-models.adoc
index 9bda1d0087..4df2b2bb50 100644
--- a/antora/components/tutorials/modules/petclinic/pages/080-view-models.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/080-view-models.adoc
@@ -6,7 +6,7 @@ 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.
+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.
@@ -28,71 +28,109 @@ mvn -pl spring-boot:run
=== Tasks
-* update `PetRepository` and `VisitRepository` to extend `JpaRepository` (rather than simply `Repository`)
+* update `VisitRepository` to list all visits after a certain time:
+
-This will provide additional finders "for free".
+[source,java]
+----
+public interface VisitRepository extends Repository<Visit, Integer> {
+
+ List<Visit> findByVisitAtAfter(LocalDateTime visitAt);
+
+ // ...
+}
+----
-* modify `HomePageViewModel` to show the current ``PetOwner``s, ``Pet``s and ``Visit``s in three separate columns:
+* modify `HomePageViewModel` to show the current ``PetOwner``s and any ``Visit``s in the future:
+
[source,java]
----
-@Named("petclinic.HomePageViewModel")
-@DomainObject( nature = Nature.VIEW_MODEL ) // <.>
-@HomePage // <.>
+@Named(SimpleModule.NAMESPACE + ".HomePageViewModel")
+@DomainObject(nature = Nature.VIEW_MODEL) // <.>
+@HomePage // <.>
@DomainObjectLayout()
public class HomePageViewModel {
- public String title() {
- return getPetOwners().size() + " owners";
- }
+ // ...
- public List<PetOwner> getPetOwners() { // <.>
- return petOwnerRepository.findAll();
- }
- public List<Pet> getPets() { // <.>
- return petRepository.findAll();
- }
- public List<Visit> getVisits() { // <.>
- return visitRepository.findAll();
+ @Collection
+ @CollectionLayout(tableDecorator = TableDecorator.DatatablesNet.class)
+ public List<Visit> getFutureVisits() { // <.>
+ LocalDateTime now = clockService.getClock().nowAsLocalDateTime();
+ return visitRepository.findByVisitAtAfter(now);
}
- @Inject PetOwnerRepository petOwnerRepository;
- @Inject PetRepository petRepository;
+ @Inject ClockService clockService;
@Inject VisitRepository visitRepository;
+
}
----
-<.> indicates that this is a view model.
+<.> indicates that this is a xref:userguide::view-models.adoc[view model].
+Causeway provides several ways of implementing view models; this is the most straightforward.
<.> exactly one view model can be annotated as the xref:refguide:applib:index/annotation/HomePage.adoc[@HomePage]
-<.> renamed derived collection, returns ``PetOwner``s.
-<.> new derived collection returning all ``Pet``s.
-<.> new derived collection returning all ``Visits``s.
+<.> new collection returning future ``Visits``s.
-* update the `HomePageViewModel.layout.xml`:
+* update the `HomePageViewModel.layout.xml`.
++
+Here it is in its entirety:
+
[source,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>
-<!-- ... -->
+<?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:
++
+[source,text]
+.HomePageViewModel#futureVisits.columnOrder.txt
+----
+pet
+visitAt
+#version
----
-
-* update or add columnOrder.txt files for the 3 collections.
-
[#exercise-8-2-add-a-convenience-action]
@@ -114,47 +152,67 @@ mvn -pl spring-boot:run
=== Tasks
-* create a bookVisit action for HomePageViewModel, as a mixin:
+* add a new finder to `VisitRepository`:
++
+[source,java]
+.VisitRepository.java
+----
+Visit findByPetAndVisitAt(Pet pet, LocalDateTime visitAt);
+----
+
+* create a `bookVisit` action for `HomePageViewModel`, as a mixin:
+
[source,java]
.HomePageViewModel_bookVisit.java
----
@Action // <.>
+@ActionLayout(associateWith = "futureVisits")
@RequiredArgsConstructor
-public class HomePageViewModel_bookVisit { // <.>
+public class HomePageViewModel_bookVisit {
final HomePageViewModel homePageViewModel;
+ @MemberSupport
public Object act(
- PetOwner petOwner, Pet pet, LocalDateTime visitAt, String reason,
- boolean showVisit) { // <.>
- Visit visit = wrapperFactory.wrapMixin(Pet_bookVisit.class, pet).act(visitAt, reason); // <.>
- return showVisit ? visit : homePageViewModel;
+ PetOwner petOwner, Pet pet, LocalDateTime visitAt,
+ boolean showVisit) { // <.>
+ wrapperFactory.wrapMixin(PetOwner_bookVisit.class, petOwner).act(pet, visitAt); // <.>
+ if (showVisit) {
+ return visitRepository.findByPetAndVisitAt(pet, visitAt);
+ }
+ return homePageViewModel;
}
+ @MemberSupport
public List<PetOwner> autoComplete0Act(final String lastName) { // <.>
- return petOwnerRepository.findByLastNameContaining(lastName);
+ return petOwnerRepository.findByNameContaining(lastName);
}
- public List<Pet> choices1Act(PetOwner petOwner) { // <.>
- if(petOwner == null) return Collections.emptyList();
- return petRepository.findByPetOwner(petOwner);
+ @MemberSupport
+ public Set<Pet> choices1Act(PetOwner petOwner) { // <.>
+ if(petOwner == null) {
+ return Collections.emptySet();
+ }
+ return petOwner.getPets();
}
+ @MemberSupport
public LocalDateTime default2Act(PetOwner petOwner, Pet pet) { // <.>
- if(pet == null) return null;
- return factoryService.mixin(Pet_bookVisit.class, pet).default0Act();
+ 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) { // <.>
- return factoryService.mixin(Pet_bookVisit.class, pet).validate0Act(visitAt);
+ return factoryService.mixin(PetOwner_bookVisit.class, petOwner).validate1Act(visitAt);
}
- @Inject PetRepository petRepository;
+ @Inject VisitRepository visitRepository;
@Inject PetOwnerRepository petOwnerRepository;
@Inject WrapperFactory wrapperFactory;
@Inject FactoryService factoryService;
}
----
<.> declares this class as a mixin action.
-<.> The action name is derived from the mixin's class ("bookVisit").
-<.> cosmetic flag to control the UI; either remain at the home page or navigate to the newly created `Visit
+<.> cosmetic flag to control the UI; either remain at the home page or navigate to the newly created `Visit`
<.> use the xref:refguide:applib:index/services/wrapper/WrapperFactory.adoc[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.
<.> Uses an xref:refguide:applib-methods:prefixes.adoc#autoComplete[autoComplete] supporting method to look up matching ``PetOwner``s based upon their name.
@@ -162,135 +220,14 @@ If additional business rules were added to that delegate, then the mistake would
<.> Computes a default for the 2^nd^ parameter, once the first two are selected.
<.> surfaces (some of) the business rules of the delegate mixin.
-* update the layout file to position:
+* update the title of `HomePageViewModel`:
+
[source,xml]
.HomePageViewModel.layout.xml
----
-<!-- ... -->
- <bs:row>
- <bs:col span="12" unreferencedActions="true">
- <domainObject/>
- <action id="bookVisit"/>
- <!-- ... -->
- </bs:col>
- </bs:row>
-<!-- ... -->
-----
-
-
-
-[#exercise-8-3-using-a-view-model-as-a-projection-of-an-entity]
-== 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:
-
-[source,java]
-.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:
-
-[source,java]
-.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 xref:refguide:applib:index/services/tablecol/TableColumnVisibilityService.adoc[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
-
-[source,bash]
-----
-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:
-+
-[source,java]
-.VisitPlusPetOwner.java
-----
-@Named("petclinic.VisitPlusPetOwner")
-@DomainObject(nature=Nature.VIEW_MODEL)
-@DomainObjectLayout(named = "Visit")
-@XmlRootElement // <.>
-@XmlType // <1>
-@XmlAccessorType(XmlAccessType.FIELD) // <1>
-@NoArgsConstructor
-public class VisitPlusPetOwner {
-
- @Property(
- projecting = Projecting.PROJECTED, // <.>
- hidden = Where.EVERYWHERE // <.>
- )
- @Getter
- private Visit visit;
-
- VisitPlusPetOwner(Visit visit) {this.visit = visit;}
-
- public Pet getPet() {return visit.getPet();} // <.>
- public String getReason() {return visit.getReason();} // <4>
- public LocalDateTime getVisitAt() {return visit.getVisitAt();} // <4>
-
- public PetOwner getPetOwner() { // <.>
- return getPet().getPetOwner();
- }
+@ObjectSupport public String title() {
+ return getPetOwners().size() + " pet owners, " +
+ getFutureVisits() + " future visits";
}
----
-<.> Boilerplate for JAXB view models
-<.> 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`).
-<.> Nevertheless, hide this property from the UI.
-<.> expose properties from the underlying `Visit` entity
-<.> add in additional derived properties, in this case the ``Pet``'s owner.
-
-* Refactor the `getVisits` collection of `HomePageViewModel` to use the new view model:
-+
-[source,java]
-.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:
-+
-[source,java]
-.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.