Wiki Wiki

« Zpět na FrontPage

DLComposer - MVC Controller

Although ZK does not enforce MVC design, it is always a good practice to split web logic into model - view - controller. ZK ships with several classes (GenericXxxxComposer) , that helps this separation. DLComposer brings several extensions of ZK Composers with utilization of Java 5 annotations.

Concepts#

ZK standard#

ZK offers several ways, how to connect controller to model and view, see MVC in ZK or MVC made easy on ZKoss wiki. Basic usage is:
  • Connect ZUL component to a composer with apply="${controller}" property
  • Autowire controller special properties with GenericAutowireComposer - self, desktop, session, request, execution, ...
  • Autowire controller properties base on ZUL component ID - Button myButton, Textbox myTextbox, ...
  • Register events to a method based on special syntax -. onSelect, myButton$onClick, ...

Lately ZK announced Annotation Based Composer, which is simlar to @ZkEvent and @ZkComponent annotations - we plan to inherit DLComposer from this class in a near future, however we still plan to use our annotations for basic usage (it express programmer's intentions more clearly).

Annotations#

Annotations helps you to emphasize the reason behind a property or method. If used properly, you can read the controller like a story - start with @Controller annotation on the class - yes, this class is a C in MVC, continue with @Autowired (or @EJB) to wire service beans, seed the @ZkModel properties and create custom @ZkControllers for data driven components, register some @ZkEvents, map some @ZkComponent from V in MVC (to change the visual behaviour directly from controller), and get @ZkParameters to know what to display.

Yes, this is similar to other solutions, but we believe, that this improves understandability a lot. It improves performance a bit by the way (because nothing is done automagicaly, but wired by ID).

Annotations overview#

  • @ZkEvent - register event on a component and call annotated method when event fires
  • @ZkComponent - attach ZUL component to this property
  • @ZkController - is accessible from ZUL page with the {controller}.property name.
    • Default value for {controller} is "ctl" unless you set it to other value with @Zkcontroller(name="controller") on the class level.
    • Example: @ZkController ListControll listCtl; ZUL: controller="${ctl.listCtl}" sets controller. Controller variables are read only in ZUL.
  • @ZkModel - is accessible from ZUL page whith the {model}.property name.
    • Default value for {model} is "ctl" unless you set it to other value with @ZkModel(name="model") on the class level.
    • Example: @ZkModel String modelProperty = "xx"; ZUL: value="@{ctl.modelProperty}" sets value to "xx". Model variables are read/write in ZUL.
  • @ZkParameter - checks ZK's parameter maps for parameter name and set field to this value in doBeforeCompose().
    • It checks these maps: execution.getArg(), execution.getAttributes(), execution.getParameters() and uses first map with this parameter set.
  • @ZkBinding - save binding before method invocation and/or load after method invocation
  • @ZkConfirm - ask a user with question dialogue before method is invoked

Master / Detail#

TODO (see DLMasterController/DLDetaiController interface javadoc).

Example#

This example shows most important annotations: @ZkMode, @ZkController and @ZkEvent. Spring @Controller and @Autowired annotations are for motivation only - it depicts, how nicely it all fits together.
 @Controller
public class TodoOverviewController extends DLComposer
{
    // Spring service
    @Autowired TodoService todoService;

    // selected Todo item
    @ZkModel Todo todo;

    // controller for main list - load data from service
    @ZkController DLListboxController<Todo> seznamCtl 
                      =  new DLListboxCriteriaController<Todo>("TodoPrehledControllerList")
    {
        @Override
        protected DLResponse<Todo> loadData(DLSearch<Todo> search)
        {
            return todoService.searchAndCount(search);
        }
    };

    @ZkComponent listbox;

    @ZkEvents(events = {
        @ZkEvent(id = "openDetailButton"),
        @ZkEvent(id = "listitem", event = Events.ON_DOUBLE_CLICK),
        @ZkEvent(id = "listitem", event = Events.ON_OK),
        @ZkEvent(id = "newButton", payload=ZkEvents.EVENT_NEW)
    })
    public void openDetail(int payload)
    {
       ZKHelper.openDetailWindow(self, DETAIL_PAGE, "todo",
               payload == ZkEvents.EVENT_NEW ? new Todo() : todo);
    }

}

Reference#

Bind the controller to a view#

 @ZkController(name="controller")
@ZkModel(name="model")
public class TodoOverviewController extends DLComposer
 <window apply="cz.datalite.example.TodoOverviewController"> 
  <label value="${model.someVariable}"/>
  <label value="${controller.someVariable}"/>
</window>
There is nothing special here - see the apply attribute in ZK documentation to understand MVC architecture. Usually you will use some integration framework as well to find appropriate controller (Spring or EJB with appropriate DelegatingVariableResolver).

To access the controller conveniently, DLComposer automatically publishes itself into the component namespace. The default value is "ctl", but you can change this behaviour with @ZkController and @ZkModel annotations on class level. You can use two variables, if you want to emphasize the different use. This is similar to what you will do manually with ZK's GenericComposer:

 @Override
    public void doBeforeComposeChildren( final Component comp ) throws Exception {
         comp.setVariable( "ctl", this, true );
    }

Although DLComposer extends GenericAutowireComposer it does not wires custom varibles. It is used solely for implicit objects.

@ZkModel#

 @ZkModel Todo todo;
    @ZkModel("otherTodo") Todo todo2;

Annotation defines that property is used like model in MVC architecture. It means that some component uses it for read/write data.

Property is published with its property name or with name defined in the annotation. If getter or setter is created for this property, it is used for getting or setting data instead of direct access. If getter or setter is not defined, value is read / setted directly from / to field even if it is private. Getter and setter have to have the same name like the alias of this field. It means, that if a name is defined in this annotation then getter and setter have to have name like definition in the annotation, no like a property name. Getter and setter are optional.

@ZkController#

 @ZkController DLListboxController<Todo> seznamCtl;

The @ZkController annotation works almost same as @ZkModel, main difference is with it's semantics. Using of @ZkController improves readability of your program - you clearly declare intention behind this property, it works like a controller for some component. @ZkController is read only, you can not change it's value from ZUL.

@ZkComponent#

 @ZkComponent Textbox myTextboxId;
Annotation marks a property in DLComposer as a view and injects the property with the target component after page composition. The example basically equals to call getFellow("myTextboxId") on "self" component in DLComposer.doAfterCompose().

Attribute id - ZUL ID of the component. If not set, property name will be used..

This annotation is similar to the @Wire annotation of ZK6 - GenericAnnotationComposer. @Wire has many more options to select one or more variables and we plan to incorporate it into DLComposer in a near future. But we consider to wire a component via id only as a best practice anyway, so we will continue to use @ZkComponent annotation in the future.

@ZkEvent#

 @ZkEvent(id = "saveButton")
    public void save()
    {
        todoService.save(todo);
    }


    @ZkEvents(events = {
        @ZkEvent(id = "openDetailButton"),
        @ZkEvent(id = "listitem", event = Events.ON_DOUBLE_CLICK),
        @ZkEvent(id = "listitem", event = Events.ON_OK),
        @ZkEvent(id = "newButton", payload=ZkEvents.EVENT_NEW)
    })
    public void openDetail(int payload)
    {
       ZKHelper.openDetailWindow(self, DETAIL_PAGE, "todo",
               payload == ZkEvents.EVENT_NEW ? new Todo() : todo);
    }

Register eventListener on the component with @id. When event is posted, this metod is invoked. If you need to register multiple events on a method, use @ZkEvents to create an array of @ZkEvent annotations.

Available event method parameters (you can specify any of these parameters in any order):

  • ForwardEvent - If you want to get original ForwardEvent, otherwise it is unwrapped with forwardEvent.getOrigin()
  • Event, MouseEvent, ... - Any other event
  • Component - Event target component (aka event.getTarget())
  • Object data - data in the event is the parameter (aka event.getData())
  • int payload - payload specified in this annotation (default is ZkEvents.EVENT_DEFAULT)

@ZkEvent parameters:

  • id - source component identifier. On this component is registered event listener. If id is empty, event listener is register on the window.
  • event - Event which is listened. Default is onClick.(Use constants defined in Events and ZkEvents classes)
  • payload - Payload may be used to distinguish multiple events on a method. If an event method contains int parameter type, the invocation will contain payload as its value.

@ZkParameter#

 @ZkParameter String postParam;

   @ZkParameter Todo todoAttribute;

    @ZkParameter(createIfNull=true)
    public void setTodo(Todo todo)
    {
        if (todo.getIdTodo() != null)
               .... 
    }

Set value of property to parameter value in doBeforeCompose(). Used by DLComposer in doBeforeComposet to set its fields to parameter values.

This method checkes Zk's maps (first map where the parameter is set is used) in this order:

  • execution.getArg() - usually set by createComponents(comp, parent, arg) the arg map.
  • execution.getAttributes() - usually set <include param="value">.
  • execution.getParameters() - request parameters.

You can set the parameter name explicitly, or the property name is used. Until you set required to false, an exception is thrown if this parameter is not found in any of the maps. In some cases you might want to create new instance of the object automatically, if the parameter is either not set at all, or set to null. Use createIfNull to autocreate new instances. However, parameter class has to implement default public constructor.

Parameters:

  • name - Parameter name. If not set, default is the property name.
  • required - Throw an error, if the parameter is not set.If false and the parameter is not set, ZkParameter annotation is ignored altogether (createIfNull is ignored).
  • createIfNull - If the parameter is not set or is set to null, create new instance of the object with default constructor.

@ZkBinding#

 @ZkEvent(id = "saveButton")
    @ZkBinding(saveBefore=true)
    public void save()
    {
        todoService.save(todo);
    }

This annotation defines binding behaviour before and after this method. This has to be used always with ZkEvent. Default settings are not save binding before the method and loadAll after the method. There are options save-before and load-after. Binding can by applied on whole page or on the specific component defined in the @component..

Parameters:

  • saveBefore - Defines if save-binding should be called before this method. In the default setting is false
  • loadAfter - Defines if load-binding should be called after this method. In the default setting is true
  • component - Specific component id which should be target of the binding. Default setting is window.

@ZkConfirm#

 @ZkEvent(id = "deleteButton")
  @ZkConfirm(message = "Are you sure?", title = "Confirm")
  public void delete()
  {
      todoService.delete(todo);
  }

Add a messagebox before the event fires. If method is annotated with ZkEvent or ZkEvents this annotation adds messagebox alert before method is invoked. There is @accessButton, which is like key to this method. If user press other button than @accessButton, method will not be invoked. If user press correct button, method is invoked.

Default is question with button with Yes and No and accessButton is Yes.

Parameters:

  • title - Title of the messagebox
  • message - Custom message
  • buttons - Button in the messagebox. Default are Messagebox.YES and Messagebox.NO
  • accessButton - Access button - key to the metod. Default is Messagebox.YES
  • icon - Messagebox type. Default is Messagebox.QUESTION
  • i18n - internationalization. Some applications have to support multiple languages and they use property files (language packs) which consist of pairs key - value where key is in code and value is translated text shown to a user. This attribute says if the text in title and message is simple text to be shown directly to user without any change or if it is key which should be translated using ZK class Labels.

How to use i18n:

 @ZkEvent(id = "deleteButton")
  @ZkConfirm(message = "msg.confirm.simple.content", title = "msg.confirm.simple.title", i18n=true)
  public void delete()
  {
      todoService.delete(todo);
  }
The annotation processor which handles ZkConfirm pops up messagebox with title returned by Labels.getLabel(title) and message Labels.getLabel(message). The class Labels load all property files defined as language pack.

The i18n attribute is not good to use for localizing whole application. Instead of it there is library property which says that all ZkAnnotations which contains some text should be translated using labels. To active it there is enough to write into zk.xml this code:

 <library-property>
      <name>zk-dl.annotation.i18n</name>
      <value>true</value>
  </library-property>

@ZkException#

 @ZkEvent(id = "deleteButton")
  @ZkException(title = "Error has occured", class = cz.datalite.sample.SampleException)
  public void delete() throws SampleException
  {
      todoService.delete(todo); // todoService can throw SampleException, it is checked exception
  }

Applications usually contains some methods which are supposed to throw some checked business exception, for example use has no permission for that operation or that is not selected any object. These exceptions are usually handled only by showing messagebox with some error message.

This annotation provides exactly this behavior. When the exception is supposed to be thrown then we can add annotation @ZkException over a method and then the annotation processor handles all exception matching given type. As a reaction is poped up messagebox with title defined in an annotation. If the annotation contains optional attribute message then this message is shown to a user. If the attribute message is empty then the shown message is got from exception.getMessage().

Another option is i18n of shown messages. The mechanism is exactly same as is described under @ZkConfirm so for more information check it there.

Last option is unwrapping an exception. A lot of frameworks wrapping inner thrown exceptions by their own so the final class of exception doesn't have to be the one we want to catch. This problem solves an attribute unwrap. If it is true then for each exception is recursively called exception.getCause() and mechanism tries to find some cause of given class. If it is found then the exception is caught and message is shown. If attribute message is not defined then the content of exception with desired type is used.

Because of lot of frameworks wrap thrown exceptions by their own there is a way who to enable unwrapping for each @ZkException annotation. The library configuration is done using file zk.xml. Just type in following code and unwrapping will be work independently on attribute unwrap

 <library-property>
      <name>zk-dl.annotation.exception.unwrap</name>
      <value>true</value>
  </library-property>

Parameters:

  • title - Title of the messagebox
  • message - Custom message (optional)
  • type - type of catching exception
  • i18n - internationalization. more under @ZkConfirm
  • unwrap - look for exception type recursively using getCause();

@ZkBlocking#

In a real applications there are occassion when some operations can take long time. In such cases there is a common problem with frozen user's interface because client's application is waiting for server's response. ZK 5 came with echo event which brings a solution how to handle these situations.

Client sends a request to a server. During server side execution is found out that this operation can take long time so the execution is postponed and some UI updates are executed instead. Finally the EchoEvent is fired. Client updates UI based on the response and sends another request to the server based on echo event. The proper listener handles it as intended long running operation. Usual UI updated lies in visual blocking UI and informing user about long running operation.

Because the UI update and firing the EchoEvent is usually same through a lot of cases we have developed @ZkBlocking annotation providing this functionality. Execution of methods annotated this way is postponed. First of all is updated UI to inform user about long running operation and after that the method is invoked.

This annotation works properly with all our annotations except ZkAsync.

 @ZkBlocking
@ZkEvent(id = "myButton")
public void longOperation() throws SampleException
{
      // some operation which takes long time
}

Parameters:

  • message - message to be shown, there is provided default message
  • i18n - if the message should be internationalized. This attribut works in the same way as in other annotations.
  • component - component identifier says which component should be blocked. As a default set up is blocked whole page.

@ZkAsync#

 @ZkAsync
@ZkEvent(id = "myButton")
public void longOperation() throws SampleException
{
      // some operation which takes long time
      // for example loading data from a database
      // and storing them to an instance property
}

@ZkEvent( event = ZkEvents.ON_ASYNC_FINISHED )
public void doAfterAsync() {
      // updating UI based on the data
}

As it was mentioned above, some operations can take long time. In most cases user needs to wait for the result but there are some occations when the operation can be executed in a background and user can do some other stuff meanwhile. These methods can be executed asynchronously on the server. The response is not interesting for the user or it is not important.

Such requests can be added to event queue and ZK can process them in another thread. A little disadvantage of this approach lies in inaccessibillity to user's session and desktop in these methods. It means that we cannot work with components or user's configuration. All of these things has to be done in sychnronously executed event. But this method is still suitable for example for loading data from database or doing some extensive computation.

The results of the async method can be proceeded in an event sent after finishing the async method. The event is executed synchronously so there we can work with UI etc.

Only one user's operation can be invoked asynchronously at the moment. Another requests for async execution are rejected.

Sometimes user can change his mind and may want to cancel running operation. For such cases there is a class ZkCancellable with interface ZkCancelCommand. If the currently invoked method is cancellable then the ZkCancellable.isSet() returns true. Otherwise it returns false. In these situations we can add instance of ZkCancelCommand to the current ZkCancellable object bound to this thread to allow to cancel the operation.

For example simple implementation can look like this. In the beginnning of the long running method we can check for the ZkCancellable.isSet(). If it returns true then we call the method addCommand() of ZkCancellable object acquired from ZkCancellable.get(). The implementation of the ZkCancellableCommand can simply set some flag (be carreful with concurrency) which the method can periodically check. Another implementation of the command can call method cancel() on the Hibernate session to cancel running query. In one ZkCancellable can be more ZkCancelCommand implementations. That is useful when the method has more phases for example.

Parameters:

  • message - message to be shown, there is provided default message
  • i18n - if the message should be internationalized. This attribut works in the same way as in other annotations.
  • component - component identifier says which component should be blocked. As a default set up is blocked whole page. It works only for cancellable = false, because cancellable = true uses different blocking mechanism which doesn't allow to block only part of UI
  • cancellable - defines if the user can interrupt the method. Default is false.
  • doAfter - name of event send to the controller after finishing the async method. The default name is ZkEvents.ON_ASYNC_FINISHED but when there are more async events in one controller it might be desired to change it. The event handling is optional.

This annotation shouldn't be used with @ZkBlocking. Both annotations tries to do similar thing in a different ways. Other annotations works properly.

Implementation details#

AnnotationProcessor read the annotations on methods when the first instance of class is initializing. Collected data are then stored and that class is not processed any more. This caching can be disabled because for example JRebel allows hot redeploy which would be blocked by cached wrong annotations. Caching can be disabled by adding this code into zk.xml. In default it is true
 <library-property>
     <name>zk-dl.annotation.cache</name>
     <value>false</value>
 </library-property>

This class implements map interface. This is usefull for map access from ZUL or with binding. In doBeforeComposeChildren, instance of controller is set to component variable as "ctl" (or custom name) and is immediately accessible from ZUL processing - ctl.property will access this map, which will return property mapped by @ZkController or @ZkModel..

Similar to GenericAutowireComposer it has basic protected properties like session, requestScope, arg etc., but it doesn't wire all properties and methods automatically (because it can be performance bottleneck if you use multiple composers with many methods.)

Notice that since this composer kept references to the components, single instance object cannot be shared by multiple components.

Zk annotations over the methods #

Annotations and their processors are divided into 2 groups - one group, initializers, creates new object which directly invokes some methods. In this category is ZkEvent and ZkEvents. Another group, wrappers, are processors which wrap existing invoke object. These wrappers provides additional functionality for existing invoke object. In this category is rest of annotations.

Each annotation consist of annotation and its handler which contains also a processor. The handler has to implement Invoke interface but it is highly recommended to extend Handler class. The process method has to have specific signature, this static method is invoked through reflection. There is recommended to copy some existing processor method and edit it. Handler also contains a few methods which can be overriden.

Order of processed annotation is defined in AnnotationProcessor in static initialization. The earlier annotation is processed the later is invoked. The concept is similar to Stack.

0 Přílohy
13909 Zobrazení
Průměr (0 Hlasů)
Komentáře