You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by da...@apache.org on 2021/01/09 19:28:32 UTC

[isis] branch ISIS-2476 updated: ISIS-2476: adds docs for custom UI vm

This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch ISIS-2476
in repository https://gitbox.apache.org/repos/asf/isis.git


The following commit(s) were added to refs/heads/ISIS-2476 by this push:
     new 4eaa3b2  ISIS-2476: adds docs for custom UI vm
4eaa3b2 is described below

commit 4eaa3b24e3e9ddcbe8cb8d4e2169d58599b7a159
Author: danhaywood <da...@haywood-associates.co.uk>
AuthorDate: Sat Jan 9 19:25:35 2021 +0000

    ISIS-2476: adds docs for custom UI vm
---
 .asciidoctorconfig                                 |   1 +
 examples/demo/domain/pom.xml                       |  12 ++
 .../_infra/resources/AsciiDocConverterService.java |  72 +++++-----
 .../_infra/resources/AsciiDocReaderService.java    |   9 +-
 .../src/main/java/demoapp/dom/menubars.layout.xml  |   2 +-
 .../dom/ui/custom/geocoding/GeoapifyClient.java    |  64 +++++----
 .../dom/ui/custom/vm/CustomUiVm-description.adoc   |   5 -
 ...{CustomUiMenu.java => WhereInTheWorldMenu.java} |  31 +++--
 .../custom/vm/WhereInTheWorldVm-description.adoc   | 151 +++++++++++++++++++++
 .../vm/{CustomUiVm.java => WhereInTheWorldVm.java} |   5 +-
 .../ui/custom/wicket/WhereInTheWorldPanel.html}    |   0
 .../dom/ui/custom/wicket/WhereInTheWorldPanel.java | 118 ++++++++++++++++
 .../custom/wicket/WhereInTheWorldPanelFactory.java |  48 +++++++
 .../java/demoapp/webapp/wicket/DemoAppWicket.java  |   4 -
 .../webapp/wicket/customview/CustomUiPanel.java    | 109 ---------------
 .../wicket/customview/CustomUiPanelFactory.java    |  42 ------
 16 files changed, 436 insertions(+), 237 deletions(-)

diff --git a/.asciidoctorconfig b/.asciidoctorconfig
index e79a779..b8f59c8 100644
--- a/.asciidoctorconfig
+++ b/.asciidoctorconfig
@@ -9,3 +9,4 @@
 // the asciidoctor-intellij-plugin will only preview interactive links if mode is [inline]
 // see discussion here https://github.com/asciidoctor/asciidoctor-intellij-plugin/issues/548
 :kroki-default-options: inline
+:isis-version: 2.0.0-M4
diff --git a/examples/demo/domain/pom.xml b/examples/demo/domain/pom.xml
index 621e5ba..69c43bf 100644
--- a/examples/demo/domain/pom.xml
+++ b/examples/demo/domain/pom.xml
@@ -118,6 +118,18 @@
 			<artifactId>isis-extensions-command-log-jdo</artifactId>
 		</dependency>
 
