Project

General

Profile

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 any DISPLAY (explicit in 4GL code or implicit as in the UPDATE statement) or explicit assignment of SCREEN-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 call LogicalTerminal.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. See ScreenBuffer.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 or ENABLE or WAIT-FOR), then it must gather up all client-side changes and send them to the server. This is done in ThinClient.getEditableClientScreenBuffers() and the specific state checking logic is in ThinClient.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 new ScreenBuffer instance. This is done by calling ScreenBuffer.putWidgetValue(), ScreenBuffer.putScreenValue() (only for ComboBox or SelectionList) or ThinClient.putBrowseColumnValues() (for browse, which really just uses ScreenBuffer.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 existing ScreenBuffer instance for the associated frame. This code calls GenericFrame.setScreenBuffer() which uses ScreenBuffer.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.