Creating views that can be used both locally and remotely
This page is a practical skill for implementing Phlow views that work in both local and remote inspectors. Explore the current implementation in the image before adopting a pattern.
A local/remote view consists of several collaborating classes:
1. View (subclass of GtRemotePhlowLocalView
GtRemotePhlowView << #GtRemotePhlowLocalView
slots: { #localOriginalView . #localBuildContext . #localDefiningMethodProvider };
package: 'GToolkit-RemotePhlow-PhlowViews'
) — the view definition created by <gtView> methods. Holds computation blocks for producing the view's data.
2. Specification (subclass of GtPhlowViewSpecification
Object << #GtPhlowViewSpecification
slots: { #phlowDataSource . #methodSelector . #title . #priority . #dataTransport . #actionSpecifications };
package: 'GToolkit-RemotePhlow-DeclarativeViews'
) — a serializable description of the view that can cross process boundaries. Knows how to serialize/deserialize itself and how to create a local rendering element.
3. Data Source (subclass of GtRemotePhlowDeclarativeViewDataSource
Object << #GtRemotePhlowDeclarativeViewDataSource
slots: { #phlowView };
package: 'GToolkit-RemotePhlow-DeclarativeViews'
) — evaluates the view's computation blocks and produces view data. Wraps errors into GtRemotePhlowViewErrorData
GtRemotePhlowBasicViewData << #GtRemotePhlowViewErrorData
slots: { #errorDescription . #errorObject . #isRemoteException };
package: 'GToolkit-RemotePhlow-DeclarativeViews'
.
4. View Data (subclass of GtRemotePhlowBasicViewData
GtPhlowDeclarativeSpecification << #GtRemotePhlowBasicViewData
slots: {};
package: 'GToolkit-RemotePhlow-DeclarativeViews'
) — a value object carrying the computed data (e.g., bytes, dimensions). Serializes to/from dictionaries for transport.
5. GemStone Proxy (subclass of GtpoGtPhlowViewSpecification
GtRsrProxyServiceClient << #GtpoGtPhlowViewSpecification
slots: {};
tag: 'Proxies';
package: 'GToolkit-GemStone-Pharo'
) — client-side proxy that fetches data from a remote GemStone specification via proxyPerform:.
For views that just need to pass a single data object around use GtRemotePhlowPictureView
GtRemotePhlowLocalView << #GtRemotePhlowPictureView
slots: { #pictureComputation . #widthComputation . #heightComputation . #mediaTypeComputation . #renderingStencilName . #shouldLoadContentLazily };
package: 'GToolkit-RemotePhlow-PhlowViews'
as a starting example. For these views, GtRemotePhlowConcreteDataViewSpecification
GtPhlowViewSpecification << #GtRemotePhlowConcreteDataViewSpecification
slots: { #renderingStencilName . #viewData };
package: 'GToolkit-RemotePhlow-DeclarativeViews'
and the generic data source GtRemotePhlowConcreteDataViewDataSource
GtRemotePhlowDeclarativeViewDataSource << #GtRemotePhlowConcreteDataViewDataSource
slots: {};
package: 'GToolkit-RemotePhlow-DeclarativeViews'
are sufficient. The application defines only the view by subclassing GtRemotePhlowLocalView
GtRemotePhlowView << #GtRemotePhlowLocalView
slots: { #localOriginalView . #localBuildContext . #localDefiningMethodProvider };
package: 'GToolkit-RemotePhlow-PhlowViews'
, its GtRemotePhlowBasicViewData
GtPhlowDeclarativeSpecification << #GtRemotePhlowBasicViewData
slots: {};
package: 'GToolkit-RemotePhlow-DeclarativeViews'
subclass, and its rendering stencil(s).
Define an application-specific specification/datasource pair only if generic concrete-data retrieval is insufficient. GtRemotePhlowWebBrowserView
GtRemotePhlowLocalView << #GtRemotePhlowWebBrowserView
slots: { #contentComputation . #staticHeaders . #dynamicHeadersComputation . #shouldLoadContentLazily };
package: 'GToolkit-RemotePhlow-PhlowViews'
is that advanced example.
The data flows through these stages:
1. A <gtView> method calls aView picture (or similar) which creates the View instance and configures it with computation blocks.
2. When the view enters the declarative pipeline, #selector
asGtDeclarativeView
^ nil
creates a Specification with a Data Source attached.
Usually this is GtRemotePhlowConcreteDataViewSpecification
GtPhlowViewSpecification << #GtRemotePhlowConcreteDataViewSpecification
slots: { #renderingStencilName . #viewData };
package: 'GToolkit-RemotePhlow-DeclarativeViews'
, backed by GtRemotePhlowConcreteDataViewDataSource
GtRemotePhlowDeclarativeViewDataSource << #GtRemotePhlowConcreteDataViewDataSource
slots: {};
package: 'GToolkit-RemotePhlow-DeclarativeViews'
; the datasource invokes the view's computeViewData (e.g., GtRemotePhlowPictureView>>#computeViewData
computeViewData
^ GtRemotePhlowPictureViewData new
content: self pictureComputation value;
mediaType: self mediaTypeComputation value;
width: self widthComputation value;
height: self heightComputation value
).
3. The specification is serialized via #selector
asDictionaryForExport
"Answer the receiver as a dictionary ready for JSON serialisation.
Subclasses will override and add to the dictionary"
| specificationData |
specificationData := self asBasicViewDataForExport.
specificationData
at: 'actionSpecifications'
put: (actionSpecifications
ifNil: [ #() ]
ifNotNil: [ :aCollection |
aCollection collect: [ :each | each asDictionaryForExport ] ]) asArray .
^ specificationData
/ #selector
asBasicViewDataForExport
"Answer the receiver as a dictionary ready for JSON serialisation.
Subclasses will override and add to the dictionary"
| specificationData |
specificationData := Dictionary new
at: 'viewName' put: self viewName;
at: '__typeName' put: self class name;
at: 'title' put: title;
at: 'priority' put: priority;
at: 'dataTransport' put: dataTransport;
at: 'methodSelector' put: methodSelector;
yourself.
^ specificationData
for transport.
4. On the receiving side, GtPhlowViewSpecification>>#fromDictionary:
fromDictionary: viewDictionary
"Answer the view specified by viewDictionary"
| viewName |
viewName := viewDictionary at: 'viewName'.
^(Smalltalk globals at: viewName asSymbol) fromJSONDictionary: viewDictionary.
reconstructs the specification.
5. #selector
initializeFromInspector: anInspector
reconnects the specification's data source (for lazy data transport).
6. When rendering, the specification and data source provide methods that compute and return the the relevant View Data. For example GtRemotePhlowConcreteDataViewSpecification>>#retrieveData
retrieveData
^self phlowDataSource retrieveData
and GtRemotePhlowConcreteDataViewDataSource>>#retrieveData
retrieveData
^ [ self computeViewData ]
on: Error
do: [ :anError | self captureErrorDataFor: anError ]
for the picture view, and GtRemotePhlowWebBrowserViewSpecification>>#retrieveContent
retrieveContent
^ self phlowDataSource retrieveContent
/GtRemotePhlowWebBrowserViewSpecification>>#retrieveDynamicHeaders
retrieveDynamicHeaders
^ self phlowDataSource retrieveDynamicHeaders
and GtRemotePhlowDeclarativeWebBrowserViewDataSource>>#retrieveContent
retrieveContent
^ [ self computeContent ]
on: Error
do: [ :anError | self captureErrorDataFor: anError ]
/GtRemotePhlowDeclarativeWebBrowserViewDataSource>>#retrieveDynamicHeaders
retrieveDynamicHeaders
^ [ self computeDynamicHeaders ]
on: Error
do: [ :anError | self captureErrorDataFor: anError ]
for the web browser view.
The view class subclasses GtRemotePhlowLocalView
GtRemotePhlowView << #GtRemotePhlowLocalView
slots: { #localOriginalView . #localBuildContext . #localDefiningMethodProvider };
package: 'GToolkit-RemotePhlow-PhlowViews'
and is the entry point for <gtView> methods. The view class:
- Provides a builder API (e.g., #selector
content: aBlock
pictureComputation := aBlock
, #selector
width: anObject
widthComputation := anObject
, #selector
dynamicHeaders: aBlock
dynamicHeadersComputation := aBlock.
, #selector
content: aBlock
self contentComputation: aBlock
)
- Implements #selector
asGtDeclarativeView
^ nil
to create the specification with a data source
- Reuses #selector
configureGenericViewSpecificationOn: aViewSpecification
aViewSpecification
title: self title;
priority: self priority;
actionSpecifications: (self actions collect: [ :each |
each asGtDeclarativeAction ]) asArray
from GtRemotePhlowView
GtRemotePhlowProtoView << #GtRemotePhlowView
slots: { #title . #priority . #definingSelector . #definingClass . #phlowActions };
package: 'GToolkit-RemotePhlow-PhlowViews'
which sets title, priority, and action specifications
For the primary generic approach, implement computeViewData to answer a serializable concrete data value. GtRemotePhlowPictureView>>#computeViewData
computeViewData
^ GtRemotePhlowPictureViewData new
content: self pictureComputation value;
mediaType: self mediaTypeComputation value;
width: self widthComputation value;
height: self heightComputation value
is the example. Its GtRemotePhlowPictureView>>#asGtDeclarativeView
asGtDeclarativeView
| viewSpecification |
viewSpecification := (GtRemotePhlowConcreteDataViewSpecification new)
phlowDataSource: (GtRemotePhlowConcreteDataViewDataSource
forPhlowView: self);
renderingStencilName: self renderingStencilName;
dataTransport: self currentDataTransport.
self configureGenericViewSpecificationOn: viewSpecification.
^viewSpecification
installs the generic concrete-data specification and datasource.
Register the same view type on GtPhlowProtoView
Object << #GtPhlowProtoView
traits: {TBlDebug};
slots: {};
sharedVariables: { #IsTaskItViewGloballyEnabled };
tag: '! Views';
package: 'GToolkit-Phlow'
and GtRemotePhlowProtoView
Object << #GtRemotePhlowProtoView
slots: {};
package: 'GToolkit-RemotePhlow-PhlowViews'
.
Example — the picture view builder on GtPhlowProtoView
Object << #GtPhlowProtoView
traits: {TBlDebug};
slots: {};
sharedVariables: { #IsTaskItViewGloballyEnabled };
tag: '! Views';
package: 'GToolkit-Phlow'
:
GtPhlowProtoView>>#picture
picture
^(GtRemotePhlowPictureView new)
originalView: self;
definingMethod: (GtPhlowDefiningMethodsCollector forContext: thisContext)
collect
Example — the picture view builder on GtRemotePhlowProtoView
Object << #GtRemotePhlowProtoView
slots: {};
package: 'GToolkit-RemotePhlow-PhlowViews'
:
GtRemotePhlowProtoView>>#picture
picture
^ self declarativeLocalRemoteViewOfType: GtRemotePhlowPictureView.
A view specification is a serializable description of a Phlow view: it carries the view’s title, priority, actions, transport configuration, and any data needed to reconstruct and render it in another place. It creates the local explicit view—typically with a declarative stencil—and coordinates retrieving or holding the concrete view data.
Most new views can use the generic GtRemotePhlowConcreteDataViewSpecification
GtPhlowViewSpecification << #GtRemotePhlowConcreteDataViewSpecification
slots: { #renderingStencilName . #viewData };
package: 'GToolkit-RemotePhlow-DeclarativeViews'
; define a custom specification, such as GtRemotePhlowWebBrowserViewSpecification
GtPhlowViewSpecification << #GtRemotePhlowWebBrowserViewSpecification
slots: { #staticHeaders . #hasDynamicHeaders . #browserContent . #dynamicHeaders };
package: 'GToolkit-RemotePhlow-DeclarativeViews'
, only when the view needs specialized retrieval, state, rendering, or refresh behavior.
GtRemotePhlowWebBrowserViewSpecification
GtPhlowViewSpecification << #GtRemotePhlowWebBrowserViewSpecification
slots: { #staticHeaders . #hasDynamicHeaders . #browserContent . #dynamicHeaders };
package: 'GToolkit-RemotePhlow-DeclarativeViews'
exists because browser content and dynamic headers are distinct computed values that must be retrieved, cached/cleared, merged, and rendered together.
Whether generic or custom the specification (subclass of GtPhlowViewSpecification
Object << #GtPhlowViewSpecification
slots: { #phlowDataSource . #methodSelector . #title . #priority . #dataTransport . #actionSpecifications };
package: 'GToolkit-RemotePhlow-DeclarativeViews'
) must implement:
- #selector
viewFor: aView
"Specifications should implement this method to create a local phlow view from the remote data."
— creates a local view that renders the view as an element and participates in the declarative pipeline. The specification serializes itself via #selector
asDictionaryForExportWithPhlowDataSource
"Answer the receiver as a dictionary ready for JSON serialisation.
Subclasses will override and add to the dictionary"
| specificationData |
specificationData := self asBasicViewDataForExport.
specificationData at: 'phlowDataSource' put: self.
specificationData
at: 'actionSpecifications'
put: (actionSpecifications
ifNil: [ #() ]
ifNotNil: [ :aCollection |
aCollection collect: [ :each | each asDictionaryForExportWithPhlowDataSource ] ]) asArray .
^ specificationData
and reconstructs a fresh instance.
- #selector
asBasicViewDataForExport
"Answer the receiver as a dictionary ready for JSON serialisation.
Subclasses will override and add to the dictionary"
| specificationData |
specificationData := Dictionary new
at: 'viewName' put: self viewName;
at: '__typeName' put: self class name;
at: 'title' put: title;
at: 'priority' put: priority;
at: 'dataTransport' put: dataTransport;
at: 'methodSelector' put: methodSelector;
yourself.
^ specificationData
— serializes view-specific fields (call super first, then add custom keys)
- #selector
fromJSONDictionary: aDictionary
"Answer an instance of the receiver from the supplied dictionary.
Subclasses will override this to add their specific attributes"
| specification |
specification := self new
title: (aDictionary at: 'title');
priority: (aDictionary at: 'priority');
dataTransport: (aDictionary at: 'dataTransport');
methodSelector: (aDictionary at: 'methodSelector');
phlowDataSource: (aDictionary
at: 'phlowDataSource' ifAbsent: [ nil ]);
yourself.
(aDictionary includesKey: 'actionSpecifications') ifTrue: [
specification actionSpecifications: ((aDictionary at: 'actionSpecifications')
collect: [ :each | GtPhlowActionSpecification fromDictionary: each ]) ].
^ specification
(class-side) — deserializes from a dictionary
- #selector
initializeFromInspector: anInspector
— reconnects the phlowDataSource after deserialization for lazy data transport as in GtRemotePhlowConcreteDataViewSpecification>>#initializeFromInspector:
initializeFromInspector: anInspector
self phlowDataSource
ifNil:
[self
phlowDataSource: (anInspector getDeclarativeViewFor: self methodSelector)]
Key pattern for implementing #selector
viewFor: aView
"Specifications should implement this method to create a local phlow view from the remote data."
- use an explicit view with a declarative specification (GtPhlowExplicitView>>#declarativeStencil:
declarativeStencil: aStencilBuilder
stencilBuilder := aStencilBuilder asPhlowDeclarativeStencilBuilder
) so the view both renders locally AND remains declarative. Examples are:
GtRemotePhlowConcreteDataViewSpecification>>#viewFor:
viewFor: aView
| pictureView |
pictureView := aView explicit
originalView: aView;
title: title;
priority: priority;
phlowViewSpecification: self;
declarativeStencil: [
| data |
data := self phlowDataSource retrieveData.
self createDeclarativeStencilForViewData: data ].
self configureViewActionsFor: pictureView.
^pictureView
GtRemotePhlowWebBrowserViewSpecification>>#viewFor:
viewFor: aView
| browserView |
browserView := aView explicit
originalView: aView;
title: title;
priority: priority;
phlowViewSpecification: self;
declarativeStencil: [ self createDeclarativeContentStencil ].
self configureViewActionsFor: browserView.
^ browserView
- set the current specificatio in the created view using GtPhlowView>>#phlowViewSpecification:
phlowViewSpecification: aViewSpecification
self
propertyAt: 'phlowViewSpecification'
put: aViewSpecification
- handle the case or error data, like in GtRemotePhlowConcreteDataViewSpecification>>#createDeclarativeStencilForViewData:
createDeclarativeStencilForViewData: aViewData
^ aViewData isPhlowErrorData
ifTrue: [
GtPhlowErrorStencil new stencilData: aViewData ]
ifFalse: [
| stecil |
stecil := GtPhlowNamedStencil new stencilClassName: self renderingStencilName.
stecil stencilData: aViewData.
stecil ]
or GtRemotePhlowWebBrowserViewSpecification>>#createDeclarativeContentStencil
createDeclarativeContentStencil
| content currentDynamicHeaders contentHeaders |
content := self browserContent.
content isPhlowErrorData ifTrue: [
^ GtPhlowErrorStencil new stencilData: content ].
currentDynamicHeaders := self dynamicHeaders.
currentDynamicHeaders isPhlowErrorData ifTrue: [
^ GtPhlowErrorStencil new stencilData: currentDynamicHeaders ].
contentHeaders := self staticHeaders copy.
currentDynamicHeaders do: [ :name :value |
contentHeaders headerAt: name put: value ].
^ GtRemotePhlowWebBrowserStencil new
browserContent: content;
contentHeaders: contentHeaders
The data source (subclass of GtRemotePhlowDeclarativeViewDataSource
Object << #GtRemotePhlowDeclarativeViewDataSource
slots: { #phlowView };
package: 'GToolkit-RemotePhlow-DeclarativeViews'
) evaluates the view's computations and wraps results in a view data object. On errors, it returns a GtRemotePhlowViewErrorData
GtRemotePhlowBasicViewData << #GtRemotePhlowViewErrorData
slots: { #errorDescription . #errorObject . #isRemoteException };
package: 'GToolkit-RemotePhlow-DeclarativeViews'
instead of propagating the exception, e.g.,
GtRemotePhlowConcreteDataViewDataSource>>#retrieveData
retrieveData
^ [ self computeViewData ]
on: Error
do: [ :anError | self captureErrorDataFor: anError ]
Most concrete-data views use GtRemotePhlowConcreteDataViewDataSource
GtRemotePhlowDeclarativeViewDataSource << #GtRemotePhlowConcreteDataViewDataSource
slots: {};
package: 'GToolkit-RemotePhlow-DeclarativeViews'
, while a custom source such as GtRemotePhlowDeclarativeWebBrowserViewDataSource
GtRemotePhlowDeclarativeViewDataSource << #GtRemotePhlowDeclarativeWebBrowserViewDataSource
slots: {};
package: 'GToolkit-RemotePhlow-DeclarativeViews'
is appropriate when a view has multiple or specialized values to retrieve.
Concrete values crossing the boundary subclass GtRemotePhlowBasicViewData
GtPhlowDeclarativeSpecification << #GtRemotePhlowBasicViewData
slots: {};
package: 'GToolkit-RemotePhlow-DeclarativeViews'
and implement dictionary export/import. Keep them independent of computation blocks.
- Holds the computed values (content bytes, dimensions, media type, etc.)
- Overrides #selector
asDictionaryForExport
"Answer the receiver as a dictionary ready for JSON serialisation.
Subclasses will override and add to the dictionary"
| specificationData |
specificationData := Dictionary new
at: '__typeName' put: self class name;
yourself.
self class typeLabel ifNotNil: [ :aLabel |
specificationData at: '__typeLabel' put: aLabel ].
^ specificationData
for serialization
- Overrides #selector
initializeFromJSONDictionary: aDictionary
for deserialization
- Reports #selector
isPhlowErrorData
^false
→ false (vs. GtRemotePhlowViewErrorData
GtRemotePhlowBasicViewData << #GtRemotePhlowViewErrorData
slots: { #errorDescription . #errorObject . #isRemoteException };
package: 'GToolkit-RemotePhlow-DeclarativeViews'
which reports true)
For the picture view all data is modeles in a single object GtRemotePhlowPictureViewData
GtRemotePhlowViewData << #GtRemotePhlowPictureViewData
slots: { #content . #mediaType . #width . #height };
package: 'GToolkit-RemotePhlow-DeclarativeViews'
that knows how to serialize itself in GtRemotePhlowPictureViewData>>#asDictionaryForExport
asDictionaryForExport
| dict |
dict := super asDictionaryForExport.
dict at: 'content' put: content.
mediaType ifNotNil: [dict at: 'mediaType' put: mediaType].
width ifNotNil: [dict at: 'width' put: width].
height ifNotNil: [dict at: 'height' put: height].
^dict
For the web browser view distinct data objects are used, namely subclasses of GtRemotePhlowWebViewContent
GtRemotePhlowBasicViewData << #GtRemotePhlowWebViewContent
slots: {};
package: 'GToolkit-RemotePhlow-DeclarativeViews'
for what the browser shows and GtRemotePhlowWebViewHeader
GtRemotePhlowBasicViewData << #GtRemotePhlowWebViewHeader
slots: { #headers };
package: 'GToolkit-RemotePhlow-DeclarativeViews'
. In this case this data can be retried in distinct calls.
The generic concrete-data specification uses a configured rendering-stencil name and GtPhlowNamedStencil
GtPhlowForwardStencil << #GtPhlowNamedStencil
slots: { #stencilClassName . #stencilDataWrapper };
package: 'GToolkit-RemotePhlow-Stencils'
. This is how GtRemotePhlowPictureView
GtRemotePhlowLocalView << #GtRemotePhlowPictureView
slots: { #pictureComputation . #widthComputation . #heightComputation . #mediaTypeComputation . #renderingStencilName . #shouldLoadContentLazily };
package: 'GToolkit-RemotePhlow-PhlowViews'
permits callers to select an alternative rendering class, including a local-only stencil.
For a fixed rendering contract, a dedicated stencil subclassing GtPhlowStencil
GtPhlowDeclarativeSpecification << #GtPhlowStencil
slots: {};
package: 'GToolkit-RemotePhlow-Stencils'
is appropriate. GtRemotePhlowWebBrowserStencil
GtPhlowStencil << #GtRemotePhlowWebBrowserStencil
slots: { #browserContent . #contentHeaders };
package: 'GToolkit-RemotePhlow-Stencils'
demonstrates this. It owns only concrete serializable values, implements #selector
asDictionaryForExport
| dictionary |
dictionary := super asDictionaryForExport.
dictionary at: 'browserContent' put: browserContent asDictionaryForExport.
contentHeaders ifNotNil: [ :headers |
dictionary at: 'contentHeaders' put: headers asDictionaryForExport ].
^ dictionary
and #selector
initializeFromJSONDictionary: aDictionary
super initializeFromJSONDictionary: aDictionary.
self browserContent: (GtRemotePhlowBasicViewData
fromDictionary: (aDictionary at: 'browserContent')).
(aDictionary at: 'contentHeaders' ifAbsent: [ nil ])
ifNotNil: [ :headers |
self contentHeaders: (GtRemotePhlowBasicViewData fromDictionary: headers) ]
, and inherits GtPhlowStencil>>#asGtStencilSpecification
asGtStencilSpecification
^ GtRemotePhlowDirectStencilSpecification new
stencilClassName: self class name;
stencilData: self asDictionaryForExport
. Its standalone API accepts concrete values, never closures.
For views to work across GemStone, a client-side proxy class is needed (subclass of GtpoGtPhlowViewSpecification
GtRsrProxyServiceClient << #GtpoGtPhlowViewSpecification
slots: {};
tag: 'Proxies';
package: 'GToolkit-GemStone-Pharo'
). The proxy lives in the package GToolkit-GemStone-Pharo in the Proxies tag.
Add a client-side Gtpo...Specification proxy in GToolkit-GemStone-Pharo / Proxies only when the specification exposes specialized retrieval or update operations not already handled by the generic pipeline.
GtpoGtRemotePhlowWebBrowserViewSpecification
GtpoGtPhlowViewSpecification << #GtpoGtRemotePhlowWebBrowserViewSpecification
slots: {};
tag: 'Proxies';
package: 'GToolkit-GemStone-Pharo'
is the advanced example: it forwards content/header retrieval and cache flushing through proxyPerform: and reconstructs returned dictionaries as basic view data. Deploy the implementation to GemStone before running its GemStone examples.
Use two complementary example hierarchies. Both are abstract on the class side; concrete subclasses select the execution context and inherit every applicable example.
GtRemotePhlowPictureViewAbstractExamples
Object << #GtRemotePhlowPictureViewAbstractExamples
slots: {};
tag: 'Examples-Views';
package: 'GToolkit-RemoteGt'
tests the view API and the specification/data path. Its Local and Remote subclasses implement GtRemotePhlowPictureViewAbstractExamples>>#createProtoView
createProtoView
self subclassResponsibility
to answer, respectively, a GtPhlowProtoView
Object << #GtPhlowProtoView
traits: {TBlDebug};
slots: {};
sharedVariables: { #IsTaskItViewGloballyEnabled };
tag: '! Views';
package: 'GToolkit-Phlow'
or a GtRemotePhlowProtoView
Object << #GtRemotePhlowProtoView
slots: {};
package: 'GToolkit-RemotePhlow-PhlowViews'
. Always create a view through self createProtoView in this hierarchy—never instantiate/configure only one proto-view type—so the same inherited examples exercise both APIs.
GtRemotePhlowExplicitViewAbstractExamples
Object << #GtRemotePhlowExplicitViewAbstractExamples
slots: {};
tag: 'Examples-Views';
package: 'GToolkit-RemoteGt'
follows the same construction-hierarchy pattern for the more complex explicit view
Put shared examples in the abstract class: configure the view, create its declarative specification, retrieve normal and error data, round-trip view data and specifications, and render the resulting stencil. Add a subclass-specific example only when its behaviour genuinely belongs to one API.
GtRemotePhlowPictureViewInspectionExamples
Object << #GtRemotePhlowPictureViewInspectionExamples
traits: {TGtRemotePhlowLocalInspectionAssertions};
slots: {};
tag: 'Examples-Views';
package: 'GToolkit-RemoteGt'
tests a view as an inspector user sees it. Its subclasses implement GtRemotePhlowPictureViewInspectionExamples>>#createTestObject
createTestObject
<gtExample>
^ GtRemotePhlowPictureViewTestObject new
(and, where needed, createErrorTestObject, and other relevant objects) to supply a local object, a remote object, or a simulation/proxy object.
Shared examples belong in the abstract superclass and verify that object views render, picture tabs create picture elements, full-size views have the expected result, and errors produce debuggable elements. Use BlScripter
Object << #BlScripter
traits: {TBlDevScripterActionStep + TBlDevScripterCheckStepCreation};
slots: { #element . #space . #events . #rootStep . #eventHandler . #maxPulseElapsedTime };
tag: 'Scripter';
package: 'Bloc-Scripter'
examples for tab interaction and definition browsing such as alt-click.
Keep object-context-specific checks in the corresponding subclass: for example, local debugging controls versus remote inspection controls, remote error presentation, or connection/version-dependent behaviour. This keeps each shared assertion meaningful for every supported object context.
Run the concrete example classes, not the abstract bases, and keep all examples green after changing a view.
For a new local/remote view that computes one custom serializable value, start with the generic concrete-data pipeline. This is now the main and simplest approach. GtRemotePhlowPictureView
GtRemotePhlowLocalView << #GtRemotePhlowPictureView
slots: { #pictureComputation . #widthComputation . #heightComputation . #mediaTypeComputation . #renderingStencilName . #shouldLoadContentLazily };
package: 'GToolkit-RemotePhlow-PhlowViews'
is the reference implementation.
The author normally creates only three application-specific components:
1. a view subclass of GtRemotePhlowLocalView
GtRemotePhlowView << #GtRemotePhlowLocalView
slots: { #localOriginalView . #localBuildContext . #localDefiningMethodProvider };
package: 'GToolkit-RemotePhlow-PhlowViews'
that owns computation blocks and implements computeViewData;
2. a serializable view-data value subclass of GtRemotePhlowBasicViewData
GtPhlowDeclarativeSpecification << #GtRemotePhlowBasicViewData
slots: {};
package: 'GToolkit-RemotePhlow-DeclarativeViews'
;
3. a declarative rendering stencil, normally selected by name.
The view does not need an application-specific specification or datasource. GtRemotePhlowPictureView>>#asGtDeclarativeView
asGtDeclarativeView
| viewSpecification |
viewSpecification := (GtRemotePhlowConcreteDataViewSpecification new)
phlowDataSource: (GtRemotePhlowConcreteDataViewDataSource
forPhlowView: self);
renderingStencilName: self renderingStencilName;
dataTransport: self currentDataTransport.
self configureGenericViewSpecificationOn: viewSpecification.
^viewSpecification
configures the generic GtRemotePhlowConcreteDataViewSpecification
GtPhlowViewSpecification << #GtRemotePhlowConcreteDataViewSpecification
slots: { #renderingStencilName . #viewData };
package: 'GToolkit-RemotePhlow-DeclarativeViews'
and GtRemotePhlowConcreteDataViewDataSource
GtRemotePhlowDeclarativeViewDataSource << #GtRemotePhlowConcreteDataViewDataSource
slots: {};
package: 'GToolkit-RemotePhlow-DeclarativeViews'
. The generic datasource invokes computeViewData and catches errors; the generic specification handles data transport, rehydration, refresh, and the explicit declarative rendering flow.
The picture view's computeViewData answers GtRemotePhlowPictureViewData
GtRemotePhlowViewData << #GtRemotePhlowPictureViewData
slots: { #content . #mediaType . #width . #height };
package: 'GToolkit-RemotePhlow-DeclarativeViews'
, which carries picture bytes, media type, width, and height. Its configured rendering-stencil name is turned into a GtPhlowNamedStencil
GtPhlowForwardStencil << #GtPhlowNamedStencil
slots: { #stencilClassName . #stencilDataWrapper };
package: 'GToolkit-RemotePhlow-Stencils'
by GtRemotePhlowConcreteDataViewSpecification>>#createDeclarativeStencilForViewData:
createDeclarativeStencilForViewData: aViewData
^ aViewData isPhlowErrorData
ifTrue: [
GtPhlowErrorStencil new stencilData: aViewData ]
ifFalse: [
| stecil |
stecil := GtPhlowNamedStencil new stencilClassName: self renderingStencilName.
stecil stencilData: aViewData.
stecil ]
.
GtRemotePhlowConcreteDataViewSpecification>>#viewFor:
viewFor: aView
| pictureView |
pictureView := aView explicit
originalView: aView;
title: title;
priority: priority;
phlowViewSpecification: self;
declarativeStencil: [
| data |
data := self phlowDataSource retrieveData.
self createDeclarativeStencilForViewData: data ].
self configureViewActionsFor: pictureView.
^pictureView
creates an explicit view with #selector
declarativeStencil: aStencilBuilder
stencilBuilder := aStencilBuilder asPhlowDeclarativeStencilBuilder
. The block retrieves generic view data and answers a declarative stencil. This preserves both local rendering and remote declarative transport.
Use this approach unless the view requires custom retrieval, multiple independently retrieved values, specialized cache invalidation, or another behavior that the generic pipeline cannot express.
Use a custom specification/datasource pair only when the generic concrete-data pipeline does not provide enough control. GtRemotePhlowWebBrowserView
GtRemotePhlowLocalView << #GtRemotePhlowWebBrowserView
slots: { #contentComputation . #staticHeaders . #dynamicHeadersComputation . #shouldLoadContentLazily };
package: 'GToolkit-RemotePhlow-PhlowViews'
is the reference implementation.
The browser view needs separate content and dynamic-header retrieval, static headers, specialized included-versus-lazy behavior, merging before rendering, and explicit local/remote cache invalidation. It therefore defines GtRemotePhlowWebBrowserViewSpecification
GtPhlowViewSpecification << #GtRemotePhlowWebBrowserViewSpecification
slots: { #staticHeaders . #hasDynamicHeaders . #browserContent . #dynamicHeaders };
package: 'GToolkit-RemotePhlow-DeclarativeViews'
and GtRemotePhlowDeclarativeWebBrowserViewDataSource
GtRemotePhlowDeclarativeViewDataSource << #GtRemotePhlowDeclarativeWebBrowserViewDataSource
slots: {};
package: 'GToolkit-RemotePhlow-DeclarativeViews'
.
Its GtRemotePhlowWebBrowserViewSpecification>>#viewFor:
viewFor: aView
| browserView |
browserView := aView explicit
originalView: aView;
title: title;
priority: priority;
phlowViewSpecification: self;
declarativeStencil: [ self createDeclarativeContentStencil ].
self configureViewActionsFor: browserView.
^ browserView
still follows the same essential rendering rule: create an explicit view, set #selector
phlowViewSpecification: aViewSpecification
self
propertyAt: 'phlowViewSpecification'
put: aViewSpecification
, and use #selector
declarativeStencil: aStencilBuilder
stencilBuilder := aStencilBuilder asPhlowDeclarativeStencilBuilder
. GtRemotePhlowWebBrowserViewSpecification>>#createDeclarativeContentStencil
createDeclarativeContentStencil
| content currentDynamicHeaders contentHeaders |
content := self browserContent.
content isPhlowErrorData ifTrue: [
^ GtPhlowErrorStencil new stencilData: content ].
currentDynamicHeaders := self dynamicHeaders.
currentDynamicHeaders isPhlowErrorData ifTrue: [
^ GtPhlowErrorStencil new stencilData: currentDynamicHeaders ].
contentHeaders := self staticHeaders copy.
currentDynamicHeaders do: [ :name :value |
contentHeaders headerAt: name put: value ].
^ GtRemotePhlowWebBrowserStencil new
browserContent: content;
contentHeaders: contentHeaders
retrieves/merges the values and answers GtRemotePhlowWebBrowserStencil
GtPhlowStencil << #GtRemotePhlowWebBrowserStencil
slots: { #browserContent . #contentHeaders };
package: 'GToolkit-RemotePhlow-Stencils'
or GtPhlowErrorStencil
GtPhlowForwardStencil << #GtPhlowErrorStencil
slots: {};
package: 'GToolkit-RemotePhlow-Stencils'
.
The dedicated browser stencil subclasses GtPhlowStencil
GtPhlowDeclarativeSpecification << #GtPhlowStencil
slots: {};
package: 'GToolkit-RemotePhlow-Stencils'
, exports its concrete state with GtRemotePhlowWebBrowserStencil>>#asDictionaryForExport
asDictionaryForExport
| dictionary |
dictionary := super asDictionaryForExport.
dictionary at: 'browserContent' put: browserContent asDictionaryForExport.
contentHeaders ifNotNil: [ :headers |
dictionary at: 'contentHeaders' put: headers asDictionaryForExport ].
^ dictionary
, and restores it with GtRemotePhlowWebBrowserStencil>>#initializeFromJSONDictionary:
initializeFromJSONDictionary: aDictionary
super initializeFromJSONDictionary: aDictionary.
self browserContent: (GtRemotePhlowBasicViewData
fromDictionary: (aDictionary at: 'browserContent')).
(aDictionary at: 'contentHeaders' ifAbsent: [ nil ])
ifNotNil: [ :headers |
self contentHeaders: (GtRemotePhlowBasicViewData fromDictionary: headers) ]
. It inherits GtPhlowStencil>>#asGtStencilSpecification
asGtStencilSpecification
^ GtRemotePhlowDirectStencilSpecification new
stencilClassName: self class name;
stencilData: self asDictionaryForExport
, which creates a GtRemotePhlowDirectStencilSpecification
GtRemotePhlowAbstractStencilSpecification << #GtRemotePhlowDirectStencilSpecification
slots: { #stencilData . #stencilClassName };
package: 'GToolkit-RemotePhlow-Stencils'
; do not add a redundant stencil-specification class.
Provide a value-based standalone API on such a stencil, but never blocks. GtRemotePhlowWebBrowserStencilTestObject>>#gtWebBrowserHeadersFor:
gtWebBrowserHeadersFor: aView
<gtView>
^ aView explicit
title: 'Browser headers';
priority: 23;
declarativeStencil: [
GtRemotePhlowWebBrowserStencil new
url: 'https://example.com';
headerAt: 'Accept' put: 'text/html';
headerAt: 'X-Static' put: 'static value';
headerAt: 'Authorization' put: 'Bearer dynamic-token';
headerAt: 'X-Dynamic' put: 'dynamic value' ]
shows the stencil used directly in an explicit declarative view.
1. Start from GtRemotePhlowPictureView
GtRemotePhlowLocalView << #GtRemotePhlowPictureView
slots: { #pictureComputation . #widthComputation . #heightComputation . #mediaTypeComputation . #renderingStencilName . #shouldLoadContentLazily };
package: 'GToolkit-RemotePhlow-PhlowViews'
and verify its current code in the image.
2. Create a GtRemotePhlowLocalView
GtRemotePhlowView << #GtRemotePhlowLocalView
slots: { #localOriginalView . #localBuildContext . #localDefiningMethodProvider };
package: 'GToolkit-RemotePhlow-PhlowViews'
subclass with computation blocks, computeViewData, a rendering-stencil name, and lazy/included transport intent.
3. Create a GtRemotePhlowBasicViewData
GtPhlowDeclarativeSpecification << #GtRemotePhlowBasicViewData
slots: {};
package: 'GToolkit-RemotePhlow-DeclarativeViews'
subclass that fully serializes/deserializes the concrete data needed by the stencil.
4. In asGtDeclarativeView, use GtRemotePhlowConcreteDataViewSpecification
GtPhlowViewSpecification << #GtRemotePhlowConcreteDataViewSpecification
slots: { #renderingStencilName . #viewData };
package: 'GToolkit-RemotePhlow-DeclarativeViews'
plus GtRemotePhlowConcreteDataViewDataSource
GtRemotePhlowDeclarativeViewDataSource << #GtRemotePhlowConcreteDataViewDataSource
slots: {};
package: 'GToolkit-RemotePhlow-DeclarativeViews'
. Do not duplicate either generic class.
5. Add builders to GtPhlowProtoView
Object << #GtPhlowProtoView
traits: {TBlDebug};
slots: {};
sharedVariables: { #IsTaskItViewGloballyEnabled };
tag: '! Views';
package: 'GToolkit-Phlow'
and GtRemotePhlowProtoView
Object << #GtRemotePhlowProtoView
slots: {};
package: 'GToolkit-RemotePhlow-PhlowViews'
.
6. Provide the named rendering stencil(s), including any locally-only alternative stencil needed by callers.
7. Create relevant view construction and inspection examples and make sure they pass
1. Start from GtRemotePhlowWebBrowserView
GtRemotePhlowLocalView << #GtRemotePhlowWebBrowserView
slots: { #contentComputation . #staticHeaders . #dynamicHeadersComputation . #shouldLoadContentLazily };
package: 'GToolkit-RemotePhlow-PhlowViews'
and identify the missing generic capability.
2. Create a specification subclass and datasource subclass only for that extra behavior.
3. Keep computations on the view; make the datasource evaluate them and convert failures to GtRemotePhlowViewErrorData
GtRemotePhlowBasicViewData << #GtRemotePhlowViewErrorData
slots: { #errorDescription . #errorObject . #isRemoteException };
package: 'GToolkit-RemotePhlow-DeclarativeViews'
.
4. Serialize every specification field that crosses the boundary and tolerate absent optional keys when reading old/incomplete dictionaries.
5. Implement #selector
initializeFromInspector: anInspector
to reconnect remote lazy retrieval.
6. Add #selector
flushCachedData
"Method called when the view is about to be update to clean all the cached data"
when values can be retained locally or remotely; clear local values and forward the operation to the data source/proxy.
7. Implement #selector
configureViewActionsFor: aView
self actionSpecifications ifNil: [ ^ self ].
self actionSpecifications do: [ :anActionSpecification |
aView addPhlowAction: (anActionSpecification actionFor: GtPhlowAction noAction) ]
to add normal actions and the refresh action.
8. Add the Gtpo...Specification proxy if GemStone needs to fetch/flush specialized values.
9. Create relevant view construction and inspection examples and make sure they pass
For any newly created class that defines initialize, also define class-side new as ^ self basicNew initialize because GemStone does not reliably perform automatic initialization in the relevant path.