This article explains how to implement a custom SOLoist UI component.
SOLoist library contains built-in UI components that enable UI programmers to create complex layouts using horizontal, vertical, table, absolute, flow, dock, and layered layout. In addition to that, SOLoist provides tabs, decks, disclosure panels, scroll panels, and dialogs. Second, it provides UI components for browsing (and editing) domain object space. These are lists, tables, trees, search components, etc. There is also a set of input controls and editor UI components that correspond to the supported OOIS UML built-in data types like Text, Integer, Real, Boolean, File, Picture, Currency, etc. These are special text boxes, combo boxes, suggest boxes, date pickers, color pickers, file/picture uploaders, and more. In order to embrace a bit of usual UI behavior and spare programmers from manual efforts in this area, SOLoist also offers a collection of so-called non-visual UI components like filters, logic components, transformers, buffers, relays, etc. They may perform some frequently used portions of UI behavior. Finally, SOLoist library provides a group of usual and simple components like labels, pictures, links, HTML components, buttons, menus, etc.
If a custom UI component is needed, it can be created by following the instructions below. A custom component can do arbitrary work on the client and/or on the server side of application. It can compensate for any type of functionality missing in the library of built-in UI components. For example, it can even embody an external widget (e.g., Google Maps or any other), or render its piece of HTML on the client on its own. Such custom component stays in line with the overall paradigm and communicates with other components in a controlled and uniform manner (via pins and bindings).
There are five runtime constitutive elements of each UI component. They are:
Figure 1 depicts the described architecture of the SOLoist GUI subsystem.
To create a custom widget:
«domain»
. The new UI component UML class should be placed in that package. The process of creating a new UI component class is as follows:GUILabelComponent
.GUIComponent
. Note that by extending the GUIComponent
class, the derived class inherits several attributes and pins. The desired behavior for events on those pins and attributes should be provided later on when implementing the behavior of the component under construction. For example, one of these attributes is the attribute enabled
; the implemented behavior should ensure that the widget is disabled when this attribute is set to false.«input»
or «output»
depending on its purpose. The type of this attribute is irrelevant and can simply be Text
.GUILabelComponent
class with the abovementioned steps conducted. The attribute text
is meant for holding the initial label’s text. The attribute textSize
holds the value that defines the size of the label’s text in, let’s say, pixels. Furthermore, there is an input pin newText
which is meant to accept a new arbitrary text so that GUILabelComponent
can change it dynamically. Finally, the UI component is able to inform its surrounding UI components that its value (text) has changed, and the output pin textChanged
is used for that.LabelInfo
.ComponentInfo
class. By this, it inherits fields that serve as a carrier for attribute values of the GUIComponent
class.createDefaultController()
method and return null
from its body only for the moment. Later on, when the controller class is created, this method should return the instance of that controller class. Do not forget to do that.LabelInfo
class like this: package org.example.soloist.client.common.info; import rs.sol.soloist.client.guiruntime.controller.Controller; public class LabelInfo extends ComponentInfo { public String text; public int textSize; @Override public Controller createDefaultController(){ return null; } }
protected abstract ComponentInfo createSpecificInfo();
protected void fillInfo(ComponentInfo info);
GUILabelComponent
example: @Override protected ComponentInfo createSpecificInfo() { return new LabelInfo(); } @Override protected void fillInfo(ComponentInfo info) { super.fillInfo(info); // fills in GUIComponent attribute values LabelInfo labelInfo = (LabelInfo)info; info.text = this.text.val().toString(); info.textSize = this.textSize.val().toInt(); }
Widget
interface. The Widget interface is just a facade hiding the implementation of the widget class from its clients (which are typically objects of Controller classes). In order to create a widget interface:rs.sol.soloist.client.guiruntime.view.Widget
interface.GUILabelComponent
, the LabelWidget
interface looks like this: public interface LabelWidget extends Widget { void setText(String text); void setTextSize(int textSize); }
GUILabelComponent
example, this looks like this: import com.google.gwt.user.client.ui.Label; public class GWTLabel extends Label implements LabelWidget { private LabelController myController; @Override setText(String text) { setText(text); } @Override setTextSize(int textSize) { DOM.setStyleAttribute(this.getElement(), "fontSize", "12px"); } @Override public void setController(Controller controller) { myController = (LabelController)controller; } @Override public Controller getController() { return myController; } @Override public void setVisibility(boolean visible) { setVisible(visible); } @Override public void addStyle(String style) { addStyleDependentName(style); } @Override public void removeStyle(String style) { removeStyleDependentName(style); } }
rs.sol.soloist.client.common.GUIComponentPins
. There is a number of String
constants, one for each pin of each SOLoist built-in UI component. For example: public static final String ENABLED_COMPONENT_I = "3"; public static final String RESET_SEARCH_RESULT_COMPONENT_I = "c"; public static final String HIDE_DIALOG_COMPONENT_I = "s"; public static final String RESULT_COMMAND_COMPONENT_O = "12"; …
The values of these constants represent unique identifiers of the pins. The names of these constant fields follow the pattern PIN_NAME_COMPONENT_NAME_PIN_DIRECTION
. Furthermore, there are two HashMap
s named PIN_NAMES
and PIN_IDS
. The former stores the mappings from the fully qualified name to the identifier of each pin of each component, while the latter stores the inverse mapping. You should first create unique String
identifiers for all pins that you created in the component under construction and then put the mappings for them into these two HashMap
s on the application startup. The next step shows the use of these constants. Finally, you should make sure that they reside in a class that gets GWT-compiled.
LabelController
.ComponentController
.constructor()
method with the same parameter. For the example of GUILabelComponent
: public LabelController(LabelInfo info) { constructor(info); }
createRepresentative()
. It should return the created widget. You can access the information from the info object (populated on the server) in the myInfo
field of this (controller) class. You should set the widget’s reference to this controller by calling setController(this)
. One possible implementation of this method could look like this: @Override protected Widget createRepresentative() { Widget representative = …(myInfo); // You can use info with attribute values representative.setController(this); return representative; }
Note that you can access the created widget through the field myRepresentative
.
acceptViaBinding
method in order to react on events on each of the component’s input pins. Do not forget to react on events received via inherited input pins (from GUIComponent
class). For the GUILabelComponent
example, we could do something like this in order to react on the event on the newText
input pin. In a similar way, you can react on events from all other input pins of this component. In order to send values on output pins, use the method sendViaBinding
. @Override public boolean acceptViaBinding(String destSlot, List<Descr> message) { if ("NEW_TEXT_LABEL_COMPONENT_I".equals(destSlot)) { // point 6 String newText = Utilities.getSingleStringValue(message); ((LabelWidget)myRepresentative).setText(newText); sendViaBinding("TEXT_CHANGED_OUTPUT_PIN_ID", null); return true; } … // similar for other input pins return false; }
signIn()
, signOut()
, and update()
methods.rs.sol.soloist.client.common.requests.Request
hierarchy (or create one of your own request class) and fill it with request parameters.Controller.requestBuffer.addRequest(Request request, ServiceConsumer client, int requestIndex)
in this way, for example: Controller.requestBuffer.addRequest(myRequest, this, 0);
public Object GUIComponent::handle(Request request)
serviceSuccessCallback(Request request, int requestIndex, Serializable result);
serviceFailureCallback(Request request, int requestIndex);