ZK/How-Tos/Pattern
Model View Controller Pattern
[edit | edit source]The ZK DevGuide states
"ZK doesn't enforce developers to use MVC or other design patterns. Whether to use them is the developer's choice"
This wiki entry considers when you might choose to use the MVC pattern and how you can implement it.
Consider the following code which does not use an MVC pattern:
<window border="normal"> <grid width="80%"> <rows> <row>onChange1 textbox: <textbox id="source1"> <attribute name="onChange"> copy.value = source1.value </attribute></textbox> </row> <row>onChange2 datebox: <datebox id="source2"> <attribute name="onChange"> copy.value = source2.value.toString(); </attribute></datebox> </row> <row>output: <textbox id="copy" readonly="true"/></row> </rows> </grid> </window>
You can run the code above simply by going to the www.zkoss.org Live Demo page. Click the 'Try Me!' button, paste the code into the edit editor, the click 'Try Me!' again to run the code. You will see that changing the textbox or databox updates the readonly output box.
In the example above as the functionality was so simple it is easy to see and understand it as a whole. In real applications it is not uncommon to have event processing logic that takes data from several components, calls out to business services, and then update several different screen components. It is also common in sophisticated ZK applications to define desktops that have a number of pop-up windows, included page files, and macro components. Then it is not uncommon to find events are fired from components that cause updates to components defined within completely different ZUML source files. Reading such code to understand how the application works can be hard. As your application evolves the components move between files as the user interface is revised. The event handler code moves with the component. This means that it can be hard for a team of programmers to easily find where to make changes within the codebase.
When you are faced with writing a sophisticated user interface then it can be a good investment in time and effort to move all of the event processing code one or a small number of classes that controls the application user interface behaviour. To do this you adopt the Model-View-Controller pattern. In terms of ZK developer the MVC patterns is made up of the following items. The 'Model' is your business objects and business services. The 'View' is the set of Components in the desktop defined in ZUML (zul, zhtml) files containing no event processing logic. The 'Controller' is a pure Java class that is registered as an EventListener one or more Components in the desktop.
Here is the example above refactored to take the MVC approach:
<window border="normal" apply="com.me.MyController"> <grid width="80%"> <rows> <row>textbox: <textbox id="source1"/></row> <row>dateBox: <datebox id="source2"/></row> <row>output: <textbox id="copy" readonly="true"/></row> </rows> </grid> </window>
The window in the page has apply="com.me.MyController" that references the following Java class:
package com.me; import org.zkoss.zk.ui.event.Event; import org.zkoss.zk.ui.util.GenericForwardComposer; import org.zkoss.zul.Datebox; import org.zkoss.zul.Textbox; public class MyController extends GenericForwardComposer { protected Textbox copy; protected Textbox source1; protected Datebox source2; public void onChange$source1(Event event) throws Exception { copy.setValue(source1.getValue()); } public void onChange$source2(Event event) throws Exception { copy.setValue(source2.getValue().toString()); } }
In the refactored example we see that the zul file (the 'View') has no event processing code ('controller' behaviour) within it. The window component has an "apply" attribute. This specifies to apply a new instance of the MyController class to the initialized Window. The MyController class is a subclass of GenericForwardComposer (that subclasses GenericAutowireComposer) to provide a doAfterCompose method which is called after the Window object has been assembled. The behaviour of GenericForwardComposer is to automatically inject the correct Components into you class using reflection and introspection (it looks for public get/set Component methods or protected Component member variables). You write event handler methods with a names like onXxx$yyy and GenericFowardComposer will automatically add event listeners "public onXxx(Event e)" to the "yyy" component within the window. In the above example there is no explicit Model code or classes as our sample code is a stateless application.
A slightly different approach is not to subclass GenericForwardComposer but it's parent class GenericAutowireComposer. This gives you more flexibility to pick your own event handler names:
public class MyController extends GenericAutowireComposer { protected Textbox copy; protected Textbox source1; protected Datebox source2; public void onSource1(Event event) throws Exception { copy.setValue(source1.getValue()); } public void onSource2(Event event) throws Exception { copy.setValue(source2.getValue().toString()); } }
however we must add "forward" attributes into our zul file to bind the onChange or onSelect events of our components to our imaginatively named MyController event handler methods:
<window id="myWindow" apply="MyController"> <grid width="80%"> <rows> <row>textbox: <textbox id="source1" forward="onChange=myWindow.onSource1"/></row> <row>dateBox: <datebox id="source2" forward="onChange=myWindow.onSource2"/></row> <row>output: <textbox id="copy" readonly="true"/></row> </rows> </grid> </window>
in that zul the "myWindow." within the forward attribute is optional but makes the code easier to understand. In this trivial example method names such as onSource1 and onSource2 don't really make our code self documenting. More realistically we would use event handler names such as onUpdateShoppingCart and onCompleteCreditCardPayment that would justify the extra typing with this slightly more wordy approach.
Back with our MVC example we should consider where the controller gets instantated. If we use a classname in the apply attribute such as apply="com.me.MyController" then a new controller object is instantiated by ZK whenever the page is loaded, reloaded or when the user opens a second window on the same web HttpSession ('Ctrl+n' in FF & IE). It is important to consider what happens when a user opens a second window on the same session. There are two parallel ZK Desktops for the user. Then you should be careful that events fired in one Desktop cannot reach events in the second desktop via a shared Controller object. For example the following is fine:
<!-- zk will instantiate a new object of the named class --> <window apply="com.me.MyController"> <textbox/> .... </window>
as is this:
<zscript> import com.me.MyController; ... MyController myController = new MyController(beanHeldInHttpSession); </zscript> <!-- We manually instantiated the new object when this page was loaded --> <window apply="${myController}"> <textbox/> .... </window>
but the following is NOT GENERALLY SAFE:
<!-- WARNING DON'T USE sessionScope FOR A COMPOSER IF IT HOLDS REFERENCES TO COMPONENTS --> <window id="myWindow" border="normal" apply="${sessionScope.myController}"> <textbox/> .... </window>
Note the warning in that code above. An acceptable solution is to make it stateless with respect to Components; explicitly look them up within the event handlers:
package com.me; import org.zkoss.zk.ui.event.Event; import org.zkoss.zk.ui.util.GenericForwardComposer; import org.zkoss.zul.Datebox; import org.zkoss.zul.Textbox; public class MyController extends GenericForwardComposer { public void onChange$source1(Event event) throws Exception { Textbox copy = (Textbox) event.getTarget().getFellow("copy"); Textbox source1 = (Textbox) event.getTarget().getFellow("source1"); copy.setValue(source1.getValue()); } public void onChange$source2(Event event) throws Exception { Textbox copy = (Textbox) event.getTarget().getFellow("copy"); Datebox source2 = (Datebox) event.getTarget().getFellow("source2"); copy.setValue(source2.getValue().toString()); } }
In this latest version of the code can share a single object across multiple windows as it is careful to use the events fired from a given Desktop to resolve the Component objects in the same Desktop.