+		<!-- custom ui -->
+        <dependency>
+            <groupId>org.apache.isis.viewer</groupId>
+            <artifactId>isis-viewer-wicket-model</artifactId>
+			<optional>true</optional>	<!-- to avoid polluting the classpath -->
+        </dependency>
+		<dependency>
+			<groupId>org.apache.isis.viewer</groupId>
+			<artifactId>isis-viewer-wicket-ui</artifactId>
+			<optional>true</optional>	<!-- to avoid polluting the classpath -->
+		</dependency>
+
 		<!-- DEV TIME -->
 		<dependency>
 			<groupId>org.springframework.boot</groupId>
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/_infra/resources/AsciiDocConverterService.java b/examples/demo/domain/src/main/java/demoapp/dom/_infra/resources/AsciiDocConverterService.java
index 9bb430e..337488f 100644
--- a/examples/demo/domain/src/main/java/demoapp/dom/_infra/resources/AsciiDocConverterService.java
+++ b/examples/demo/domain/src/main/java/demoapp/dom/_infra/resources/AsciiDocConverterService.java
@@ -40,61 +40,61 @@ import lombok.val;
 @Named("demo.AsciiDocConverterService")
 public class AsciiDocConverterService {
 
-    private static ThreadLocal<Class<?>> context = new ThreadLocal<>();
+    private final static ThreadLocal<Class<?>> context = new ThreadLocal<>();
 
+    private final ResourceReaderService resourceReaderService;
 
-    private Asciidoctor asciidoctor;
-    private Options options;
+    private final Asciidoctor asciidoctor;
+    private final Options options;
+
+    @Inject
+    public AsciiDocConverterService(ResourceReaderService resourceReaderService) {
+        this.resourceReaderService = resourceReaderService;
+        this.asciidoctor = createAsciidoctor();
+        this.options = OptionsBuilder.options()
+                .safe(SafeMode.UNSAFE)
+                .toFile(false)
+                .attributes(AttributesBuilder.attributes()
+                        .sourceHighlighter("prism")
+                        .icons("font")
+                        .get())
+                .get();
 
-    private Asciidoctor getAsciidoctor() {
-        if(asciidoctor == null) {
-            asciidoctor = Asciidoctor.Factory.create();
-            asciidoctor.javaExtensionRegistry().includeProcessor(new LocalIncludeProcessor());
-        }
-        return asciidoctor;
     }
 
-     class LocalIncludeProcessor extends IncludeProcessor {
+    private Asciidoctor createAsciidoctor() {
 
-        @Override
-        public boolean handles(String target) {
-            return true;
-        }
+        class LocalIncludeProcessor extends IncludeProcessor {
 
-        @Override
-        public void process(Document document, PreprocessorReader reader, String target, Map<String, Object> attributes) {
-            Class<?> contextClass = context.get();
-            val content = resourceReaderService.readResource(contextClass, target, attributes);
-            reader.push_include(content, target, target, 1, attributes);
-        }
-    }
+            @Override
+            public boolean handles(String target) {
+                return true;
+            }
 
-    private Options getOptions() {
-        if (options == null) {
-            options = OptionsBuilder.options()
-                    .safe(SafeMode.UNSAFE)
-                    .toFile(false)
-                    .attributes(AttributesBuilder.attributes()
-                            .sourceHighlighter("prism")
-                            .icons("font")
-                            .get())
-                    .get();
+            @Override
+            public void process(Document document, PreprocessorReader reader, String target, Map<String, Object> attributes) {
+                val contextClass = context.get();
+                val content = resourceReaderService.readResource(contextClass, target, attributes);
+                reader.push_include(content, target, target, 1, attributes);
+            }
         }
-        return options;
+
+        val asciidoctor = Asciidoctor.Factory.create();
+        asciidoctor.javaExtensionRegistry().includeProcessor(new LocalIncludeProcessor());
+        return asciidoctor;
     }
 
 
-    String adocToHtml(Class<?> contextClass, String adoc) {
+
+    String adocToHtml(final Class<?> contextClass, final String adoc) {
         try {
             context.set(contextClass);
-            return getAsciidoctor().convert(adoc, getOptions());
+            return asciidoctor.convert(adoc, options);
         } finally {
             context.remove();
         }
     }
 
 
-    @Inject
-    ResourceReaderService resourceReaderService;
 
 }
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/_infra/resources/AsciiDocReaderService.java b/examples/demo/domain/src/main/java/demoapp/dom/_infra/resources/AsciiDocReaderService.java
index 7f86d52..978d8cc 100644
--- a/examples/demo/domain/src/main/java/demoapp/dom/_infra/resources/AsciiDocReaderService.java
+++ b/examples/demo/domain/src/main/java/demoapp/dom/_infra/resources/AsciiDocReaderService.java
@@ -23,6 +23,7 @@ import javax.inject.Named;
 
 import org.springframework.stereotype.Service;
 
+import org.apache.isis.core.config.environment.IsisSystemEnvironment;
 import org.apache.isis.valuetypes.asciidoc.applib.value.AsciiDoc;
 
 import lombok.val;
@@ -42,16 +43,20 @@ public class AsciiDocReaderService {
 
     public AsciiDoc readFor(Class<?> aClass) {
         val adocResourceName = String.format("%s.adoc", aClass.getSimpleName());
-        val asciiDoc = resourceReaderService.readResource(aClass, adocResourceName);
+        val asciiDoc = readResourceAndReplaceProperties(aClass, adocResourceName);
         return AsciiDoc.valueOfHtml(asciiDocConverterService.adocToHtml(aClass, asciiDoc));
     }
 
     public AsciiDoc readFor(Class<?> aClass, final String member) {
         val adocResourceName = String.format("%s-%s.%s", aClass.getSimpleName(), member, "adoc");
-        val asciiDoc = resourceReaderService.readResource(aClass, adocResourceName);
+        val asciiDoc = readResourceAndReplaceProperties(aClass, adocResourceName);
         return AsciiDoc.valueOfHtml(asciiDocConverterService.adocToHtml(aClass, asciiDoc));
     }
 
+    private String readResourceAndReplaceProperties(Class<?> aClass, String adocResourceName) {
+        val adoc = resourceReaderService.readResource(aClass, adocResourceName);
+        return adoc.replace("{isis-version}", IsisSystemEnvironment.VERSION);
+    }
 
     @Inject
     AsciiDocConverterService asciiDocConverterService;
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/menubars.layout.xml b/examples/demo/domain/src/main/java/demoapp/dom/menubars.layout.xml
index c0f5baa..3cf359a 100644
--- a/examples/demo/domain/src/main/java/demoapp/dom/menubars.layout.xml
+++ b/examples/demo/domain/src/main/java/demoapp/dom/menubars.layout.xml
@@ -232,7 +232,7 @@ For latest we use: https://raw.githubusercontent.com/apache/isis/master/antora/s
             <mb3:named>UI</mb3:named>
             <mb3:section>
                 <mb3:named>Custom UI</mb3:named>
-                <mb3:serviceAction objectType="demo.CustomUiMenu" id="customUiVm" />
+                <mb3:serviceAction objectType="demo.WhereInTheWorldUiMenu" id="whereInTheWorld" />
             </mb3:section>
         </mb3:menu>
 
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/geocoding/GeoapifyClient.java b/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/geocoding/GeoapifyClient.java
index 96fd4f9..f299695 100644
--- a/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/geocoding/GeoapifyClient.java
+++ b/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/geocoding/GeoapifyClient.java
@@ -23,9 +23,11 @@ import lombok.val;
 
 import demoapp.dom.AppConfiguration;
 
+//tag::class[]
 @Service
 public class GeoapifyClient implements Serializable {
 
+//end::class[]
     private final static ObjectMapper objectMapper =
                 new ObjectMapper()
                     .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
@@ -37,10 +39,40 @@ public class GeoapifyClient implements Serializable {
         this.apiKey = appConfiguration.getGeoapify().getApiKey();
     }
 
+
+//tag::class[]
+    @Data
+    public class GeocodeResponse {
+        @Getter
+        private final String latitude;
+        @Getter
+        private final String longitude;
+    }
+
+    @SneakyThrows
+    public GeocodeResponse geocode(final String address) {
+        //...
+//end::class[]
+
+        val url = new URL(String.format(
+                "https://api.geoapify.com/v1/geocode/search?text=%s&apiKey=%s"
+                , URLEncoder.encode(address, "UTF-8")
+                , apiKey));
+
+        val response = objectMapper.readValue(url, Response.class);
+
+        return new GeocodeResponse(
+                response.getFeatures().get(0).getProperties().getLat(),
+                response.getFeatures().get(0).getProperties().getLon()
+        );
+//tag::class[]
+    }
+
+//end::class[]
+
     @Data
     @Builder
     public static class JpegRequest {
-
         String latitude;
         String longitude;
         int zoom;
@@ -48,9 +80,14 @@ public class GeoapifyClient implements Serializable {
         @Builder.Default int height = 600;
     }
 
+//tag::class[]
     public byte[] toJpeg(final String latitude, final String longitude, int zoom) throws IOException {
+        //...
+//end::class[]
         return toJpeg(JpegRequest.builder().latitude(latitude).longitude(longitude).zoom(zoom).build());
+//tag::class[]
     }
+//end::class[]
 
     public byte[] toJpeg(final JpegRequest request) throws IOException {
         val urlStr = String.format(
@@ -80,28 +117,5 @@ public class GeoapifyClient implements Serializable {
         List<Feature> features;
     }
 
-    @Data
-    public class GeocodeResponse {
-        @Getter
-        private final String latitude;
-        @Getter
-        private final String longitude;
-    }
-
-    @SneakyThrows
-    public GeocodeResponse geocode(final String address) {
-
-        val url = new URL(String.format(
-                "https://api.geoapify.com/v1/geocode/search?text=%s&apiKey=%s"
-                , URLEncoder.encode(address, "UTF-8")
-                , apiKey));
-
-        val response = objectMapper.readValue(url, Response.class);
-
-        return new GeocodeResponse(
-                response.getFeatures().get(0).getProperties().getLat(),
-                response.getFeatures().get(0).getProperties().getLon()
-        );
-    }
-
 }
+//end::class[]
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/vm/CustomUiVm-description.adoc b/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/vm/CustomUiVm-description.adoc
deleted file mode 100644
index d5a08f8..0000000
--- a/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/vm/CustomUiVm-description.adoc
+++ /dev/null
@@ -1,5 +0,0 @@
-:Notice: 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 ag [...]
-
-//TODO
-
-Describe how this page works...
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/vm/CustomUiMenu.java b/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/vm/WhereInTheWorldMenu.java
similarity index 71%
rename from examples/demo/domain/src/main/java/demoapp/dom/ui/custom/vm/CustomUiMenu.java
rename to examples/demo/domain/src/main/java/demoapp/dom/ui/custom/vm/WhereInTheWorldMenu.java
index 1d00826..4d8ba1f 100644
--- a/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/vm/CustomUiMenu.java
+++ b/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/vm/WhereInTheWorldMenu.java
@@ -18,34 +18,39 @@
  */
 package demoapp.dom.ui.custom.vm;
 
+import java.util.Arrays;
+import java.util.List;
+
+import javax.inject.Inject;
+
 import org.apache.isis.applib.annotation.Action;
 import org.apache.isis.applib.annotation.ActionLayout;
 import org.apache.isis.applib.annotation.DomainService;
 import org.apache.isis.applib.annotation.NatureOfService;
 import org.apache.isis.applib.annotation.SemanticsOf;
 
-import lombok.RequiredArgsConstructor;
 import lombok.val;
 
 import demoapp.dom.ui.custom.geocoding.GeoapifyClient;
 import demoapp.dom.ui.custom.latlng.Zoom;
 
-@DomainService(nature=NatureOfService.VIEW, objectType = "demo.CustomUiMenu")
-@RequiredArgsConstructor
-public class CustomUiMenu {
+@DomainService(nature=NatureOfService.VIEW, objectType = "demo.WhereInTheWorldMenu")
+public class WhereInTheWorldMenu {
 
-    private final GeoapifyClient geoapifyClient;
+//tag::action[]
+    @Inject
+    private GeoapifyClient geoapifyClient;
 
     @Action(semantics = SemanticsOf.SAFE)
     @ActionLayout(
             cssClassFa="fa-globe",
-            describedAs="Opens a Custom UI page displaying a map"
+            describedAs="Opens a Custom UI page displaying a map for the provided address"
     )
-    public CustomUiVm customUiVm(
+    public WhereInTheWorldVm whereInTheWorld(
             final String address,
             @Zoom final int zoom
     ){
-        val vm = new CustomUiVm();
+        val vm = new WhereInTheWorldVm();
 
         val latLng = geoapifyClient.geocode(address);
         vm.setAddress(address);
@@ -55,11 +60,15 @@ public class CustomUiMenu {
 
         return vm;
     }
+//end::action[]
 
-    public String default0CustomUiVm() {
-        return "London, UK";
+    public List<String> choices0WhereInTheWorld() {
+        return Arrays.asList("Malvern, UK", "Vienna, Austria", "Leeuwarden, Netherlands", "Dublin, Ireland");
+    }
+    public String default0WhereInTheWorld() {
+        return "Malvern, UK";
     }
-    public int default1CustomUiVm() {
+    public int default1WhereInTheWorld() {
         return 14;
     }
 
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/vm/WhereInTheWorldVm-description.adoc b/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/vm/WhereInTheWorldVm-description.adoc
new file mode 100644
index 0000000..72b8e1c
--- /dev/null
+++ b/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/vm/WhereInTheWorldVm-description.adoc
@@ -0,0 +1,151 @@
+:Notice: 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 ag [...]
+
+The link:https://isis.apache.org/vw/{isis-version}/about.html[Wicket viewer] renders the generic UI for entities and view models using a series of Wicket ``Component``s, with each such `Component` created by a `ComponentFactory`.
+
+You can customise the UI by providing alternate implementations of `ComponentFactory`, for any component on the page, including the top-level component for the entire domain object.
+
+This custom UI uses the technique to provide a custom page for a `WhereInTheWorldVm` view model.
+
+
+== Domain Objects
+
+The domain objects involved are the `WhereInTheWorldMenu` domain service and `WhereInTheWorldVm` view model.
+
+The `WhereInTheWorldMenu` domain service prompts for an address, and uses the injected `GeoapifyClient` service to determine the latitude and longitude.
+A `zoom` level, which will be used when retrieving the map, is also prompted for:
+
+[source,java,indent=0]
+.WhereInTheWorldMenu.java
+----
+include::WhereInTheWorldMenu.java[tags=action]
+----
+
+The `WhereInTheWorldVm` view model returned by the menu is defined normally.
+In this case, we have a JAXB view model with four properties:
+
+[source,java]
+.WhereInTheWorldVm.java
+----
+include::WhereInTheWorldVm.java[tags=class]
+----
+
+== Wicket components
+
+To provide a custom component, we need to implement the `o.a.i.viewer.wicket.ui.ComponentFactory` interface, as a domain service.
+
+TIP: For more on this topic, see the link:https://isis.apache.org/vw/{isis-version}/extending.html#replacing-page-elements[Wicket viewer documentation].
+
+There are various subclasses available; as we want to replace the component for the entire entity, we can subclass from `EntityComponentFactoryAbstract`:
+
+
+[source,java]
+.WhereInTheWorldPanelFactory.java
+----
+include::../wicket/WhereInTheWorldPanelFactory.java[tags=class]
+----
+<.> the framework use the chain-of-responsibility pattern to look for a component factory to render the domain object.
+This `@Order` precedence ensures that this custom implementation is consulted early on.
+<.> indicates that this component applies when rendering a domain object (applies to view models as well as domain entities)
+<.> the superclass ensures that an `EntityModel` is provided to inspect.
+This is a Wicket model that is a serializable equivalent to the core framework's notion of a domain object.
+<.> the `ManagedObject` is the core framework's (aforementioned) notion of a domain object (providing access into the metamodel).
+<.> the actual domain object pojo, wrapped by and obtained from `ManagedObject`.
+<.> this component factory only applies if the domain object is an instance of `WhereInTheWorldVm`
+<.> safe to downcast, because of the `ComponentType specified in the constructor.
+<.> instantiates the actual Wicket component
+<.> the `GeoapifyClient` is required by the Wicket component.
+The framework doesn't inject into Wicket components, so instead this domain service is passed into the constructor.
+
+
+The `WhereInTheWorldPanel` is the actual custom Wicket component, using the Wicket API:
+
+* its constructor is:
++
+[source,java]
+.WhereInTheWorldPanel.java
+----
+include::../wicket/WhereInTheWorldPanel.java[tags=class]
+----
+<.> Wicket components are required to be serializable.
+<.> The `GeoapifyClient` as provided by the component factory, above.
+Note that this must _also_ be serializable.
+
+* the `onInitialize` method actually builds the UI:
++
+[source,java,indent=0]
+.WhereInTheWorldPanel.java
+----
+include::../wicket/WhereInTheWorldPanel.java[tags=onInitialize]
+----
+<.> obtain the core framework's `ManagedObject` from the Wicket model...
+<.> \... and obtain the domain object in turn.
+We can downcast to the view model because of the `appliesTo` check in the component factory, earlier.
+<.> create a Wicket `Label` component to display the latitude
+<.> similarly for the longitude
+<.> similarly for the address
+<.> call a helper method (shown below) to create the map's `Image` component
+<.> call a helper method (shown below) to create the Wicket viewer's normal component for the view model's `sources` property ...
+<.> \... and its `description` property.
+You are reading the content of this `description` property right now!
+<.> add all of these Wicket components to the containing div.
++
+Wicket requires there to be corresponding HTML (`WhereInTheWorld.html`) file for this component, and this has an HTML element for each subcomponent, identified using the `wicket:id` attribute.
+
+* the `createMapComponent()` helper is:
++
+[source,java,indent=0]
+.WhereInTheWorldPanel.java
+----
+include::../wicket/WhereInTheWorldPanel.java[tags=createMapComponent]
+----
+<.> call the `GeoapifyClient` to download the JPEG...
+<.> \... and returns an `Image` component holding same
+
+* the `createPropertyComponent()` helper is:
+*
+[source,java,indent=0]
+.WhereInTheWorldPanel.java
+----
+include::../wicket/WhereInTheWorldPanel.java[tags=createPropertyComponent]
+----
+<.> obtains the `ObjectSpecification` (the framework's equivalent of `java.lang.Class`) for the domain object
+<.> obtains the `OneToOneAssociation` (the framework's equivalent of a `java.lang.reflect.Method`) for the specified property
+<.> creates a Wicket (serializable) model to represent this aspect of the framework's metamodel
+<.> uses the parent entity model to create a scalar model holding the actual value of the property of the domain object
+<.> uses the Wicket viewer's `ComponentFactoryRegistry` to create an appropriate component for this property value.
+
+This example therefore shows that the resultant page can be a mix of entirely custom Wicket components, and also reusing components provided by the Wicket viewer.
+
+== GeoapifyClient
+
+We have already seen the `GeoapifyClient` domain service; it provides a geocode lookup of (lat, lng) for an address, and provides a jpeg image for that location, at the specified zoom:
+
+[source,java]
+.GeoapifyClient.java
+----
+include::../geocoding/GeoapifyClient.java[tags=class]
+----
+
+== Classpath
+
+It's probably best practice for the custom Wicket component classes to be in the `webapp` module, rather than here in the domain module; we've placed them here just to have all the code together and to be easily locatable.
+
+It does however mean that we had to tweak the classpath to bring in a dependency on the Wicket viewer modules:
+
+[source,xml]
+.pom.xml
+----
+<dependencies>
+    <dependency>
+        <groupId>org.apache.isis.viewer</groupId>
+        <artifactId>isis-viewer-wicket-model</artifactId>
+        <optional>true</optional>	<!--1-->
+    </dependency>
+    <dependency>
+        <groupId>org.apache.isis.viewer</groupId>
+        <artifactId>isis-viewer-wicket-ui</artifactId>
+        <optional>true</optional>	<!--1-->
+    </dependency>
+</dependencies>
+----
+<.> to avoid polluting the classpath
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/vm/CustomUiVm.java b/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/vm/WhereInTheWorldVm.java
similarity index 94%
rename from examples/demo/domain/src/main/java/demoapp/dom/ui/custom/vm/CustomUiVm.java
rename to examples/demo/domain/src/main/java/demoapp/dom/ui/custom/vm/WhereInTheWorldVm.java
index bcfd70d..d7c6611 100644
--- a/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/vm/CustomUiVm.java
+++ b/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/vm/WhereInTheWorldVm.java
@@ -37,11 +37,12 @@ import demoapp.dom.ui.custom.latlng.Latitude;
 import demoapp.dom.ui.custom.latlng.Longitude;
 import demoapp.dom.ui.custom.latlng.Zoom;
 
+//tag::class[]
 @XmlRootElement(name = "demo.CustomUiVm")
 @XmlType
 @XmlAccessorType(XmlAccessType.FIELD)
 @DomainObject(nature=Nature.VIEW_MODEL, objectType = "demo.CustomUiVm")
-public class CustomUiVm implements HasAsciiDocDescription, Serializable {
+public class WhereInTheWorldVm implements HasAsciiDocDescription, Serializable {
 
     @Title
     @Getter @Setter
@@ -58,5 +59,5 @@ public class CustomUiVm implements HasAsciiDocDescription, Serializable {
     @Zoom
     @Getter @Setter
     private int zoom;
-
 }
+//end::class[]
diff --git a/examples/demo/wicket/src/main/java/demoapp/webapp/wicket/customview/CustomUiPanel.html b/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/wicket/WhereInTheWorldPanel.html
similarity index 100%
rename from examples/demo/wicket/src/main/java/demoapp/webapp/wicket/customview/CustomUiPanel.html
rename to examples/demo/domain/src/main/java/demoapp/dom/ui/custom/wicket/WhereInTheWorldPanel.html
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/wicket/WhereInTheWorldPanel.java b/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/wicket/WhereInTheWorldPanel.java
new file mode 100644
index 0000000..69cdf2d
--- /dev/null
+++ b/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/wicket/WhereInTheWorldPanel.java
@@ -0,0 +1,118 @@
+/*
+ *  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.
+ */
+
+package demoapp.dom.ui.custom.wicket;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.image.Image;
+import org.apache.wicket.request.resource.ByteArrayResource;
+
+import org.apache.isis.core.metamodel.spec.ManagedObject;
+import org.apache.isis.core.metamodel.spec.ObjectSpecification;
+import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
+import org.apache.isis.viewer.common.model.object.ObjectUiModel;
+import org.apache.isis.viewer.wicket.model.hints.UiHintContainer;
+import org.apache.isis.viewer.wicket.model.mementos.PropertyMemento;
+import org.apache.isis.viewer.wicket.model.models.EntityModel;
+import org.apache.isis.viewer.wicket.model.models.ScalarModel;
+import org.apache.isis.viewer.wicket.ui.ComponentType;
+import org.apache.isis.viewer.wicket.ui.panels.PanelAbstract;
+
+import lombok.SneakyThrows;
+import lombok.val;
+
+import demoapp.dom.ui.custom.geocoding.GeoapifyClient;
+import demoapp.dom.ui.custom.vm.WhereInTheWorldVm;
+
+//tag::class[]
+public class WhereInTheWorldPanel extends PanelAbstract<EntityModel>  {
+
+    private static final long serialVersionUID = 1L;    // <.>
+
+    private final GeoapifyClient geoapifyClient;        // <.>
+
+    public WhereInTheWorldPanel(
+            final String id,
+            final EntityModel model,
+            final GeoapifyClient geoapifyClient) {
+        super(id, model);
+        this.geoapifyClient = geoapifyClient;
+    }
+    // ...
+//end::class[]
+
+
+    @Override
+    public UiHintContainer getUiHintContainer() {
+        // disables hinting by this component
+        return null;
+    }
+
+//tag::onInitialize[]
+    @Override
+    public void onInitialize() {
+        super.onInitialize();
+
+        val managedObject = getModel().getObject();;                       // <.>
+        val customUiVm = (WhereInTheWorldVm) managedObject.getPojo();      // <.>
+
+        val latitude = new Label("latitude", customUiVm.getLatitude());    // <.>
+        val longitude = new Label("longitude", customUiVm.getLongitude()); // <.>
+        val address = new Label("address", customUiVm.getAddress());       // <.>
+
+        val map = createMapComponent("map", customUiVm);                   // <.>
+
+        val sourcesComponent = createPropertyComponent("sources");         // <.>
+        val descriptionComponent = createPropertyComponent("description"); // <.>
+
+        addOrReplace(
+                latitude, longitude, address, map,
+                sourcesComponent, descriptionComponent);                   // <.>
+    }
+//end::onInitialize[]
+
+//tag::createMapComponent[]
+    @SneakyThrows
+    private Image createMapComponent(String id, WhereInTheWorldVm vm)  {
+        val bytes = geoapifyClient.toJpeg(
+                        vm.getLatitude(), vm.getLongitude(), vm.getZoom());  // <.>
+        return new Image(id, new ByteArrayResource("image/jpeg", bytes));    // <.>
+    }
+//end::createMapComponent[]
+
+//tag::createPropertyComponent[]
+    private Component createPropertyComponent(final String propertyId) {
+        val managedObject = getModel().getManagedObject();
+        val spec = managedObject.getSpecification();                               // <.>
+        val otoa = (OneToOneAssociation) spec.getAssociationElseFail(propertyId);  // <.>
+        val pm = new PropertyMemento(otoa);                                        // <.>
+
+        val scalarModel =
+                getModel().getPropertyModel(                                       // <.>
+                    pm, ObjectUiModel.Mode.VIEW,
+                    ObjectUiModel.RenderingHint.REGULAR);
+        return getComponentFactoryRegistry().createComponent(                      // <.>
+                ComponentType.SCALAR_NAME_AND_VALUE, propertyId, scalarModel);
+    }
+//end::createPropertyComponent[]
+
+//tag::class[]
+}
+//end::class[]
diff --git a/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/wicket/WhereInTheWorldPanelFactory.java b/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/wicket/WhereInTheWorldPanelFactory.java
new file mode 100644
index 0000000..686187d
--- /dev/null
+++ b/examples/demo/domain/src/main/java/demoapp/dom/ui/custom/wicket/WhereInTheWorldPanelFactory.java
@@ -0,0 +1,48 @@
+package demoapp.dom.ui.custom.wicket;
+
+import javax.inject.Inject;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.model.IModel;
+import org.springframework.core.annotation.Order;
+
+import org.apache.isis.applib.annotation.OrderPrecedence;
+import org.apache.isis.core.metamodel.spec.ManagedObject;
+import org.apache.isis.viewer.wicket.model.models.EntityModel;
+import org.apache.isis.viewer.wicket.ui.ComponentType;
+import org.apache.isis.viewer.wicket.ui.components.entity.EntityComponentFactoryAbstract;
+
+import demoapp.dom.ui.custom.geocoding.GeoapifyClient;
+import demoapp.dom.ui.custom.vm.WhereInTheWorldVm;
+
+//tag::class[]
+@org.springframework.stereotype.Component
+@Order(OrderPrecedence.EARLY)                                             // <.>
+public class WhereInTheWorldPanelFactory extends EntityComponentFactoryAbstract {
+
+    public WhereInTheWorldPanelFactory() {
+        super(
+            ComponentType.ENTITY                                          // <.>
+            , WhereInTheWorldPanel.class
+        );
+    }
+
+    @Override
+    protected ApplicationAdvice doAppliesTo(EntityModel entityModel) {    // <.>
+        final ManagedObject managedObject = entityModel.getObject();      // <.>
+        final Object domainObject = managedObject.getPojo();              // <.>
+        return ApplicationAdvice.appliesIf(
+                domainObject instanceof WhereInTheWorldVm);               // <.>
+    }
+
+    @Override
+    public Component createComponent(final String id, final IModel<?> model) {
+        EntityModel entityModel = (EntityModel) model;                    // <.>
+        return new WhereInTheWorldPanel(id, entityModel, geoapifyClient); // <.>
+    }
+
+    @Inject
+    private GeoapifyClient geoapifyClient;                                // <.>
+
+}
+//end::class[]
diff --git a/examples/demo/wicket/src/main/java/demoapp/webapp/wicket/DemoAppWicket.java b/examples/demo/wicket/src/main/java/demoapp/webapp/wicket/DemoAppWicket.java
index 723d360..e997895 100644
--- a/examples/demo/wicket/src/main/java/demoapp/webapp/wicket/DemoAppWicket.java
+++ b/examples/demo/wicket/src/main/java/demoapp/webapp/wicket/DemoAppWicket.java
@@ -33,7 +33,6 @@ import org.apache.isis.valuetypes.sse.ui.wkt.IsisModuleValSseUiWkt;
 import org.apache.isis.viewer.wicket.viewer.IsisModuleViewerWicketViewer;
 
 import demoapp.web.DemoAppManifest;
-import demoapp.webapp.wicket.customview.CustomUiPanelFactory;
 
 /**
  * Bootstrap the application.
@@ -55,9 +54,6 @@ import demoapp.webapp.wicket.customview.CustomUiPanelFactory;
     // Persistence (JDO/DN5)
     IsisModuleValAsciidocPersistenceJdoDn5.class,
     IsisModuleValMarkdownPersistenceJdoDn5.class,
-
-    // @Component's
-    CustomUiPanelFactory.class
 })
 //@Log4j2
 public class DemoAppWicket extends SpringBootServletInitializer {
diff --git a/examples/demo/wicket/src/main/java/demoapp/webapp/wicket/customview/CustomUiPanel.java b/examples/demo/wicket/src/main/java/demoapp/webapp/wicket/customview/CustomUiPanel.java
deleted file mode 100644
index 52bd118..0000000
--- a/examples/demo/wicket/src/main/java/demoapp/webapp/wicket/customview/CustomUiPanel.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- *  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.
- */
-
-package demoapp.webapp.wicket.customview;
-
-import java.io.IOException;
-
-import org.apache.wicket.Component;
-import org.apache.wicket.markup.html.basic.Label;
-import org.apache.wicket.markup.html.image.Image;
-import org.apache.wicket.request.resource.ByteArrayResource;
-
-import org.apache.isis.core.metamodel.spec.ManagedObject;
-import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation;
-import org.apache.isis.viewer.common.model.object.ObjectUiModel;
-import org.apache.isis.viewer.wicket.model.hints.UiHintContainer;
-import org.apache.isis.viewer.wicket.model.mementos.PropertyMemento;
-import org.apache.isis.viewer.wicket.model.models.EntityModel;
-import org.apache.isis.viewer.wicket.ui.ComponentFactory;
-import org.apache.isis.viewer.wicket.ui.ComponentType;
-import org.apache.isis.viewer.wicket.ui.panels.PanelAbstract;
-
-import lombok.SneakyThrows;
-import lombok.val;
-
-import demoapp.dom.ui.custom.geocoding.GeoapifyClient;
-import demoapp.dom.ui.custom.vm.CustomUiVm;
-
-public class CustomUiPanel extends PanelAbstract<EntityModel>  {
-
-    private static final long serialVersionUID = 1L;
-
-    private final GeoapifyClient geoapifyClient;
-
-    public CustomUiPanel(
-            final String id,
-            final EntityModel model,
-            final ComponentFactory componentFactory,
-            final GeoapifyClient geoapifyClient) {
-        super(id, model);
-        this.geoapifyClient = geoapifyClient;
-    }
-
-
-    @Override
-    public UiHintContainer getUiHintContainer() {
-        // disables hinting by this component
-        return null;
-    }
-
-    /**
-     * Build UI only after added to parent.
-     */
-    @Override
-    public void onInitialize() {
-        super.onInitialize();
-        buildGui();
-    }
-
-
-    @SneakyThrows
-    private void buildGui() {
-        val managedObject = (ManagedObject) getModelObject();
-        val customUiVm = (CustomUiVm) managedObject.getPojo();
-
-        val latitude = new Label("latitude", customUiVm.getLatitude());
-        val longitude = new Label("longitude", customUiVm.getLongitude());
-        val address = new Label("address", customUiVm.getAddress());
-        val map = createMapComponent("map", customUiVm);
-        val sourcesComponent = createPropertyComponent(managedObject, "sources");
-        val descriptionComponent = createPropertyComponent(managedObject, "description");
-
-        addOrReplace(latitude, longitude, address, map, sourcesComponent, descriptionComponent);
-    }
-
-    private Image createMapComponent(String id, CustomUiVm customUiVm) throws IOException {
-        val bytes = geoapifyClient.toJpeg(customUiVm.getLatitude(), customUiVm.getLongitude(), customUiVm.getZoom());
-        val map = new Image(id, new ByteArrayResource("image/jpeg", bytes));
-        return map;
-    }
-
-    private Component createPropertyComponent(ManagedObject managedObject, String propertyId) {
-        val spec = managedObject.getSpecification();
-        val descriptionAssoc = (OneToOneAssociation) spec.getAssociationElseFail(propertyId);
-        val descriptionPm = new PropertyMemento(descriptionAssoc);
-
-        val entityModel = EntityModel.ofAdapter(getCommonContext(), managedObject);
-        val descriptionModel = entityModel.getPropertyModel(descriptionPm, ObjectUiModel.Mode.VIEW, ObjectUiModel.RenderingHint.REGULAR);
-        return getComponentFactoryRegistry().createComponent(ComponentType.SCALAR_NAME_AND_VALUE, propertyId, descriptionModel);
-    }
-
-
-}
diff --git a/examples/demo/wicket/src/main/java/demoapp/webapp/wicket/customview/CustomUiPanelFactory.java b/examples/demo/wicket/src/main/java/demoapp/webapp/wicket/customview/CustomUiPanelFactory.java
deleted file mode 100644
index 8e19de0..0000000
--- a/examples/demo/wicket/src/main/java/demoapp/webapp/wicket/customview/CustomUiPanelFactory.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package demoapp.webapp.wicket.customview;
-
-import javax.inject.Inject;
-
-import org.apache.wicket.Component;
-import org.apache.wicket.model.IModel;
-import org.springframework.core.annotation.Order;
-
-import org.apache.isis.applib.annotation.OrderPrecedence;
-import org.apache.isis.viewer.wicket.model.models.EntityModel;
-import org.apache.isis.viewer.wicket.ui.ComponentType;
-import org.apache.isis.viewer.wicket.ui.components.entity.EntityComponentFactoryAbstract;
-
-import demoapp.dom.ui.custom.geocoding.GeoapifyClient;
-import demoapp.dom.ui.custom.vm.CustomUiVm;
-
-@org.springframework.stereotype.Component
-@Order(OrderPrecedence.EARLY)
-public class CustomUiPanelFactory extends EntityComponentFactoryAbstract {
-
-    private static final long serialVersionUID = 1L;
-
-    public CustomUiPanelFactory() {
-        super(ComponentType.ENTITY, CustomUiPanel.class);
-    }
-
-    @Override
-    protected ApplicationAdvice doAppliesTo(EntityModel entityModel) {
-        return ApplicationAdvice.appliesIf(entityModel.getObject().getPojo() instanceof CustomUiVm);
-    }
-
-    @Override
-    public Component createComponent(final String id, final IModel<?> model) {
-        final EntityModel entityModel = (EntityModel) model;
-
-        return new CustomUiPanel(id, entityModel, this, geoapifyClient);
-    }
-
-    @Inject
-    private GeoapifyClient geoapifyClient;
-
-}