ZK 10 Preview: Migrating from Stateful components to Stateless components"
Jumperchen (talk | contribs) |
m |
||
(44 intermediate revisions by 2 users not shown) | |||
Line 3: | Line 3: | ||
|author=Jumper Chen, Director of Products and Technology, Potix Corporation | |author=Jumper Chen, Director of Products and Technology, Potix Corporation | ||
|date=Sep. TBD, 2022 | |date=Sep. TBD, 2022 | ||
− | |version=ZK 10.0.0.FL. | + | |version=ZK 10.0.0.FL.20220822-Eval |
+ | }} | ||
+ | {{Template:UnderConstruction | ||
}} | }} | ||
= Introduction = | = Introduction = | ||
− | + | Some of you may wonder how it is possible to migrate from an existing ZK Stateful component-based application to a ZK 10 Stateless component-based application with only a few changes. To answer this question, today, we are going to share with you the step-by-step guidance taking [https://www.zkoss.org/zksandbox/ ZK Sandbox Demo] as an example. | |
+ | |||
+ | Before we dive into the details, we want to clarify with you that the goal of this demo is different from [https://www.zkoss.org/wiki/Small_Talks/2022/April/ZK10_Preview:_ZK_10_is_ready_for_building_Cloud-Native_Applications the previous ZK 10 article], which focused on building a pure cloud-native application. | ||
+ | |||
+ | In this exercise, we are not building a cloud-native application. We will be replacing the existing components with ZK 10 stateless components and running the resulting application in a single server instance. This way, it can adopt more classic ZK features while saving more than 30% of memory use. Also, the migration process is much simpler. | ||
− | + | Note that we assume you already know Stateless components. If not, we recommend you read [[Small_Talks/2022/April/ZK10_Preview:_ZK_10_is_ready_for_building_Cloud-Native_Applications | this article]] first. | |
= Overview = | = Overview = | ||
− | The ZK Sandbox demo is a decade project that started in 2008, and it was composed of lots of ZUL files with a few Composers | + | The ZK Sandbox demo is a decade project that started in 2008, and it was composed of lots of ZUL files with a few Composers. Each ZUL file is executed by the ZK event handler (namely <code>onClick=”xxx”</code>) and a ''ZScript'' (a snippet Java code). And our goal is to migrate this sandbox into a stateless components-based sandbox and enjoy a light and performant experience. |
+ | |||
+ | Note that both the event handling and zscript features will no longer work in the Stateless component application -- we will talk about this more later. | ||
+ | |||
+ | Let’s start today’s journey. | ||
== Configuration == | == Configuration == | ||
− | To enable the ZK 10 Stateless | + | To enable the ZK 10 Stateless components, we have to specify the following Filter setting in web.xml |
<source lang="xml" line highlight="5-6"> | <source lang="xml" line highlight="5-6"> | ||
<filter> | <filter> | ||
Line 30: | Line 40: | ||
</filter-mapping> | </filter-mapping> | ||
</source> | </source> | ||
− | As you can see in lines 5 and 6, we declared the package ''org.zkoss.zksandbox.ui'' for scanning the Richlet annotation ''@RichletMapping'' to allow | + | As you can see in lines 5 and 6, we declared the package ''org.zkoss.zksandbox.ui'' for scanning the Richlet annotation ''@RichletMapping'' to allow dispatching of the HTTP request as RESTFul API. |
== Migrate Stateful Composer to Stateless Richlet == | == Migrate Stateful Composer to Stateless Richlet == | ||
− | The ZK Sandbox demo is composed of two primary Composers, ''[https://github.com/zkoss/zk/blob/v9.6.2/zksandbox/src/org/zkoss/zksandbox/MainLayoutComposer.java MainLayoutComposer]'' and ''[https://github.com/zkoss/zk/blob/v9.6.2/zksandbox/src/org/zkoss/zksandbox/DemoWindowComposer.java DemoWindowComposer]'', | + | The ZK Sandbox demo is composed of two primary Composers, ''[https://github.com/zkoss/zk/blob/v9.6.2/zksandbox/src/org/zkoss/zksandbox/MainLayoutComposer.java MainLayoutComposer]'' and ''[https://github.com/zkoss/zk/blob/v9.6.2/zksandbox/src/org/zkoss/zksandbox/DemoWindowComposer.java DemoWindowComposer]'', they communicate with each other and provide major functions. In the Stateless component approach, we are going to migrate the ''MainLayoutComposer'', which is the outer layout of the demo, to ''MainLayoutRichlet'' by using the ZK 10 feature to dispatch the HTTP request to a RESTful-like API and lay out the UI components in the Stateless component APIs. |
'''MainLayoutRichlet.java''' | '''MainLayoutRichlet.java''' | ||
Line 47: | Line 57: | ||
− | Note: A ZK composer is associated with a component of a ZUL file, so here we need to migrate the code in the ZUL file into Java API first. ( | + | Note: A ZK composer is associated with a component of a ZUL file, so here we need to migrate the code in the ZUL file into Java API first. (You might be wondering if we can use ''StatelessComposer'' here instead? Yes, it’s possible, but in this case we choose to use ''StatelessRichlet'') |
=== ZUL Processing Instructions === | === ZUL Processing Instructions === | ||
− | These are the ZUL file [https://www.zkoss.org/wiki/ZK_Developer%27s_Guide/Fundamental_ZK/ZK_User_Interface_Markup_Language/ZK_Processing_Instructions Processing Instructions] declared in a ZUL file. | + | These are the ZUL file [https://www.zkoss.org/wiki/ZK_Developer%27s_Guide/Fundamental_ZK/ZK_User_Interface_Markup_Language/ZK_Processing_Instructions Processing Instructions] declared in a ZUL file. Instead of using processing instructions to add headers, we can use the PageCtrl accessible from the page object in the stateless richlet. |
'''Before''' (''index.zul'') | '''Before''' (''index.zul'') | ||
Line 57: | Line 67: | ||
<?link rel="stylesheet" type="text/css" href="/macros/zksandbox.css.dsp?v=${desktop.webApp.build}"?> | <?link rel="stylesheet" type="text/css" href="/macros/zksandbox.css.dsp?v=${desktop.webApp.build}"?> | ||
<?script type="text/javascript" src="/macros/zksandbox.js.dsp?v=${desktop.webApp.build}"?> | <?script type="text/javascript" src="/macros/zksandbox.js.dsp?v=${desktop.webApp.build}"?> | ||
− | |||
<?meta name="keywords" content="ZK, Ajax, Mobile, Framework, Ajax framekwork, RIA, Direct RIA, Java, JSP, JSF, Open Source, Comet" ?> | <?meta name="keywords" content="ZK, Ajax, Mobile, Framework, Ajax framekwork, RIA, Direct RIA, Java, JSP, JSF, Open Source, Comet" ?> | ||
</source> | </source> | ||
Line 74: | Line 83: | ||
</source> | </source> | ||
− | === ZUL Component | + | === ZUL Component Directives === |
+ | A component directive lets us create a customized version of a stock component, by replacing its widget class, or other attributes. | ||
+ | IComponents can follow a similar pattern, by using the DEFAULT static field from the relevant IComponent class. | ||
+ | |||
'''Before''' (''index.zul'') | '''Before''' (''index.zul'') | ||
<source lang="xml"> | <source lang="xml"> | ||
Line 101: | Line 113: | ||
'''After''' (''MainLayoutRichlet.java'') | '''After''' (''MainLayoutRichlet.java'') | ||
− | <source lang="java"> | + | <source lang="java" highlight="7"> |
@RichletMapping("") | @RichletMapping("") | ||
public List index() { | public List index() { | ||
Line 116: | Line 128: | ||
} | } | ||
</source> | </source> | ||
+ | As you can see from the highlighted text above, we created an ''index.css.dsp'' to format the code so that it's easier to read and maintain. | ||
=== Model and Renderer === | === Model and Renderer === | ||
− | Model is a state to store the application data and Renderer is a way to layout the UI according to the application Data. In the Stateless component, by default we don’t have a way to store the Model and Renderer for later use | + | Model is a state to store the application data, and Renderer is a way to layout the UI according to the application Data. In the Stateless component, by default, we don’t have a way to store the Model and Renderer for later use on the server. However, ZK 10 does provide some utility APIs named ''Controller'' that you can use. |
In this case, we employ ''IListboxController'' instead of using Model and Renderer directly inside a ''Listbox'' component. For example, | In this case, we employ ''IListboxController'' instead of using Model and Renderer directly inside a ''Listbox'' component. For example, | ||
'''Before''' (''MainLayoutComposer.java'') | '''Before''' (''MainLayoutComposer.java'') | ||
− | <source lang="java" line highlight=" | + | <source lang="java" line highlight="14"> |
public void onMainCreate(Event event) { | public void onMainCreate(Event event) { | ||
final Execution exec = Executions.getCurrent(); | final Execution exec = Executions.getCurrent(); | ||
Line 160: | Line 173: | ||
} | } | ||
</source> | </source> | ||
− | After initiating the ''IListboxController'', we store the instance in the ''service'' instance (line 8), a ZK Desktop scope storage one per | + | After initiating the ''IListboxController'', we store the instance in the ''service'' instance (line 8), a ZK Desktop scope storage one per browser page. Then we can receive it back when some '''Action''' handlers are triggered. |
− | Note: ZK Richlet mechanism is a singleton pattern per application (we could assume it’s like ''static'' instance in Java), so it’s not thread-safe in the Java Servlet world. Thankfully, Stateless components are immutable to be run in Richlet safely, yet the stateful data is doubtful here. | + | Note: ZK Richlet mechanism is a singleton pattern per application (we could assume it’s like the ''static'' instance in Java), so it’s not thread-safe in the Java Servlet world. Thankfully, Stateless components are immutable to be run in Richlet safely, yet the stateful data is doubtful here. What we can do is store the demo data per thread per ZK Desktop scope to workaround the thread-safe issue here. |
=== Component and Event for AutoWiring === | === Component and Event for AutoWiring === | ||
− | In | + | In order to use Stateless components, we need to replace each ZK event listener with an action handler. |
For example, | For example, | ||
'''Before''' (''MainLayoutComposer.java'') | '''Before''' (''MainLayoutComposer.java'') | ||
− | <source lang="java"> | + | <source lang="java" highlight="1"> |
public void onBookmarkChange$main(BookmarkEvent event) { | public void onBookmarkChange$main(BookmarkEvent event) { | ||
String id = event.getBookmark(); | String id = event.getBookmark(); | ||
Line 191: | Line 204: | ||
} | } | ||
</source> | </source> | ||
+ | As you can see above, the ''onBookmarkChange$main'' event is registered by ZK ''GenericForwardComposer'' automatically that concatenates the ''onBookmarkChange'' event with the event target ''$main'' (The id with ''"main"'' component). | ||
'''After''' (''MainLayoutRichlet.java'') | '''After''' (''MainLayoutRichlet.java'') | ||
− | <source lang="java"> | + | <source lang="java" highlight="1,16"> |
@Action(from = "#main", type = Events.ON_BOOKMARK_CHANGE) | @Action(from = "#main", type = Events.ON_BOOKMARK_CHANGE) | ||
public void doBookmarkChange$main(UiAgent uiAgent, BookmarkData data) { | public void doBookmarkChange$main(UiAgent uiAgent, BookmarkData data) { | ||
Line 217: | Line 231: | ||
} | } | ||
</source> | </source> | ||
− | + | The action handler is registered by declaring an ''@Action'' annotation with the ''Events.ON_BOOKMARK_CHANGE'' action type from the ''#main'' client widget selector. (This is equivalent to the Stateful component example above) | |
+ | And each ZK event in the ZK Stateless component uses the term ''Action'' with the postfix ''Data''. For example, ''BookmarkEvent'' becomes ''BookmarkData'', and ''MouseEvent'' becomes ''MouseData'', and so on. | ||
+ | |||
+ | Note: ''Xconctents'' is not an ''Include'' component here for the Stateless component. It is a regular div. | ||
+ | |||
+ | === Reuse ZUL to Build Stateless components === | ||
+ | As we mentioned earlier, each page in the ZK Sandbox demo is a ZUL file with a ''DemoWindowComposer''. For this migration, we need to move from ''GenricForwardComposer'' to ''StatelessComposer''. This can be done by implementing the only method of the ''StatelessComposer'': <code>build(BuildContext<IWindow<IAnyGroup>> ctx)</code>. This method receives ctx BuildContext as argument, and returns the resulting IComponent tree. As an useful shortcut we can generate the IComponent tree from our existing zul files directly using Immutables.createComponents("/bar.zul", null). (Note: Immutables.createComponents is not 100% compatible with every stateful features, please see the [[#Appendix|appendix]].) | ||
+ | |||
+ | '''Before''' (Stateful ''DemoWindowComposer.java'') | ||
+ | <source lang="java" highlight="9"> | ||
+ | public void doAfterCompose(Window comp) throws Exception { | ||
+ | super.doAfterCompose(comp); | ||
+ | comp.setContentSclass("demo-main-cnt"); | ||
+ | comp.setSclass("demo-main"); | ||
+ | final Div inc = new Div(); | ||
+ | Executions.createComponents("/bar.zul", inc, null); | ||
+ | inc.setStyle("float:right"); | ||
+ | comp.insertBefore(inc, comp.getFirstChild()); | ||
+ | if (view != null) execute(); | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | '''After''' (Stateless ''DemoWindowComposer.java'') | ||
+ | <source lang="java" highlight="1-2,7"> | ||
+ | public IWindow build(BuildContext<IWindow<IAnyGroup>> ctx) { | ||
+ | final String code = ISelectors.<ITextbox>findById(ctx.getOwner(), | ||
+ | "codeView").getValue(); | ||
+ | |||
+ | // post a dummy event to execute the code above later. | ||
+ | Component dummy = Locator.of("dummy") | ||
+ | .toComponent((evt, scope) -> execute(code, true)); | ||
+ | Events.postEvent(dummy, new Event("onAfterBuild")); | ||
− | + | List newChildren = new ArrayList(ctx.getOwner().getChildren()); | |
+ | newChildren.add(0, IDiv.DEFAULT.withStyle("float:right") | ||
+ | .withChildren(Immutables.createComponents("/bar.zul", null))); | ||
− | + | return ctx.getOwner().withContentSclass("demo-main-cnt") | |
− | + | .withSclass("demo-main").withChildren(newChildren); | |
+ | } | ||
+ | </source> | ||
+ | Unlike the Stateful ''DemoWindowComposer'', there is no way to bind the component directly inside the Stateless ''DemoWindowComposer'' class method field. Instead, we can utilize ''ISelectors'' API (similar to ''Selectors'' in the former ZK API) to look up from an ''IComponent'' tree to receive the Stateless component data, which is established by the ZUL file. | ||
− | + | For example, the original Stateful composer was to execute the ''codeView'' data of a textbox component inside the ''doAfterCompose'' method directly. However, in the Stateless composer, we have no chance to bind the ''codeView'' component to get its data there, so we utilize ''ISelectors'' API to get the component data back, and then execute its code later. There are two different approaches to execute this '''"later”''' in the Stateless composer, one is to use an async mechanism (must enable ZK Server Push feature), and the other is to simulate a ZK classic event post. | |
==== Async Mechanism ==== | ==== Async Mechanism ==== | ||
− | We can apply the <code>UiAgentAPI.runAsync()</code> method to create an async function callback | + | We can apply the <code>UiAgentAPI.runAsync()</code> method to create an async function callback provided by ZK 10. |
For example, | For example, | ||
Line 236: | Line 286: | ||
</source> | </source> | ||
− | + | This approach works, but there will be a small delay in UI display, as it depends on a server push to execute another Java thread and waiting for the next HTTP request to execute the code. | |
==== Simulate ZK Classic Event Post ==== | ==== Simulate ZK Classic Event Post ==== | ||
Line 245: | Line 295: | ||
Events.postEvent(dummy, new Event("onAfterBuild")); | Events.postEvent(dummy, new Event("onAfterBuild")); | ||
</source> | </source> | ||
− | |||
− | Note: In the Stateless Composer, | + | The ''Locator'' API in ZK 10 is to locate a client widget target to do some updates with ''UiAgent'' APIs, and it also provides an API to construct a fake Component by its <code>toComponent()</code> method. The first argument for this method is a callback for providing the mimic ZK classic event post, and the code will execute in the same thread in the next event loop that ZK provides. |
+ | |||
+ | Note: In the Stateless Composer, no actual ZK stateful component exists. | ||
=== Communication between Composers === | === Communication between Composers === | ||
− | In the Stateful composer, it uses <code>Path.getCompoent()</code> API to receive the stateful component back, and does the <code>invalidate()</code> method to reset any changes in the ''xcontents'' component and reload with its origin ''src'' property that ''Include'' component | + | In the Stateful composer, it uses <code>Path.getCompoent()</code> API to receive the stateful component back, and does the <code>invalidate()</code> method to reset any changes in the ''xcontents'' component, and reload with its origin ''src'' property that the ''Include'' component offers. For example, |
<source lang="java"> | <source lang="java"> | ||
Line 259: | Line 310: | ||
</source> | </source> | ||
− | In the Stateless composer, we couldn’t | + | In the Stateless composer, we couldn’t use <code>Path.getComponent()</code> or <code>Selectors</code> API to receive the component back on the server side. Instead, we can leverage ZK Event Queue here to notify the information to the outer Composer or Richlet we use in this case. |
In ''MainLayoutRichlet'', we need to subscribe to an Event Queue (by default ZK Desktop scope) for example, | In ''MainLayoutRichlet'', we need to subscribe to an Event Queue (by default ZK Desktop scope) for example, | ||
Line 272: | Line 323: | ||
} | } | ||
</source> | </source> | ||
− | As you see above, we override the <code>service()</code> method which is called once per page visit, and use ''UiAgent'' API to update the client state with the data that we store | + | As you see above, we override the <code>service()</code> method, which is called once per page visit, and use ''UiAgent'' API to update the client state with the data that we store in the server’s ''service'' instance. (equivalent to ''Include'' component’s <code>invalidate()</code>) |
Here is the post of the event in the ''DemoWindowComposer'', for example, | Here is the post of the event in the ''DemoWindowComposer'', for example, | ||
Line 285: | Line 336: | ||
== Limitations == | == Limitations == | ||
+ | I have demonstrated how you can turn an existing ZK application into a stateless component-based ZK application in a manageable way with some auto-conversion features. There are a few limitations: | ||
# ZScript Function | # ZScript Function | ||
− | #: If some codes in the ''ZScript'' | + | #: If some codes in the ''ZScript'' access the component, that will not work in the Stateless component world. |
− | # Event Handler | + | # Event Handler in ZUL File |
− | #: | + | #: <code>onClick=”doSomething”</code>: this kind of event handler can't be transformed into a Stateless component-based app automatically. However, you can rewrite them in a ''StatelessComposer'' instead. |
# Fulfill Mechanism | # Fulfill Mechanism | ||
− | #: ''Fulfill'' is a feature in ZK to apply the content | + | #: ''Fulfill'' is a feature in ZK to apply the content to a ZUL file when an event handler is triggered. However, this feature needs to be rewritten with pure Java code directly in the Stateless world. |
# MVVM Mechanism | # MVVM Mechanism | ||
− | #: ZK 10 Stateless component is | + | #: ZK 10 Stateless component is not designed for MVVM pattern. If you wish to use MVVM mechanism in ZK 10, you can check – [https://www.zkoss.org/wiki/Small_Talks/2022/May/ZK10_Preview:_Using_the_new_and_light_Client_MVVM ZK Client MVVM]. |
= Download = | = Download = | ||
− | * The ZK 10 Sandbox demo (Stateless Component) | + | * The ZK 10 Sandbox demo (Stateless Component) can be downloaded from [https://github.com/zkoss-demo/zk10-zksandbox-demo here]. |
− | * The ZK 9 Sandbox demo (Stateful Component) | + | * The ZK 9 Sandbox demo (Stateful Component) can be accessed [https://github.com/zkoss/zk/tree/v9.6.2/zksandbox here]. |
= Appendix = | = Appendix = | ||
− | == | + | == Known incompatibilities == |
+ | The following components are not compatible with ZK 10 stateless components. | ||
{| class="wikitable" | {| class="wikitable" | ||
|+ Compatible Table | |+ Compatible Table | ||
|- | |- | ||
− | ! Function !! ZK 9 (Stateful Component) !! ZK 10 (Stateless Component) !! Solution | + | ! Function !! ZK 9 (Stateful Component) !! ZK 10 (Stateless Component) !! Solution for incompatible (!) items |
|- | |- | ||
| ''Component'' || Include || | | ''Component'' || Include || | ||
Line 354: | Line 407: | ||
| || Fulfill Mechanism || Java Code || rewrite | | || Fulfill Mechanism || Java Code || rewrite | ||
|- | |- | ||
− | | || Model | + | | || Model, Renderer, and Template || ''IXxxController'' || rewrite |
|- | |- | ||
| || || || | | || || || | ||
Line 367: | Line 420: | ||
|} | |} | ||
− | == | + | == Feature Table == |
+ | Stateless components can be used in different modes, | ||
+ | |||
+ | ''default'' mode: use stateless components but run in a single server (or a sticky session-enabled cluster). Easy migration. | ||
+ | |||
+ | ''cloud'' mode: doesn't rely on user sessions and component states, and can be used for building cloud-native applications. | ||
+ | |||
{| class="wikitable" | {| class="wikitable" | ||
|+ Features Table | |+ Features Table | ||
|- | |- | ||
− | ! ZK 10 (Stateless Component) !! ''default'' mode <br> (Desktop alive) !! ''cloud'' mode <br> (namely ''cloud-native application'') !! Solution | + | ! ZK 10 (Stateless Component) !! ''default'' mode <br> (Desktop alive) !! ''cloud'' mode <br> (namely ''cloud-native application'') !! Solution for incompatible (!) items |
|- | |- | ||
| StatelessRichlet || V || V || | | StatelessRichlet || V || V || | ||
Line 379: | Line 438: | ||
| Action Handler: (Lambda function) || V || || | | Action Handler: (Lambda function) || V || || | ||
|- | |- | ||
− | | ZK Event Queue || V || ! || | + | | ZK Event Queue || V || ! || use other mechanisms, such as Redis, RabbitMQ... |
|- | |- | ||
| Model Controller || V || || | | Model Controller || V || || | ||
|- | |- | ||
− | | ZK Server Push || V || ! || | + | | ZK Server Push || V || ! || use other mechanisms |
|- | |- | ||
| ZK Websocket Channel || V || || | | ZK Websocket Channel || V || || | ||
Line 400: | Line 459: | ||
|} | |} | ||
| | ||
+ | = Further reading = | ||
+ | We hope you find this article useful. Please try it out and let us know how you like it. | ||
+ | |||
+ | For more ZK 10 articles, [https://www.zkoss.org/wiki/Small_Talks/2022 visit here]. | ||
+ | |||
+ | Participate in the [https://blog.zkoss.org/2022/07/05/zk-10-preview-training-winner-announcement-and-feedback-event/ Feedback Event] and give us feedback on ZK 10 features. | ||
{{Template:CommentedSmalltalk_Footer_new| | {{Template:CommentedSmalltalk_Footer_new| |
Latest revision as of 12:54, 21 September 2022
Jumper Chen, Director of Products and Technology, Potix Corporation
Sep. TBD, 2022
ZK 10.0.0.FL.20220822-Eval
Introduction
Some of you may wonder how it is possible to migrate from an existing ZK Stateful component-based application to a ZK 10 Stateless component-based application with only a few changes. To answer this question, today, we are going to share with you the step-by-step guidance taking ZK Sandbox Demo as an example.
Before we dive into the details, we want to clarify with you that the goal of this demo is different from the previous ZK 10 article, which focused on building a pure cloud-native application.
In this exercise, we are not building a cloud-native application. We will be replacing the existing components with ZK 10 stateless components and running the resulting application in a single server instance. This way, it can adopt more classic ZK features while saving more than 30% of memory use. Also, the migration process is much simpler.
Note that we assume you already know Stateless components. If not, we recommend you read this article first.
Overview
The ZK Sandbox demo is a decade project that started in 2008, and it was composed of lots of ZUL files with a few Composers. Each ZUL file is executed by the ZK event handler (namely onClick=”xxx”
) and a ZScript (a snippet Java code). And our goal is to migrate this sandbox into a stateless components-based sandbox and enjoy a light and performant experience.
Note that both the event handling and zscript features will no longer work in the Stateless component application -- we will talk about this more later.
Let’s start today’s journey.
Configuration
To enable the ZK 10 Stateless components, we have to specify the following Filter setting in web.xml
1 <filter>
2 <filter-name>DispatcherRichletFilter</filter-name>
3 <filter-class>org.zkoss.zephyr.ui.http.DispatcherRichletFilter</filter-class>
4 <init-param>
5 <param-name>basePackages</param-name>
6 <param-value>org.zkoss.zksandbox.ui</param-value>
7 </init-param>
8 </filter>
9 <filter-mapping>
10 <filter-name>DispatcherRichletFilter</filter-name>
11 <url-pattern>/*</url-pattern>
12 </filter-mapping>
As you can see in lines 5 and 6, we declared the package org.zkoss.zksandbox.ui for scanning the Richlet annotation @RichletMapping to allow dispatching of the HTTP request as RESTFul API.
Migrate Stateful Composer to Stateless Richlet
The ZK Sandbox demo is composed of two primary Composers, MainLayoutComposer and DemoWindowComposer, they communicate with each other and provide major functions. In the Stateless component approach, we are going to migrate the MainLayoutComposer, which is the outer layout of the demo, to MainLayoutRichlet by using the ZK 10 feature to dispatch the HTTP request to a RESTful-like API and lay out the UI components in the Stateless component APIs.
MainLayoutRichlet.java
1 @RichletMapping("/")
2 public class MainLayoutRichlet implements StatelessRichlet {
3 @RichletMapping("")
4 public List index() {
5 // omitted
6 }
7 }
Note: A ZK composer is associated with a component of a ZUL file, so here we need to migrate the code in the ZUL file into Java API first. (You might be wondering if we can use StatelessComposer here instead? Yes, it’s possible, but in this case we choose to use StatelessRichlet)
ZUL Processing Instructions
These are the ZUL file Processing Instructions declared in a ZUL file. Instead of using processing instructions to add headers, we can use the PageCtrl accessible from the page object in the stateless richlet.
Before (index.zul)
<?page id="userGuide" title="ZK Sandbox"?>
<?link rel="stylesheet" type="text/css" href="/macros/zksandbox.css.dsp?v=${desktop.webApp.build}"?>
<?script type="text/javascript" src="/macros/zksandbox.js.dsp?v=${desktop.webApp.build}"?>
<?meta name="keywords" content="ZK, Ajax, Mobile, Framework, Ajax framekwork, RIA, Direct RIA, Java, JSP, JSF, Open Source, Comet" ?>
After (MainLayoutRichlet.java)
private void initHeaderInfo(Page page) {
page.setTitle("ZK Sandbox");
PageCtrl pc = (PageCtrl) page;
pc.addBeforeHeadTags("<meta name=\"keywords\" content=\"ZK, Ajax, Mobile, Framework, Ajax framekwork, RIA, Direct RIA, Java, JSP, JSF, Open Source, Comet\">");
// after ZK JS files
pc.addAfterHeadTags("<link rel=\"stylesheet\" type=\"text/css\" href=\"macros/zksandbox.css.dsp?v=" + page.getDesktop().getWebApp().getBuild() + "\">");
pc.addAfterHeadTags("<script type=\"text/javascript\" src=\"macros/zksandbox.js.dsp?v=" + page.getDesktop().getWebApp().getBuild() + "\"></script>");
}
ZUL Component Directives
A component directive lets us create a customized version of a stock component, by replacing its widget class, or other attributes. IComponents can follow a similar pattern, by using the DEFAULT static field from the relevant IComponent class.
Before (index.zul)
<?component name="category" extends="button" widgetClass="zksandbox.Category" mold="default"?>
<?component name="categorybar" extends="div" widgetClass="zksandbox.Categorybar"?>
After (MainLayoutRichlet.java)
private final static IButton CATEGORY = IButton.DEFAULT.withWidgetClass("zksandbox.Category");
private final static IDiv<IAnyGroup> CATEGORY_BAR = IDiv.DEFAULT.withWidgetClass("zksandbox.Categorybar");
ZUL UI Component
Before (index.zul)
<style>
.z-html {
display: block;
}
</style>
<style if="${zk.mobile != null}">…</script>
<borderlayout id="main" sclass="${themeSClass}" apply="org.zkoss.zksandbox.MainLayoutComposer">…</borderlayout>
After (MainLayoutRichlet.java)
@RichletMapping("")
public List index() {
initHeaderInfo(((ExecutionCtrl)Executions.getCurrent()).getCurrentPage());
Double number = Executions.getCurrent().getBrowser("mobile");
return Arrays.asList(
IStyle.of(".z-html {display: block;}"),
number != null ? IStyle.ofSrc("~./style/index.css.dsp") : IStyle.DEFAULT,
initMainLayout(),
"www.zkoss.org".equals(Executions.getCurrent().getServerName()) ||
"www.potix.com".equals(Executions.getCurrent().getServerName()) ?
IScript.ofSrc("macros/ga.js") : IScript.ofSrc("")
);
}
As you can see from the highlighted text above, we created an index.css.dsp to format the code so that it's easier to read and maintain.
Model and Renderer
Model is a state to store the application data, and Renderer is a way to layout the UI according to the application Data. In the Stateless component, by default, we don’t have a way to store the Model and Renderer for later use on the server. However, ZK 10 does provide some utility APIs named Controller that you can use.
In this case, we employ IListboxController instead of using Model and Renderer directly inside a Listbox component. For example,
Before (MainLayoutComposer.java)
1 public void onMainCreate(Event event) {
2 final Execution exec = Executions.getCurrent();
3 final String id = exec.getParameter("id");
4 Listitem item = null;
5 if (id != null) {
6 try {
7 final LinkedList<DemoItem> list = new LinkedList<DemoItem>();
8 final DemoItem[] items = getItems();
9 for (int i = 0; i < items.length; i++) {
10 if (items[i].getId().equals(id))
11 list.add(items[i]);
12 }
13 if (!list.isEmpty()) {
14 itemList.setModel(new ListModelList<DemoItem>(list, true));
15 itemList.renderAll();
16 item = (Listitem) self.getFellow(id);
17 setSelectedCategory(item);
18 }
19 } catch (ComponentNotFoundException ex) { // ignore
20 }
21 }
After (MainLayoutRichlet.java)
1 private IListboxController initListboxController() {
2 IListboxController<DemoItem, IListitem> itemList = IListboxController.of(
3 IListbox.ofId("itemList")... , // The IListbox instance
4 getSelectedModel(), // The ListModel we need.
5 (DemoItem di, Integer index) -> IListitem.ofId(di.getId()) // The Render to layout the UI elements
6 .withChildren(IListcell.of(di.getLabel(), di.getIcon())
7 .withHeight("30px")));
8 service.setListboxController(itemList);
9 return itemList;
10 }
After initiating the IListboxController, we store the instance in the service instance (line 8), a ZK Desktop scope storage one per browser page. Then we can receive it back when some Action handlers are triggered.
Note: ZK Richlet mechanism is a singleton pattern per application (we could assume it’s like the static instance in Java), so it’s not thread-safe in the Java Servlet world. Thankfully, Stateless components are immutable to be run in Richlet safely, yet the stateful data is doubtful here. What we can do is store the demo data per thread per ZK Desktop scope to workaround the thread-safe issue here.
Component and Event for AutoWiring
In order to use Stateless components, we need to replace each ZK event listener with an action handler. For example,
Before (MainLayoutComposer.java)
public void onBookmarkChange$main(BookmarkEvent event) {
String id = event.getBookmark();
if (id.length() > 0) {
final DemoItem[] items = getItems();
for (int i = 0; i < items.length; i++) {
if (items[i].getId().equals(id)) {
_selected = (Button)self.getFellow(items[i].getCateId());
itemList.setModel(getSelectedModel());
itemList.renderAll();
Listitem item = ((Listitem)itemList.getFellow(id));
item.setSelected(true);
itemList.invalidate();
setSelectedCategory(item);
xcontents.setSrc(((DemoItem) item.getValue()).getFile());
item.focus();
return;
}
}
}
}
As you can see above, the onBookmarkChange$main event is registered by ZK GenericForwardComposer automatically that concatenates the onBookmarkChange event with the event target $main (The id with "main" component).
After (MainLayoutRichlet.java)
@Action(from = "#main", type = Events.ON_BOOKMARK_CHANGE)
public void doBookmarkChange$main(UiAgent uiAgent, BookmarkData data) {
String id = data.getBookmark();
if (id.length() > 0) {
final DemoItem[] items = getItems();
for (int i = 0; i < items.length; i++) {
DemoItem demoItem = items[i];
if (demoItem.getId().equals(id)) {
service.setSelectedCategory(demoItem.getCateId());
IListboxController<DemoItem, IListitem> listboxController = service.getListboxController();
listboxController.setModel(getSelectedModel());
listboxController.setSelectedObject(demoItem);
uiAgent.replaceWith(Locator.ofId("itemList"), listboxController.build());
uiAgent.smartUpdate(Locator.ofId(id), new IListitem.Updater().selected(true).focus(true));
setSelectedCategory(demoItem);
uiAgent.replaceChildren(Locator.ofId("xcontents"), Immutables.createComponents(
demoItem.getFile(), null));
return;
}
}
}
}
The action handler is registered by declaring an @Action annotation with the Events.ON_BOOKMARK_CHANGE action type from the #main client widget selector. (This is equivalent to the Stateful component example above) And each ZK event in the ZK Stateless component uses the term Action with the postfix Data. For example, BookmarkEvent becomes BookmarkData, and MouseEvent becomes MouseData, and so on.
Note: Xconctents is not an Include component here for the Stateless component. It is a regular div.
Reuse ZUL to Build Stateless components
As we mentioned earlier, each page in the ZK Sandbox demo is a ZUL file with a DemoWindowComposer. For this migration, we need to move from GenricForwardComposer to StatelessComposer. This can be done by implementing the only method of the StatelessComposer: build(BuildContext<IWindow<IAnyGroup>> ctx)
. This method receives ctx BuildContext as argument, and returns the resulting IComponent tree. As an useful shortcut we can generate the IComponent tree from our existing zul files directly using Immutables.createComponents("/bar.zul", null). (Note: Immutables.createComponents is not 100% compatible with every stateful features, please see the appendix.)
Before (Stateful DemoWindowComposer.java)
public void doAfterCompose(Window comp) throws Exception {
super.doAfterCompose(comp);
comp.setContentSclass("demo-main-cnt");
comp.setSclass("demo-main");
final Div inc = new Div();
Executions.createComponents("/bar.zul", inc, null);
inc.setStyle("float:right");
comp.insertBefore(inc, comp.getFirstChild());
if (view != null) execute();
}
After (Stateless DemoWindowComposer.java)
public IWindow build(BuildContext<IWindow<IAnyGroup>> ctx) {
final String code = ISelectors.<ITextbox>findById(ctx.getOwner(),
"codeView").getValue();
// post a dummy event to execute the code above later.
Component dummy = Locator.of("dummy")
.toComponent((evt, scope) -> execute(code, true));
Events.postEvent(dummy, new Event("onAfterBuild"));
List newChildren = new ArrayList(ctx.getOwner().getChildren());
newChildren.add(0, IDiv.DEFAULT.withStyle("float:right")
.withChildren(Immutables.createComponents("/bar.zul", null)));
return ctx.getOwner().withContentSclass("demo-main-cnt")
.withSclass("demo-main").withChildren(newChildren);
}
Unlike the Stateful DemoWindowComposer, there is no way to bind the component directly inside the Stateless DemoWindowComposer class method field. Instead, we can utilize ISelectors API (similar to Selectors in the former ZK API) to look up from an IComponent tree to receive the Stateless component data, which is established by the ZUL file.
For example, the original Stateful composer was to execute the codeView data of a textbox component inside the doAfterCompose method directly. However, in the Stateless composer, we have no chance to bind the codeView component to get its data there, so we utilize ISelectors API to get the component data back, and then execute its code later. There are two different approaches to execute this "later” in the Stateless composer, one is to use an async mechanism (must enable ZK Server Push feature), and the other is to simulate a ZK classic event post.
Async Mechanism
We can apply the UiAgentAPI.runAsync()
method to create an async function callback provided by ZK 10.
For example,
UiAgent.getCurrent().runAsync(uiAgent -> {
execute(code, true);
});
This approach works, but there will be a small delay in UI display, as it depends on a server push to execute another Java thread and waiting for the next HTTP request to execute the code.
Simulate ZK Classic Event Post
In the Stateless component API, we can simulate a ZK classic event callback by a Locator API. For example,
Component dummy = Locator.of("dummy")
.toComponent((evt, scope) -> execute(code, true));
Events.postEvent(dummy, new Event("onAfterBuild"));
The Locator API in ZK 10 is to locate a client widget target to do some updates with UiAgent APIs, and it also provides an API to construct a fake Component by its toComponent()
method. The first argument for this method is a callback for providing the mimic ZK classic event post, and the code will execute in the same thread in the next event loop that ZK provides.
Note: In the Stateless Composer, no actual ZK stateful component exists.
Communication between Composers
In the Stateful composer, it uses Path.getCompoent()
API to receive the stateful component back, and does the invalidate()
method to reset any changes in the xcontents component, and reload with its origin src property that the Include component offers. For example,
public void onClick$reloadBtn(Event event) {
demoView.setSelected(true);
Path.getComponent("//userGuide/xcontents").invalidate();
}
In the Stateless composer, we couldn’t use Path.getComponent()
or Selectors
API to receive the component back on the server side. Instead, we can leverage ZK Event Queue here to notify the information to the outer Composer or Richlet we use in this case.
In MainLayoutRichlet, we need to subscribe to an Event Queue (by default ZK Desktop scope) for example,
public void service(Page page) throws Exception {
StatelessRichlet.super.service(page);
// init event queue
EventQueues.lookup("mainLayoutRichlet").subscribe((event -> {
UiAgent.getCurrent().replaceChildren(Locator.ofId("xcontents"), Immutables.createComponents(service.getListboxController().getSelectedObject().getFile(), null));
}));
}
As you see above, we override the service()
method, which is called once per page visit, and use UiAgent API to update the client state with the data that we store in the server’s service instance. (equivalent to Include component’s invalidate()
)
Here is the post of the event in the DemoWindowComposer, for example,
@Action(from = "#reloadBtn", type = Events.ON_CLICK)
public void onClick$reloadBtn() {
UiAgent.getCurrent().smartUpdate(Locator.ofId("demoView"),
new ITab.Updater().selected(true));
EventQueues.lookup("mainLayoutRichlet").publish(new Event("reloadBtn"));
}
Limitations
I have demonstrated how you can turn an existing ZK application into a stateless component-based ZK application in a manageable way with some auto-conversion features. There are a few limitations:
- ZScript Function
- If some codes in the ZScript access the component, that will not work in the Stateless component world.
- Event Handler in ZUL File
onClick=”doSomething”
: this kind of event handler can't be transformed into a Stateless component-based app automatically. However, you can rewrite them in a StatelessComposer instead.
- Fulfill Mechanism
- Fulfill is a feature in ZK to apply the content to a ZUL file when an event handler is triggered. However, this feature needs to be rewritten with pure Java code directly in the Stateless world.
- MVVM Mechanism
- ZK 10 Stateless component is not designed for MVVM pattern. If you wish to use MVVM mechanism in ZK 10, you can check – ZK Client MVVM.
Download
- The ZK 10 Sandbox demo (Stateless Component) can be downloaded from here.
- The ZK 9 Sandbox demo (Stateful Component) can be accessed here.
Appendix
Known incompatibilities
The following components are not compatible with ZK 10 stateless components.
Function | ZK 9 (Stateful Component) | ZK 10 (Stateless Component) | Solution for incompatible (!) items |
---|---|---|---|
Component | Include |
|
rewrite |
HBox | IHlayout | rewrite | |
VBox | IVlayout | rewrite | |
Splitter | ISplitlayout | rewrite | |
Chart | x | ||
Nodom | x | ||
Jasperreport | x | ||
Captcha | x | ||
HtmlMacroComponent | x | ||
Fragment | x | (MVVM) | |
Apply (Shadow Components) | x | (MVVM) | |
ZHTML Components | ! | not available yet | |
Addons | Keikai 5 | x | |
ZK CKEditor | x | ||
ZK Charts | x | ||
ZK Gmaps | x | ||
ZK Calendar | x | ||
ZK Pivottable | x | ||
APIs | Event Handler (ZScript) on ZUL | x | use StatelessComposer instead |
ZScript | Java Code | rewrite | |
Fulfill Mechanism | Java Code | rewrite | |
Model, Renderer, and Template | IXxxController | rewrite | |
Deprecated Components | Applet | x | |
Flash | x | ||
Flashchart | x | ||
Fusionchart | x |
Feature Table
Stateless components can be used in different modes,
default mode: use stateless components but run in a single server (or a sticky session-enabled cluster). Easy migration.
cloud mode: doesn't rely on user sessions and component states, and can be used for building cloud-native applications.
ZK 10 (Stateless Component) | default mode (Desktop alive) |
cloud mode (namely cloud-native application) |
Solution for incompatible (!) items |
---|---|---|---|
StatelessRichlet | V | V | |
StatelessComposer (ZUL & ZPR) | V | ||
Action Handler: (Lambda function) | V | ||
ZK Event Queue | V | ! | use other mechanisms, such as Redis, RabbitMQ... |
Model Controller | V | ||
ZK Server Push | V | ! | use other mechanisms |
ZK Websocket Channel | V | ||
Reduce Application Memory | 30+% | totally free | |
Load-balance Support | ! | V | sticky session, session replication, or spring session-data |
Resilient Application | ! | V | session replication or spring session-data |
Dynamically Scaling Application | ! | V | session replication or spring session-data |
Further reading
We hope you find this article useful. Please try it out and let us know how you like it.
For more ZK 10 articles, visit here.
Participate in the Feedback Event and give us feedback on ZK 10 features.
Comments
Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License. |