tapestry-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From conflue...@apache.org
Subject [CONF] Apache Tapestry > Implementing the Hi-Lo Guessing Game
Date Wed, 01 Dec 2010 19:12:00 GMT
<html>
<head>
    <base href="https://cwiki.apache.org/confluence">
            <link rel="stylesheet" href="/confluence/s/1810/9/12/_/styles/combined.css?spaceKey=TAPESTRY&amp;forWysiwyg=true" type="text/css">
    </head>
<body style="background: white;" bgcolor="white" class="email-body">
<div id="pageContent">
<div id="notificationFormat">
<div class="wiki-content">
<div class="email">
    <h2><a href="https://cwiki.apache.org/confluence/display/TAPESTRY/Implementing+the+Hi-Lo+Guessing+Game">Implementing the Hi-Lo Guessing Game</a></h2>
    <h4>Page <b>edited</b> by             <a href="https://cwiki.apache.org/confluence/display/~hlship">Howard M. Lewis Ship</a>
    </h4>
        <br/>
                         <h4>Changes (2)</h4>
                                 
    
<div id="page-diffs">
            <table class="diff" cellpadding="0" cellspacing="0">
            <tr><td class="diff-changed-lines" ><span class="diff-deleted-words"style="color:#999;background-color:#fdd;text-decoration:line-through;">{tutorialnav}</span> <span class="diff-added-words"style="background-color: #dfd;">{scrollbar}</span> <br></td></tr>
            <tr><td class="diff-unchanged" > <br>Let&#39;s start building a basic Hi-Lo Guessing game. <br></td></tr>
            <tr><td class="diff-snipped" >...<br></td></tr>
            <tr><td class="diff-unchanged" > <br> <br></td></tr>
            <tr><td class="diff-changed-lines" ><span class="diff-deleted-words"style="color:#999;background-color:#fdd;text-decoration:line-through;">{tutorialnav}</span> <span class="diff-added-words"style="background-color: #dfd;">{scrollbar}</span> <br></td></tr>
        </table>
</div>                            <h4>Full Content</h4>
                    <div class="notificationGreySide">
        <style type='text/css'>/*<![CDATA[*/
