Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Fixed bad links due to copy-paste from cwiki-test

...

Scrollbar

...


Let's start building a basic 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 to identify the target number. Even a simple example like this will demonstrate several important concepts in Tapestry:

...

Let's get to work on the Index page and template. Make Index.tml look like this:

Code Block
languagexml
langxml
titleIndex.tmllangxml
<html t:type="layout" title="Hi/Lo Guess"
    xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd">

    <p>
        I'm thinking of a number between one and ten ...
    </p>

    <p>
        <a href="#">start guessing</a>
    </p>

</html>

And edit the corresponding Java class, Index.java, removing its body (but leaving you can leave the imports in place for now):

Code Block
languagejava
langjava
titleIndex.java
langjava
package com.example.tutorial1.pages;

public class Index
{
}

Running the application gives us our start:

Image Modified

However, clicking the link doesn't do anything yet, as its just a placeholder \<a\> <a> tag, not an actual Tapestry component. Let's think about what should happen when the user clicks that link:

...

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 \<a\> tag <a> tag to an ActionLink component:

Code Block
languagexml
langxml
titleIndex.tml (partial)
langxml
    <p>
        <t:actionlink t:id="start">start guessing</t:actionlink>
    </p>

If you refresh the browser and hover your mouse over the "start guessing" link, you'll see that the URL for the "start guessing" link its URL is now /tutorial1/index.start, which identifies the name of the page ("index") and the id of the component ("start").

If you click the link now, you'll get an error:

Image Modified

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

An event handler is a method of the Java class with a special name. The name is onevent-nameEventnameFromcomponentComponent-id ... here we want a method named onActionFromStart(). How do we know that "action" is the right event name? Because that's what ActionLink does, that's why its named _Action_Link.

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 Tapestry Users' Guide. We'll be sticking with the naming convention approach for the tutorial.

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:

Code Block
languagejava
langjava
titleIndex.java
langjava
package com.example.tutorialtutorial1.pages;

public class Index
{
    void onActionFromStart()
    {

    }
}

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.

...

Hmm... right now you have to trust me us 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.

How about this: add the @Log annotation to the method:

Code Block
languagejava
langjava
titleIndex.java (partial)
langjava
  import org.apache.tapestry5.annotations.Log;

  . . .

    @Log
    void onActionFromStart()
    {

    }

When you next click the link you should see the following in the Eclipse console:

...

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 set up that target value.

Code Block
languagejava
titleGuess.java
package com.example.tutorialtutorial1.pages;

public class Guess
{
    private int target;

    void setup(int target)
    {
        this.target = target;
    }
}

With Create that in mindGuess.java file in the same folder as Index.java. Next, we can modify Index to invoke this new the setup() method of our new Guess page class:

Code Block
languagejava
langjava
titleIndex.java (revised)
langjava
package com.example.tutorialtutorial1.pages;

import java.util.Random;

import org.apache.tapestry5.annotations.InjectPage;
import org.apache.tapestry5.annotations.Log;

public class Index
{
    private final Random random = new Random(System.nanoTime());

    @InjectPage
    private Guess guess;

    @Log
    Object onActionFromStart()
    {
        int target = random.nextInt(10) + 1;

        guess.setup(target);

        return guess;
    }
}

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.

Note

All fields in a Tapestry page or component class must be private non-public.

Once we have that Guess page instance, we can invoke methods on it normally.

...

So ... let's click the link and see what we get:

Image Modified

Ah! We didn't create a Guess page template. Tapestry was really expecting us to create one, so we better do so.

Code Block
languagejava
langxml
titlesrc/main/resources/com/example/tutorial/pages/Guess.tml
langxml
<html t:type="layout" title="Guess The Number"
    xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd">

    <p>
        The secret number is: ${target}.
    </p>
  
</html>

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

Image Modified

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.

...

Code Block
languagejava
langjava
    @Property
    private int target;

The @Property 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.

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!

Image Modified

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

...

Code Block
languagejava
langjava
    @Property  
    @Persist
    private int target;

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.

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

Image Modified

That's 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.

When building Tapestry pages, you sometimes start with the Java code and build the template to match, and sometime start with the template and build the Java code to match. Both approaches are valid. Here, lets start with the markup in the template, then figure out what we need in the Java code to make it work.

