Because every widget is split into an API and a real user interface element we call the widgets on both sides "half objects", where the application half is the "faceless half" and the UI half is the "UI half" (see Figure 4).
Figure 5: Half Objects
Applications communicate with the UI Engine via the "Half
Object Protocol" which consists of requests, events,
and callbacks. The application sends requests to the UI Engine. User interaction typically results in some low level events ("mouse click") which are handled by the UI Engine first and then converted into a semantic event ("execute action") which is passed back to the application. These events are typically used to synchronise the other half of the object and then to trigger some application specific action. If the UI Engine needs some data from the application's half object it sends a callback. Callbacks are identical to normal requests but their direction is reversed.
Half objects form a hierarchy: at the root is an Application object which provides methods for manipulating the global state of the application (terminating etc.) and maintaining a list of windows or "Shells." A Shell represents a top-level window with an optional menubar (a tree of menus with menu items) and a content area. The content area is a tree of composite widgets. Composite widgets form the inner nodes of the tree and implement the layout. Simple widgets are the leaves of the tree. Figure 5 shows a part of the tree from the sample application Dossier. A more detailed description of the objects used is here.
Figure 6: Half Object Tree of Dossier Example
Maintaining state across this half object split is a tricky business. You don't want to render the application unusable just because there was a communication problem or your client machine crashed.
Therefore the UI Engine is "conceptually stateless", that is all state is kept in the application. Of course, some state is held in the UI Engine too (e.g. the widget hierarchy) but only as a type of cache. It is always possible to clear that cache (for instance by killing the UI Engine) and re-transmitting all information from the application to the UI Engine.
This functionality has a major impact on the communication philosophy between half objects. Methods of the faceless half objects typically modify some state on their half and then try to synchronise their half with the UI half. If there is no UI half (e.g. because the communication timed out or the UI Engine is down), the synchronise step will be a no-op but the faceless half will be in a consistent state. If the UI Engine comes back later the application reconnects to it and synchronizes to the current state.
ULC is designed to scale down to thin pipe connections.Therefore network latency and network bandwidth influences some design decisions. Communication between both half objects has to be minimized and requests should always be batched together as a single message to avoid a sluggish user interface. ULC minimizes communication overhead by only transmitting presentation data which is visible (e.g. just
the visible 10 rows in a table with 10000 rows) or where chances are high that it will become visible soon (e.g. the next 10 rows in that table).
To address high latency environments ULC has to batch single requests. But batching is only possible if requests are mostly asynchronous, that is, if they don't require an immediate answer and don't block the sender. As a consequence communication between the application and the UI Engine is mostly asynchronous. For example if the UI Engine has to draw a table it sends a request back to the application to request the data for the visible part of the table. The engine doesn't expect to get the requested data immediately and draws a placeholder instead. Because it doesn't wait for the data to arrive the UI Engine doesn't block and remains responsive. Depending on the network latency and application responsiveness the requested data will asynchronously arrive sometime later and will replace the placeholder data.
top
Class Overview
The ULC classes fall into the following five categories:
- Resources
are all objects which are not user interface elements themselves but are used to configure these elements and therefore should live in the UI Engine as well as the application. Examples are fonts, bitmaps, images, and cursors.
- Widgets
are all kinds of user interface elements ranging from simple
ones like buttons, labels, editable fields,
menus and menu items to more complex ones like one-dimensional
scrolling lists and two dimensional tables.
- Layout
Widgets in this category are composite widgets implementing
a specific layout policy for their children.
- Shells
Shells are the top level widgets forming the root of every widget tree. A shell is typically represented as a modal or non-modal window and optionally has a menu bar. The shell controls the collaboration of these elements. Examples for shells are the "standard"
Shell, Dialogues, Alertsand the root shell
("Application") which manages the overall application.
- Models
This category contains classes which can be used as a data source for some of the widgets from the Widget category above. Models are identical to the models of the MVC paradigm and widgets represent the view and controller components.
top
Model Based Widget
Most ULC widgets from the widget category support two different APIs. The first API assumes that every widget has a built-in model and provides an API for accessing and changing the model (see Figure 6). For example a Field has a text model and methods setText and getText. In addition the widget provides methods for specifying the visual appearance of the user interface element, e.g. what font to use or how many characters to display.
Figure 7: Widgets with built-in model
The second API uses an external model which is a separate object (Figure 7) and can be shared between different widgets (multiple views of the same object).
Figure 8: Widget with
external model
Under this paradigm the application typically creates and configures both the model and the widget and then forgets about the widget. Whenever the application has to update data presented in the UI Engine it talks to the model. When the application wants to retrieve changed data it asks the model for it, not the widget.
An example for this API is the Table widget and the TableModel (in fact the Table widget doesn't have a built-in model and only supports the model-based API). The TableModel implements a two
dimensional data structure with rows and columns. The UI Engine half of the object contains a cache so that data access requests of the Table widget can be fulfilled immediately. If the cache doesn't contain valid data the TableModel returns place holders and asynchronously requests new data from the application half object. When this data arrives the dependant widgets are notified
and updated.
The update policy used between the two halves of the TableModel is configurable. One synchronisation policy optimizes communication for low bandwidth, low latency connections by reducing the amount of data transmitted. Another policy for high bandwidth, high latency reduces the number of requests going back and forth but doesn't minimise the size of the data transmitted.
The FormModel is another example for the model based API. A FormModel represents a set of named and typed attributes, similar to a heterogeneous dictionary. Most ULC widgets from category 2 can be initialized with a FormModel and an attribute name. Fields can be used on string atributes, CheckBoxes on boolean attributes and groups of RadioButtons on enumeration type attributes. These widgets will ignore their built-in model but will track changes and allow updates of the named attributes of the specified FormModel.
Similar to the TableModel different synchronisation policies can accommodate different network characteristics. One policy updates the application half of the FormModel on every attribute change.
Another policy collects all changes and sends them back to the application in a single call back only on explicit request.
top
Optimizing Round Trips
In order to reduce the number of round trips between the UI Engine and the application ULC provides built-in mechanisms for the following commonly used functionalities:
- Enabling and Disabling
Special conditions on some widgets can be used to enable/disable other widgets without any communication between the UI Engine and application. Examples are empty/non-empty Fields, empty/non-empty
selections in List and Table widgets.
- Validation
Predefined Validator and Formatter objects can be attached to some Widgets in order to perform validations like range checking and syntax checking without any communication between the UI Engine and the application. This is especially useful in FormModel based widgets, where changes
are not transmitted immediately but batched.
- Optional Events
From R1.3 onwards events that are deemed optional are sent to the application only if there is a registered handler for the event.
top
Layout
Because the implementation of the UI Engine can be based on different widget sets, different look-and-feels, and different fonts, the layout specification attempts to be visually appealing in different environments. Explicit placement of widgets doesn't work very well in this situation because the widget's sizes are not known in advance. Moreover, the precise horizontal and vertical alignment of widgets and the specification of the resize behaviour is a tedious task even with the help of the Composition Editor. This led to the integration of layout management in ULC, based on a hierarchical and high level layout description rather than on the explicit placement of widgets, and which handles resize behaviour automatically.
top
Here is a more detailed description of the available layout widgets and how to use them.
top