Best Model-View-Controller Patterns"
m (Created page with '{{Template:Smalltalk_Author| |author=Simon Massey, AVP, Marsh, The world's leading insurance broker and strategic risk advisor |date=June 9, 2008 |version=Applicable to ZK 3.0.5 …') |
m (moved Small Talks/Best Model-View-Controller Patterns to Small Talks/2008/June/Best Model-View-Controller Patterns) |
(No difference)
|
Latest revision as of 06:16, 20 September 2010
Simon Massey, AVP, Marsh, The world's leading insurance broker and strategic risk advisor
June 9, 2008
Applicable to ZK 3.0.5 and later.
Why MVC?
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>
In the example above as the functionality was so simple it is easy to see and understand it as a whole. However, in real applications, the requirements is much more complex for the following reasons,
- Complicated event processing logic that takes data from several components, calls out to business services, and then updates several different screen components.
- Sophisticated ZK applications define desktops that have a number of pop-up windows, included page files, and macro components.
- Events are fired from components that cause updates to components defined within completely different ZUML source files.
Here are some possible risks to build a real-world application without using MVC.
- Reading such code to understand how the application works can be hard.
- Hard for a team of programmers to easily find where to make changes within the codebase.
Thus, 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 control application behavior.
A Simple MVC Example
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 on an EventListener one or more Components in the desktop.
Here is the example above refactored to take the MVC approach:
<window border="normal" apply="MyController">
<grid width="80%">
<rows>
<row>textbox: <textbox id="source1" forward="onChange=onSource1"/></row>
<row>dateBox: <datebox id="source2" forward="onChange=onSource2"/></row>
<row>output: <textbox id="copy" readonly="true"/></row>
</rows>
</grid>
</window>
The window in the page has apply="MyController" that references the following Java class:
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.util.Composer;
import org.zkoss.zul.Datebox;
import org.zkoss.zul.Textbox;
public class MyController implements Composer {
protected Textbox copy;
protected Textbox source1;
protected Datebox source2;
public void doAfterCompose(Component window) {
copy = (Textbox) window.getFellow("copy");
source1 = (Textbox) window.getFellow("source1");
source2 = (Datebox) window.getFellow("source2");
window.addEventListener("onSource1", new EventListener() {
public void onEvent(Event event) throws Exception {
copy.setValue(source1.getValue());
}
});
window.addEventListener("onSource2", new EventListener() {
public void onEvent(Event event) throws Exception {
copy.setValue(source2.getValue().toString());
}
});
}
}
Adopt Forward, and Apply Concepts
In the refactored example we see that the zul file has no event processing code within it. Instead the input components each have a "forward" attribute defined. This tells ZK to forward on their onChange events onto a different method on a different object. By default the target of the forward event will be the space owner of the components which in this case is the enclosing window component.
The window component has an "apply" attribute. This specifies to use an instance of the MyController class to initialize the window. Within the Java code of the MyController class we have a doAfterCompose method which is called after the desktop has been assembled. The window component is passed into that method. Within that method we look up and store references to the three components on the page using the getFellow method. Then two event handlers are registered on the window object. The event handler names match the two "forward" attribute on the components in the zul file. In the above example there is no explicit Model code or class as our sample code is a stateless application. If it were a real program the MyController would make calls to business objects that would represent the model of the application.
More Codes?
At first look the refactored code appears to be a lot more effort with very little gain. We have replaced two lines of zscript event handler code with a class that takes 30 lines of code. However the two lines of code in the first example are not actually executed as two lines of Java code. Lets go back and revisit those original event handlers:
copy.value = source1.value
copy.value = source2.value.toString();
These two event handlers look like Java but they are not compiled and run as Java. The are actually zscript that is run in one of a choice of server side scripting engines. By default zscript is run using the BeanShell interpreter. We can instead choose to use Rhino the Javascript interpreter written in Java which comes with JDK1.6 JSR223 and many J2EE containers such as Websphere 6. Simply ensure that zkmax.jar and js.jar from the Rhino project are on the classpath and use this version of the original code:
<?page zscript-language="JavaScript"?>
<window border="normal">
<grid width="80%">
<rows>
<row>onChange1 textbox:
<textbox id="source1">
<attribute name="onChange">
copy.value = 'js says: '+source1.value
</attribute>
</textbox>
</row>
<row>onChange2 datebox:
<datebox id="source2">
<attribute name="onChange">
copy.value = 'js says: '+source2.value;
</attribute>
</datebox>
</row>
<row>output: <textbox id="copy" readonly="true"/></row>
</rows>
</grid>
</window>
This version of the code has javascript strings in it such as 'js says: ' as the event handler is run on the server using the Rhino interpreter in js.jar. Very similar technology is used in the very first example that makes us of the BeanShell interpreter in bsh.jar. The event handler is parsed by the interpreter at run-time. Then the parsed script is usually run via Java reflection and introspection. The single line of zscript is actually acted out by very many more classes and methods within the interpreter engine. With a real application that has big event handlers running them in an interpreter gives a run-time overhead. If the interpreter encounters an error you cannot attach a debugger to easily debug the script. Typically you use System.out.println output to debug your script. Once again this is all very manageable in small event handlers. Switching to the MVC pattern the event handlers that are pure Java gives you the option to use your favourite Java debugger to allow you to inspect and modify the state of the running application at a breakpoint.
Back with our MVC example we should consider where the controller gets instantiated. If we use a classname in the apply attribute such as apply="MyController" then a new controller object is instantiated whenever the page is loaded or reloaded. We may chose to instantiate a controller once and only once when the user zk session is created. To do this we can use a SessionInit class such as:
public class MySessionInit implements SessionInit {
public void init(Session session, Object request) throws Exception {
MyController myController = new MyController();
session.setAttribute("myController", myController);
return;
}
}
In the init method we could do Spring bean or JNDI look-ups to get business services and business objects to pass to the controller object. To configure ZK to run the session initializer we add an entry to zk.xml such as:
<listener>
<description>MyController Session Initialiser</description>
<listener-class>MySessionInit</listener-class>
</listener>
Within the zul file we change the apply attribute of the window component reference the controller object in the session:
<!-- WARNING ONLY USE sessionScope FOR COMPOSER IF IT DOES NOT HOLD REFERENCES TO COMPONENTS -->
<window id="myWindow" border="normal" apply="${sessionScope.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 last version of the code the forward attributes were changed to reference 'myWindow' which is the ID given to the window component. This is not needed but it does make the code clearer. We may also chose to apply the same controller to the the main window and a number of pop-up windows. The doAfterCompose can then check the ID of the window in question and register its components and EventListeners.
Note the warning in that code. It warns that you should not use a controller held in the users session if the composer object holds references to Components in the desktop. What and why is that? Users might open up a new window by using Ctrl+N on IE or FF. If a user does that they will get two independent desktops with two versions of everything, one in each browser window. If the user fires events in the first Desktop, then the composer tries to update Components in the second Desktop things will go wrong. A solution to this issue would be to make your composer stateless with respect to components:
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.util.Composer;
import org.zkoss.zul.Datebox;
import org.zkoss.zul.Textbox;
public class MyController implements Composer {
public void doAfterCompose(Component window) {
window.addEventListener("onSource1", new EventListener() {
public void onEvent(Event event) throws Exception {
Textbox copy = (Textbox) event.getTarget().getFellow("copy");
Textbox source1 = (Textbox) event.getTarget().getFellow("source1");
copy.setValue(source1.getValue());
}
});
window.addEventListener("onSource2", new EventListener() {
public void onEvent(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 the getFellow calls return references to components that are scoped to the event handlers. Imagine onSource1 firing in one browser window then onSource2 firing in the second browser window. With this latest code our single composer held in the websession behaves well. Naturally any business objects that it calls out to during event handlers should be stateless or singleton to support being shared across two concurrent user desktops.
Another approach to the problem of a user opening a second window is go back to the technique of to passing the class name to the apply attribute as discussed above. Then we may return to the version of our controller that does hold references to components in the desktop as a composer can never be shared between desktops. Then we might choose to use a SessionInit object to obtain singleton or statless business service objects that we cache in the session that we would resolve in doAfterCompose as follows:
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.util.Composer;
import org.zkoss.zul.Datebox;
import org.zkoss.zul.Textbox;
public class MyController implements Composer {
protected Textbox copy;
protected Textbox source1;
protected Datebox source2;
protected MyBusinessStatelessService service;
public void doAfterCompose(Component window) {
service= (MyBusinessStatelessService)
window.getSession().getAttribute("service");
copy = (Textbox) window.getFellow("copy");
source1 = (Textbox) window.getFellow("source1");
source2 = (Datebox) window.getFellow("source2");
window.addEventListener("onSource1", new EventListener() {
public void onEvent(Event event) throws Exception {
copy.setValue(service.getByText(source1.getValue()));
}
});
window.addEventListener("onSource2", new EventListener() {
public void onEvent(Event event) throws Exception {
copy.setValue(service.getByDate(source2.getValue()));
}
});
}
}
In the upcoming ZK version 3.0.6, some utility classes and methods that helps developers to bind events and variables automatically:
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());
}
}
You can see that the MyController.java is much more clean. You declare protected Component member variable in your Java class using variable names that match the IDs in the ZUML page. The behaviour of GenericAutowireComposer is to automatically "inject" the correct Components into you class using reflection and introspection. You write your onXxx event handler methods and GenericAutowireComposer will automatically "add" them as event listeners on the component that you apply your composer to. If you cannot inherit from GenericAutowireComposer directly then you can call Components.wireVariables() and Events.addEventListeners() to achieve same results.
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"
Simon Massey is a AVP working for Marsh (London, UK). He is also the host deveoper of the ZKFoodToGo project for ZK.forge .
Copyright © Simon Massey. This article is licensed under GNU Free Documentation License. |