Differences
This shows you the differences between two versions of the page.
|
customui [2011/12/28 13:42] srdjan.lukovic |
customui [2011/12/28 16:24] (current) srdjan.lukovic [Architecture] |
||
|---|---|---|---|
| Line 9: | Line 9: | ||
| 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). | 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 == | + | ======= Architecture ======= |
| There are five runtime constitutive elements of each UI component. They are: | There are five runtime constitutive elements of each UI component. They are: | ||
| Line 20: | Line 20: | ||
| Figure 1 depicts the described architecture of the SOLoist GUI subsystem. | Figure 1 depicts the described architecture of the SOLoist GUI subsystem. | ||
| - | **Figure 1.** SOLoist GUI runtime environment. | + | {{: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 ''<<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: | ||
| + | - 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 ''<<input>>'' or ''<<output>>'' 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: <code java> | ||
| + | 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; | ||
| + | } | ||
| + | } | ||
| + | </code> | ||
| + | -**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: <code java> | ||
| + | @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(); | ||
| + | } | ||
| + | </code> | ||
| + | - **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: <code java> | ||
| + | public interface LabelWidget extends Widget { | ||
| + | void setText(String text); | ||
| + | void setTextSize(int textSize); | ||
| + | } | ||
| + | </code> | ||
| + | - **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: <code java> | ||
| + | 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); | ||
| + | } | ||
| + | } | ||
| + | </code> | ||
| + | - **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: <code java> | ||
| + | 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"; | ||
| + | … | ||
| + | </code> 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'': <code java> | ||
| + | public LabelController(LabelInfo info) { | ||
| + | constructor(info); | ||
| + | } | ||
| + | </code> | ||
| + | - 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: <code java> | ||
| + | @Override | ||
| + | protected Widget createRepresentative() { | ||
| + | Widget representative = …(myInfo); // You can use info with attribute values | ||
| + | representative.setController(this); | ||
| + | return representative; | ||
| + | } | ||
| + | </code> 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''. <code java> | ||
| + | @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; | ||
| + | } | ||
| + | </code> | ||
| + | - 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: <code java>Controller.requestBuffer.addRequest(myRequest, this, 0);</code> | ||
| + | - 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.** | ||