Building ULC applications requires a special set of objects or
widgets. These widgets have an API and functional behavior that is
similar to that of "normal" widgets sets, but they completely lack a user
interface. These widgets communicate with a corresponding "real" widget
in the UI Engine by a socket-based mechanism. They act as a proxy to
the real widget. Because every widget is split into an API and a user
interface element, we call the widgets on both sides half
objects. The application half is termed
faceless.
Applications communicate with the UI Engine through the Half Object Protocol, which consists of requests, events, and callbacks. The application sends requests to the UI Engine. User interaction typically results in low-level events (for example, a mouse click) that are handled by the UI Engine first and then converted into a semantic event (for example, an execute action) that is passed back to the application. These events are typically used to synchronize 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
that provides methods for manipulating the global state of the application
(for example, stopping) and maintaining a list of windows, or
shells. A shell represents a top-level window with a content
area and, possibly, a menu bar. 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.
The following illustration shows a part of the tree from the sample Dossier
application:
Maintaining state across this half object split is a tricky business. You do not want to render the application unusable just because there was a communication problem or your client machine crashed. To prevent this, 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 as well (for instance, the widget hierarchy), but only as a type of cache. It is always possible to clear that cache (for instance, by stopping and restarting the UI Engine) and reconnect to the application from the UI Engine.
This functionality has a major impact on communication between half objects. Methods of the faceless half objects typically modify some state on their half and then try to synchronize their half with the UI half. If the UI half is not available (for example, because the socket timed out or the UI Engine is down), synchronization cannot occur, but the faceless half remains in a consistent state. When the UI half becomes available once again, the faceless half sets the UI half to the correct state.
ULC is designed for thin-pipe connections, so network latency and network bandwidth influence some design decisions. Communication between half objects must be minimized, and requests should be batched together as a single message to avoid a sluggish user interface. ULC minimizes communication overhead by transmitting only presentation data that is visible (for example, just the visible 10 rows in a table with 10000 rows) or likely to become visible soon (for example, the next 10 rows in that table).
To address high-latency environments, communication between the application and the UI Engine is mostly asynchronous. For example, if the UI Engine has to draw a table, it requests the data for the visible part of the table from the application. The UI Engine does not wait for the requested data; it draws a placeholder instead. As a result, the UI Engine remains responsive through the wait. Depending on network latency and application responsiveness, the requested data asynchronously arrives later and replaces the placeholder data.