Split Server-Client UI¶
Introduction¶
In OpenEdge, the UI is implemented in a traditional manner. This means that the entire code for the UI is exists inside a client process. All of this code was implemented by hard coding the UI features into platform-specific implementation code. 4GL GUI features directly use WIN32 APIs and widgets to implement the user interface. 4GL ChUI features directly use terminal/console APIs at the OS level. This has the advantage that it is simple and relatively easy to implement. A big downside of this approach is that it is very inflexible.
In contrast, FWD was designed to move as much of the application processing to the server as possible. This makes it easier to move applications to a cloud based approach without requiring the infeasible rewrite/refactoring challenge that exists in the OpenEdge design. Even in a server/cloud approach, some portion of the user interface must actually run in front of a user. Although it might seem easy to just push the UI portions into a client process, this does not work well for 4GL applications. The reason is that the UI code and the business logic are tightly coupled. Even in 4GL GUI applications where the data access has been split off into an appserver, a huge amount of business logic remains in the interactive 4GL client portion. This traditional desktop design means that the business logic and control flow is inextricably linked with the UI itself. Instead of pusing all of this into the client process, FWD inverts the model. In FWD, all of this business logic, control flow and the 4GL UI contructs are run on the server. Only a limited set of 4GL primitives are implemented on the client side. These get invoked remotely from the server when needed to display or interact with the actual user.
This FWD design is thought of as a "thin client" approach. It is why so much of the client side processing is managed by a class named ThinClient
.
Splitting the UI into a heavy server and thin client has many implications.
- It is a more modern approach.
- It makes it feasible to move desktop/heavy interactive applications to the cloud without a rewrite.
- There is a well defined API that splits the server from the client. This makes it easier to create an abstraction layer for the low level UI technologies that are used to implement the actual thin client UI code. This means that there are Pluggable UI Drivers that can implement different UI modalities (Swing/desktop, web browser, TTY, mobile). In FWD it is possible to run the same 4GL UI regardless of how the low level UI is implemented.
- It is more complex to implement which means it is harder to maintain and requires more understanding and care.
Design¶
The following are the key design elements.
- Frame Definitions
- Server-Side Widgets
- Pushing
ScreenDefinion
Instances to the Client ScreenBuffer
Implementation- Attribute Synchronization
- Client-Side Widgets
- Widget Values and Editing State
Insert Diagram Here
Implementation¶
Frame Definitions¶
Every frame in a 4GL program has a Java frame definition created at conversion time. This is a sub-interface of CommonFrame
and it has an inner child subclass of WidgetList
which also implements Settable
. Settable
has the setup()
method that is called by GenericFrame
to initialize the frame every time a new instance is created. This is what I'm referring to as the "frame definition". It is the analog to the compiled frame inside the 4GL r-code.
Server-Side Widgets¶
Pushing ScreenDefinion
Instances to the Client¶
ScreenBuffer
Implementation¶
Purpose¶
On the server, the ScreenBuffer
represents the screen-buffer in the same sense as used in the 4GL. That means that it is a place that stores widget data values that will be displayed or edited.
It also is used to store the frame header values, which can be expressions in the 4GL so they are a kind of dynamic thing that is managed on the server and must be visually represented on the client.
The other critical function of the ScreenBuffer
is to facilitate the transport of state changes from server to client AND from client to server.
On the client side, each individual widget instance internally stores its own data value as a BaseDataType
instance that can be directly modified AND there is a DisplayFormat
instance that controls how this value visually maps to the UI. We don't use the ScreenBuffer
on the client for editing/displaying. We only use it to transport to/from the server.
Over time the ScreenBuffer
design was modified to handle side-labels and some special cases like ComboBox
/SelectionList
. These changes were never handled directly in the core design, but instead we took a short cut and added the ScreenBuffer.values
. Yes, this is very confusing. It really needs to be cleaned up at some point.
How it Works¶
Server to Client Synchronization
- The widget data values accumulate in the
ScreenBuffer.buffer
array, copied from some variable or field reference in the converted code. This occurs from anyDISPLAY
(explicit in 4GL code or implicit as in theUPDATE
statement) or explicit assignment ofSCREEN-VALUE
. - This can happen more than once for a given widget or for multiple widgets before the changes are ever pushed down to the client.
- The next time the UI "flow of control" in the application requires executing on the client side, the
ScreenBuffer
changes are gathered up for the target frame and any frames that are being edited currently and these are sent to the client as part of the method call. See all the places that callLogicalTerminal.getEditableServerScreenBuffers()
. - This shift of UI flow of control can be either a "down call" (to the client) such as
WAIT-FOR
or it can be a return from an "up call" (from the client) such as the firing of a UI trigger or the execution of a validation expression. - We serialize only the diffs that need to be applied to the client and send them to the client over the remote object protocol as an array of
ScreenBuffer
instances. SeeScreenBuffer.getChanged()
for how we gather the diffs. - On the client side of these UI down calls or up call returns, the array of changes are pushed into the individual widgets. See
ThinClient.setScreenBuffers(ScreenBuffer[] frameBuf, boolean checkFrameExists)
for how this is done.
Client to Server Synchronization
- On the client, we track when any changes are made to a given widget. This can occur in response to user input when a widget is
SENSITIVE
/enabled and an event loop is executing (WAIT-FOR
). - This can occur for multiuple widgets and/or for the same widget more than once. All these changes accumulate inside the widget itself, but we track that the widget has been modified. See
Widget.getState()
. - When the client is going to make an up call (fire a trigger or execute a validation expression) or return from a down call (e.g.
APPLY
orENABLE
orWAIT-FOR
), then it must gather up all client-side changes and send them to the server. This is done inThinClient.getEditableClientScreenBuffers()
and the specific state checking logic is inThinClient.getScreenBuffer(int frameId, boolean force, Set<WidgetId> widgets)
. - The array of
ScreenBuffer
instances will include the current frame that is targetted by this up call/down return AND all other frames currently in edit mode. - The changes (again, only the diffs) are gathered up by
ThinClient.getScreenBuffer(int frameId, boolean force, Set<WidgetId> widgets)
and placed in a newScreenBuffer
instance. This is done by callingScreenBuffer.putWidgetValue()
,ScreenBuffer.putScreenValue()
(only for ComboBox or SelectionList) orThinClient.putBrowseColumnValues()
(for browse, which really just usesScreenBuffer.putWidgetValue()
internally). - These changes are serialized and transferred to the server as a
ScreenBuffer[]
using the remote object protocol. - On the server side, the down call return or up call will use
LogicalTerminal.handleScreenBuffers(ScreenBuffer[] sb, int indexStart)
to merge the changes into the existingScreenBuffer
instance for the associated frame. This code callsGenericFrame.setScreenBuffer()
which usesScreenBuffer.mergeChanged()
to apply the diffs.
Server-Side Widget Value Access
Any access to a widget's SCREEN-VALUE
on the server (or anything that will copy from the screen buffer to variables/fields like an explicit ASSIGN
or the implicit assign in UPDATE
) will read from the frame's ScreenBuffer
instance and return that value which is associated with the specific widget.
Attribute Synchronization¶
Accessing and modifying widget attributes in the set of config classes (inheriting from WidgetConfig
) on the server must be done through the methods GenericWidget.getAttr
and GenericWidget.setAttr
. These ensure proper synchronization of the dirty state to the client and back.
Use setAttr
to modify an attribute state. The method ensures the attribute value is directly set in the respective config class instance as well as enqueues the new attribute state for server-client synchronization. I.e. setAttr
doesn't cause a network call to the client when an attribute is set.
Use getAttr
to read an attribute state. The method optionally pushes the attribute states enqueued with setAttr
to the client and then returns the attribute state from the respective config class instance. Whether enqueued attributes will be pushed to the client depends on the flush
argument passed to the method. If the getAttr
overload without this parameter is used the enqueued attributes won't be pushed, i.e. server-client synchronization must be explicitly requested by the caller.
Beside getAttr
the enqueued attribute states are pushed to the client when any remote client method is called. This ensures proper state synchronization before any client-side logic is invoked by the server. This happens with the help of ServerState
, see below.
Alternatively use GenericWidget.pushWidgetAttr()
. This method is useful when multiple attributes must be enqueued on the server and the list of attributes is dynamic. Note the method only enqueues the attribute states. The actual server-client synchronization is conducted with the same semantics as when using setAttr
.
Any processing on the client which causes a change to the widget state will likewise save those changes into the synchronization set which will be transferred back to the server on the next trip up to the server.
This synchronization is handled via piggybacking this state synchronization on our network protocol, creating a kind of "side-channel" to the client. In some cases we will call down immediately from the server to apply the change instead of using the state synchronization mechanism. During frame setup we batch all these up instead of making many trips down.
Of course, all of these changes are really to the internal "config object" of each widget. We don't ever send the widget itself over the wire. We send the part of the configuration which has changes. See *ConfigurationManager
classes which manage this.
The state synchronization classes (which act like a kind of plug-in to the low level protocol) can be seen in ServerState
, ClientState
and the implementation of net/StateSynchronizer
by both LogicalTerminal
(server side) and ThinClient
(client side).
I know of no way that client code would cause a widget to be enabled that isn't already driven by the server. If the 4GL code calls ENABLE
, this will go through GenericFrame.enable()
which will call LogicalTerminal.enable()
which will call down to the client. But the client itself won't spontaneously set a widget to enabled/sensitive.
Client-Side Widgets¶
Widget Values and Editing State¶
ScreenBuffer
© 2004-2021 Golden Code Development Corporation. ALL RIGHTS RESERVED.