Παρασκευή 23 Αυγούστου 2013

Bo2 concepts part 1: Service Panels


The service panel is not a core Bo2 concept to be honest. Core Bo2 concepts like Workers, Operations and RuntimeCommands should be explained by Spyros, anyway (see what I did there? Now you HAVE to write). However, it is our take on using effectively component based frameworks like Wicket and AWT/Swing. And by effectively, I mean to build applications on top of such frameworks that respect all the good traits that increase code quality and will be less likely to become maintenance nightmares. That is the rationale behind creating Bo2 anyway - really, I am not the person to talk about this, moving on...
A panel is a common concept for component based UI frameworks. It is a basic building block that encapsulates any group of individual UI components. It can then be reused to build more complex UI elements, including other panels. Elementary UI components (text fields, drop down menus, file choosers, date picker fields, data tables etc) are usually provided out of the box by your component based UI framework of choice. So, it is on reusable panel creation that the application developers should invest in, in order to to improve their application's code quality.
Ideally, a reusable panel should fulfill two requirements:
  1. It should have no knowledge of its container hierarchy. This one is quite clear; if a panel's implementation has to make assumptions about the container it will be placed in, and, worse even, other components its container will have or what the container of its container is, it is not really re-usable.
  2. It should not require its users to know it's implementation compile-time. This will allow to switch the implementing class at some point with no impact to the code base. This is normally achieved using interfaces compile time and providing some means for the application to resolve and instantiate concrete implementations on runtime. This enforces the concept of defining contracts (interfaces) and using these to reference actual object instances compile time. A lot of frameworks enable this; Bo2 supports it (ObjectFactory), Google Guice provides even more flexibility. However, a panel can't be referenced using an interface, since the component based framework might not (and usually doesn't) define integral methods for the panel's use like #add(Component) in an interface that you might extend. The best you can do is to reference the panel instances with the base panel implementation class.
We will call such a panel a ServicePanel. It will be very low coupled with its clients. The clients are actually other UI components that create it and add it to their layout. Furhtermore, as all services, it will provide a contract via which its clients interact with it. The contract will be called a service panel definition, ServicePanelDef for short. The proof of concept examples presented below are based on the ajax capabilities of Apache Wicket.
But first, an example of the thought process behind the creation of a service panel. A lot of commonly seen UI use cases, involve changes taking place to multiple UI components apart from the panel a UI event is triggered in. Consider a panel whose job is to display a system entity and offer three buttons: 'cancel', 'execute' and 'clear'. 'cancel' removes this panel from its container's layout, 'clear' resets/clears the panel's components used to display the entity and 'execute' is user defined. Let's call it a SingleBeanPanel. 'Clear' will not affect anything else, so it is an internal implementation detail - no problems there. 'Cancel' affects the panel's container, but getting a reference to the container is a fairly basic facility of most component based frameworks. So is removing a component from the view of its container. It looks like 'cancel' is easily done too. But, wait a minute. To implement 'cancel' we actually needed to get a reference to the containing component and then use that reference to remove ourselves from our container's layout. Were it not for the ubiquity of (presumed method names) #getContainer() and #remove(Component) methods inherited from the base component class of our component based framework, our panel would be seriously coupled to it's container. It's doable, but does not feel quite right.
Discussing the 'execute' button requires a more concrete example. Consider this: panel A shows a table of entities. Under that table is an 'add' button. When that button is pressed, a SingleBeanPanel is shown whose 'execute' button (actually labeled with 'save') will have to do the following operations: (a) collect the user input for the new entity, (b) add a row to the table contained within A and (c) remove the SingleBeanPanel from view. Apart from the first part, the rest looks like it is logic that concerns the implementation of panel A. If we cannot keep it that way, our SingleBeanPanel won't be re-usable at all - at best it will have to assume that its container is of a specific type that exposes the addition logic. We will use the command pattern (standard practice with component based frameworks) and A will supply the SingleBeanPanel a command that the SingleBeanPanel will execute at some point after the 'execute' button is pressed. Using some reflection, the logic of the command might very well be encapsulated in a single instance method of A. Let's call the type of the command that A will give to the SingleBeanPanel a CallbackAction. And let's create a button that can execute it when pressed (CallbackAjaxButton). Finally, let's create a CallbackAction implementation that runs an instance method of A (MethodBasedCallbackAction).
We are not done yet. Our panel has no means to show the UI elements that the user will use to input the data for the new entity. This really calls for an extension point that allows the developer that uses the SingleBeanPanel to define what will be shown. We will let the developer supply a PanelCreator (abstract factory) instance that creates a panel that can display the specific entity type.
So, in order to create a SingleBeanPanel we should provide a CallbackAction instance, a PanelCreator, possibly a label for the 'execute' button and whatever else our component based framework requires in order to create a panel. These are starting to become a crowd, so we will encapsulate them in a single java bean that our panel will receive as a single constructor argument.
To further promote low coupling, we will add a property to this java bean that will be a placeholder for the entity created with the user input. When the 'execute' button is pressed, the SingleBeanPanel will assign that entity instance to this property before executing the CallbackAction. This way, A will be able to retrieve the entity instance from the java bean that he used to create the SingleBeanPanel. Below is, give or take, the sequence of events:
  1. SingleBeanPanel (SBP): User fills out the fields and presses the button.
  2. SBP: The entity is created, populated with user input and assigned to the javabean placeholder property.
  3. The CallbackAction is executed.
  4. A: The instance method of A associated with the callback action runs.
  5. A: The entity created with the user input is retrieved from the javabean property.
  6. A goes on to add a row to the data table, hide SBP, and possibly update some database.
At this point, the java bean is starting to look a lot like a service interface for the SingleBeanPanel. Not only it encapsulates all its creation ingredients, but it allows its client (panel A) to retrieve results from the end-user interaction with it. The java bean is as close to a contract as it gets and it is naturally suited to help us fulfill the second requirement. We will reference the panel instances we create everywhere with the base panel class of the component-based framework we are using and utilize the java bean as input for the creation of the SingleBeanPanel. The actual creation will be performed by a factory (ServicePanelFactory). This way the service panel implementation class is not known compile time and the association of a service panel definition type with service panel implementation is a matter of configuration. There are two well-known advanrages here. First, switching to a new service panel implementation should require no changes compile time. Second, it is harder to leak service panel implementation details to the clients.
In general, each time you create a new service panel definition, you will have to ask yourself these:
  • What are the events of your service panel that need to give execution control to the service panel client? What has to be done before execution control is given? What has to be done after?
  • What data does the service panel need to share with its client?
The first point requires means to invert control during CallbackAction execution. The most common cases can be tackled using a decorator (CallbackWrapper) or a simplistic chain of responsibility (ChainedCallbackAction).
Using a similar thought process, Bo2 creates a number of service panels for common use cases of data management applications. The interested reader may refer to the implementation of the WicketUtils module of Bo2. This module depends only on Wicket and Bo2 utilities, so it is an ideal start with the framework, or, at least, a place to get some cool wicket component implementation ideas from. By the way wicket integration with core Bo2 stuff like transaction management, workers, operations etc is done on a separete module, WicketBo2.
  • SingleBeanPanel (already discussed).
  • PickerPanel: shows a list of entities to select from and perform some action with the one selected.
  • MultipleSelectionsPanel: same as above, but supporting multiple picks.
  • CrudPickerPanel: PickerPanel that allows to edit and delete the listed entities or add new ones. When editing or creating new entities a SingleBeanPanel is internally used to facilitate the corresponding update or save.
  • SearchFlowPanel: allows to execute a query and shows the results in one of the previous three. The query execution is performed by a SingleBeanPanel (the bean in this case is the query criteria).
Whoever survived this wall of text deserves a picture. Using the powers of the screenshot button and non-existing Gimp skills (drum roll):


Figure: Nested crudpickers

The dark blue rectangle is a CrudPickerPanel for customers. Only one customer is shown and the user has selected the corresponding radio button and pressed 'Edit'. The light blue rectangle is a SingleBeanPanel that allows the user to edit the data of a customer. That SingleBeanPanel includes another CrudPickerPanel for the addresses associated with the said customer (purple rectangle). The user has selected the second address and pressed 'Edit'. The SingleBeanPanel used to edit the address data is enclosed in the small light purple rectangle. Scales well by the way, doesn't it?
Last, but not least, an example of how the application code looks like using a service panel (a PickerPanel in this case).

public class ClientPanel extends Panel {

    static final String PRODUCT_PICKER_ID = "productPickerPanel";
 
    PickerPanelDef<Product> ppDef;

    Panel createProductPickerPanel() {
        CallbackAction back = new MethodBasedCallbackAction("closeProductPickerPanel", this);
        CallbackAction picked = new MethodBasedCallbackAction("productSelectedAction", this);
        String[] columns = new String[]{"id", "name", "price"};
        DataTableCreator<Product> creator = new PropertiesBasedPickerDataTableCreator<Product>(Product.class, columns);
  
        ppDef = new PickerPanelDefImpl<Product>();
        ppDef.setWicketId(PRODUCT_PICKER_ID);
        ppDef.setBackAction(back);
        ppDef.setList(getProducts());
        ppDef.setBeanModel(new Model<Product>());
        ppDef.setDataTableCreator(creator);
        ppDef.setItemSelectedAction(picked);
 
        return PanelFactory.create(ppDef);
    }

    @SuppressWarnings("unused")
    private void productSelectedAction(AjaxRequestTarget target) {
        Product selected = ppDef.getBeanModel().getObject();
        //do something with it
        closeProductPickerPanel(target);
    }

    @SuppressWarnings("unused")
    private void closeProductPickerPanel(AjaxRequestTarget target) {
        target.addComponent(this);
        get(PRODUCT_PICKER_ID).replaceWith(new EmptyPanel(PRODUCT_PICKER_ID));
    }
 
    //rest of ClientPanel code
    //getProducts() implementation, creation and addition of PickerPanel
}



Δεν υπάρχουν σχόλια:

Δημοσίευση σχολίου