table.ScrollbarTable  {border: none;padding: 3px;width: 100%;padding: 3px;margin: 0px;background-color: #f0f0f0}
table.ScrollbarTable td.ScrollbarPrevIcon {text-align: center;width: 16px;border: none;}
table.ScrollbarTable td.ScrollbarPrevName {text-align: left;border: none;}
table.ScrollbarTable td.ScrollbarParent {text-align: center;border: none;}
table.ScrollbarTable td.ScrollbarNextName {text-align: right;border: none;}
table.ScrollbarTable td.ScrollbarNextIcon {text-align: center;width: 16px;border: none;}

/*]]>*/</style><div class="Scrollbar"><table class='ScrollbarTable'><tr><td class='ScrollbarPrevIcon'><a href="/confluence/display/TAPESTRY/Exploring+the+Project"><img border='0' align='middle' src='/confluence/images/icons/back_16.gif' width='16' height='16'></a></td><td width='33%' class='ScrollbarPrevName'><a href="/confluence/display/TAPESTRY/Exploring+the+Project">Exploring the Project</a>&nbsp;</td><td width='33%' class='ScrollbarParent'><sup><a href="/confluence/display/TAPESTRY/Tapestry+Tutorial"><img border='0' align='middle' src='/confluence/images/icons/up_16.gif' width='8' height='8'></a></sup><a href="/confluence/display/TAPESTRY/Tapestry+Tutorial">Tapestry Tutorial</a></td><td width='33%' class='ScrollbarNextName'>&nbsp;<a href="/confluence/display/TAPESTRY/Forms">Forms</a></td><td class='ScrollbarNextIcon'><a href="/confluence/display/TAPESTRY/Forms"><img border='0' align='middle' src='/confluence/images/icons/forwd_16.gif' width='16' height='16'></a></td></tr></table></div>

<p>Let's start building a basic Hi-Lo Guessing game.</p>

<p>In the game, the computer selects a number between 1 and 10. You try and guess the number, clicking links. At the end, the computer tells you how many guesses you required.</p>

<p>We'll build it in small pieces, using the kind of iterative development that Tapestry makes so easy.</p>

<p><span class="image-wrap" style=""><img src="/confluence/download/attachments/23340505/hilo-flow.png?version=2&amp;modificationDate=1286828602000" style="border: 0px solid black" /></span></p>

<p>Our page flow is very simple, consisting of three pages: Index (the starting page), Guess and GameOver. The Index page introduces the application and includes a link to start guessing. The Guess page presents the user with ten links, plus feedback such as "too low" or "too high". The GameOver page tells the user how many guesses they took before finding the target number.</p>

<h1><a name="ImplementingtheHi-LoGuessingGame-IndexPage"></a>Index Page</h1>

<p>Let's get to work on the Index page and template.</p>

<div class="code panel" style="border-width: 1px;"><div class="codeHeader panelHeader" style="border-bottom-width: 1px;"><b>Index.tml</b></div><div class="codeContent panelContent">
<pre class="code-xml">
&lt;html t:type=<span class="code-quote">"layout"</span> title=<span class="code-quote">"Hi/Lo Guess"</span>
  <span class="code-keyword">xmlns:t</span>=<span class="code-quote">"http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"</span>&gt;

  <span class="code-tag">&lt;p&gt;</span>
    I'm thinking of a number between one and ten ... <span class="code-tag">&lt;/p&gt;</span>

  <span class="code-tag">&lt;p&gt;</span>
    <span class="code-tag">&lt;a href=<span class="code-quote">"#"</span>&gt;</span>start guessing<span class="code-tag">&lt;/a&gt;</span>
  <span class="code-tag">&lt;/p&gt;</span>

<span class="code-tag">&lt;/html&gt;</span>
</pre>
</div></div>

<p>Running the application gives us our start:</p>

<p><span class="image-wrap" style="display: block; text-align: center"><a class="confluence-thumbnail-link 1026x746" href='https://cwiki.apache.org/confluence/download/attachments/23340505/hilo-1.png'><img src="/confluence/download/thumbnails/23340505/hilo-1.png" style="border: 0px solid black" /></a></span></p>

<p>However, clicking the link doesn't do anything yet. </p>

<p>Let's fix that. </p>

<p>First: what should happen when the link is clicked?</p>

<ul>
	<li>A random target number between 1 and 10 should be selected</li>
	<li>The number of guesses taken should be reset to 0</li>
	<li>The user should be sent to the Guess page to make a guess</li>
</ul>


<p>Our first step is to find out when the user clicks that "start guessing" link.  In a typical web application framework, we might start thinking about URLs and handlers and maybe some XML configuration file.  But this is Tapestry, so we're going to work with components and methods on our classes.</p>

<p>First, the component.  We want to perform an action (selecting the number) before continuing on to the Guess page.  The ActionLink component is just what we need; it creates a link with a URL that will trigger an action event in our code ... but that's getting ahead of ourselves.  First up, convert the \&lt;a\&gt; tag to an ActionLink component:</p>

<div class="code panel" style="border-width: 1px;"><div class="codeHeader panelHeader" style="border-bottom-width: 1px;"><b>Index.tml (partial)</b></div><div class="codeContent panelContent">
<pre class="code-xml">
  <span class="code-tag">&lt;p&gt;</span>
    <span class="code-tag">&lt;t:actionlink t:id=<span class="code-quote">"start"</span>&gt;</span>start guessing<span class="code-tag">&lt;/t:actionlink&gt;</span>
  <span class="code-tag">&lt;/p&gt;</span>
</pre>
</div></div>

<p>If you refresh the browser, you'll see that the URL for the "start guessing" link is now <tt>/tutorial1/index.start</tt>}, which identifies the name of the page ("index") and the id of the component ("start").</p>

<p>If you click the link, you'll get an error:</p>

<p><span class="image-wrap" style="display: block; text-align: center"><a class="confluence-thumbnail-link 1026x746" href='https://cwiki.apache.org/confluence/download/attachments/23340505/hilo-index-missing-action-error.png'><img src="/confluence/download/thumbnails/23340505/hilo-index-missing-action-error.png" style="border: 0px solid black" /></a></span></p>

<p>Tapestry is telling us that we need to provide some kind of event handler for that event.  What does that look like?</p>

<p>An event handler is a method of the Java class with a special name. The name is <tt>on</tt><em>event-name</em><tt>From</tt><em>component-id</em> ... here we want a method named <tt>onActionFromStart()</tt>.  How do we know that "action" is the right event name?  Because that's what ActionLink does, that's why its named _Action_Link.</p>

<p>Once again, Tapestry gives us options; if you don't like naming conventions, there's an @OnEvent annotation you can place on the method instead, which restores the freedom to name the method as you like. Details about this approach are in the <a href="/confluence/display/TAPESTRY/Component+Events" title="Component Events">Tapestry Users' Guide</a>. We'll be sticking with the naming convention approach for the tutorial.</p>

<p>When handling a component event request (the kind of request triggered by the ActionLink component's URL), Tapestry will find the component and trigger a component event on it. This is the callback our server-side code needs to figure out what the user is doing on the client side.  Let's start with an empty event handler:</p>

<div class="code panel" style="border-width: 1px;"><div class="codeHeader panelHeader" style="border-bottom-width: 1px;"><b>Index.java</b></div><div class="codeContent panelContent">
<pre class="code-java">
<span class="code-keyword">package</span> com.example.tutorial.pages;

<span class="code-keyword">public</span> class Index
{
  void onActionFromStart()
  {

  }
}
</pre>
</div></div>

<p>In the browser, we can re-try the failed component event request by hitting the refresh button ... or we can restart the application.  In either case, we get the default behavior, which is simply to re-render the page.</p>

<p>Note that the event handler method does not have to be public; it can be protected, private, or package private (as in this example). By convention, such methods are package private, if for no other reason than it is the minimal amount of characters to type.</p>

<p>Hm. Right now you have to trust me that the method got invoked.  That's no good ... what's a quick way to tell for sure?  One way would be have the method throw an exception, but that's a bit ugly.</p>

<p>How about this: add the @<a href="http://tapestry.apache.org/current/apidocs/org/apache/tapestry5/annotations/Log.html" class="external-link" rel="nofollow">Log</a> annotation to the method:</p>

<div class="code panel" style="border-width: 1px;"><div class="codeHeader panelHeader" style="border-bottom-width: 1px;"><b>Index.java (partial)</b></div><div class="codeContent panelContent">
<pre class="code-java">
  @Log
  void onActionFromStart()
  {

  }
</pre>
</div></div>

<p>When you next click the link you should see the following in the Eclipse console:</p>

<div class="preformatted panel" style="border-width: 1px;"><div class="preformattedContent panelContent">
<pre>[DEBUG] pages.Index [ENTER] onActionFromStart()
[DEBUG] pages.Index [ EXIT] onActionFromStart
[INFO] AppModule.TimingFilter Request time: 3 ms
[INFO] AppModule.TimingFilter Request time: 5 ms
</pre>
</div></div>

<p>The @Log annotation directs Tapestry to log method entry and exit.  You'll get to see any parameters passed into the method, and any return value from the method ... as well as any exception thrown from within the method. It's a powerful debugging tool.  This is an example of Tapestry's meta-programming power, something we'll use quite a bit of in the tutorial.</p>

<p>Why do we see two requests for one click?  Tapestry uses an approach based on the <a href="http://en.wikipedia.org/wiki/Post/Redirect/Get" class="external-link" rel="nofollow">Redirect After Post</a> pattern. In fact, Tapestry performs a redirect after each component event. So the first request was to process the action, and the second request was to re-render the Index page. You can see this in the browser, because the URL is still "/tutorial1" (the URL for rendering the Index page).  We'll return to this in a bit.</p>

<p>We're ready for the next step, which involves tying together the Index and Guess pages. Index will select a target number for the user to Guess, then "pass the baton" to the Guess page.</p>

<p>Let's start by thinking about the Guess page. It needs a variable to store the target value in, and it needs a method that the Index page can invoke, to setup that target value.</p>

<div class="code panel" style="border-width: 1px;"><div class="codeHeader panelHeader" style="border-bottom-width: 1px;"><b>Guess.java</b></div><div class="codeContent panelContent">
<pre class="code-java">
<span class="code-keyword">package</span> com.example.tutorial.pages;

<span class="code-keyword">public</span> class Guess
{
  <span class="code-keyword">private</span> <span class="code-object">int</span> target;

  void setup(<span class="code-object">int</span> target)
  {
    <span class="code-keyword">this</span>.target = target;
  }
}
</pre>
</div></div>

<p>With that in mind, we can modify Index to invoke this new <tt>setup()</tt> method:</p>

<div class="code panel" style="border-width: 1px;"><div class="codeHeader panelHeader" style="border-bottom-width: 1px;"><b>Index.java (revised)</b></div><div class="codeContent panelContent">
<pre class="code-java">
<span class="code-keyword">package</span> com.example.tutorial.pages;

<span class="code-keyword">import</span> java.util.Random;

<span class="code-keyword">import</span> org.apache.tapestry5.annotations.InjectPage;
<span class="code-keyword">import</span> org.apache.tapestry5.annotations.Log;

<span class="code-keyword">public</span> class Index
{
  <span class="code-keyword">private</span> <span class="code-keyword">final</span> Random random = <span class="code-keyword">new</span> Random(<span class="code-object">System</span>.nanoTime());

  @InjectPage
  <span class="code-keyword">private</span> Guess guess;

  @Log
  <span class="code-object">Object</span> onActionFromStart()
  {
    <span class="code-object">int</span> target = random.nextInt(10) + 1;

    guess.setup(target);

    <span class="code-keyword">return</span> guess;
  }
}
</pre>
</div></div>

<p>The new event handler method now chooses the target number, and tells the Guess page about it. Because Tapestry is a managed environment, we don't just create an instance of Guess ... it is Tapestry's responsibility to manage the life cycle of the Guess page. Instead, we ask Tapestry for the Guess page, using the @InjectPage annotation. </p>

<div class='panelMacro'><table class='noteMacro'><colgroup><col width='24'><col></colgroup><tr><td valign='top'><img src="/confluence/images/icons/emoticons/warning.gif" width="16" height="16" align="absmiddle" alt="" border="0"></td><td>All fields in a Tapestry page or component class must be <b>private</b>.</td></tr></table></div>

<p>Once we have that Guess page instance, we can invoke methods on it normally.</p>

<p>Returning a page instance from an event handler method directs Tapestry to send a client-side redirect to the returned page, rather than sending a redirect for the active page. Thus once the user clicks the "start guessing" link, they'll see the Guess page.</p>

<div class='panelMacro'><table class='warningMacro'><colgroup><col width='24'><col></colgroup><tr><td valign='top'><img src="/confluence/images/icons/emoticons/forbidden.gif" width="16" height="16" align="absmiddle" alt="" border="0"></td><td>When creating your own applications, make sure that the objects stored in final variables are thread safe. It seems counter-intuitive, but final variables are shared across many threads. Ordinary instance variables are not. Fortunately, the implementation of Random is, in fact, thread safe.</td></tr></table></div>

<p>So ... let's click the link and see what we get:</p>

<p><span class="image-wrap" style="display: block; text-align: center"><a class="confluence-thumbnail-link 1026x746" href='https://cwiki.apache.org/confluence/download/attachments/23340505/guess-template-missing.png'><img src="/confluence/download/thumbnails/23340505/guess-template-missing.png" style="border: 0px solid black" /></a></span></p>

<p>Ah! We didn't create a Guess page template.  Tapestry was really expecting us to create one, so we better do so. Remember to create it as src/main/resources/com/examples/pages/Guess.tml.</p>

<div class="code panel" style="border-width: 1px;"><div class="codeHeader panelHeader" style="border-bottom-width: 1px;"><b>Guess.tml</b></div><div class="codeContent panelContent">
<pre class="code-xml">
&lt;html t:type=<span class="code-quote">"layout"</span> title=<span class="code-quote">"Guess The Number"</span>
  <span class="code-keyword">xmlns:t</span>=<span class="code-quote">"http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"</span>&gt;

  <span class="code-tag">&lt;p&gt;</span>
  The secret number is: ${target}.
  <span class="code-tag">&lt;/p&gt;</span>
  
<span class="code-tag">&lt;/html&gt;</span>
</pre>
</div></div>

<p>Hit the browser's back button, then click the "start guessing" link again. We're getting closer:</p>

<p><span class="image-wrap" style="display: block; text-align: center"><a class="confluence-thumbnail-link 1026x746" href='https://cwiki.apache.org/confluence/download/attachments/23340505/guess-no-target-prop.png'><img src="/confluence/download/thumbnails/23340505/guess-no-target-prop.png" style="border: 0px solid black" /></a></span></p>

<p>If you scroll down, you'll see the line of the Guess.tml template that has the error. We have a field named target, but it is private and there's no corresponding property, so Tapestry was unable to access it.</p>

<p>We just need to write the missing JavaBeans accessor methods <tt>getTarget()</tt> (and <tt>setTarget()</tt> for good measure).  Or we could let Tapestry do it instead:</p>

<div class="code panel" style="border-width: 1px;"><div class="codeContent panelContent">
<pre class="code-java">
  @Property
  <span class="code-keyword">private</span> <span class="code-object">int</span> target;
</pre>
</div></div>

<p>The @<a href="http://tapestry.apache.org/current/apidocs/org/apache/tapestry5/annotations/Property.html" class="external-link" rel="nofollow">Property</a> annotation very simply directs Tapestry to write the getter and setter method for you. You only need to do this if you are going to reference the field from the template.</p>

<p>We are getting very close but there's one last big oddity to handle. Once you refresh the page you'll see that target is 0!</p>

<p><span class="image-wrap" style="display: block; text-align: center"><a class="confluence-thumbnail-link 1026x746" href='https://cwiki.apache.org/confluence/download/attachments/23340505/guess-target-zero.png'><img src="/confluence/download/thumbnails/23340505/guess-target-zero.png" style="border: 0px solid black" /></a></span></p>

<p>What gives?  We know it was set to at least 1 ... where did the value go?</p>

<p>Welcome to Tapestry state management.  By default, at the end of each request, Tapestry wipes out the value in each instance variable.  So that means that target <em>was</em> a non-zero number during the component event request ... but by the time a new request comes up from the web browser to render the Guess page, the value of the target field has reverted back to its default, zero.</p>

<p>The solution here is to mark which fields have values that should persist from one request to the next (and next, and next ...).  That's what the @<a href="http://tapestry.apache.org/current/apidocs/org/apache/tapestry5/annotations/Persist.html" class="external-link" rel="nofollow">Persist</a> annotation is for:</p>

<div class="code panel" style="border-width: 1px;"><div class="codeContent panelContent">
<pre class="code-java">
  @Property  
  @Persist
  <span class="code-keyword">private</span> <span class="code-object">int</span> target;
</pre>
</div></div>

<p>This doesn't have anything to do with database persistence (that's coming up in a later chapter). It means that the value is stored in the HttpSession between requests.</p>

<p>Go back to the Index page and click the link again.  Finally, we have a target number:</p>

<p><span class="image-wrap" style="display: block; text-align: center"><a class="confluence-thumbnail-link 1026x746" href='https://cwiki.apache.org/confluence/download/attachments/23340505/guess-target.png'><img src="/confluence/download/thumbnails/23340505/guess-target.png" style="border: 0px solid black" /></a></span></p>

<p>That enough for us to get started. Let's build out the Guess page, and get ready to let the user make guesses. We'll show the count of guesses, and increment that count when they make them. We'll worry about high and low and actually selecting the correct value later.</p>

<p>You can go either way here; let's start with the markup in the template, then figure out what we need in the Java code to make it work.</p>

<div class="code panel" style="border-width: 1px;"><div class="codeHeader panelHeader" style="border-bottom-width: 1px;"><b>Guess.tml (revised)</b></div><div class="codeContent panelContent">
<pre class="code-xml">
&lt;html t:type=<span class="code-quote">"layout"</span> title=<span class="code-quote">"Guess The Number"</span>
  <span class="code-keyword">xmlns:t</span>=<span class="code-quote">"http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"</span>
  <span class="code-keyword">xmlns:p</span>=<span class="code-quote">"tapestry:parameter"</span>&gt;

  <span class="code-tag">&lt;p:sidebar&gt;</span>
    <span class="code-tag">&lt;p&gt;</span>
      The secret number is: ${target}.
  <span class="code-tag">&lt;/p&gt;</span>
  <span class="code-tag">&lt;/p:sidebar&gt;</span>

  <span class="code-tag">&lt;strong&gt;</span>Guess #${guessCount}<span class="code-tag">&lt;/strong&gt;</span>

  <span class="code-tag">&lt;p&gt;</span>Make a guess from the options below:<span class="code-tag">&lt;/p&gt;</span>

  <span class="code-tag">&lt;ul&gt;</span>
    <span class="code-tag">&lt;t:loop source=<span class="code-quote">"1..10"</span> value=<span class="code-quote">"current"</span>&gt;</span>
      <span class="code-tag">&lt;li&gt;</span>
        <span class="code-tag">&lt;t:actionlink t:id=<span class="code-quote">"makeGuess"</span> context=<span class="code-quote">"current"</span>&gt;</span>${current}
        <span class="code-tag">&lt;/t:actionlink&gt;</span>
      <span class="code-tag">&lt;/li&gt;</span>
    <span class="code-tag">&lt;/t:loop&gt;</span>
  <span class="code-tag">&lt;/ul&gt;</span>

<span class="code-tag">&lt;/html&gt;</span>
</pre>
</div></div>

<p>So it looks like we need a <tt>guessCount</tt> property that starts at 1.</p>

<p>We're also seeing one new component, the Loop component. A Loop component iterates over the values passed to it in its <tt>source</tt> parameter, and renders it body once for each value. It updates the property bound to its <tt>value</tt> parameter before rendering its body.</p>

<p>That special property expression, <tt>1..10</tt>, generates a series of numbers from 1 to 10, inclusive. usually, when you use the Loop component, you are iterating over a List or Collection of values, such as the results of a database query.</p>

<p>So, the Loop component is going to set the <tt>current</tt> property to 1, and render its body (the \&lt;li\&gt; tag, and the ActionLink component).  Then its going to set the <tt>current</tt> property to 2 and render its body again ... all the way up to 10.</p>

<p>And notice what we're doing with the ActionLink component; its no longer enough to know the user clicked on the ActionLink ... we need to know <em>which iteration</em> the user clicked on. The <tt>context</tt> parameter allows a value to be added to the ActionLink's URL, and we can get it back in the event handler method.</p>

<div class='panelMacro'><table class='infoMacro'><colgroup><col width='24'><col></colgroup><tr><td valign='top'><img src="/confluence/images/icons/emoticons/information.gif" width="16" height="16" align="absmiddle" alt="" border="0"></td><td>The URL for the ActionLink will be <tt>/tutorial1/guess.makeguess/3</tt>. That's the page name, "Guess", the component id, "makeGuess", and the context value, "3".</td></tr></table></div>

<div class="code panel" style="border-width: 1px;"><div class="codeHeader panelHeader" style="border-bottom-width: 1px;"><b>Guess.java (revised)</b></div><div class="codeContent panelContent">
<pre class="code-java">
<span class="code-keyword">package</span> com.example.tutorial.pages;

<span class="code-keyword">import</span> org.apache.tapestry5.annotations.Persist;
<span class="code-keyword">import</span> org.apache.tapestry5.annotations.Property;

<span class="code-keyword">public</span> class Guess
{
  @Property
  @Persist
  <span class="code-keyword">private</span> <span class="code-object">int</span> target, guessCount;

  @Property
  <span class="code-keyword">private</span> <span class="code-object">int</span> current;

  void setup(<span class="code-object">int</span> target)
  {
    <span class="code-keyword">this</span>.target = target;
    guessCount = 1;
  }

  void onActionFromMakeGuess(<span class="code-object">int</span> value)
  {
    guessCount++;
  }

}
</pre>
</div></div>

<p>The revised version of Guess includes two new properties: <tt>current</tt> and <tt>guessCount</tt>. There's also a handler for the action event from the makeGuess ActionLink component; currently it just increments the count.</p>

<p>Notice that the <tt>onActionFromMakeGuess()</tt> method now has a parameter: the context value that was encoded into the URL by the ActionLink. When then user clicks the link, Tapestry will automatically extract the string from the URL,  convert it to an int and pass that int value into the event handler method.  More boilerplate code you don't have to write.</p>

<p>At this point, the page is partially operational:</p>

<p><span class="image-wrap" style="display: block; text-align: center"><a class="confluence-thumbnail-link 1026x746" href='https://cwiki.apache.org/confluence/download/attachments/23340505/guess-1.png'><img src="/confluence/download/thumbnails/23340505/guess-1.png" style="border: 0px solid black" /></a></span></p>



<div class='panelMacro'><table class='warningMacro'><colgroup><col width='24'><col></colgroup><tr><td valign='top'><img src="/confluence/images/icons/emoticons/forbidden.gif" width="16" height="16" align="absmiddle" alt="" border="0"></td><td>From here on is unfinished and out of date.  Keep checking back!</td></tr></table></div>



<p>On the Java side, the Guess page needs to have a target property:</p>

<div class="code panel" style="border-width: 1px;"><div class="codeContent panelContent">
<pre class="code-java">
<span class="code-keyword">package</span> org.apache.tapestry5.tutorial.pages;

<span class="code-keyword">public</span> class Guess
{
  <span class="code-keyword">private</span> <span class="code-object">int</span> target;

  <span class="code-object">Object</span> initialize(<span class="code-object">int</span> target)
  {
    <span class="code-keyword">this</span>.target = target;

    <span class="code-keyword">return</span> <span class="code-keyword">this</span>;
  }
}
</pre>
</div></div>

<p>The key method here is initialize(): It is invoked to tell the Guess page what the target number is. Notice that the method is package private, not public; it is only expected to be invoked from the Index page (as we'll see in a moment), so there's no need to make it public. Later we'll see that there's more initialization to be done than just storing a value into the target instance variable (which is why we don't simply name the method setTarget() ).</p>

<p>Now we can move back to the Index page. What we want is to have the ActionLink component invoke a method on the Index page. We can then generate a random target number. We'll tell the Guess page what the target number is and then make sure that it is the Guess page, and not the Index page, that renders the response into the user's web browser. That's actually quite a few concepts to take in all at once.</p>

<p>Let's start with the code, and break it down:</p>

<div class="code panel" style="border-width: 1px;"><div class="codeHeader panelHeader" style="border-bottom-width: 1px;"><b>src/main/java/org/apache/tapestry5/tutorial/pages/Index.java</b></div><div class="codeContent panelContent">
<pre class="code-java">
<span class="code-keyword">package</span> org.apache.tapestry5.tutorial.pages;

<span class="code-keyword">import</span> java.util.Random;

<span class="code-keyword">import</span> org.apache.tapestry5.annotations.InjectPage;

<span class="code-keyword">public</span> class Start
{
  <span class="code-keyword">private</span> <span class="code-keyword">final</span> Random random = <span class="code-keyword">new</span> Random();

  @InjectPage
  <span class="code-keyword">private</span> Guess guess;

  <span class="code-object">Object</span> onAction()
  {
    <span class="code-object">int</span> target = random.nextInt(10) + 1;

    <span class="code-keyword">return</span> guess.initialize(target);
  }
}
</pre>
</div></div>

<p>What we're talking about here is <em>communication</em> of information from the Index page to the Guess page. In traditional servlet development, this is done in a bizarre way ... storing attributes into the shared HttpSession object. Of course, for that to work, both (or all) parties have to agree on the type of object stored, and the well-known name used to access the attribute. That's the source of a large number of bugs. It's also not very object oriented ... state is something that should be <em>inside</em> objects (and private), not <em>outside</em> objects (and public).</p>

<p>The Tapestry way is very object oriented: everything is done in terms of objects and methods and properties of those objects.</p>

<p>This communication starts with the connection between the two pages: in this case, the <tt>@InjectPage</tt> annotation allows another page in the application to be injected into the Index page.</p>

<p>Injection can be a somewhat nebulous concept. In terms of Tapestry, it means that some cooperating object needed by one class is provided to it. The other object is often referred to as a "dependency"; in this case, the Index page <em>depends on</em> the Guess page to be fully functional, and an instance of the Guess page is made available as the guess instance variable. The Index page doesn't, and can't, <em>create</em> the Guess page, it can only advertise, to Tapestry, that it needs the Guess page. Tapestry will take care of the rest.</p>

<p>Let's see what we do with this injected page. It's used inside onAction(). You might guess that this method is invoked when the link ("Start guessing") is clicked. But why?</p>

<p>This is a strong example of <em>convention over configuration</em>. Tapestry has a naming convention for certain methods: "on<em>EventType</em>From<em>ComponentId</em>". Here, the event type is "action" and the component id is not even specified. This translates to "when the action event is fired from any component, invoke this method".</p>

<p>"The action event?" This underlines a bit about how Tapestry processes requests. When you click a link generated by the ActionLink component, Tapestry is able to identify the underlying component inside the request: it knows that the component is on the Index page, and it knows the component within the page. Here we didn't give the ActionLink component a specific id, so Tapestry supplied one. An "action" event is triggered inside the ActionLink component, and that event bubbles up to the page, where the onAction() method acts as an <em>event handler method</em>.</p>

<p>So ... ActionLink component &#45;&#45;&gt; action request &#45;&#45;&gt; onAction() event handler method.</p>

<p>Event handler methods don't have to be public; they are usually package private (as in this example). Also, it isn't an error if a request never matches an event handler. Before we added the onAction() event handler, that's exactly what happened; the request passed through without any event handler match, and Tapestry simply re-rendered the Start page.</p>

<p>What can you do inside an event handler method? Any kind of business logic you like; Tapestry doesn't care. Here we're using a random number generator to set the target number to guess.</p>

<p>We also use the injected Guess page; we invoke the initialize() method to tell it about the number the user is trying to guess.</p>

<p>The <em>return value</em> of an event handler method is very important; the value returned informs Tapestry about what page will render the response to the client. By returning the injected Guess page, we're telling Tapestry that the Guess page should be the one to render the response.</p>

<p>This idiom: having the Guess page provide an initialize() method and return itself, is very common in Tapestry. It allows the event handler method to be very succinct; it's as if the event handler method says "initialize the Guess page and render it to the client".</p>

<p>Again, this is a big difference between Tapestry and servlets (or Struts). Tapestry tightly binds the controller (the Java class) to the template. Using JSPs, you would have extra configuration to select a view (usually by a logical name, such as "success") to a "view" (a JSP). Tapestry cuts through all that cruft for you. Objects communicate with, and defer to, other objects and all the templates and rendering machinery comes along for free.</p>

<p>In later chapters, we'll see other possibilities besides returning a page instance from an event handler method.</p>

<p>For the moment, make sure all the changes are saved, and click the "Start guessing" link.</p>

<p><span class="image-wrap" style=""><img src="/confluence/download/attachments/23340505/hilo-exception.png?version=1&amp;modificationDate=1286828602000" height="686" width="761" style="border: 1px solid black" /></span></p>

<p>Exception on the Guess page</p>

<p>This may not quite be what you were expecting ... but it is a useful digression into one of Tapestry's most important features: <b>feedback</b>.</p>

<p>Something was wrong with the Guess page, and Tapestry has reported the error to you so that you can make a correction.</p>

<p>Here, the root problem was that we didn't define a getTarget() method in the Guess class. Ooops. Deep inside Tapestry, a RuntimeException was thrown to explain this.</p>

<p>As often happens in frameworks, that RuntimeException was caught and rethrown wrapped inside a new exception, the TapestryException. This added a bit more detail to the exception message, and linked the exception to a <em>location</em>. Since the error occurred inside a component template, Tapestry is able to display that portion of the template, highlighting the line in error.</p>

<p>If you scroll down, you'll see that after the stack trace, Tapestry provides a wealth of information about the current request, including headers and query parameters. It also displays information stored in the HttpSession (if the session exists), and other information that may be of use.</p>

<p>Of course, in a production application, this information can be hidden&#33;</p>

<p>Let's fix this problem, by adding the following to the Guess class:</p>

<div class="code panel" style="border-width: 1px;"><div class="codeContent panelContent">
<pre class="code-java">
  <span class="code-keyword">public</span> <span class="code-object">int</span> getTarget()
  {
    <span class="code-keyword">return</span> target;
  }
</pre>
</div></div>

<h3><a name="ImplementingtheHi-LoGuessingGame-Persistingdatabetweenrequests"></a>Persisting data between requests</h3>

<p>That fixes the problem, but introduces another:</p>

<p><span class="image-wrap" style=""><img src="/confluence/download/attachments/23340505/hilo-guess-v1.png?version=1&amp;modificationDate=1286828603000" style="border: 1px solid black" /></span></p>

<p>Hi/Lo Guess Page</p>

<p>Why is the target number zero? Didn't we set it to a random value between 1 and 10?</p>

<p>At issue here is the how Tapestry organizes requests. Tapestry has two main types of requests: <b>action</b> requests and <b>render</b> requests. Render requests are easy, the URL includes just the page name, and that page is rendered out.</p>

<p>Action requests are more complicated; the URL will include the name of the page and the id of the component within the page, and perhaps the type of event.</p>

<p>After your event handler method is executed, Tapestry determine what page will render the response; as we've seen, that is based on the return value of the event handler method.</p>

<p>Tapestry doesn't, however, render the response directly, the way most servlet applications would; instead it sends a <em>redirect URL</em> to the client web browser. The URL is a render request URL for the page that will render the response.</p>

<p>You may have seen this before. It is commonly called the <em>redirect after post pattern</em>. Most often, it is associated with form submissions (and as we'll see in later chapters, a form submission <em>is</em> another type of action request).</p>

<p>So why does that affect the target value? At the end of any request (action or render), Tapestry will "clean house", resetting any instance variables back to their initial, default values (usually, null or zero).</p>

<p>This cleaning is very necessary to the basic way Tapestry operates: pages are expensive entities to create; too expensive to create fresh each request, and too large and complicated to store in the HttpSession. Tapestry <em>pools</em> pages, using and reusing them in request after request.</p>

<p>For the duration of a single request from a single user, a <em>page instance</em> is <em>bound</em> to the request. It is only accessible to the one request. Other requests may be bound to other instances of the same page. The same page instance will be used for request after request.</p>

<p>So, inside the action request, the code inside the onAction() event handler method <em>did</em> call the initialize() method, and a value between 1 and 10 was stored in the target instance variable. But at the end of that request, the value was lost, and in the subsequent render request for the Guess page, the value was zero.</p>

<p>Fortunately, it is very easy to transcend this behavior. We'll use an annotation, Persist, on the instance variable:</p>

<div class="code panel" style="border-width: 1px;"><div class="codeContent panelContent">
<pre class="code-java">
  @Persist
  <span class="code-keyword">private</span> <span class="code-object">int</span> target;
</pre>
</div></div>

<p>Now we can use the browser back button to return to the Start page, and click the link again.</p>

<p><span class="image-wrap" style=""><img src="/confluence/download/attachments/23340505/hilo-number.png?version=1&amp;modificationDate=1286828603000" style="border: 1px solid black" /></span></p>

<p>The target number</p>

<p>One of the nice things about this approach, the use of redirects, is that hitting the refresh button does <em>not</em> choose a new target number. It simply redraws the Guess page with the target number previously selected. In many servlet applications, the URL would be for the action "choose a random number" and refreshing would re-execute that action.</p>

<h3><a name="ImplementingtheHi-LoGuessingGame-Creatingguessablelinks"></a>Creating guessable links</h3>

<p>Now it's time to start the game in earnest. We don't want to just tell the user what the target number is, we want to make them guess, and we want to track how many attempts they take.</p>

<p>What we want is to create 10 links, and combine those links with logic on the server side, an event handler method, that can interpret what value the user selected.</p>

<p>Let's start with those links. We're going to use a new component, Loop, to loop over a set of values:</p>

<div class="code panel" style="border-width: 1px;"><div class="codeHeader panelHeader" style="border-bottom-width: 1px;"><b>src/main/webapp/Guess.tml</b></div><div class="codeContent panelContent">
<pre class="code-xml">
<span class="code-tag">&lt;html <span class="code-keyword">xmlns:t</span>=<span class="code-quote">"http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"</span>&gt;</span>
  <span class="code-tag">&lt;head&gt;</span>
    <span class="code-tag">&lt;title&gt;</span>Guess A Number<span class="code-tag">&lt;/title&gt;</span>
  <span class="code-tag">&lt;/head&gt;</span>
  <span class="code-tag">&lt;body&gt;</span>

  <span class="code-tag">&lt;p&gt;</span>Make a guess between one and ten:<span class="code-tag">&lt;/p&gt;</span>

    <span class="code-tag">&lt;t:loop source=<span class="code-quote">"1..10"</span> value=<span class="code-quote">"guess"</span> xml:space=<span class="code-quote">"preserve"</span>&gt;</span>
      <span class="code-tag">&lt;t:actionlink t:id=<span class="code-quote">"link"</span> context=<span class="code-quote">"guess"</span>&gt;</span>${guess}<span class="code-tag">&lt;/t:actionlink&gt;</span>
    <span class="code-tag">&lt;/t:loop&gt;</span>

  <span class="code-tag">&lt;/body&gt;</span>
<span class="code-tag">&lt;/html&gt;</span>
</pre>
</div></div>

<p>The Loop component's source attribute identifies the values to loop over. Often this is a list or array, but here the special special syntax, "1..10" means iterate over the numbers between 1 and 10, inclusive.</p>

<p>What about the <tt>xml:space="preserve"</tt> attribute? Normally, Tapestry is pretty draconian about stripping out unnecessary whitespace from the template. Most whitespace (spaces, tabs, newlines, etc.) is reduced to a single space. This can help a lot with reducing the size of the output and with making complex nested layouts easier to read ... but occasionally, as here, the whitespace is needed to keep the numbers from running together. <tt>xml:space="preserve"</tt> turns on full whitespace retention for the element.</p>

<p>The value attribute gets assigned the current item from the loop. We'll use a property of the Guess page as a kind of scratchpad for this purpose. We could manually write a getter and a setter method as we did before, or we can let Tapestry generate those accessors:</p>

<div class="code panel" style="border-width: 1px;"><div class="codeContent panelContent">
<pre class="code-java">
  @Property
  <span class="code-keyword">private</span> <span class="code-object">int</span> guess;
</pre>
</div></div>

<p>Tapestry will automatically create the methods needed so that the guess property (it's smart about stripping off leading underscores) is accessible in the template.</p>

<p>The context parameter of the ActionLink is how we get extra information into the action request URL. The context can be a single value, or an array or list of values. The values are converted to strings and tacked onto the action request URL. The end result is <a href="http://localhost:8080/tutorial1/guess.link/4" class="external-link" rel="nofollow">http://localhost:8080/tutorial1/guess.link/4</a>.</p>

<p>What is "guess.link"? That's the name of the page, "guess", and the id of the component ("link", as explicitly set with the t:id attribute). Remember this is an action link: as soon as the user clicks the click, it is replaced with a render link such as <a href="http://localhost:8080/tutorial1/guess" class="external-link" rel="nofollow">http://localhost:8080/tutorial1/guess</a>.</p>

<p>Now, to handle those guesses. We're going to add an event handler method that gets invoked when a link is clicked. We're also going to add a new property, message, to store the message that says "too high" or "too low".</p>

<div class="code panel" style="border-width: 1px;"><div class="codeContent panelContent">
<pre class="code-java">
  @Persist
  @Property
  <span class="code-keyword">private</span> <span class="code-object">String</span> message;

  <span class="code-object">String</span> onActionFromLink(<span class="code-object">int</span> guess)
  {
    <span class="code-keyword">if</span> (guess == target) <span class="code-keyword">return</span> <span class="code-quote">"GameOver"</span>;

    <span class="code-keyword">if</span> (guess &lt; target)
      message = <span class="code-object">String</span>.format(<span class="code-quote">"%d is too low."</span>, guess);
    <span class="code-keyword">else</span>
      message = <span class="code-object">String</span>.format(<span class="code-quote">"%d is too high."</span>, guess);

    <span class="code-keyword">return</span> <span class="code-keyword">null</span>;
  }
</pre>
</div></div>

<p>Here's the big news: Tapestry will convert the number from the URL back into an integer automatically, so that it can pass it in to the onActionFromLink() event handler method as a method parameter. We can then compare the guess from the user to the secret target number.</p>

<p>Notice how Tapestry adapts to the return value. Here it may be null ... Tapestry interprets that as "stay on the same page". You may also return a string ("GameOver"); Tapestry interprets <em>that</em> as the name of the page to render the response.</p>

<p>We need to update the Guess page to actually display the message; this is done by adding the following:</p>

<div class="code panel" style="border-width: 1px;"><div class="codeContent panelContent">
<pre class="code-html">
  <span class="code-tag">&lt;p&gt;</span>${message}<span class="code-tag">&lt;/p&gt;</span>
</pre>
</div></div>

<p>This is truly bare bones and, when message is null, will output an empty &lt;p&gt; element. A real application would dress this up a bit more (using CSS and the like to make it prettier).</p>

<p>We do need a basic GameOver page:</p>

<div class="code panel" style="border-width: 1px;"><div class="codeHeader panelHeader" style="border-bottom-width: 1px;"><b>src/main/webapp/GameOver.tml</b></div><div class="codeContent panelContent">
<pre class="code-xml">
<span class="code-tag">&lt;html <span class="code-keyword">xmlns:t</span>=<span class="code-quote">"http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"</span>&gt;</span>
  <span class="code-tag">&lt;head&gt;</span>
    <span class="code-tag">&lt;title&gt;</span>Game Over!<span class="code-tag">&lt;/title&gt;</span>
  <span class="code-tag">&lt;/head&gt;</span>
  <span class="code-tag">&lt;body&gt;</span>

    <span class="code-tag">&lt;h1&gt;</span>Game Over<span class="code-tag">&lt;/h1&gt;</span>

    <span class="code-tag">&lt;p&gt;</span> You guessed the secret number!  <span class="code-tag">&lt;/p&gt;</span>


  <span class="code-tag">&lt;/body&gt;</span>
<span class="code-tag">&lt;/html&gt;</span>
</pre>
</div></div>

<div class="code panel" style="border-width: 1px;"><div class="codeHeader panelHeader" style="border-bottom-width: 1px;"><b>src/main/java/org/apache/tapestry5/tutorial/pages/GameOver.java</b></div><div class="codeContent panelContent">
<pre class="code-java">
<span class="code-keyword">package</span> org.apache.tapestry5.tutorial.pages;

<span class="code-keyword">public</span> class GameOver
{

}
</pre>
</div></div>

<p>With this in place, we can make guesses, and get feedback from the application:</p>

<p><span class="image-wrap" style=""><img src="/confluence/download/attachments/23340505/hilo-feedback.png?version=1&amp;modificationDate=1286828602000" style="border: 1px solid black" /></span></p>

<p>Feedback from the game</p>

<h3><a name="ImplementingtheHi-LoGuessingGame-Countingthenumberofguesses"></a>Counting the number of guesses</h3>

<p>It would be nice to provide some feedback about how many guesses the user took to find the number. That's easy enough to do.</p>

<p>First we update Guess to store the number of guesses:</p>

<div class="code panel" style="border-width: 1px;"><div class="codeContent panelContent">
<pre class="code-java">
  @Persist
  @Property
  <span class="code-keyword">private</span> <span class="code-object">int</span> count;
</pre>
</div></div>

<p>Next we modified initialize() to ensure that count is set to 0. This is a safety precaution in case we add logic to play the game again.</p>

<div class="code panel" style="border-width: 1px;"><div class="codeContent panelContent">
<pre class="code-java">
  <span class="code-object">Object</span> initialize(<span class="code-object">int</span> target)
  {
    <span class="code-keyword">this</span>.target = target;
    <span class="code-keyword">this</span>.count = 0;

    <span class="code-keyword">return</span> <span class="code-keyword">this</span>;
  }
</pre>
</div></div>

<p>We have a couple of changes to make to the event handler method. We want to communicate to the GameOver page the guess count; so we'll inject the GameOver page so we can initialize it.</p>

<div class="code panel" style="border-width: 1px;"><div class="codeContent panelContent">
<pre class="code-java">
  <span class="code-object">Object</span> onActionFromLink(<span class="code-object">int</span> guess)
  {
    count++;

    <span class="code-keyword">if</span> (guess == target) <span class="code-keyword">return</span> gameOver.initialize(count);

    <span class="code-keyword">if</span> (guess &lt; target)
      message = <span class="code-object">String</span>.format(<span class="code-quote">"%d is too low."</span>, guess);
    <span class="code-keyword">else</span>
      message = <span class="code-object">String</span>.format(<span class="code-quote">"%d is too high."</span>, guess);

    <span class="code-keyword">return</span> <span class="code-keyword">null</span>;
  }
</pre>
</div></div>

<p>So, we update the count before comparing and, instead of returning the name of the GameOver page, we return the configured instance.</p>

<p>Lastly, we need to make some changes to the GameOver class.</p>

<div class="code panel" style="border-width: 1px;"><div class="codeHeader panelHeader" style="border-bottom-width: 1px;"><b>src/main/java/org/apache/tapestry5/tutorial/GameOver.java</b></div><div class="codeContent panelContent">
<pre class="code-java">
<span class="code-keyword">package</span> org.apache.tapestry5.tutorial.pages;

<span class="code-keyword">import</span> org.apache.tapestry5.annotations.Persist;
<span class="code-keyword">import</span> org.apache.tapestry5.annotations.Property;

<span class="code-keyword">public</span> class GameOver
{
  @Persist
  @Property
  <span class="code-keyword">private</span> <span class="code-object">int</span> count;

  <span class="code-object">Object</span> initialize(<span class="code-object">int</span> count)
  {
    <span class="code-keyword">this</span>.count = count;

    <span class="code-keyword">return</span> <span class="code-keyword">this</span>;
  }
}
</pre>
</div></div>

<div class="code panel" style="border-width: 1px;"><div class="codeHeader panelHeader" style="border-bottom-width: 1px;"><b>src/main/webapp/GameOver.tml</b></div><div class="codeContent panelContent">
<pre class="code-java">
&lt;html xmlns:t=<span class="code-quote">"http:<span class="code-comment">//tapestry.apache.org/schema/tapestry_5_1_0.xsd"</span>&gt;
</span>  &lt;head&gt;
    &lt;title&gt;Game Over!&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;

    &lt;h1&gt;Game Over&lt;/h1&gt;

    &lt;p&gt; You guessed the secret number in ${count} guesses! &lt;/p&gt;

  &lt;/body&gt;
&lt;/html&gt;
</pre>
</div></div>

<h2><a name="ImplementingtheHi-LoGuessingGame-Pausingtoreflect"></a>Pausing to reflect</h2>

<p>What we've gone after here is the Tapestry way: pages as classes that store internal state and communicate with each other. We've also seen the Tapestry development pattern: lots of simple small steps that leverage Tapestry's ability to reload templates and classes on the fly.</p>

<p>We've also seen how Tapestry stores data for us, sometimes in the session (via the @Persist annotation) and sometimes in the URL.</p>

<p>Our code is wonderfully free of anything related to HTTP or the Java Servlet API. We're coding using real objects, with their own instance variables and internal state.</p>

<p>Our application is still pretty simple; here's a few challenges:</p>

<ul>
	<li>Add a restart link to the GameOver page to allow a new game to start. Can you refactor the application so that the code for the random number selection occurs in only one place?</li>
	<li>As we guess, we're identifying ranges of valid and invalid numbers. Can you only show valid guesses to the user?</li>
	<li>What would it take to change the the game to choose a number between 1 and 20? Between 1 and 100?</li>
	<li>What about setting an upper-limit on the number of guesses allowed?</li>
</ul>



<style type='text/css'>/*<![CDATA[*/
table.ScrollbarTable  {border: none;padding: 3px;width: 100%;padding: 3px;margin: 0px;background-color: #f0f0f0}
table.ScrollbarTable td.ScrollbarPrevIcon {text-align: center;width: 16px;border: none;}
table.ScrollbarTable td.ScrollbarPrevName {text-align: left;border: none;}
table.ScrollbarTable td.ScrollbarParent {text-align: center;border: none;}
table.ScrollbarTable td.ScrollbarNextName {text-align: right;border: none;}
table.ScrollbarTable td.ScrollbarNextIcon {text-align: center;width: 16px;border: none;}

/*]]>*/</style><div class="Scrollbar"><table class='ScrollbarTable'><tr><td class='ScrollbarPrevIcon'><a href="/confluence/display/TAPESTRY/Exploring+the+Project"><img border='0' align='middle' src='/confluence/images/icons/back_16.gif' width='16' height='16'></a></td><td width='33%' class='ScrollbarPrevName'><a href="/confluence/display/TAPESTRY/Exploring+the+Project">Exploring the Project</a>&nbsp;</td><td width='33%' class='ScrollbarParent'><sup><a href="/confluence/display/TAPESTRY/Tapestry+Tutorial"><img border='0' align='middle' src='/confluence/images/icons/up_16.gif' width='8' height='8'></a></sup><a href="/confluence/display/TAPESTRY/Tapestry+Tutorial">Tapestry Tutorial</a></td><td width='33%' class='ScrollbarNextName'>&nbsp;<a href="/confluence/display/TAPESTRY/Forms">Forms</a></td><td class='ScrollbarNextIcon'><a href="/confluence/display/TAPESTRY/Forms"><img border='0' align='middle' src='/confluence/images/icons/forwd_16.gif' width='16' height='16'></a></td></tr></table></div>
    </div>
        <div id="commentsSection" class="wiki-content pageSection">
        <div style="float: right;">
            <a href="https://cwiki.apache.org/confluence/users/viewnotifications.action" class="grey">Change Notification Preferences</a>
        </div>
        <a href="https://cwiki.apache.org/confluence/display/TAPESTRY/Implementing+the+Hi-Lo+Guessing+Game">View Online</a>
        |
        <a href="https://cwiki.apache.org/confluence/pages/diffpagesbyversion.action?pageId=23340505&revisedVersion=10&originalVersion=9">View Changes</a>
            </div>
</div>
</div>
</div>
</div>
</body>
</html>

Mime
View raw message