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)

Example:

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.