1. Introduction简介
Wicket has been around since 2004 and it has been an Apache project since 2007. During these years it has proved to be a solid and valuable solution for building enterprise web applications.
Wicket自从2004年开始就存在了。它从2007开始是一个Apache的工程。经过这些年,它已经被证实是一个solid而且有价值的企业级Web端程序解决方案。
Wicket core developers have done a wonderful job with this framework and they continue to improve it release after release. However Wicket never provided a freely available documentation and even if you can find on Internet many live examples and many technical articles on it (most of them at Wicket Examples Site and at Wicket in Action), the lack of an organized and freely available documentation has always been a sore point for this framework.
Wicket核心开发人员在这个框架上做得很好,他们在一次又一次的发布中继续改进它。然而,Wicket从未提供过免费提供的文档,即使你可以在互联网上找到许多实时示例和许多技术文章(其中大多数在Wicket examples Site和Wicket in Action),但缺乏有组织的免费文档一直是这个框架的痛点。
That’s quite an issue because many other popular frameworks (like Spring, Hibernate or Struts) offer a vast and very good documentation which substantially contributed to their success.
这是一个很大的问题,因为许多其他流行的框架(如Spring、Hibernate或Struts)提供了大量非常好的文档,这对它们的成功起到了很大的作用。
This document is not intended to be a complete reference for Wicket but it simply aims to be a straightforward introduction to the framework that should significantly reduce its learning curve. What you will find here reflects my experience with Wicket and it’s strictly focused on the framework. The various Wicket-related topics are gradually introduced using pragmatic examples of code that you can find in the according repository on Github. However remember that Wicket is a vast and powerful tool, so you should feel confident with the topics exposed in this document before starting to code your real applications!
本文件并非旨在作为Wicket的完整参考,但其目的只是简单介绍该框架,从而显著减少其学习曲线。你会发现这里反映了我对Wicket的经验,它严格专注于框架。各种与Wicket相关的主题都是通过使用实用的代码示例逐步介绍的,您可以在Github上的相应存储库中找到这些示例。但是请记住,Wicket是一个庞大而强大的工具,因此在开始编写真正的应用程序之前,您应该对本文档中公开的主题充满信心!
For those who need further documentation on Wicket, there are many good books available for this framework.
对于那些需要关于Wicket的进一步文档的人来说,有很多关于这个框架的好书。
Hope you’ll find this guide helpful. Have fun with Wicket!
希望你会觉得这本指南很有帮助。和Wicket玩得开心!
Editors:
PS: this guide is based on Wicket 6. However if you are using an older version you should find this guide useful as well, but it’s likely that the code and the snippets won’t work with your version.
附言:本指南基于Wicket 6。然而,如果您使用的是旧版本,您应该会发现本指南也很有用,但代码和片段很可能不适用于您的版本。
PPS: although I’ve tried to do my best working on this tutorial, this document is a work in progress and may contain errors and/or omissions. That’s why any feedback of any kind is REALLY appreciated!
PPS:尽管我已经尽力完成本教程,但本文档仍在进行中,可能会包含错误和/或遗漏。这就是为什么任何形式的反馈都会受到真正的赞赏!
2. How to use the example code
Most of the code you will find in this document is available as a Git repository and is licensed under the ASF 2.0. Examples are hosted live at {externalink:wicket.tutorial.examples.url}. To get a local copy of the repository you can run the clone command from shell:
git clone https://github.com/bitstorm/Wicket-tutorial-examples.git
If you aren’t used to Git, you can simply download the whole source as a zip archive:
Chapter 4. Wicket says “Hello world!”
Wicket allows us to design our web pages in terms of components and containers, just like AWT does with desktop windows. Both frameworks share the same component-based architecture: in AWT we have a Windows instance which represents the physical windows containing GUI components (like text fields, radio buttons, drawing areas, etc…), in Wicket we have a WebPage instance which represents the physical web page containing HTML components (pictures, buttons, forms, etc… ) .
Wicket允许我们根据组件和容器来设计网页,就像AWT对桌面窗口所做的那样。两个框架共享相同的基于组件的架构:在AWT中,我们有一个Windows实例,它表示包含GUI组件(如文本字段、单选按钮、绘图区域等)的物理窗口,在Wicket中,我们还有一个WebPage实例,它代表包含HTML组件(图片、按钮、表单等)的实体网页。
In both frameworks we find a base class for GUI components called Component. Wicket pages can be composed (and usually are) by many components, just like AWT windows are composed by Swing/AWT components. Both frameworks promote the reuse of presentation code and GUI elements building custom components. Even if Wicket already comes with a rich set of ready-to-use components, building custom components is a common practice when working with this framework. We’ll learn more about custom components in the next chapters.
在这两个框架中,我们都找到了GUI组件的基类Component。Wicket页面可以由许多组件组成(通常也是),就像AWT窗口由Swing/AWT组件组成一样。这两个框架都促进了构建自定义组件的表示代码和GUI元素的重用。即使Wicket已经提供了一套丰富的现成组件,在使用该框架时,构建自定义组件也是一种常见的做法。我们将在下一章中了解更多关于自定义组件的信息。
4.1. Wicket distribution and modules
Wicket is available as a binary package on the main site http://wicket.apache.org . Inside this archive we can find the distribution jars of the framework. Each jar corresponds to a sub-module of the framework. The following table reports these modules along with a short description of their purpose and with the related dependencies:
Wicket在主站点上以二进制包的形式提供http://wicket.apache.org . 在这个档案中,我们可以找到框架的分发jar。每个jar对应于框架的一个子模块。下表报告了这些模块及其用途和相关依赖关系的简短描述:
Module’sname | Description | Dependencies |
wicket-core | 包含框架的主要类,如类Component和Application。 | wicket-request, wicket-util |
wicket-request | 此模块包含web请求处理所涉及的类. | wicket-util |
wicket-util | 包含用于I/O、lang、字符串操作、安全性等功能领域的通用实用程序类… | None |
wicket-datetime | 包含专门用于处理日期和时间的组件。 | wicket-core |
wicket-bean-validation | 提供对JSR303标准验证的支持。 | wicket-core |
wicket-devutils | 包含实用程序类和组件,以帮助开发人员完成调试、类检查等任务。 | wicket-core, wicket-extensions |
wicket-extensions | 包含大量内置组件,用于为我们的web应用程序构建丰富的UI(Ajax支持是本模块的一部分)。 | wicket-core |
wicket-auth-roles | 提供对基于角色的授权的支持。 | wicket-core |
wicket-ioc | 此模块提供了支持控制反转的公共类。Spring和Guice集成模块都使用它。 | wicket-core |
wicket-guice | 该模块提供了与谷歌开发的依赖注入框架的集成。 | wicket-core, wicket-ioc |
wicket-spring | 该模块提供与Spring框架的集成。 | wicket-core, wicket-ioc |
wicket-velocity | 该模块提供面板和实用程序类,用于将Wicket与Velocity模板引擎集成。 | wicket-core |
wicket-jmx | 该模块提供面板和实用程序类,用于将Wicket与Java管理扩展集成。 | wicket-core |
wicket-objectsizeof-agent | 提供与Java代理库和检测工具的集成。 | wicket-core |
Please note that the core module depends on the utility and request modules, hence it cannot be used without them.
请注意,核心模块取决于实用程序模块和请求模块,因此没有它们就无法使用。
4.2. Configuration of Wicket applications
In this chapter we will see a classic Hello World! example implemented using a Wicket page with a built-in component called Label (the code is from project the HelloWorldExample). Since this is the first example of the guide, before looking at Java code we will go through the common artifacts needed to build a Wicket application from scratch.
在这一章中,我们将看到一个经典的Hello World!示例使用一个名为Label的内置组件的Wicket页面实现(代码来自HelloWorldExample项目)。由于这是本指南的第一个示例,在查看Java代码之前,我们将了解从头开始构建Wicket应用程序所需的常见工件。
注意:All the example projects presented in this document have been generated using Maven and the utility page at http://wicket.apache.org/start/quickstart.html . Appendix A contains the instructions needed to use these projects and build a quickstart application using Apache Maven. All the artifacts used in the next example (files web.xml, HomePage.class and HomePage.html) are automatically generated by Maven.本文档中提供的所有示例项目都是使用Maven和位于http://wicket.apache.org/start/quickstart.html . 附录A包含使用这些项目和使用ApacheMaven构建快速启动应用程序所需的说明。下一个例子中使用的所有工件(文件web.xml、HomePage.class和HomePage.html)都是由Maven自动生成的。
4.2.1. Wicket application structure
A Wicket application is a standard Java EE web application, hence it is deployed through a web.xml file placed inside folder WEB-INF:
图解: 一个Wicket应用的标准目录结构
The content of web.xml declares a servlet filter (class org.apache.wicket.Protocol.http.WicketFilter) which dispatches web requests to our Wicket application:
web.xml的内容声明了一个servlet过滤器(类org.apache.wicket.Protocol.http.WicketFilter),它将web请求发送到我们的wicket应用程序:
<?xml version="1.0" encoding="UTF-8"?> <web-app> <display-name>Wicket Test</display-name> <filter> <filter-name>TestApplication</filter-name> <filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class> <init-param> <param-name>applicationClassName</param-name> <param-value>org.wicketTutorial.WicketApplication</param-value> </init-param> </filter> <filter-mapping> <filter-name>TestApplication</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>
Since this is a standard servlet filter we must map it to a specific set of URLs through the <filter-mapping> tag). In the xml above we have mapped every URL to our Wicket filter.
由于这是一个标准的servlet过滤器,我们必须通过<filter-mapping>标记将其映射到一组特定的URL)。在上面的xml中,我们已经将每个URL映射到我们的Wicket过滤器。
If we are using Servlet 3 or a later version, we can of course use a class in place of web.xml to configure our application. The following example uses annotation _WebFilter.
如果我们使用Servlet 3或更高版本,我们当然可以使用类来代替web.xml来配置我们的应用程序。以下示例使用注释_WebFilter。
4.2.2. The application class
If we look back at web.xml we can see that we have provided the Wicket filter with a parameter called applicationClassName. This value must be the fully qualified class name of a subclass of org.apache.wicket.Application. This subclass represents our web application built upon Wicket and it’s responsible for configuring it when the server is starting up. Most of the times our custom application class won’t inherit directly from class Application, but rather from class org.apache.wicket.protocol.http.WebApplication which provides a closer integration with servlet infrastructure. Class Application comes with a set of configuration methods that we can override to customize our application’s settings. One of these methods is getHomePage() that must be overridden as it is declared abstract:
如果我们回顾web.xml,我们可以看到我们为Wicket过滤器提供了一个名为applicationClassName的参数。这个值必须是org.apache.wicket的子类的完全限定类名。应用这个子类代表我们基于Wicket构建的web应用程序,它负责在服务器启动时对其进行配置。大多数时候,我们的自定义应用程序类不会直接从类application继承,而是从类org.apache.wicket.procol.http继承。WebApplication,它提供了与servlet基础结构的更紧密集成。类应用程序附带了一组配置方法,我们可以覆盖这些方法来自定义应用程序的设置。其中一个方法是getHomePage(),它必须被重写,因为它被声明为抽象的:
public abstract Class<? extends Page> getHomePage()
As you may guess from its name, this method specifies which page to use as homepage for our application. Another important method is init():
protected void init()
This method is called when our application is loaded by the web server (Tomcat, Jetty, etc…) and is the ideal place to put our configuration code. The Application class exposes its settings grouping them into interfaces (you can find them in package org.apache.wicket.settings). We can access these interfaces through getter methods that will be gradually introduced in the next chapters when we will cover the related settings.
当我们的应用程序由web服务器(Tomcat、Jetty等)加载时,就会调用此方法,它是放置配置代码的理想位置。应用程序类公开其设置,将它们分组为接口(您可以在org.apache.wicket.settings包中找到它们)。我们可以通过getter方法访问这些接口,这些方法将在下一章介绍相关设置时逐步介绍。
The current application’s instance can be retrieved at any time calling static method Application.get() in our code. We will give more details about this method in chapter 9.3. The content of the application class from project HelloWorldExample is the following:
在我们的代码中调用静态方法application.get()可以随时检索当前应用程序的实例。我们将在第9.3章中详细介绍这种方法。项目HelloWorldExample中应用程序类的内容如下:
public class WicketApplication extends WebApplication { @Override public Class<? extends WebPage> getHomePage() { return HomePage.class; } @Override public void init() { super.init(); // add your configuration here } }
Since this is a very basic example of a Wicket application, we don’t need to specify anything inside the init method. The home page of the application is the HomePage class. In the next paragraph we will see how this page is implemented and which conventions we have to follow to create a page in Wicket.
由于这是Wicket应用程序的一个非常基本的示例,因此我们不需要在init方法中指定任何内容。应用程序的主页是HomePage类。在下一段中,我们将看到这个页面是如何实现的,以及在Wicket中创建页面必须遵循哪些约定。
4.3. The HomePage class
To complete our first Wicket application we must explore the home page class that is returned by the Application's method getHomePage() seen above. In Wicket a web page is a subclass of org.apache.wicket.WebPage. This subclass must have a corresponding HTML file which will be used by the framework as template to generate its HTML markup. This file is a regular plain HTML file (its extension must be html).
要完成我们的第一个Wicket应用程序,我们必须探索由上面所示的应用程序的方法getHomePage()返回的主页类。在Wicket中,网页是org.apache.Wicket的一个子类。网页。这个子类必须有一个相应的HTML文件,该文件将被框架用作生成其HTML标记的模板。此文件是一个普通的纯HTML文件(其扩展名必须是HTML)。
By default this HTML file must have the same name of the related page class and must be in the same package:
默认情况下,此HTML文件必须具有与相关页面类相同的名称,并且必须位于同一个包中:
If you don’t like to put class and html side by side (let’s say you want all your HTML files in a separated folder) you can use Wicket settings to specify where HTML files can be found. We will cover this topic later in chapter 16.9.
如果你不喜欢把类和html放在一起(假设你想把所有的html文件放在一个单独的文件夹中),你可以使用Wicket设置来指定html文件的位置。我们稍后将在第16.9章中介绍这个主题。
The Java code for the HomePage class is the following:
HomePage类的Java代码如下:
package org.wicketTutorial; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.WebPage; public class HomePage extends WebPage { public HomePage() { add(new Label("helloMessage", "Hello WicketWorld!")); } }
Apart from subclassing WebPage, HomePage defines a constructor that adds a Label component to itself. Method add(Component component) is inherited from ancestor class org.apache.wicket.MarkupContainer and is used to add children components to a web page. We’ll see more about MarkupContainer later in chapter 5.2. Class _org.apache.wicket.markup.html.basic.Label is the simplest component shipped with Wicket. It just inserts a string (the second argument of its constructor) inside the corresponding HTML tag. Just like any other Wicket component, Label needs a textual id ('helloMessage' in our example) to be instantiated. At runtime Wicket will use this value to find the HTML tag we want to bind to the component. This tag must have a special attribute called wicket:id and its value must be identical to the component id (comparison is casesensitive!).
除了对WebPage进行子类化之外,HomePage还定义了一个构造函数,该构造函数将Label组件添加到自身中。方法add(组件组件)继承自祖先类org.apache.wicket。MarkupContainer,用于将子组件添加到网页中。我们’;我稍后将在第5.2章中看到有关MarkupContainer的更多信息。类_org.apache.wicket.markup.html.basic。标签是Wicket附带的最简单的组件。它只是在相应的HTML标记中插入一个字符串(构造函数的第二个参数)。就像任何其他Wicket组件一样,Label需要一个文本id(在我们的示例中为“helloMessage”)来实例化。在运行时,Wicket将使用此值来查找要绑定到组件的HTML标记。此标记必须具有一个名为wicket:id的特殊属性,并且其值必须与组件id相同(比较区分大小写!)。
Here is the HTML markup for HomePage (file HomePage.html):
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Apache Wicket HelloWorld</title> </head> <body> <div wicket:id="helloMessage"> [Label's message goes here] </div> </body> </html>
4.4. Wicket Links
The basic form of interaction offered by web applications is to navigate through pages using links. In HTML a link is basically a pointer to another resource that most of the time is another page. Wicket implements links with component org.apache.wicket.markup.html.link.Link, but due to the component-oriented nature of the framework, this component is quite different from classic HTML links. Following the analogy with GUI frameworks, we can consider Wicket link as a “click” event handler: its purpose is to perform some actions (on server side!) when the user clicks on it.
web应用程序提供的基本交互形式是使用链接浏览页面。在HTML中,链接基本上是指向另一个资源的指针,该资源大多数时候是另一个页面。Wicket实现了与组件org.apache.Wicket.markup.html.link的链接。链接,但由于框架面向组件的特性,该组件与经典的HTML链接截然不同。根据与GUI框架的类比,我们可以将Wicket链接视为“点击”事件处理程序:其目的是在用户点击它时执行一些操作(在服务器端!)。
That said, you shouldn’t be surprised to find an abstract method called onClick() inside the Link class. In the following example we have a page with a Link containing an empty implementation of onClick:
也就是说,在Link类中发现一个名为onClick()的抽象方法并不奇怪。在以下示例中,我们有一个带有链接的页面,该链接包含onClick的空实现:
public class HomePage extends WebPage { public HomePage(){ add(new Link("id"){ @Override public void onClick() { //link code goes here } }); } }
By default after onClick has been executed, Wicket will send back to the current page to the client web browser. If we want to navigate to another page we must use method setResponsePage of class Component:
默认情况下,在执行onClick后,Wicket会将当前页面发送回客户端web浏览器。如果我们想导航到另一个页面,我们必须使用类Component的方法setResponsePage:
public class HomePage extends WebPage { public HomePage(){ add(new Link("id"){ @Override public void onClick() { //we redirect browser to another page. setResponsePage(AnotherPage.class); } }); } }
In the example above we used a version of setResponsePage which takes as input the class of the target page. In this way a new instance of AnotherPage will be created each time we click on the link. The other version of setResponsePage takes in input a page instance instead of a page class:
在上面的例子中,我们使用了setResponsePage的一个版本,它将目标页面的类作为输入。这样,每次我们点击链接时,都会创建一个新的AnotherPage实例。setResponsePage的另一个版本接受页面实例而不是页面类的输入:
@Override public void onClick() { //we redirect browser to another page. AnotherPage anotherPage = new AnotherPage(); setResponsePage(anotherPage); }
The difference between using the first version of setResponsePage rather than the second one will be illustrated in chapter 8, when we will introduce the topic of stateful and stateless pages. For now, we can consider them as equivalent.
使用第一个版本的setResponsePage与第二个版本的区别将在第8章中说明,届时我们将介绍有状态页面和无状态页面的主题。目前,我们可以认为它们是等效的。
Wicket comes with a rich set of link components suited for every need (links to static URL, Ajaxenhanced links, links to a file to download, links to external pages and so on). We will see them in chapter 10.
Wicket提供了一套丰富的链接组件,适合各种需求(静态URL链接、Ajaxe-enhanced链接、下载文件链接、外部页面链接等)。我们将在第10章中看到它们。
We can specify the content of a link (i.e. the text of the picture inside it) with its method setBody. This method takes in input a generic Wicket model, which will be the topic of chapter 11.
注:我们可以用它的方法setBody指定链接的内容(即其中图片的文本)。这种方法输入一个通用的Wicket模型,这将是第11章的主题。
4.5. Summary
In this chapter we have seen the basic elements that compose a Wicket application. We have started preparing the configuration artifacts needed for our applications. As promised in chapter 2.4, we needed to put in place just a minimal amount of XML with an application class and a home page. Then we have continued our “first contact” with Wicket learning how to build a simple page with a label component as child. This example page has shown us how Wicket maps components to HTML tags and how it uses both of them to generate the final HTML markup. In the last paragraph we had a first taste of Wicket links and we have seen how they can be considered as a “click” event listener and how they can be used to navigate from a page to another.
在本章中,我们看到了组成Wicket应用程序的基本元素。我们已经开始准备应用程序所需的配置工件。正如第2.4章中所承诺的那样,我们只需要用一个应用程序类和一个主页来放置最少量的XML。然后,我们继续与Wicket进行“第一次接触”,学习如何在孩子时代构建一个带有标签组件的简单页面。这个示例页面向我们展示了Wicket如何将组件映射到HTML标记,以及如何使用这两个标记来生成最终的HTML标记。在上一段中,我们第一次体验了Wicket链接,我们看到了它们如何被视为“点击”事件侦听器,以及如何用于从一个页面导航到另一个页面。
Chapter 5. Wicket as page layout manager
Before going ahead with more advanced topics, we will see how to maintain a consistent layout across our site using Wicket and its component-oriented features. Probably this is not the most interesting use we can get out of Wicket, but it is surely the simplest one so it’s the best way to get our hands dirty with some code.
在继续讨论更高级的主题之前,我们将了解如何使用Wicket及其面向组件的功能在整个网站上保持一致的布局。也许这不是我们能从Wicket中得到的最有趣的用法,但它肯定是最简单的用法,所以这是用一些代码弄脏我们的手的最好方法。
5.1. Header, footer, left menu, content, etc…
There was a time in the 90s when Internet was just a buzzword and watching a plain HTML page being rendered by a browser was a new and amazing experience. In those days we used to organize our page layout using the <frame> HTML tag. Over the years this tag has almost disappeared from our code and it survives only in few specific domains. For example is still being used by JavaDoc.
90年代有一段时间,互联网只是一个流行词,观看浏览器渲染的纯HTML页面是一种新的、令人惊叹的体验。在那些日子里,我们经常使用<frame>HTML标记来组织页面布局。多年来,这个标签几乎从我们的代码中消失了,它只在少数特定的领域中存在。例如,JavaDoc仍在使用。
With the adoption of server side technologies like JSP, ASP or PHP the tag <frame> has been replaced by a template-based approach where we divide our page layout into some common areas that will be present in each page of our web application. Then, we manually insert these areas in every page including the appropriate markup fragments.
随着JSP、ASP或PHP等服务器端技术的采用,标签<frame>已被基于模板的方法所取代,在这种方法中,我们将页面布局划分为一些公共区域,这些区域将出现在web应用程序的每个页面中。然后,我们在每个页面中手动插入这些区域,包括适当的标记片段。
In this chapter we will see how to use Wicket to build a site layout. The sample layout we will use is a typical page layout consisting of the following areas:
在本章中,我们将了解如何使用Wicket构建站点布局。我们将使用的示例布局是由以下区域组成的典型页面布局:
• a header which could contain site title, some logos, a navigation bar, etc…
• a left menu with a bunch of links to different areas/functionalities of the site.
• a footer with generic informations like web master’s email, the company address, etc…
• a content area which usually contains the functional part of the page.
The following picture summarises the layout structure:
下图总结了布局结构:
Once we have chosen a page layout, our web designer can start building up the site theme. The result is a beautiful mock of our future web pages. Over this mock we can map the original layout areas:
一旦我们选择了页面布局,我们的网页设计师就可以开始构建网站主题。其结果是对我们未来网页的美丽模仿。在这个模型上,我们可以映射原始布局区域:
Now in order to have a consistent layout across all the site, we must ensure that each page will include the layout areas seen above. With an old template-based approach we must manually put them inside every page. If we were using JSP we would probably end up using include directive to add layout areas in our pages. We would have one include for each of the areas (except for the content):
现在,为了在所有网站上有一个一致的布局,我们必须确保每个页面都包括上面看到的布局区域。使用旧的基于模板的方法,我们必须手动将它们放在每个页面中。如果我们使用JSP,我们可能最终会使用include指令在页面中添加布局区域。我们将为每个领域提供一个包含(内容除外):
For the sake of simplicity we can consider each included area as a static HTML fragment.
为了简单起见,我们可以将每个包含的区域视为静态HTML片段。
Now let’s see how we can handle the layout of our web application using Wicket.
现在让我们看看如何使用Wicket处理web应用程序的布局。
5.2. Here comes the inheritance!
The need of ensuring a consistent layout across our pages unveiled a serious limit of the HTML: the inability to apply inheritance to web pages and their markup. Wouldn’t be great if we could write our layout once in a page and then inherit it in the other pages of our application? One of the goals of Wicket is to overcome this kind of limit.
确保页面布局一致的需求暴露了HTML的一个严重限制:无法将继承应用于网页及其标记。如果我们可以在一个页面中编写一次布局,然后在应用程序的其他页面中继承它,那不是很好吗?Wicket的目标之一就是克服这种限制。
5.2.1. Markup inheritance
As we have seen in the previous chapter, Wicket pages are pure Java classes, so we can easily write a page which is a subclass of another parent page. But in Wicket inheritance is not limited to the classic object-oriented code inheritance. When a class subclasses a WebPage it also inherits the HTML file of the parent class. This type of inheritance is called markup inheritance. To better illustrate this concept let’s consider the following example where we have a page class called GenericSitePage with the corresponding HTML file GenericSitePage.html. Now let’s create a specific page called OrderCheckOutPage where users can check out their orders on our web site. This class extends GenericSitePage but we don’t provide it with any corresponding HTML file. In this scenario OrderCheckOutPage will use GenericSitePage.html as markup file:
正如我们在前一章中所看到的,Wicket页面是纯Java类,所以我们可以很容易地编写一个页面,它是另一个父页面的子类。但在Wicket中继承并不局限于经典的面向对象代码继承。当一个类将WebPage子类化时,它也会继承父类的HTML文件。这种类型的继承称为标记继承。为了更好地说明这个概念,让我们考虑下面的例子,其中我们有一个名为GenericSitePage的页面类和相应的HTML文件GenericSitePage.HTML。现在,让我们创建一个名为OrderCheckOutPage的特定页面,用户可以在我们的网站上查看他们的订单。这个类扩展了GenericSitePage,但我们没有为它提供任何相应的HTML文件。在这种情况下,OrderCheckOutPage将使用GenericSitePage.html作为标记文件:
Markup inheritance comes in handy for page layout management as it helps us avoid the burden of checking that each page conforms to the site layout. However to fully take advantage of markup inheritance we must first learn how to use another important component of the framework that supports this feature: the panel.
标记继承对页面布局管理非常有用,因为它可以帮助我们避免检查每个页面是否符合网站布局的负担。然而,要充分利用标记继承,我们必须首先学习如何使用支持此功能的框架的另一个重要组件:面板。
Notice:If no markup is found (nor directly assigned to the class, neither inherited from an ancestor) a MarkupNotFoundException is thrown.
注意:如果没有找到标记(也没有直接分配给类,也没有从祖先继承),则抛出MarkupNotFoundException。
5.2.2. Panel class
Class org.apache.wicket.markup.html.panel.Panel is a special component which lets us reuse GUI code and HTML markup across different pages and different web applications. It shares a common ancestor class with WebPage class, which is org.apache.wicket.MarkupContainer:
类org.apache.wicket.markup.html.panel。Panel是一个特殊的组件,它允许我们在不同的页面和不同的web应用程序中重用GUI代码和HTML标记。它与WebPage类共享一个共同的祖先类,即org.apache.wicket.MarkupContainer:
Subclasses of MarkupContainercan contain children components that can be added with method add(Component…​)(seen in chapter 3.3). _MarkupContainerimplements a full set of methods to manage children components. The basic operations we can do on them are:
MarkupContainercan的子类包含可以使用add方法添加的子组件(Component…;​;)(见第3.3章)_MarkupContainer提供了一整套管理子组件的方法。我们可以对它们进行的基本操作是:
• add one or more children components (with method add).
• remove a specific child component (with method remove).
• retrieve a specific child component with method get(String). The string parameter is the id of the component or its relative path if the component is nested inside other _MarkupContainer_s.
This path is a colon-separated string containing also the ids of the intermediate containers traversed to get to the child component. To illustrate an example of component path, let’s consider the code of the following page:
•添加一个或多个子组件(使用add方法)。
•移除特定的子组件(使用remove方法)。
•使用get(String)方法检索特定的子组件。如果组件嵌套在其他_MarkupContainer_中,则字符串参数是组件的id或其相对路径。
此路径是一个以冒号分隔的字符串,其中还包含为到达子组件而遍历的中间容器的id。为了说明组件路径的示例,让我们考虑以下页面的代码:
MyPanel myPanel = new MyPanel ("innerContainer"); add(myPanel);
Component MyPanel is a custom panel containing only a label having _ [name] as id. Under those conditions we could retrieve this label from the container page using the following path expression:
组件MyPanel是一个自定义面板,仅包含一个以_[name]为id的标签。在这些条件下,我们可以使用以下路径表达式从容器页面检索该标签:
Label name = (Label)get("innerContainer:name");
• replace a specific child component with a new component having the same id (with method replace).
• iterate thought children components with the iterator returned by method iterator or using visitor pattern1 with methods visitChildren.
•用具有相同id的新组件替换特定子组件(使用方法replace)。
•使用方法迭代器返回的迭代器迭代think-children组件,或使用方法visitChildren的访问者模式1。
Both Panel and WebPage have their own associated markup file which is used to render the corresponding component. If such file is not provided, Wicket will apply markup inheritance looking for a markup file through their ancestor classes. When a panel is attached to a container, the content of its markup file is inserted into its related tag.
Panel和WebPage都有自己的关联标记文件,用于呈现相应的组件。如果没有提供这样的文件,Wicket将应用标记继承,通过其祖先类查找标记文件。当面板附着到容器时,其标记文件的内容将插入到其相关标记中。
While panels and pages have much in common, there are some notable differences between these two components that we should keep in mind. The main difference between them is that pages can be rendered as standalone entities while panels must be placed inside a page to be rendered. Another important difference is the content of their markup file: for both WebPage and Panel this is a standard HTML file, but Panel uses a special tag to indicate which part of the whole file will be considered as markup source. This tag is <wicket:panel>. A markup file for a panel will typically look like this:
虽然面板和页面有很多共同点,但我们应该记住这两个组件之间有一些显著的差异。它们之间的主要区别在于,页面可以作为独立实体进行渲染,而面板必须放置在要渲染的页面内部。另一个重要的区别是它们的标记文件的内容:对于WebPage和Panel来说,这都是一个标准的HTML文件,但Panel使用一个特殊的标记来指示整个文件的哪一部分将被视为标记源。这个标签是<wicket:panel>。面板的标记文件通常如下所示:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> ... </head> <body> <wicket:panel> <!-- Your markup goes here --> </wicket:panel> </body> </html>
The HTML outside tag <wicket:panel> will be removed during rendering phase. The space outside this tag can be used by both web developers and web designers to place some mock HTML to show how the final panel should look like.
HTML外部标记<wicket:panel>将在渲染阶段被删除。web开发人员和web设计者都可以使用此标记外的空间来放置一些模拟HTML,以显示最终面板的外观。
5.3. Divide et impera!
Let’s go back to our layout example. In chapter 5.1 we have divided our layout in common areas that must be part of every page. Now we will build a reusable template page for our web application combining pages and panels. The code examples are from project MarkupInheritanceExample.
让我们回到我们的布局示例。在第5.1章中,我们将布局划分为公共区域,这些区域必须是每页的一部分。现在,我们将为我们的web应用程序构建一个可重复使用的模板页面,将页面和面板组合在一起。代码示例来自项目MarkupInheritanceExample。
5.3.1. Panels and layout areas
First, let’s build a custom panel for each layout area (except for 'content' area). For example given the header area
首先,让我们为每个布局区域(“内容”区域除外)构建一个自定义面板。例如,给定标题区域
we can build a panel called HeaderPanel with a related markup file called HeaderPanel.html containing the HTML for this area:
我们可以使用名为HeaderPanel.html的相关标记文件构建一个名为HeaderPanel的面板,该文件包含该区域的html:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> ... </head> <body> <wicket:panel> <table width="100%" style="border: 0px none;"> <tbody> <tr> <td> <img alt="Jug4Tenda" src="wicketLayout_files/logo_jug4tenda.gif"> </td> <td> <h1>Gestione Anagrafica</h1> </td> </tr> </tbody> </table> </wicket:panel> </body> <html>
The class for this panel simply extends base class Panel:
此面板的类只是扩展基类panel:
package helloWorld.layoutTenda; import org.apache.wicket.markup.html.panel.Panel; public class HeaderPanel extends Panel { public HeaderPanel(String id) { super(id); } }
For each layout area we will build a panel like the one above that holds the appropriate HTML markup. In the end we will have the following set of panels:
对于每个布局区域,我们将构建一个类似上面的面板,其中包含适当的HTML标记。最终,我们将拥有以下一组面板:
• HeaderPanel
• FooterPanel
• MenuPanel
Content area will change from page to page, so we don’t need a reusable panel for it.
内容区域会随着页面的变化而变化,所以我们不需要可重复使用的面板。
5.3.2. Template page
Now we can build a generic template page using our brand new panels. Its markup is quite straightforward :
现在,我们可以使用全新的面板构建一个通用模板页面。它的标记非常简单:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> ... <!--Include CSS--> ... </head> <body> <div id="header" wicket:id="headerPanel">header</div> <div id="body"> <div id="menu" wicket:id="menuPanel">menu</div> <div id="content" wicket:id="contentComponent">content</div> </div> <div id="footer" wicket:id="footerPanel">footer</div> </body> </html>
The HTML code for this page implements the generic left-menu layout of our site. You can note the 4 <div> tags used as containers for the corresponding areas. The page class contains the code to physically assemble the page and panels:
这个页面的HTML代码实现了我们网站的通用左菜单布局。您可以注意到4个<div>标记用作相应区域的容器。页面类包含物理组装页面和面板的代码:
package helloWorld.layoutTenda; import org.apache.wicket.markup.html.WebPage; import org.apache.wicket.Component; import org.apache.wicket.markup.html.basic.Label; public class JugTemplate extends WebPage { public static final String CONTENT_ID = "contentComponent"; private Component headerPanel; private Component menuPanel; private Component footerPanel; public JugTemplate(){ add(headerPanel = new HeaderPanel("headerPanel")); add(menuPanel = new MenuPanel("menuPanel")); add(footerPanel = new FooterPanel("footerPanel")); add(new Label(CONTENT_ID, "Put your content here")); } //getters for layout areas //... }
Done! Our template page is ready to be used. Now all the pages of our site will be subclasses of this parent page and they will inherit the layout and the HTML markup. They will only substitute the Label inserted as content area with their custom content.
完成!我们的模板页面已准备好使用。现在,我们网站的所有页面都将是这个父页面的子类,它们将继承布局和HTML标记。它们将仅使用自定义内容替换作为内容区域插入的标签。
5.3.3. Final example
As final example we will build the login page for our site. We will call it SimpleLoginPage. First, we need a panel containing the login form. This will be the content area of our page. We will call it LoginPanel and the markup is the following:
作为最后一个例子,我们将为我们的网站构建登录页面。我们称之为SimpleLoginPage。首先,我们需要一个包含登录表单的面板。这将是我们页面的内容区域。我们将其称为LoginPanel,标记如下:
<wicket:panel> <div style="margin: auto; width: 40%;"> <form id="loginForm" method="get" wicket:id="loginForm"> <fieldset> <legend style="color: #F90">Login</legend> <p wicket:id="loginStatus"></p> <span >Username: </span><input wicket:id="username" type="text" id="username" /><br/> <span >Password: </span><input wicket:id="password" type="password" id="password" /> <p> <input type="submit" name="Login" value="Login"/> </p> </fieldset> </form> </div> </wicket:panel>
The class for this panel just extends Panel class so we won’t see the relative code. The form of this panel is for illustrative purpose only. We will see how to work with Wicket forms in chapters 11 and 12. Since this is a login page we don’t want it to display the left menu area. That’s not a big deal as Component class exposes a method called setVisible which sets whether the component and its children should be displayed.
这个面板的类只是扩展了panel类,所以我们看不到相关的代码。此面板的形式仅用于说明目的。我们将在第11章和第12章中看到如何使用Wicket形式。由于这是一个登录页面,我们不希望它显示左侧菜单区域。这没什么大不了的,因为Component类公开了一个名为setVisible的方法,该方法设置是否应该显示组件及其子级。
The resulting Java code for the login page is the following:
登录页面的Java代码如下所示:
package helloWorld.layoutTenda; import helloWorld.LoginPanel; import org.apache.wicket.event.Broadcast; import org.apache.wicket.event.IEventSink; public class SimpleLoginPage extends JugTemplate { public SimpleLoginPage(){ super(); replace(new LoginPanel(CONTENT_ID)); getMenuPanel().setVisible(false); } }
Obviously this page doesn’t come with a related markup file. You can see the final page in the following picture:
显然,此页面没有附带相关的标记文件。您可以在下图中看到最后一页:
5.4. Markup inheritance with the wicket:extend tag
With Wicket we can apply markup inheritance using another approach based on the tag <wicket:child>. This tag is used inside the parent’s markup to define where the children pages/panels can “inject” their custom markup extending the markup inherited from the parent component. An example of a parent page using the tag <wicket:child> is the following:
有了Wicket,我们可以使用另一种基于标签<Wicket:child>的方法来应用标记继承。该标记在父组件的标记中用于定义子页面/面板可以在哪里“插入”其自定义标记,扩展从父组件继承的标记。使用标签<wicket:child>的父页面示例如下:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> </head> <body> This is parent body! <wicket:child/> </body> </html>
The markup of a child page/panel must be placed inside the tag <wicket:extend>. Only the markup inside <wicket:extend> will be included in final markup. Here is an example of child page markup:
子页面/面板的标记必须放置在标签<wicket:extend>内。只有<wicket:extend>中的标记才会包含在最终标记中。以下是子页面标记的示例:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> </head> <body> <wicket:extend> This is child body! </wicket:extend> </body> </html>
Considering the two pages seen above, the final markup generated for child page will be the following:
考虑到上面看到的两个页面,为子页面生成的最终标记如下:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> </head> <body> This is parent body! <wicket:child> <wicket:extend> This is child body! </wicket:extend> </wicket:child> </body> </html>
5.4.1. Our example revisited
Applying <wicket:child> tag to our layout example, we obtain the following markup for the main template page:
将<wicket:child>标记应用于我们的布局示例,我们获得了主模板页面的以下标记:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> </head> <body> <div id="header" wicket:id="headerPanel">header</div> <div id="body"> <div id="menu" wicket:id="menuPanel">menu</div> <wicket:child/> </div> <div id="footer" wicket:id="footerPanel">footer</div> </body> </html>
We have replaced the <div> tag of the content area with the tag <wicket:child>. Going forward with our example we can build a login page creating class SimpleLoginPage which extends the JugTemplate page, but with a related markup file like this:
我们已经将内容区域的<div>标记替换为<wicket:child>标记。继续我们的例子,我们可以构建一个登录页面创建类SimpleLoginPage,它扩展了JugTemplate页面,但使用了这样一个相关的标记文件:
<html> <head> </head> <body> <wicket:extend> <div style="margin: auto; width: 40%;"> <form id="loginForm" method="get"> <fieldset id="login" class="center"> <legend >Login</legend> <span >Username: </span><input type="text" id="username"/><br/> <span >Password: </span><input type="password" id="password" /> <p> <input type="submit" name="login" value="login"/> </p> </fieldset> </form> </div> </wicket:extend> </body> </html>
As we can see this approach doesn’t require to create custom panels to use as content area and it can be useful if we don’t have to handle a GUI with a high degree of complexity.
正如我们所看到的,这种方法不需要创建自定义面板来用作内容区域,如果我们不必处理高度复杂的GUI,它会很有用。
5.5. Summary
Wicket applies inheritance also to HTML markup making layout management much easier and less error-prone. Defining a master template page to use as base class for the other pages is a great way to build a consistent layout and use it across all the pages on the web site. During the chapter we have also introduced the Panel component, a very important Wicket class that is primarily designed to let us divide our pages in smaller and reusable UI components.
Wicket还将继承应用于HTML标记,使布局管理更加简单,不易出错。定义一个主模板页面用作其他页面的基类是构建一致布局并在网站上的所有页面上使用它的好方法。在本章中,我们还介绍了Panel组件,这是一个非常重要的Wicket类,主要用于将页面划分为更小且可重复使用的UI组件。
Chapter 6. Keeping control over HTML
Many Wicket newbies are initially scared by its approach to web development because they have the impression that the component-oriented nature of the framework prevents them from having direct control over the generated markup. This is due to the fact that many developers come from other server-side technologies like JSP where we physically implement the logic that controls how the final HTML is generated.
许多Wicket新手最初对其web开发方法感到害怕,因为他们认为框架的面向组件的特性使他们无法直接控制生成的标记。这是因为许多开发人员来自其他服务器端技术,如JSP,在这里我们物理地实现了控制最终HTML生成方式的逻辑。
This chapter will prevent you from having any initial misleading feeling about Wicket showing you how to control and manipulate the generated HTML with the built-in tools shipped with the framework.
本章将防止您对Wicket有任何最初的误导感,它将向您展示如何使用框架附带的内置工具控制和操作生成的HTML。
6.1. Hiding or disabling a component
At the end of the previous chapter we have seen how to hide a component calling its method setVisible. In a similar fashion, we can also decide to disable a component using method setEnabled. When a component is disabled all the links inside it will be in turn disabled (they will be rendered as <span>) and it can not fire JavaScript events.
在上一章的末尾,我们已经了解了如何隐藏调用其方法setVisible的组件。以类似的方式,我们也可以决定使用方法setEnabled禁用组件。当一个组件被禁用时,它内部的所有链接将依次被禁用(它们将被渲染为<span>),并且它不能激发JavaScript事件。
Class Component provides two getter methods to determinate if a component is visible or enabled: isVisible and isEnabled.
类组件提供了两个getter方法来确定组件是否可见或启用:isVisible和isEnabled。
Even if nothing prevents us from overriding these two methods to implement a custom logic to determinate the state of a component, we should keep in mind that methods isVisible and isEnabled are called multiple times before a component is fully rendered. Hence, if we place non-trivial code inside these two methods, we can sensibly deteriorate the responsiveness of our pages.
即使没有什么可以阻止我们重写这两个方法来实现自定义逻辑来确定组件的状态,我们也应该记住,在组件完全呈现之前,方法isVisible和isEnabled会被多次调用。因此,如果我们将非琐碎的代码放在这两个方法中,我们可能会明显降低页面的响应能力。
As we will see in the next chapter, class Component provides method onConfigure which is more suited to contain code that contributes to determinate component states because it is called just once during rendering phase.
正如我们将在下一章中看到的,类Component提供了方法onConfigure,该方法更适合包含有助于确定组件状态的代码,因为它在渲染阶段只调用一次。
6.2. Modifing tag attributes
To modify tag attributes we can use class org.apache.wicket.AttributeModifier. This class extends org.apache.wicket.behavior.Behavior and can be added to any component via the Component's add method. Class Behavior is used to expand component functionalities and it can also modify component markup. We will see this class in detail later in chapter 19.1.
要修改标记属性,我们可以使用org.apache.wicket类。AttributeModifier。这个类扩展了org.apache.wicketbehavior。行为,并且可以通过组件的添加方法添加到任何组件。类行为用于扩展组件功能,还可以修改组件标记。我们稍后将在第19.1章中详细介绍这一类。
As first example of attribute manipulation let’s consider a Label component bound to the following markup:
作为属性操作的第一个示例,让我们考虑绑定到以下标记的Label组件:
<span wicket:id="simpleLabel"></span>
Suppose we want to add some style to label content making it red and bolded. We can add to the label an AttributeModifier which creates the tag attribute style with value _ :[color:red;fontweight:bold]
假设我们想为标签内容添加一些样式,使其变为红色和粗体。我们可以在标签中添加AttributeModifier,它创建值为_的标记属性样式:[color:red;fontweight:bold]
label.add(new AttributeModifier("style", "color:red;font-weight:bold"));
If attribute style already exists in the original markup, it will be replaced with the value specified by AttributeModifier. If we don’t want to overwrite the existing value of an attribute we can use subclass AttributeAppender which will append its value to the existing one:
如果原始标记中已存在属性样式,则会将其替换为AttributeModifier指定的值。如果我们不想覆盖属性的现有值,我们可以使用子类AttributeAppender,它会将其值附加到现有值:
label.add(new AttributeAppender("style", "color:red;font-weight:bold"));
We can also create attribute modifiers using factory methods provided by class AttributeModifier and it’s also possible to prepend a given value to an existing attribute:
我们还可以使用AttributeModifier类提供的工厂方法创建属性修饰符,也可以在现有属性前面加上给定值:
//replaces existing value with the given one label.add(AttributeModifier.replace("style", "color:red;font-weight:bold")); //appends the given value to the existing one label.add(AttributeModifier.append("style", "color:red;font-weight:bold")); //prepends the given value to the existing one label.add(AttributeModifier.prepend("style", "color:red;font-weight:bold"));
6.3. Generating tag attribute 'id'
Tag attribute id plays a crucial role in web development as it allows JavaScript to identify a DOM element. That’s why class Component provides two dedicated methods to set this attribute. With method setOutputMarkupId(boolean output) we can decide if the id attribute will be rendered or not in the final markup (by default is not rendered). The value of this attribute will be automatically generated by Wicket and it will be unique for the entire page. If we need to specify this value by hand, we can use method setMarkupId(String id). The value of the id can be retrieved with method getMarkupId().
标记属性id在web开发中起着至关重要的作用,因为它允许JavaScript识别DOM元素。这就是为什么类Component提供了两个专门的方法来设置这个属性。使用方法setOutputMarkupId(布尔输出),我们可以决定是否在最终标记中渲染id属性(默认情况下不渲染)。该属性的值将由Wicket自动生成,并且对于整个页面而言是唯一的。如果我们需要手动指定这个值,我们可以使用方法setMarkupId(Stringid)。可以使用方法getMarkupId()检索id的值。
Wicket generates markup ids using an instance of interface org.apache.wicket.IMarkupIdGenerator. The default implementation is org.apache.wicket.DefaultMarkupIdGenerator and it uses a sessionscoped counter to generate the final id. A different generator can be set with the markup settings class org.apache.wicket.settings.MarkupSettings available in the application class:
Wicket使用接口org.apache.wicket.IMarkupIdGenerator的实例生成标记id。默认实现是org.apache.wicket.DefaultMarkupIdGenerator,它使用会话计数计数器生成最终id。可以使用标记设置类org.apache.wicket.settings.MarkupSettings设置不同的生成器:
@Override public void init() { super.init(); //wrap disabled links with <b> tag getMarkupSettings().setMarkupIdGenerator(myGenerator); }
6.4. Creating in-line panels with WebMarkupContainer
Create custom panels is a great way to handle complex user interfaces. However, sometimes we may need to create a panel which is used only by a specific page and only for a specific task.
创建自定义面板是处理复杂用户界面的好方法。然而,有时我们可能需要创建一个面板,该面板仅由特定页面使用,并且仅用于特定任务。
In situations like these component org.apache.wicket.markup.html.WebMarkupContainer is better suited than custom panels because it can be directly attached to a tag in the parent markup without needing a corresponding html file (hence it is less reusable). Let’s consider for example the main page of a mail service where users can see a list of received mails. Suppose that this page shows a notification box where user can see if new messages have arrived. This box must be hidden if there are no messages to display and it would be nice if we could handle it as if it was a Wicket component.
在类似这些情况下,组件org.apache.wicket.markup.html.WebMarkupContainer比自定义面板更适合,因为它可以直接附加到父标记中的标记,而不需要相应的html文件(因此它的可重用性较低)。例如,让我们考虑一下邮件服务的主页面,用户可以在其中看到收到的邮件列表。假设这个页面显示一个通知框,用户可以在其中查看是否有新消息到达。如果没有消息显示,这个框必须隐藏,如果我们能像处理Wicket组件一样处理它,那就太好了。
Suppose also that this information box is a <div> tag like this inside the page:
还假设这个信息框是页面内的一个<div>标记,如下所示:
<div wicket:id="informationBox"> //here's the body You've got <span wicket:id="messagesNumber"></span> new messages. </div>
Under those conditions we can consider using a WebMarkupContainer component rather than implementing a new panel. The code needed to handle the information box inside the page could be the following:
在这种情况下,我们可以考虑使用WebMarkupContainer组件,而不是实现一个新的面板。处理页面内的信息框所需的代码可能如下:
//Page initialization code WebMarkupContainer informationBox = new WebMarkupContainer ("informationBox"); informationBox.add(new Label("messagesNumber", messagesNumber)); add(informationBox); //If there are no new messages, hide informationBox informationBox.setVisible(false);
As you can see in the snippet above we can handle our information box from Java code as we do with any other Wicket component.
正如您在上面的片段中所看到的,我们可以像处理任何其他Wicket组件一样,从Java代码中处理信息框。
6.5. Working with markup fragments
Another circumstance in which we may prefer to avoid the creation of custom panels is when we want to conditionally display in a page small fragments of markup. In this case if we decided to use panels, we would end up having a huge number of small panel classes with their related markup file.
我们可能更喜欢避免创建自定义面板的另一种情况是,当我们希望在页面中有条件地显示标记的小片段时。在这种情况下,如果我们决定使用面板,我们最终会有大量的小面板类及其相关的标记文件。
To better cope with situations like this, Wicket defines component Fragment in package org.apache.wicket.markup.html.panel. Just like its parent component WebMarkupContainer, Fragment doesn’t have its own markup file but it uses a markup fragment defined in the markup file of its parent container, which can be a page or a panel. The fragment must be delimited with tag <wicket:fragment> and must be identified by a wicket:id attribute. In addition to the component id, Fragment's constructor takes as input also the id of the fragment and a reference to its container.
为了更好地处理这种情况,Wicket在包org.apache.Wicket.markup.html.panel中定义了组件Fragment。与其父组件WebMarkupContainer一样,Fragment没有自己的标记文件,但它使用在其父容器的标记文件中定义的标记片段,该标记文件可以是页面或面板。片段必须用标签<wicket:frage>分隔,并且必须由wicket:id属性标识。除了组件id之外,Fragment的构造函数还将片段的id和对其容器的引用作为输入。
Page markup:
<html> ... <body> ... <div wicket:id="contentArea"></div> <wicket:fragment wicket:id="fragmentId"> <!-- Fragment markup goes here --> </wicket:fragment> </body> </html>
Java code:
Fragment fragment = new Fragment ("contentArea", "fragmentId", this); add(fragment);
Fragments can be very helpful with complex pages or components. For example let’s say that we have a page where users can register to our forum. This page should first display a form where user must insert his/her personal data (name, username, password, email and so on), then, once the user has submitted the form, the page should display a message like “Your registration is complete! Please check your mail to activate your user profile.”.
片段对于复杂的页面或组件非常有用。例如,假设我们有一个页面,用户可以在其中注册到我们的论坛。此页面应首先显示一个表单,用户必须在其中插入他/她的个人数据(姓名、用户名、密码、电子邮件等),然后,一旦用户提交了表单,页面应显示一条消息,如“您的注册已完成!请检查您的邮件以激活您的用户配置文件”。
Instead of displaying this message with a new component or in a new page, we can define two fragments: one for the initial form and one to display the confirmation message. The second fragment will replace the first one after the form has been submitted:
我们可以定义两个片段,而不是用新组件或在新页面中显示此消息:一个用于初始表单,另一个用于显示确认消息。提交表格后,第二个片段将取代第一个片段:
Page markup:
<html> <body> <div wicket:id="contentArea"></div> <wicket:fragment wicket:id="formFrag"> <!-- Form markup goes here --> </wicket:fragment> <wicket:fragment wicket:id="messageFrag"> <!-- Message markup goes here --> </wicket:fragment> </body> </html>
Java code:
Fragment fragment = new Fragment ("contentArea", "formFrag", this); add(fragment); //form has been submitted Fragment fragment = new Fragment ("contentArea", "messageFrag", this); replace(fragment);
6.6. Adding header contents to the final page
Panel’s markup can also contain HTML tags which must go inside header section of the final page, like tags <script> or <style>. To tell Wicket to put these tags inside page <head>, we must surround them with the <wicket:head> tag.
面板的标记还可以包含HTML标记,这些标记必须位于最后一页的标题部分,如标记<script>或<style>。要告诉Wicket将这些标签放在页面<head>中,我们必须用<Wicket:head>标签包围它们。
Considering the markup of a generic panel, we can use <wicket:head> tag in this way
考虑到通用面板的标记,我们可以这样使用<wicket:head>标记。
<wicket:head> <script type="text/javascript"> function myPanelFunction(){ } </script> <style> .myPanelClass{ font-weight: bold; color: red; } </style> </wicket:head> <body> <wicket:panel> </wicket:panel> </body>
Wicket will take care of placing the content of <wicket:head> inside the <head> tag of the final page.
Wicket将负责将<Wicket:head>的内容放置在最终页面的<head>标签内。
Notice:The parent markup using tag <wicket:head> tag can also be used with children pages/panels which extend <wicket:extend>.
注意:使用标签<wicket:head>的父标记也可以与扩展<wicket:extend>的子页面/面板一起使用。
Notice:The content of the <wicket:head> tag is added to the header section once per component class. In other words, if we add multiple instances of the same panel to a page, the <head> tag will be populated just once with the content of <wicket:head>.
注意:<wicket:head>标记的内容在每个组件类的头部分添加一次。换句话说,如果我们将同一面板的多个实例添加到一个页面中,<head>标记将只填充一次<wicket:head>的内容。
The <wicket:head> tag is ideal if we want to define small in-line blocks of CSS or JavaScript. However Wicket provides also a more sophisticated technique to let components contribute to header section with in-line blocks and resource files like CSS or JavaScript files. We will see this technique later in chapter 16.
如果我们想定义CSS或JavaScript的小型内联块,那么<wicket:head>标记是理想的选择。然而,Wicket还提供了一种更复杂的技术,让组件通过内联块和资源文件(如CSS或JavaScript文件)为头段做出贡献。我们将在后面的第16章中看到这种技术。
6.7. Using stub markup in our pages/panels
Wicket’s <wicket:remove> tag can be very useful when our web designer needs to show us how a page or a panel should look like. The markup inside this tag will be stripped out in the final page, so it’s the ideal place for web designers to put their stub markup:
当我们的网页设计师需要向我们展示页面或面板的外观时,Wicket的<Wicket:remove>标签非常有用。该标记中的标记将在最终页面中被剥离,因此它是web设计师放置存根标记的理想位置:
<html> <head> </head> <body> <wicket:remove> <!-- Stub markup goes here --> </wicket:remove> </body> </html>
6.8. How to render component body only
When we bind a component to its corresponding tag we can choose to get rid of this outer tag in the final markup. If we call method setRenderBodyOnly(true) on a component Wicket will remove the surrounding tag.
当我们将组件绑定到其相应的标记时,我们可以选择在最终标记中去掉这个外部标记。如果我们在组件上调用方法setRenderBodyOnly(true),Wicket将删除周围的标记。
For example given the following markup and code:
例如,给定以下标记和代码:
HTML markup:
<html> <head> <title>Hello world page</title> </head> <body> <div wicket:id="helloWorld">[helloWorld]</div> </body> </html>
Java code:
Label label = new Label("helloWorld", “Hello World!”); label.setRenderBodyOnly(true); add(label);
the output will be:
<html> <head> <title>Hello world page</title> </head> <body> Hello World! </body> </html>
As you can see the <div> tag used for component Label is not present in the final markup
6.9. Hiding decorating elements with the wicket:enclosure tag
Our data are rarely displayed alone without a caption or other graphic elements that make clear the meaning of their value. For example:
我们的数据很少在没有标题或其他图形元素的情况下单独显示,以明确其价值的含义。例如
<label>Total amount: </label><span wicket:id="totalAmount"></span>
Wicket comes with a nice utility tag called <wicket:enclosure> that automatically hides those decorating elements if the related data value is not visible. All we have to do is to put the involved markup inside this tag. Applying <wicket:enclosure> to the previous example we get the following markup:
Wicket附带了一个名为<Wicket:cenclosure>的实用标签,如果相关的数据值不可见,它会自动隐藏这些装饰元素。我们所要做的就是将所涉及的标记放入这个标记中。将<wicket:cenclosure>应用于前面的示例,我们得到以下标记:
<wicket:enclosure> <label>Total amount: </label><span wicket:id="totalAmount"></span> </wicket:enclosure>
Now if component totalAmount is not visible, its description (Total amount:) will be automatically hidden. If we have more than a Wicket component inside <wicket:enclosure> we can use child attribute to specify which component will control the overall visibility:
现在,如果组件totalAmount不可见,其描述(Total amount:)将自动隐藏。如果<Wicket:cenclosure>中有多个Wicket组件,我们可以使用子属性来指定哪个组件将控制总体可见性:
<wicket:enclosure child="totalAmount"> <label>Total amount: </label><span wicket:id="totalAmount"></span><br/> <label>Expected delivery date: </label><span wicket:id="delivDate"></span> </wicket:enclosure>
child attribute supports also nested components with a colon-separated path:
子属性还支持具有冒号分隔路径的嵌套组件:
<wicket:enclosure child="totalAmountContainer:totalAmount"> <div wicket:id="totalAmountContainer"> <label>Total amount: </label><span wicket:id="totalAmount"></span> </div> <label>Expected delivery date: </label><span wicket:id="delivDate"></span> </wicket:enclosure>
6.10. Surrounding existing markup with Border
Component org.apache.wicket.markup.html.border.Border is a special purpose container created to enclose its tag body with its related markup. Just like panels and pages, borders also have their own markup file which is defined following the same rules seen for panels and pages. In this file <wicket:border> tag is used to indicate which part of the content is to be considered as border markup:
组件org.apache.wicket.markup.html.border.Border是一个特殊用途的容器,用于将其标记体及其相关标记括起来。就像面板和页面一样,边框也有自己的标记文件,其定义遵循与面板和页面相同的规则。在该文件中,<wicket:border>标记用于指示内容的哪一部分将被视为边界标记:
<?xml version="1.0" encoding="UTF-8"?> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org"> <head></head> <body> <!-- everything above <wicket:border> tag will be discarded...--> <wicket:border> <div> foo<br /> <wicket:body/><br /> buz <br /> </div> </wicket:border> <!-- everything below </wicket:border> tag will be discarded...--> </body> </html>
The <wicket:body/> tag used in the example above is used to indicate where the body of the tag will be placed inside border markup. Now if we attached this border to the following tag
上面示例中使用的<wicket:body/>标记用于指示标记的主体将放置在边界标记内的位置。现在,如果我们将此边界附加到以下标签
<span wicket:id="myBorder"> bar </span>
we would obtain the following resulting HTML:
<span wicket:id="myBorder"> <div> foo<br /> bar<br /> buz <br /> </div> </span>
Border can also contain children components which can be placed either inside its markup file or inside its corresponding HTML tag. In the first case children must be added to the border component with method addToBorder(Component…), while in the second case we must use the add(Component…) method.
Border还可以包含子组件,这些子组件可以放置在其标记文件中,也可以放置在相应的HTML标记中。在第一种情况下,必须使用addToBorder(component…)方法将子项添加到border组件中,而在第二种情况下我们必须使用add(component.)方法。
The following example illustrates both use cases:
以下示例说明了这两个用例:
Border class:
public class MyBorder extends Border { public MyBorder(String id) { super(id); } }
Border Markup:
<?xml version="1.0" encoding="UTF-8"?> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org"> <head></head> <body> <wicket:border> <div> <div wicket:id="childMarkup"></div> <wicket:body/><br /> </div> </wicket:border> </body> </html>
Border tag:
<div wicket:id="myBorder"> <span wicket:id="childTag"></span> </div>
Initialization code for border:
MyBorder myBorder = new MyBorder("myBorder"); myBorder.addToBorder(new Label("childMarkup", "Child inside markup.")); myBorder.add(new Label("childTag", "Child inside tag.")); add(myBorder);
6.11. Summary
In this chapter we have seen the tools provided by Wicket to gain complete control over the generated HTML. However we didn’t see yet how we can repeat a portion of HTML with Wicket. With classic server-side technologies like PHP or JSP we use loops (like while or for) inside our pages to achieve this result. To perform this task Wicket provides a special-purpose family of components called repeaters and designed to repeat their markup body to display a set of items.
在本章中,我们看到了Wicket提供的工具,这些工具可以完全控制生成的HTML。然而,我们还没有看到如何使用Wicket重复HTML的一部分。对于PHP或JSP等经典的服务器端技术,我们在页面内部使用循环(如while或for)来实现这一结果。为了执行这项任务,Wicket提供了一个称为中继器的特殊用途组件系列,旨在重复其标记体以显示一组项目。
But to fully understand how these components work, we must first learn more of Wicket’s basics. That’s why repeaters will be introduced later in chapter 13.
但要全面了解这些组件是如何工作的,我们必须首先了解更多Wicket的基础知识。这就是为什么中继器将在第13章后面介绍。
Chapter 7. Components lifecycle
Just like applets and servlets, also Wicket components follow a lifecycle during their existence. In this chapter we will analyze each stage of this cycle and we will learn how to make the most of the hook methods that are triggered when a component moves from one stage to another.
就像小程序和servlet一样,Wicket组件在其存在过程中也遵循生命周期。在本章中,我们将分析这个周期的每个阶段,并学习如何充分利用组件从一个阶段移动到另一个阶段时触发的挂钩方法。
7.1. Lifecycle stages of a component
During its life a Wicket component goes through three basic stages:
Wicket组件在其使用寿命中经历三个基本阶段:
1. Initialization: a component is instantiated by Wicket and prepared for the rendering phase.
2. Rendering: in this stage Wicket generates component markup. If a component contains children (i.e. is a subclass of MarkupContainer) it must first wait for them to be rendered before starting its own rendering.
3. Removing: this stage is triggered when a component is explicitly removed from its component hierarchy, i.e. when its parent invokes remove(component) on it. This stage is facultative and is never triggered for pages
1.初始化:组件由Wicket实例化,并为渲染阶段做好准备。
2.渲染:在此阶段,Wicket生成组件标记。如果组件包含子级(即MarkupContainer的子类),则必须先等待它们被渲染,然后才能开始自己的渲染。
3.移除:当组件从其组件层次结构中显式移除时,即当其父级调用移除(组件)时,会触发此阶段。此阶段是临时性的,从不为页面触发
The following picture shows the state diagram of component lifecycle:
Once a component has been removed it can be added again to a container, but the initialization stage won’t be executed again.
一旦组件被移除,它就可以再次添加到容器中,但初始化阶段不会再次执行。
Notice:If you read the JavaDoc of class Component you will find a more detailed description of component lifecycle. However this description introduces some advanced topics we didn’t covered yet hence, to avoid confusion, in this chapter some details have been omitted and they will be covered later in the next chapters.
注意:如果您阅读组件类的JavaDoc,您会发现组件生命周期的更详细描述。然而,本描述介绍了一些我们尚未涵盖的高级主题,因此,为了避免混淆,本章省略了一些细节,稍后将在下一章中介绍。
For now you can consider just the simplified version of the lifecycle described above.
现在,您可以只考虑上面描述的生命周期的简化版本。
7.2. Hook methods for component lifecycle
Class Component comes with a number of hook methods that can be overridden in order to customize component behavior during its lifecycle. In the following table these methods are grouped according to the stage in which they are invoked (and they are sorted by execution order):
类组件附带了许多钩子方法,这些方法可以被重写,以便在组件的生命周期中自定义组件行为。在下表中,这些方法根据调用它们的阶段进行分组(并按执行顺序排序):
Cycle stage | Involved methods |
Initialization | onInitialize |
Rendering | onConfigure, onBeforeRender, onRender, onComponentTag, onComponentTagBody, onAfterRenderChildren, onAfterRender |
Removing | onRemove |
Now let’s take a closer look at each stage and to at hook methods
现在让我们更仔细地看一下每个阶段和at hook方法
7.3. Initialization stage
This stage is performed at the beginning of the component lifecycle. During initialization, the component has already been inserted into its component hierarchy so we can safely access to its parent container or to its page with methods getParent() or getPage(). The only method triggered during this stage is onInitialize(). This method is a sort of “special” constructor where we can execute a custom initialization of our component.
此阶段在组件生命周期开始时执行。在初始化过程中,组件已经插入到其组件层次结构中,因此我们可以使用方法getParent()或getPage()安全地访问其父容器或页面。在此阶段触发的唯一方法是onInitialize()。这个方法是一种“特殊”构造函数,我们可以在其中执行组件的自定义初始化。
Since onInitialize is similar to a regular constructor, when we override this method we have to call super.onInitialize inside its body, usually as first instruction.
由于onInitialize类似于常规构造函数,当我们重写此方法时,我们必须在其内部调用super.onInitialize,通常作为第一条指令。
7.4. Rendering stage
This stage is triggered each time a component is rendered by Wicket, typically when its page is requested or when it is refreshed via AJAX.
每当Wicket呈现组件时,通常是在请求页面或通过AJAX刷新页面时,都会触发此阶段。
7.4.1. Method onConfigure
Method onConfigure()has been introduced in order to provide a good point to manage the component states such as its visibility or enabled state. This method is called before the render phase starts. As stated in chapter 6.1, _isVisibleandisEnabledare called multiple times when a page or a component is rendered, so it’s highly recommended not to directly override these method, but rather to useonConfigureto change component states. On the contrary method onBeforeRender(see the next paragraph) is not indicated for this task because it will not be invoked if component visibility is set to false.
引入方法onConfigure()是为了提供一个很好的点来管理组件状态,例如其可见性或启用状态。此方法在渲染阶段开始之前调用。如第6.1章所述,在呈现页面或组件时会多次调用_isVisibleandsEnabled,因此它;强烈建议不要直接重写这些方法,而是使用onConfigure更改组件状态。相反,onBeforeRender方法(请参见下一段)不指示此任务,因为如果组件可见性设置为false,则不会调用该方法。
7.4.2. Method onBeforeRender
The most important hook method of this stage is probably onBeforeRender(). This method is called before a component starts its rendering phase and it is our last chance to change its children hierarchy.
这个阶段最重要的钩子方法可能是在BeforeRender()上。在组件开始渲染阶段之前调用此方法,这是我们更改其子层次结构的最后机会。
If we want add/remove children components this is the right place to do it. In the next example (project LifeCycleStages) we will create a page which alternately displays two different labels, swapping between them each time it is rendered:
如果我们想添加/删除子组件,这是正确的做法。在下一个例子(项目LifeCycleStages)中,我们将创建一个页面,交替显示两个不同的标签,每次渲染时在它们之间交换:
public class HomePage extends WebPage { private Label firstLabel; private Label secondLabel; public HomePage(){ firstLabel = new Label("label", "First label"); secondLabel = new Label("label", "Second label"); add(firstLabel); add(new Link("reload"){ @Override public void onClick() { } }); } @Override protected void onBeforeRender() { if(contains(firstLabel, true)) replace(secondLabel); else replace(firstLabel); super.onBeforeRender(); } }
The code inside onBeforeRender() is quite trivial as it just checks which label among firstLabel and secondLabel is currently inserted into the component hierarchy and it replaces the inserted label with the other one.
onBeforeRender()内部的代码非常简单,因为它只检查当前插入组件层次结构的第一个标签和第二个标签中的哪个标签,并用另一个标签替换插入的标签·
This method is also responsible for invoking children onBeforeRender() so if we decide to override it we have to call super.onBeforeRender(). However, unlike onInitialize(), the call to superclass method should be placed at the end of method’s body in order to affect children’s rendering with our custom code.
这个方法还负责调用子对象onBeforeRender(),所以如果我们决定覆盖它,我们必须调用super.onBeforeRende()。但是,与onInitialize()不同的是,对超类方法的调用应该放在方法体的末尾,以便通过我们的自定义代码影响子级的呈现。
Please note that in the example above we can trigger the rendering stage pressing F5 key or clicking on link “reload”.
请注意,在上面的例子中,我们可以按下F5键或点击链接“重新加载”来触发渲染阶段。
Notice:If we forget to call superclass version of methods onInitialize() or onBeforeRender(),Wicket will throw an IllegalStateException with the following message:
注意:如果我们忘记调用方法onInitialize()或onBeforeRender()的超类版本,Wicket将抛出一个IllegalStateException,并显示以下消息:
java.lang.IllegalStateException: org.apache.wicket.Component has not been properly initialized. Something in the hierarchy of <page class name> has not called super.onInitialize()/onBeforeRender() in the override of onInitialize()/ onBeforeRender() method
java.lang.IllegalStateException:org.apache.wicket.Component未正确初始化。在onInitialize()/onBeforeRender()方法的重写中,<page class name>层次结构中的某些东西尚未调用super.onitialize()/onBeforeRender()
7.4.3. Method onComponentTag
Method onComponentTag(ComponentTag) is called to process component tag, which can be freely manipulated through its argument of type org.apache.wicket.markup.ComponentTag. For example we can add/remove tag attributes with methods put(String key, String value) and remove(String key), or we can even decide to change the tag or rename it with method setName(String) (the following code is taken from project OnComponentTagExample):
调用方法onComponentTag(ComponentTag)来处理组件标记,该标记可以通过其org.apache.wicket.markup.ComponentTag类型的参数自由操作。例如,我们可以使用put(String key,String value)和remove(String key)方法添加/删除标记属性,或者我们甚至可以决定更改标记或使用setName(String)方法重命名它(以下代码取自项目OnComponentTagExample):
Markup code:
<head> <meta charset="utf-8" /> <title></title> </head> <body> <h1 wicket:id="helloMessage"></h1> </body>
Java code:
public class HomePage extends WebPage { public HomePage() { add(new Label("helloMessage", "Hello World"){ @Override protected void onComponentTag(ComponentTag tag) { super.onComponentTag(tag); //Turn the h1 tag to a span tag.setName("span"); //Add formatting style tag.put("style", "font-weight:bold"); } }); } }
Generated markup:
<head> <meta charset="utf-8" /> <title></title> </head> <body> <span wicket:id="helloMessage" style="font-weight:bold">Hello World</span> </body>
Just like we do with onInitialize, if we decide to override onComponentTag we must remember to call the same method of the super class because also this class may also customize the tag. Overriding onComponentTag is perfectly fine if we have to customize the tag of a specific component, but if we wanted to reuse the code across different components we should consider to use a behavior in place of this hook method.
就像我们对onInitialize所做的那样,如果我们决定覆盖onComponentTag,我们必须记住调用超级类的相同方法,因为这个类也可以自定义标记。如果我们必须自定义特定组件的标记,那么覆盖onComponentTag是完全可以的,但如果我们想在不同的组件之间重用代码,我们应该考虑使用行为来代替这个钩子方法。
We have already seen in <a anchor="<em>modifing_tag_attributes">chapter 6.2</a> how to use behavior _AttributeModifier</em> to manipulate the tag’s attribute. In <a anchor="<em>enriching_components_with_behaviors">chapter 19.1</a> we will see that base class _Behavior</em> offers also a callback method named <em>onComponentTag(ComponentTag, Component)</em> that can be used in place of the hook method <em>onComponentTag(ComponentTag)</em>.
我们已经在第6.2章中看到了如何使用behavior _AttributeModifier来操作标签属性。在第19.1章中,我们将看到基类_Behavior还提供了一个名为nComponentTag(ComponentTag,Component)的回调方法,该方法可用于代替onComponentTag(ComponentTag)上的钩子方法。
7.4.4. Methods onComponentTagBody
Method onComponentTagBody(MarkupStream, ComponentTag) is called to process the component tag’s body. Just like onComponentTag it takes as input a ComponentTag parameter representing the component tag. In addition, we also find a MarkupStream parameter which represents the page markup stream that will be sent back to the client as response.
调用方法onComponentTagBody(MarkupStream,ComponentTag)来处理组件标记的主体。就像onComponentTag一样,它将表示组件标记的ComponentTag参数作为输入。此外,我们还找到了一个MarkupStream参数,该参数表示将作为响应发送回客户端的页面标记流。
onComponentTagBody can be used in combination with the Component's method replaceComponentTagBody to render a custom body under specific conditions. For example (taken from project OnComponentTagExample) we can display a brief description instead of the body if the label component is disabled:
onComponentTagBody可以与Component的方法replaceComponentTagBody组合使用,以在特定条件下呈现自定义实体。例如(取自项目OnComponentTagExample),如果标签组件被禁用,我们可以显示简短的描述而不是正文:
public class HomePage extends WebPage { public HomePage() { add(new Label("helloMessage", "Hello World"){ @Override protected void onComponentTagBody(MarkupStream markupStream, ComponentTag tag) { if(!isEnabled()) replaceComponentTagBody(markupStream, tag, "(the component is disabled)"); else super.onComponentTagBody(markupStream, tag); } }); } }
Note that the original version of onComponentTagBody is invoked only when we want to preserve the standard rendering mechanism for the tag’s body (in our example this happens when the component is enabled).
请注意,只有当我们希望保留标记主体的标准呈现机制时,才会调用onComponentTagBody的原始版本(在我们的示例中,当启用组件时会发生这种情况)。
7.5. Removing stage
This stage is triggered when a component is removed from its container hierarchy. The only hook method for this phase is onRemove(). If our component still holds some resources needed during rendering phase, we can override this method to release them.
当组件从其容器层次结构中删除时,会触发此阶段。此阶段唯一的挂钩方法是onRemove()。如果我们的组件在渲染阶段仍然拥有一些所需的资源,我们可以覆盖此方法来释放它们。
Once a component has been removed we are free to add it again to the same container or to a different one. Starting from version 6.18.0 Wicket added a further hook method called onReAdd() which is triggered every time a previously removed component is re-added to a cointainer. Please note that while onInitialize is called only the very first time a component is added, onReAdd is called every time it is re-added after having been removed.
一旦组件被移除,我们就可以自由地将其再次添加到同一个容器或不同的容器中。从6.18.0版本开始,Wicket添加了一个名为onReAdd()的钩子方法,每次将先前删除的组件重新添加到CoinContainer时都会触发该方法。请注意,虽然只有在第一次添加组件时才会调用onInitialize,但每次删除组件后重新添加时都会调用onReAdd。
7.6. Summary
In this chapter we have seen which stages compose the lifecycle of Wicket components and which hook methods they provide. Overriding these methods we can dynamically modify the component hierarchy and we can enrich the behavior of our custom components.
在本章中,我们看到了哪些阶段构成了Wicket组件的生命周期,以及它们提供了哪些钩子方法。通过覆盖这些方法,我们可以动态修改组件层次结构,并丰富自定义组件的行为。
Chapter 8. Page versioning and caching
This chapter explains how Wicket manages page instances, underlining the difference between stateful and stateless pages. The chapter also introduces some advanced topics like Java Serialization and multi-level cache. However, to understand what you will read you are not required to be familiar with these concepts.
本章解释了Wicket如何管理页面实例,强调了有状态页面和无状态页面之间的区别。本章还介绍了一些高级主题,如Java序列化和多级缓存。然而,为了理解你将要阅读的内容,你不需要熟悉这些概念。
8.1. Stateful pages vs stateless
Wicket pages can be divided into two categories: stateful and stateless pages. Stateful pages are those which rely on user session to store their internal state and to keep track of user interaction. On the contrary stateless pages are those which don’t change their internal state during their lifecycle and they don’t need to occupy space into user session.
Wicket页面可以分为两类:有状态页面和无状态页面。有状态页面是那些依靠用户会话来存储其内部状态并跟踪用户交互的页面。相反,无状态页面是那些在其生命周期中不改变其内部状态的页面,它们不需要占用用户会话的空间。
From Wicket’s point of view the biggest difference between these two types of page is that stateful pages are versioned, meaning that they will be saved into user session every time their internal state has changed. Wicket automatically assigns a session to the user the first time a stateful page is requested. Page versions are stored into user session using Java Serialization mechanism. Stateless pages are never versioned and that’s why they don’t require a valid user session. If we want to know whether a page is stateless or not, we can call the isPageStateless() method of class Page.
从Wicket的角度来看,这两种类型的页面之间最大的区别是有状态页面是有版本的,这意味着每当它们的内部状态发生变化时,它们都会被保存到用户会话中。Wicket在第一次请求有状态页面时自动将会话分配给用户。页面版本使用Java序列化机制存储到用户会话中。无状态页面从不进行版本控制,这就是为什么它们不需要有效的用户会话。如果我们想知道一个页面是否是无状态的,我们可以调用page类的isPageStateless()方法。
8.2. Stateful pages
Stateful pages are versioned in order to support browser’s back button: when this button is pressed Wicket must respond by rendering the same page instance previously used.
有状态页面的版本控制是为了支持浏览器的后退按钮:当按下该按钮时,Wicket必须通过呈现以前使用的相同页面实例来响应。
A new page version is created when a stateful page is requested for the first time or when an existing instance is modified (for example changing its component hierarchy). To identify each page version Wicket uses a session-relative identifier called page id. This is a unique number and it is increased every time a new page version is created.
当第一次请求有状态页面或修改现有实例(例如更改其组件层次结构)时,将创建新的页面版本。为了识别每个页面版本,Wicket使用一个称为页面id的会话相对标识符。这是一个唯一的数字,每次创建新的页面版本时都会增加。
In the final example of the previous chapter (project LifeCycleStages), you may have noticed the number appended at the end of URL. This number is the page id we are talking about:
在上一章的最后一个例子(生命周期阶段项目)中,您可能已经注意到URL末尾附加的数字。这个数字就是我们所说的页面id:
In this chapter we will use a revised version of this example project where the component hierarchy is modified inside the Link’s onClick()method. This is necessary because Wicket creates a new page version only if the page is modified before its method onBeforeRender() is invoked. The code of the new home page is the following:
在本章中,我们将使用此示例项目的修订版本,其中组件层次结构在Link的onClick()方法中进行修改。这是必要的,因为只有在调用其方法onBeforeRender()之前修改页面时,Wicket才会创建新的页面版本。新主页的代码如下:
public class HomePage extends WebPage { private static final long serialVersionUID = 1L; private Label firstLabel; private Label secondLabel; public HomePage(){ firstLabel = new Label("label", "First label"); secondLabel = new Label("label", "Second label"); add(firstLabel); add(new Link("reload"){ @Override public void onClick() { if(getPage().contains(firstLabel, true)) getPage().replace(secondLabel); else getPage().replace(firstLabel); } }); } }
Now if we run the new example (project LifeCycleStagesRevisited) and we click on the “Reload” button, a new page version is created and the page id is increased by one:
现在,如果我们运行新的示例(项目LifeCycleStagesRevisited),并单击“重新加载”按钮,就会创建一个新的页面版本,页面id将增加一:
If we press the back button the page version previously rendered (and serialized) will be retrieved (i.e. deserialized) and it will be used again to respond to our request (and page id is decremented):
如果我们按下后退按钮,先前呈现(和序列化)的页面版本将被检索(即反序列化),并且它将再次用于响应我们的请求(并且页面id递减):
Notice:For more details about page storing you can take a look at paragraph [Page storing] from chapter [Wicket Internals] The content of this paragraph is from wiki page https://cwiki.apache.org/confluence/display/WICKET/Page+Storage.
注意:有关页面存储的更多详细信息,您可以查看第[Wicket Internals]章中的段落[页面存储]。该段落的内容来自wiki页面https://cwiki.apache.org/confluence/display/WICKET/Page存储
As we have stated at the beginning of this chapter, page versions are stored using Java serialization, therefore every object referenced inside a page must be serializable. In paragraph 11.6 we will see how to overcome this limit and work with non-serializable objects in our components using detachable Wicket models.
正如我们在本章开头所述,页面版本是使用Java序列化存储的,因此页面内引用的每个对象都必须是可序列化的。在第11.6段中,我们将了解如何克服这一限制,并使用可拆卸的Wicket模型处理组件中的不可序列化对象。
8.2.1. Using a specific page version with PageReference
To retrieve a specific page version in our code we can use class org.apache.wicket.PageReference by providing its constructor with the corresponding page id:
要在代码中检索特定的页面版本,我们可以使用org.apache.wicket.PageReference,方法是为其构造函数提供相应的页面id:
//load page version with page id = 3 PageReference pageReference = new PageReference(3); //load the related page instance Page page = pageReference.getPage();
To get the related page instance we must use the method getPage.
要获取相关的页面实例,我们必须使用getPage方法。
8.2.2. Turning off page versioning
If for any reason we need to switch off versioning for a given page, we can call its method setVersioned(false).
如果出于任何原因,我们需要关闭给定页面的版本控制,我们可以调用其方法setVersioned(false)。
8.2.3. Pluggable serialization
Starting from version 1.5 it is possible to choose which implementation of Java serialization will be used by Wicket to store page versions. Wicket serializes pages using an implementation of interface org.apache.wicket.serialize.ISerializer. The default implementation is org.apache.wicket.serialize.java.JavaSerializer and it uses the standard Java serialization mechanism based on classes ObjectOutputStream and ObjectInputStream. However on Internet we can find other interesting serialization libraries like Kryo
从1.5版本开始,可以选择Wicket将使用哪种Java序列化实现来存储页面版本。Wicket使用接口org.apache.Wicket.serialize的实现来序列化页面。I串行化器。默认实现是org.apache.wicket.serialize.java。JavaSerializer,它使用基于类ObjectOutputStream和ObjectInputStream的标准Java序列化机制。然而,在互联网上,我们可以找到其他有趣的序列化库,如Kryo
We can access this class inside the method init of the class Application using the getFrameworkSettings() method :
我们可以使用getFrameworkSettings()方法在类Application的方法init中访问这个类:
@Override public void init() { super.init(); getFrameworkSettings().setSerializer(yourSerializer); }
A serializer based on Kryo library and another one based on Fast are provided by the WicketStuff project. You can find more information on this project, as well as the instructions to use its modules, in Appendix B.
8.2.4. Page caching
By default Wicket persists versions of pages into a session-relative file on disk, but it uses a twolevels cache to speed up this process. The first level of the cache uses a http session attribute called “wicket:persistentPageManagerData-<APPLICATION_NAME>” to store pages. The second level cache stores pages into application-scoped variables which are identified by a session id and a page id.
默认情况下,Wicket将页面的版本持久化到磁盘上与会话相关的文件中,但它使用两级缓存来加快这一过程。缓存的第一级使用名为“wicket:persistentPageManagerData-<APPLICATION_NAME>”的http会话属性来存储页面。第二级缓存将页面存储到应用程序范围的变量中,这些变量由会话id和页面id标识。
The following picture is an overview of these two caching levels:
The session-scoped cache is faster then the other memory levels but it contains only the pages used to serve the last request. Wicket allows us to set the maximum amount of memory allowed for the application-scoped cache and for the page store file. Both parameters can be configured via setting class org.apache.wicket.settings.StoreSettings.
会话范围的缓存比其他内存级别更快,但它只包含用于服务最后一个请求的页面。Wicket允许我们设置应用程序范围的缓存和页面存储文件所允许的最大内存量。这两个参数都可以通过设置类org.apache.wicket.settings进行配置。StoreSettings。
This interface provides the setMaxSizePerSession(Bytes bytes) method to set the size for page store file. The Bytes parameter is the maximum size allowed for this file:
此接口提供了setMaxSizePerSession(字节)方法来设置页面存储文件的大小。Bytes参数是此文件允许的最大大小:
@Override public void init() { super.init(); getStoreSettings().setMaxSizePerSession(Bytes.kilobytes(500)); }
Class org.apache.wicket.util.lang.Bytes is an utility class provided by Wicket to express size in bytes (for further details refer to the JavaDoc). For the second level cache we can use the setInmemoryCacheSize(int inmemoryCacheSize) method. The integer parameter is the maximum number of page instances that will be saved into application-scoped cache:
类org.apache.wicket.util.lang.Bytes是wicket提供的一个实用类,用于以字节表示大小(有关更多详细信息,请参阅JavaDoc)。对于二级缓存,我们可以使用setInmemoryCacheSize(int-inmemoryCacheSize)方法。integer参数是将保存到应用程序范围的缓存中的最大页面实例数:
@Override public void init() { super.init(); getStoreSettings().setInmemoryCacheSize(50); }
8.2.5. Page expiration
Page instances are not kept in the user session forever. They can be discarded when the limit set with the setMaxSizePerSession method is reached or (more often) when user session expires. When we ask Wicket for a page id corresponding to a page instance removed from the session, we bump into a PageExpiredException and we get the following default error page:
页面实例不会永远保存在用户会话中。当达到使用setMaxSizePerSession方法设置的限制时,或者(更常见的情况下)当用户会话到期时,可以丢弃它们。当我们向Wicket请求与从会话中删除的页面实例相对应的页面id时,我们会遇到PageExpiredException,并得到以下默认错误页面:
This error page can be customized with the setPageExpiredErrorPage method of class org.apache.wicket.settings.ApplicationSettings:
可以使用org.apache.wicket.settings类的setPageExpiredErrorPage方法自定义此错误页面。应用程序设置:
@Override public void init() { super.init(); getApplicationSettings().setPageExpiredErrorPage( CustomExpiredErrorPage.class); }
The page class provided as custom error page must have a public constructor with no argument or a constructor that takes as input a single PageParameters argument (the page must be bookmarkable as described in paragraph 10.1.1).
作为自定义错误页面提供的页面类必须有一个没有参数的公共构造函数,或者一个将单个PageParameters参数作为输入的构造函数(页面必须可作为书签,如第10.1.1段所述)
8.3. Stateless pages
Wicket makes it very easy to build stateful pages, but sometimes we might want to use an “old school” stateless page that doesn’t keep memory of its state in the user session. For example consider the public area of a site or a login page: in those cases a stateful page would be a waste of resources or even a security threat, as we will see in paragraph [paragraph 12.10|guide:forms2_10].
Wicket使构建有状态页面变得非常容易,但有时我们可能希望使用一个“老派”无状态页面,该页面不在用户会话中保留其状态的内存。例如,考虑一个网站或登录页面的公共区域:在这些情况下,一个有状态的页面会浪费资源,甚至是安全威胁,正如我们将在第[第12.10段|指南:forms2_10]段中看到的那样。
In Wicket a page can be stateless only if it satisfies the following requirements:
在Wicket中,只有满足以下要求,页面才能是无状态的:
1. it has been instantiated by Wicket (i.e. we don’t create it with operator new) using a constructor with no argument or a constructor that takes as input a single PageParameters argument (class
PageParameters will be covered in chapter 10.1).
1.它是由Wicket实例化的(即,我们不使用运算符new创建它),使用没有参数的构造函数或以单个PageParameters参数作为输入的构造函数(类PageParameters将在第10.1章中介绍。
2. All its children components (and behaviors) are in turn stateless, which means that their method isStateless must return true.
2.它的所有子组件(和行为)反过来都是无状态的,这意味着它们的方法isStateless必须返回true。
The first requirement implies that, rather than creating a page by hand, we should rely on Wicket’s capability of resolving page instances, like we do when we use method setResponsePage(Class page).
第一个要求意味着,我们不应该手动创建页面,而应该依赖Wicket解析页面实例的能力,就像我们使用方法setResponsePage(类页面)时所做的那样。
In order to comply with the second requirement it could be helpful to check if all children components of a page are stateless. To do this we can leverage method visitChildren and the visitor pattern to iterate over components and test if their method isStateless actually returns true:
为了符合第二个要求,检查页面的所有子组件是否都是无状态的可能会很有帮助。要做到这一点,我们可以利用方法visitChildren和访问者模式来迭代组件,并测试他们的方法isStateless是否真的返回true:
@Override protected void onInitialize() { super.onInitialize(); visitChildren(new IVisitor<Component, Void>() { @Override public void component(Component component, IVisit<Void> arg1) { if(!component.isStateless()) System.out.println("Component " + component.getId() + " is not stateless"); } }); }
Alternatively, we could use the StatelessComponent utility annotation along with the StatelessChecker class (they are both in package org.apache.wicket.devutils.stateless). StatelessChecker will throw an IllegalArgumentException if a component annotated with StatelessComponent doesn’t respect the requirements for being stateless. To use StatelessComponent annotation we must first add the StatelessChecker to our application as a component render listener:
或者,我们可以将StatelessComponent实用程序注释与StatelessChecker类一起使用(它们都在org.apache.wicket.devutils.stateless包中)。如果用StatelessComponent注释的组件不遵守无状态的要求,则StatelessChecker将抛出IllegalArgumentException。要使用StatelessComponent注释,我们必须首先将StatelessChecker作为组件呈现侦听器添加到我们的应用程序中:
@Override public void init() { super.init(); getComponentPostOnBeforeRenderListeners().add(new StatelessChecker()); }
Notice:Most of the Wicket’s built-in components are stateful, hence they can not be used with a stateless page. However some of them have also a stateless version which can be adopted when we need to keep a page stateless. In the rest of the guide we will point out when a built-in component comes also with a stateless version.
注意:Wicket的大多数内置组件都是有状态的,因此它们不能用于无状态页面。然而,其中一些还具有无状态版本,当我们需要保持页面无状态时,可以采用该版本。在本指南的其余部分中,我们将指出内置组件何时也带有无状态版本。
A page can be also explicitly declared as stateless setting the appropriate flag to true with the setStatelessHint(true) method. This method will not prevent us from violating the requirements for a stateless page, but if we do so we will get the following warning log message:
页面也可以显式声明为无状态,使用setStatelessHint(true)方法将相应的标志设置为true。这种方法不会阻止我们违反无状态页面的要求,但如果我们这样做,我们将收到以下警告日志消息:
Notice:Page '<page class>' is not stateless because of component with path '<component path>'
注意:页面“<Page class>”不是无状态的,因为组件的路径为“<component path>”
8.4. Summary
In this chapter we have seen how page instances are managed by Wicket. We have learnt that pages can be divided into two families: stateless and stateful pages. Knowing the difference between the two types of pages is important to build the right page for a given task.
在本章中,我们已经了解了Wicket是如何管理页面实例的。我们了解到页面可以分为两类:无状态页面和有状态页面。了解这两种类型的页面之间的差异对于为给定任务构建正确的页面非常重要。
However, to complete the discussion about stateless pages we still have to deal with two topics we have just outlined in this chapter: class PageParameters and bookmarkable pages. The first part of chapter 10 will cover these missing topics.
然而,为了完成关于无状态页面的讨论,我们仍然需要处理本章中刚刚概述的两个主题:类PageParameters和可书签页面。第10章的第一部分将介绍这些缺失的主题。
Chapter 9. Under the hood of the request processing
Although Wicket was born to provide a reliable and comprehensive object oriented abstraction for web development, sometimes we might need to work directly with “raw” web entities such as user session, web request, query parameters, and so on. For example this is necessary if we want to store an arbitrary parameter in the user session.
尽管Wicket的诞生是为了为web开发提供可靠和全面的面向对象抽象,但有时我们可能需要直接处理“原始”web实体,如用户会话、web请求、查询参数等。例如,如果我们想在用户会话中存储任意参数,这是必要的。
Wicket provides wrapper classes that allow us to easily access to web entities without the burden of using the low-level APIs of Java Servlet Specification. However it will always be possible to access standard classes (like HttpSession, HttpServletRequest, etc…) that lay under our Wicket application. This chapter will introduce these wrapper classes and it will explain how Wicket uses them to handle the web requests initiated by the user’s browser.
Wicket提供了包装类,使我们能够轻松访问web实体,而无需使用Java Servlet规范的低级API。然而,始终可以访问Wicket应用程序下的标准类(如HttpSession、HttpServletRequest等)。本章将介绍这些包装器类,并解释Wicket如何使用它们来处理用户浏览器发起的web请求。
9.1. Class Application and request processing
Beside configuring and initializing our application, the Application class is responsible for creating the internal entities used by Wicket to process a request. These entities are instances of the following classes: RequestCycle, Request, Response and Session.
除了配置和初始化我们的应用程序外,application类还负责创建Wicket用于处理请求的内部实体。这些实体是以下类的实例:RequestCycle、Request、Response和Session。
The next paragraphs will illustrate each of these classes, explaining how they are involved into request processing.
接下来的段落将说明这些类中的每一个,解释它们是如何参与请求处理的。
9.2. Request and Response classes
The Request and Response classes are located in package org.apache.wicket.request and they provide an abstraction of the concrete request and response used by our web application.
Request和Response类位于org.apache.wicket.Request包中,它们提供了我们的web应用程序所使用的具体请求和响应的抽象。
Both classes are declared as abstract but if our application class inherits from WebApplication it will use their sub classes ServletWebRequest and ServletWebResponse, both of them located inside the package org.apache.wicket.protocol.http.servlet.ServletWebRequest and ServletWebResponse wrap respectively a HttpServletRequest and a HttpServletResponse object. If we need to access to these low-level objects we can call Request's method getContainerRequest() and Response's method getContainerResponse().
这两个类都被声明为抽象的,但如果我们的应用程序类从WebApplication继承,它将使用它们的子类ServletWebRequest和ServletWebResponse,这两个子类都位于包org.apache.wicket.procol.http.servlet中。Servlet WebRequest和Servlet WebResponse分别包装一个HttpServlet Request和一个HttpServletResponse对象。如果我们需要访问这些低级对象,我们可以调用Request的方法getContainerRequest()和Response的方法getContainerResponse()。
9.3. The “director” of request processing - RequestCycle
Class org.apache.wicket.request.cycle.RequestCycle is the entity in charge of serving a web request. Our application class creates a new RequestCycle on every request with its method createRequestCycle(request, response).
类org.apache.wicket.request.cycle.RequestCycle是负责提供web请求的实体。我们的应用程序类使用其方法createRequestCycle(request,response)为每个请求创建一个新的RequestCycle。
Method createRequestCycle is declared as final, so we can’t override it to return a custom subclass of RequestCycle. Instead, we must build a request cycle provider implementing interface org.apache.wicket.IRequestCycleProvider, and then we must tell our application class to use it via the setRequestCycleProvider method.
方法createRequestCycle被声明为final,所以我们不能重写它来返回RequestCycle的自定义子类。相反,我们必须构建一个实现接口org.apache.wicket的请求周期提供程序。IRequestCycleProvider,然后我们必须告诉应用程序类通过setRequestCycleProvide方法使用它。
The current running request cycle can be retrieved at any time by calling its static method <em>RequestCycle.get()</em>. Strictly speaking this method returns the request cycle associated with the current (or local) thread, which is the thread that is serving the current request. A similar <em>get()</em> method is also implemented in classes <em>org.apache.wicket.Application</em> (as we have seen in <a anchor="<em>configuration_of_wicket_applications">paragraph 4.2.2</a>) and _org.apache.wicket.Session</em> in order to get the application and the session in use by the current thread.
通过调用其静态方法<em>RequestCycle.get()</em>,可以随时检索当前运行的请求周期。严格地说,此方法返回与当前(或本地)线程相关联的请求周期,该线程是为当前请求提供服务的线程。类似的<em>get()</em>方法也在类<em>org.apache.wicket中实现。应用程序</em>(正如我们在<a anchor=“<em>configuration_of_wicket_applications”>第4.2.2段</a>中所看到的)和_org.apache.wicket。会话</em>,以便获取当前线程正在使用的应用程序和会话。
Notice:The implementation of the get method takes advantage of the standard class java.lang.ThreadLocal. See its JavaDoc for an introduction to local-thread variables.
注意:get方法的实现利用了标准类java.lang.ThreadLocal。有关本地线程变量的介绍,请参阅其JavaDoc。
Class org.apache.wicket.Component provides the getRequestCycle() method which is a convenience method that internally invokes RequestCycle.get():
类org.apache.wicket。组件提供了getRequestCycle()方法,这是一个内部调用RequestCycle.get()的方便方法:
public final RequestCycle getRequestCycle() { return RequestCycle.get(); }
9.3.1. RequestCycle and request processing
参考:https://nightlies.apache.org/wicket/guide/7.x/single.html