====== Implementation of Custom UI Component ====== This article explains how to implement a custom SOLoist UI component. ==== Introduction ==== 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). ======= Architecture ======= There are five runtime constitutive elements of each UI component. They are: - UI component class. This class resides on the server side and it has the usual SOLoist OOIS UML semantics. It is instantiated on the server. This class is responsible for maintaining the configuration parameters of the component in its attributes, as well as for serializing its configuration parameters into a so-called info structure that is transferred to the client. - Info class. An object of this class serves as a carrier of UI configuration data (attribute values) for a particular UI component. This object is also responsible for creating an appropriate controller object when it reaches the web browser on the client. - Controller class. An object of this class resides on the client (in the web browser) and, along with its widget, represents an incarnation of the UI component on the client. It is responsible of creating its widget instance, for managing it, for handling UI events captured by that widget instance, for handling events on input pins, for generating events on output pins, and for communicating with server. - Widget interface. This interface is a façade of a widget instance. The controller object uses it to communicate with its widget. Its purpose is to keep the controller code independent from the UI implementation framework, which is GWT in this case. In other words, the controller object accesses the GWT widget through this interface. - GWT widget class. An object of this class is the actual (usually, but not necessarily tangible and visible) widget. Its purpose is to provide visual information to the user of the application, as well as to capture user-generated events and forward them to its controller object. It accesses its controller object through a direct Java reference. Figure 1 depicts the described architecture of the SOLoist GUI subsystem. {{:guienvironment.jpg?400|Figure 1. SOLoist GUI runtime environment}} ====== Implementation Procedure ====== To create a custom widget: - **Create the UI component class**. Creating the UI component class is performed in the UML model of the application under construction. At least one UML package for custom UI components should be reserved and created in advance. This package should be stereotyped with ''<>''. The new UI component UML class should be placed in that package. The process of creating a new UI component class is as follows: - Provide the class with an appropriate name, for example, ''GUILabelComponent''. - Derive it from the top-level SOLoist UI class ''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. - If the UI component is to be configurable in terms of its appearance or functionality, a number of UML attributes should be modeled for the configuration. - Each UI component communicates with other components via pins and wires. For each pin, a UML attribute stereotyped with ''<>'' or ''<>'' depending on its purpose. The type of this attribute is irrelevant and can simply be ''Text''.\\ **Figure 2** shows the example ''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.\\ {{:guilabelcomponent.jpg?nolink&|}} - **Create the corresponding //info// class**. The info class carries all necessary configuration data from the component to its representation on the client. Its objects are created on the server, then they are populated with values from the UI component attributes, then they are serialized, and finally sent to the client (web browser). This class is a plain Java serializable class that is serialized by GWT. The Java project should be configured so that this class gets GWT-compiled into JavaScript by the GWT compiler. To create an info class: - Provide it with a name that corresponds to the name of the UI component, for example ''LabelInfo''. - Make it extend the ''ComponentInfo'' class. By this, it inherits fields that serve as a carrier for attribute values of the ''GUIComponent'' class. - For each attribute of the corresponding component class, a corresponding Java field should be created in the info class. - Override the ''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.\\ For example, create the ''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; } } -**Perform serialization.** Override two serialization methods in the component class. These are: - ''protected abstract ComponentInfo createSpecificInfo();'' - ''protected void fillInfo(ComponentInfo info);''\\ The former should instantiate the object of the corresponding info class. The latter should populate its fields with values from the attributes of the component object.\\ For the ''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(); } - **Create the client-side ''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: - Provide it with an appropriate name corresponding to the UI component class. - Make it extend ''rs.sol.soloist.client.guiruntime.view.Widget'' interface. - Create additional arbitrary methods.\\ For ''GUILabelComponent'', the ''LabelWidget'' interface looks like this: public interface LabelWidget extends Widget { void setText(String text); void setTextSize(int textSize); } - **Create the client-side GWT widget class.** SOLoist relies on GWT in its UI system. Every SOLoist widget is actually a GWT widget. In order to create a SOLoist widget class: - Provide it with a name according to the UI component name. - Make it implement the previously created widget interface. - Make it extend a GWT widget that you find adequate. - Implement all methods from the implemented widget interface. - Provide it with a reference to the corresponding controller object. - Implement the widget-specific functionality. Draw the widget. Inform its controller when necessary by calling its public methods.\\ For the ''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); } } - **A small hack.** Consider the class ''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. - **Create the client-side controller class.** The controller object is responsible for maintaining the UI representation (through a GWT widget), handling events from the widget, reacting on events on input pins, generating events on output pins, and communicating with the UI component object on the server, if necessary. It is strictly a client-side class – controller objects reside in the web browser. Hence, the controller class needs to be GWT-compiled as well.\\ In order to create a controller class: - Provide it with a name corresponding to the name of the UI component, for example ''LabelController''. - Make it extend the SOLoist’s root client class: ''ComponentController''. - Create a constructor with the corresponding info as a parameter and call the inherited ''constructor()'' method with the same parameter. For the example of ''GUILabelComponent'': public LabelController(LabelInfo info) { constructor(info); } - Implement the inherited abstract method for creating a widget: ''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''. - Override the ''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 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; } - In order to react on notifications from the domain object space (i.e., from the server), override ''signIn()'', ''signOut()'', and ''update()'' methods. - In order to communicate with the server: - Use one of existing classes from the ''rs.sol.soloist.client.common.requests.Request'' hierarchy (or create one of your own request class) and fill it with request parameters. - Call ''Controller.requestBuffer.addRequest(Request request, ServiceConsumer client, int requestIndex)'' in this way, for example: Controller.requestBuffer.addRequest(myRequest, this, 0); - Override the method in the component class (created in step 1) to handle the request and create a response (a response must be serializable and its class must be GWT-compiled):\\ ''public Object GUIComponent::handle(Request request)'' - Wait for the response on the client by overriding both: - ''serviceSuccessCallback(Request request, int requestIndex, Serializable result);'' - ''serviceFailureCallback(Request request, int requestIndex);'' - **Compile the project with the GWT JavaScript compiler.**