Code Block
languagexml
langxml
titleGuess.tml (revised)langxml
<html t:type="layout" title="Guess The Number"
    xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"
    xmlns:p="tapestry:parameter">

  <p:sidebar>
    <p>
        The secret number is: ${target}.
    </p>
  </p:sidebar>


    <strong>Guess #$number ${guessCount}</strong>
 
    <p>Make a guess from the options below:</p>
 
   <ul> <ul class="list-inline">
        <t:loop source="1..10" value="current">
            <li>
            <t:actionlink t:id="makeGuess" context="current">${current}
            </t:actionlink>
            </li>
        </t:loop>
    </ul>
 
</html>

So it looks like we need a guessCount property that starts at 1.

...

Info

The URL for the ActionLink will be /tutorial1/guess.makeguess/3. That's the page name, "Guess", the component id, "makeGuess", and the context value, "3".

Code Block
languagejava
langjava
titleGuess.java (revised)
langjava
package com.example.tutorialtutorial1.pages;

import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;

public class Guess
{
    @Property
    @Persist
    private int target, guessCount;

    @Property
    private int current;

    void setup(int target)
    {
        this.target = target;
        guessCount = 1;
    }

    void onActionFromMakeGuess(int value)
    {
        guessCount++;
    }

}

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

...

At this point, the page is partially operational:

Image Modified

Our next step is to actually check the value provided by the user against the target and provide feedback: either they guessed too high, or too low, or just right. If they get it just right, we'll switch to the GameOver page .

For wrong guesses, we'll see an update such as:

Image Removed

And correct guesses will send us to the GameOver page:

Image Removed

with a message such as "You guessed the number 5 in 2 guesses".

Let's start with Let's start with the Guess page; it now needs a new property to store the message to be displayed to the user, and needs a field for the injected GameOver page:

Code Block
languagejava
langjava
titleGuess.java (partial)
langjava
    @Property
    @Persist(PersistenceConstants.FLASH)
    private String message;

    @InjectPage
    private GameOver gameOver;

...

Next, we need some more logic in the onActionFromMakeGuess() event handler method:

Code Block
languagejava
langjava
titleGuess.java (partial)
langjava
    Object onActionFromMakeGuess(int value)
    {
        if (value == target)
        {
            gameOver.setup(target, guessCount);
      return      return gameOver;
        }

        guessCount++;

        message = String.format("Your guess of %d is too %s.", value,
            value < target ? "low" : "high");

        return null;
    }

Again, very straight-forward. If the value is correct, then we configure the GameOver page and return it, causing a redirect to that page. Otherwise, we increment the number of guesses, and format the message to display to the user.

In the template, we just need to add some markup to display the message:

Code Block
languagexml
langxml
titleGuess.tml (partial)
langxml
    <strong>Guess #$number ${guessCount}</strong>

    <t:if test="message">
        <p>
            <strong>${message}</strong>
        </p>
    </t:if>

This snippet uses Tapestry's If component. The If component evaluates its test parameter and, if the value evaluates to true, renders its body. The property bound to test doesn't have to be a boolean; Tapestry treats null as false, it treats zero as false and non-zero as true, it treats an empty Collection as false ... and for Strings (such as message) it treats a blank string (one that is null, or consists only of whitespacewhite space) as false, and a non-blank string is true.

We can wrap up with the "GameOver" page:

Code Block
languagejava
titleGameOver.java
package com.example.tutorialtutorial1.pages;

import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;

public class GameOver
{
    @Property
    @Persist
    private int target, guessCount;
	
    void setup(int target, int guessCount)
    {
        this.target = target;
        this.guessCount = guessCount;
    }
}
Code Block
languagexml
titleGameOver.tml
<html t:type="layout" title="Game Over"
    xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"
    xmlns:p="tapestry:parameter">

    <p>
        You guessed the number
        <strong>${target}</strong>
        in
        <strong>${guessCount}</strong>
        guesses.
    </p>
  
</html>

The result, when you guess correctly, should be this:

Image Added

That wraps up the basics of Tapestry; we've demonstrated the basics of linking pages together and passing information from page to page in code as well as incorporating data inside URLs.

There's still more room to refactor this toy application; for example, making it possible to start a new game from the GameOver page (and doing it in a way that doesn't duplicate code). In addition, later we'll see other ways of sharing information between pages that are less cumbersome than the setup-and-persist approach shown here.

Next up, we'll start delving into : let's find out how Tapestry handles HTML forms and user input.

...

Next: Using BeanEditForm To Create User Forms

Scrollbar