blob: b5e9c302d6b61b59c94b755d3021100cdce9680a [file] [log] [blame]
---
Implementing the Hi/Lo Guessing Game
---
Chapter 3: Implementing the Hi/Lo Guessing Game
Let's start building the Hi/Lo Guessing game.
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.
We'll build it in small pieces, using the kind of iterative development
that Tapestry makes so easy.
[hilo-flow.png] Page flow for Hi/Lo Game
Our page flow is very simple, consisting of three pages: Start, Guess and GameOver. The Start 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.
Let's get to work on the Index page and template.
<<src/main/webapp/Index.tml:>>
----
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<head>
<title>tutorial1 Start Page</title>
</head>
<body>
<h1>Hi/Lo Guess</h1>
<p>I'm thinking of a number between one and ten ... </p>
<p>
<t:actionlink>Start guessing</t:actionlink>
</p>
</body>
</html>
----
Here we've taken the template created by the quickstart archetype and changed it around to fit our needs. The ActionLink
component will create a link that will trigger a method inside our Java class. You can launch the application to try it out:
[hilo-start.png] Start page with Hi/Lo Game link
However, clicking the link doesn't do anything yet. We haven't told Tapestry what to do when the link gets clicked.
Let's fix that. We'll change the Start class so that it will react when the link is clicked ... but what should it do?
Well, to start the guessing process, we need to come up with a random number (between one and ten). We need to tell
the Guess page about that number, and we need to make sure the Guess page is started up to display the response.
First, the Guess page. Just to get started, we'll create a Guess page without much guessing: it'll just show us the
target number, the number we're supposed to be guessing.
<<src/main/webapp/Guess.tml:>>
----
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<head>
<title>Guess A Number</title>
</head>
<body>
<h1>The target number is ${target}.</h1>
</body>
</html>
----
On the Java side, the Guess page needs to have a target property:
----
package org.apache.tapestry.tutorial.pages;
public class Guess
{
private int target;
Object initialize(int target)
{
this.target = target;
return this;
}
}
----
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() ).
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.
Let's start with the code, and break it down:
<<src/main/java/org/apache/tapestry/tutorial/pages/Index.java>>
----
package org.apache.tapestry.tutorial.pages;
import java.util.Random;
import org.apache.tapestry.annotation.InjectPage;
public class Start
{
private final Random random = new Random();
@InjectPage
private Guess guess;
Object onAction()
{
int target = random.nextInt(10) + 1;
return guess.initialize(target);
}
}
----
What we're talking about here is <communication> 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 <inside> objects (and private), not <outside> objects (and public).
The Tapestry way is very object oriented: everything is done in terms of objects and methods and properties of those objects.
This communication starts with the connection between the two pages: in this case, the
{{{../apidocs/org/apache/tapestry/annotations/InjectPage.html}InjectPage}} annotation allows another page in the application
to be injected into the Index page.
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 <depends on> 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, <create> the Guess page, it can only
advertise, to Tapestry, that it needs the Guess page. Tapestry will take care of the rest.
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?
This is a strong example of <convention over configuration>. Tapestry has a naming convention for certain methods: "on<EventType>[From<ComponentId>]". 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".
"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 <event handler method>.
So ... ActionLink component --> action request --> onAction() event handler method.
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.
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.
We also use the injected Guess page; we invoke the initialize() method to tell it about the number the user is trying to guess.
The <return value> 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.
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".
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.
In later chapters, we'll see other possibilities besides returning a page instance from an event handler method.
For the moment, make sure all the changes are saved, and click the "Start guessing" link.
[hilo-exception.png] Exception on the Guess page
This may not quite be what you were expecting ... but it is a useful digression into one of Tapestry's most important features: <<feedback>>.
Something was wrong with the Guess page, and Tapestry has reported the error to you so that you can make a correction.
Here, the root problem was that we didn't define a getTarget() method in the Guess class. Ooops. Deep inside Tapestry, a RuntmeException was thrown to explain this.
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 <location>. Since the error occurred inside a component template, Tapestry is able to display that portion of the
template, highlighting the line in error.
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.
Of course, in a production application,
this information can be hidden!
Let's fix this problem, by adding the following to the Guess class:
---
public int getTarget()
{
return target;
}
---
* Persisting data between requests
That fixes the problem, but introduces another:
[hilo-guess-v1.png] Hi/Lo Guess Page
Why is the target number zero? Didn't we set it to a random value between 1 and 10?
At issue here is the how Tapestry organizes requests. Tapestry has two main types
of requests: <<action>> requests and <<render>> requests. Render requests
are easy, the URL includes just the page name, and that page is rendered out.
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.
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.
Tapestry doesn't, however, render the response directly, the way most servlet
applications would; instead it sends a <redirect URL> to the client web browser.
The URL is a render request URL for the page that will render the response.
You may have seen this before. It is commonly called the <redirect after post pattern>. Most often,
it is associated with form submissions (and as we'll see in later chapters, a form submission <is>
another type of action request).
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).
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 <pools> pages, using and reusing them in request after request.
For the duration of a single request from a single user, a <page instance> is <bound> 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.
So, inside the action request, the code inside the onAction() event handler method <did> 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.
Fortunately, it is very easy to transcend this behavior. We'll use an annotation, {{{../apidocs/org/apache/tapestry/annotations/Persist.html}Persist}},
on the instance variable:
----
@Persist
private int target;
----
Now we can use the browser back button to return to the Start page, and click the link again.
[hilo-number.png] The target number
One of the nice things about this approach, the use of redirects, is that hitting the refresh button
does <not> 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.
* Creating guessable links
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.
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.
Let's start with those links. We're going to use a new component, Loop, to loop
over a set of values:
<<src/main/webapp/Guess.tml:>>
---
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<head>
<title>Guess A Number</title>
</head>
<body>
<p>Make a guess between one and ten:</p>
<t:loop source="1..10" value="guess" xml:space="preserve">
<t:actionlink t:id="link" context="guess">${guess}</t:actionlink>
</t:loop>
</body>
</html>
---
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.
What about the <<<xml:space="preserve">>> 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. <<<xml:space="preserve">>>
turns on full whitespace retention for the element.
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:
---
@Property
private int guess;
---
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.
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 <<<http://localhost:8080/tutorial1/guess.link/4>>>.
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 <<<http://localhost:8080/tutorial1/guess>>>.
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".
---
@Persist
@Property
private String message;
String onActionFromLink(int guess)
{
if (guess == target) return "GameOver";
if (guess < target)
message = String.format("%d is too low.", guess);
else
message = String.format("%d is too high.", guess);
return null;
}
---
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.
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 <that>
as the name of the page to render the response.
We need to update the Guess page to actually display the message; this is done by adding the following:
---
<p>${message}</p>
---
This is truly bare bones and, when message is null, will output an empty \<p\> element. A real application
would dress this up a bit more (using CSS and the like to make it prettier).
We do need a basic GameOver page.
<<src/main/webapp/GameOver.tml:>>
---
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<head>
<title>Game Over!</title>
</head>
<body>
<h1>Game Over</h1>
<p> You guessed the secret number! </p>
</body>
</html>
---
<<src/main/java/org/apache/tapestry/tutorial/pages/GameOver.java:>>
---
package org.apache.tapestry.tutorial.pages;
public class GameOver
{
}
---
With this in place, we can make guesses, and get feedback from the application:
[hilo-feedback.png] Feedback from the game
* Counting the number of guesses
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.
First we update Guess to store the number of guesses:
---
@Persist
@Property
private int count;
---
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.
---
Object initialize(int target)
{
this.target = target;
this.count = 0;
return this;
}
---
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.
---
Object onActionFromLink(int guess)
{
count++;
if (guess == target) return gameOver.initialize(count);
if (guess < target)
message = String.format("%d is too low.", guess);
else
message = String.format("%d is too high.", guess);
return null;
}
---
So, we update the count before comparing and, instead of returning the
name of the GameOver page, we return the configured instance.
Lastly, we need to make some changes to the GameOver class.
<<src/main/java/org/apache/tapestry/tutorial/GameOver.java:>>
---
package org.apache.tapestry.tutorial.pages;
import org.apache.tapestry.annotation.Persist;
import org.apache.tapestry.annotation.Property;
public class GameOver
{
@Persist
@Property
private int count;
Object initialize(int count)
{
this.count = count;
return this;
}
}
---
<<src/main/webapp/GameOver.tml:>>
---
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<head>
<title>Game Over!</title>
</head>
<body>
<h1>Game Over</h1>
<p> You guessed the secret number in ${count} guesses! </p>
</body>
</html>
---
* Parting thoughts
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.
We've also seen how Tapestry stores data for us, sometimes in the session
(via the @Persist annotation) and sometimes in the URL.
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.
Our application is still pretty simple; here's a few challenges:
* 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?
* As we guess, we're identifying ranges of valid and invalid numbers. Can you only show valid
guesses to the user?
* What would it take to change the the game to choose a number between 1 and 20? Between 1 and 100?
* What about setting an upper-limit on the number of guesses allowed?
[]
===
{{{forms.html}Continue on to Chapter 4: Tapestry and Forms}}