跳转到内容

ZK/如何操作/模式

来自维基教科书,开放世界中的开放书籍

模型-视图-控制器模式

[编辑 | 编辑源代码]

ZK 开发指南指出

"ZK doesn't enforce developers to use MVC or other design patterns. 
Whether to use them is the developer's choice"

本维基条目探讨了何时可能选择使用 MVC 模式以及如何实现它。

考虑以下不使用 MVC 模式的代码

      <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>

您只需访问 www.zkoss.org 的“实时演示”页面即可运行上述代码。单击“尝试我!”按钮,将代码粘贴到编辑器中,然后再次单击“尝试我!”以运行代码。您将看到,更改文本框或数据框会更新只读输出框。

在上面的示例中,由于功能非常简单,因此很容易将其作为一个整体进行查看和理解。在实际应用中,处理来自多个组件的数据、调用业务服务,然后更新多个不同屏幕组件的事件处理逻辑并不罕见。在复杂的 ZK 应用程序中,定义具有多个弹出窗口、包含页面文件和宏组件的桌面也很常见。然后,从导致更新完全不同的 ZUML 源文件中定义的组件的组件触发的事件并不少见。阅读此类代码以了解应用程序的工作原理可能很困难。随着应用程序的发展,组件会随着用户界面修改而移动到不同的文件中。事件处理程序代码与组件一起移动。这意味着开发人员团队很难轻松地找到代码库中需要更改的位置。

当您需要编写复杂的的用户界面时,将所有事件处理代码移动到一个或少量控制应用程序用户界面行为的类中,可以是对时间和精力的良好投资。为此,您可以采用模型-视图-控制器模式。从 ZK 开发人员的角度来看,MVC 模式由以下内容组成:“模型”是您的业务对象和业务服务。“视图”是在 ZUML(zul、zhtml)文件中定义的桌面上的组件集,不包含任何事件处理逻辑。“控制器”是一个纯 Java 类,它注册为桌面上的一个或多个组件的事件监听器。

以下是上述示例重构后的 MVC 方法

<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>

页面中的窗口具有 apply="com.me.MyController",它引用以下 Java 类

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());
    	}
}

在重构后的示例中,我们看到 zul 文件(“视图”)中没有事件处理代码(“控制器”行为)。窗口组件具有“apply”属性。这指定将 MyController 类的新的实例应用于已初始化的 Window。MyController 类是 GenericForwardComposer 的子类(继承自 GenericAutowireComposer),以提供在 Window 对象组装后调用的 doAfterCompose 方法。GenericForwardComposer 的行为是使用反射和内省(它查找公共的 get/set 组件方法或受保护的组件成员变量)将正确的组件自动注入到您的类中。您编写名为 onXxx$yyy 的事件处理程序方法,GenericFowardComposer 将自动将事件监听器“public onXxx(Event e)”添加到窗口中的“yyy”组件。在上面的示例中,没有显式的模型代码或类,因为我们的示例代码是一个无状态应用程序。

另一种稍微不同的方法是不继承 GenericForwardComposer,而是继承其父类 GenericAutowireComposer。这为您提供了更多灵活性来选择自己的事件处理程序名称

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());
	}
}

但是,我们必须在 zul 文件中添加“forward”属性,以将组件的 onChange 或 onSelect 事件绑定到我们想象的 MyController 事件处理程序方法

<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>

在该 zul 中,forward 属性中的“myWindow.”是可选的,但可以使代码更易于理解。在这个简单的示例中,onSource1 和 onSource2 之类的名称不会使我们的代码具有自记录性。更现实地说,我们会使用 onUpdateShoppingCart 和 onCompleteCreditCardPayment 之类的事件处理程序名称,这将证明这种稍微更冗长的方法需要额外键入是合理的。

回到我们的 MVC 示例,我们应该考虑控制器在哪里实例化。如果我们在 apply 属性中使用类名,例如 apply="com.me.MyController",那么每次页面加载、重新加载或用户在同一个 Web HttpSession 上打开第二个窗口(FF & IE 中的“Ctrl+n”)时,ZK 都会实例化一个新的控制器对象。重要的是要考虑用户在同一个会话中打开第二个窗口时会发生什么。用户有两个并行的 ZK 桌面。然后,您应该小心,在同一个桌面中触发的事件不能通过共享的控制器对象到达第二个桌面中的事件。例如,以下内容是可行的

<!-- zk will instantiate a new object of the named class -->
<window apply="com.me.MyController">
        <textbox/>
        ....
</window>

同样,这也是可以的

<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>

但是以下内容一般是不安全的

<!-- WARNING DON'T USE sessionScope FOR A COMPOSER IF IT HOLDS REFERENCES TO COMPONENTS -->
<window id="myWindow" border="normal" apply="${sessionScope.myController}">
        <textbox/>
        ....
</window>

请注意上面代码中的警告。一个可接受的解决方案是使其相对于组件而言是无状态的;在事件处理程序中显式地查找它们

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());
    	}
}

在此最新版本的代码中,可以跨多个窗口共享单个对象,因为它谨慎地使用从给定桌面触发的事件来解析同一桌面中的组件对象。

华夏公益教科书