コンポーネントレンダリング

Tapestry 4 のアプローチ

レンダリングは再帰的な処理です。各コンポーネントが(IRenderインターフェースから継承した)render()メソッドを実装しています。 コンポーネントはテンプレート内に含まれている他のコンポーネントのrender()メソッドを実行します。

「もし目眩がするようなら、JSFのスタックトレースの縁に立つのはやめて下を見てごらん」と Bruce Tate が言いました。 同じことがTapestry 4にもあてはまります。コンポーネントを深くネストしたりループしたりすると、 スタックトレースはとても深くなります。

Tapestry 5 のアプローチ

コンポーネントのレンダリングは、末尾再帰の代わりに ステートマシンキュー に基づいています。 これはレンダリング処理を小さな断片に分割し、実装とオーバーライドを簡単にします。 心配しないでください。書かなければならないコード量はあっと言わせるほど少ないのです。

レンダリングフェーズ

コンポーネント個々のレンダリングは多数のフェーズに分割されます。それを以下に示します。

コンポーネントのレンダリング状態図

(SetupRender, BeginRender, BeforeRenderBody 等の)オレンジ色のフェーズにはそれぞれ同名のアノテーションがあり、 そのアノテーションをあなたのクラスのひとつまたは複数のメソッドに置くことができます。 このアノテーションでTapestryに指示をすると、そのフェーズの一部としてあなたのメソッドが実行されるようになります。

これらのアノテーションが付けられたメソッドを レンダーフェーズメソッド と呼びます。

あなたのメソッドはvoidまたはbooleanを戻り値にすることができます。 値を返すことによりフェーズをスキップしたり再実行することが可能になります。図中の実線は通常の処理の経路を示しています。 破線は、レンダーフェーズメソッドがtrue(またはvoid)ではなくfalseを返した時に実行される別の経路です。

レンダーフェーズメソッドはパラメータ無し、もしくは MarkupWriter のひとつだけを受け取ることができます。 メソッドの可視性はどれでも好きなものにできますが ... パッケージプライベートにするのが一般的です。 それは、そのメソッドをコンポーネントの 公開 APIとすること無しに、(同じJavaパッケージ内の)ユニットテストコードからは利用できるようにするためです。

これらのメソッドは オプション です。デフォルトの動作は各フェーズ毎に決まっています。

多くのフェーズが、レンダーフェーズにも接続するコンポーネントMixinの使用を想定しています。 いくつかのフェーズは専らMixin用です。

通常、あなたのコードで使用するフェーズは SetupRender, BeginRender, AfterRender, CleanupRender で ... 大抵はこれらのうちのひとつかふたつです。

以下は繰り返し処理を行うコンポーネントのソースで、ふたつの値の間でカウントアップまたはカウントダウンを行い、 ボディを繰り返しレンダリングし、現在のインデックス値をパラメータに格納します。

package org.example.app.components;

import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.AfterRender;
import org.apache.tapestry5.annotations.SetupRender;

public class Count
{
    @Parameter
    private int start = 1;

    @Parameter(required = true)
    private int end;

    @Parameter
    private int value;

    private boolean increment;

    @SetupRender
    void initializeValue()
    {
        value = start;

        increment = start < end;
    }

    @AfterRender
    boolean next()
    {
        if (increment)
        {
            int newValue = value + 1;

            if (newValue <= end)
            {
                value = newValue;
                return false;
            }
        }
        else
        {
            int newValue = value - 1;

            if (newValue >= end)
            {
                value = newValue;
                return false; 
            }
        }

        return true;
    }
}

next()からfalseが返されると、TapestryはBeginRenderフェーズを再実行し、 コンポーネントのボディを再レンダリングします(このコンポーネントにはテンプレートがありません)。 trueが返されるとCleanupRenderフェーズに進みます。

アノテーションに従ってTapestryがあなたのメソッドに適応していることに注目してください。 メソッドのパラメータにも適応しています; この例ではアノテーションした2つのメソッドからは何も出力しないので、 それらのメソッドにMarkupWriterは必要ありません。

コンポーネントのテンプレートやボディが、しばしば他のコンポーネントを ... たくさんのコンポーネントを含むというのはとても興味深いことです。 それは、多数のコンポーネントが自身のステートマシンを持ち、それらがそれぞれ異なるフェーズにあるということを意味します。

SetupRender

レンダリング毎に一度だけ行う必要があるセットアップをコンポーネントに対して行うことができる場所です。 コンポーネントパラメータを参照し、それらを用いて一時的なインスタンス変数をセットするのに適しています。

BeginRender

タグをレンダリングするコンポーネントはここで開始タグをレンダリングすべきです (終了タグはAfterRenderフェーズ内でレンダリングすべきです)。 コンポーネントのテンプレートやボディがレンダリングされないようにするために、falseを返すこともできます。

テンプレートを持つコンポーネントとそうでないコンポーネントがあります。 テンプレートを持つコンポーネントでテンプレート内に<body>要素がある場合、 BeforeRenderBodyフェーズが実行されます(ボディをレンダリングするかどうかのオプションが与えられます)。

テンプレート内に<body>要素が無い場合はBeforeRenderBodyフェーズは実行されません。

テンプレートを持たないコンポーネントでも、ボディがある場合にはBeforeRenderBodyフェーズが実行されます。

BeginRenderでアノテーションされたメソッドが無い場合、このフェーズの間に特別な出力は行われず、 テンプレート(テンプレートがある場合)またはボディ(テンプレートはなく、しかしボディがある場合)がレンダリングされます。

BeforeRenderTemplate

このフェーズでは、コンポーネントが自身のテンプレートを(テンプレートが出力したマークアップを囲むマークアップを生成して)装飾することを可能にします。 テンプレートをスキップすることもできます。

BeforeRenderBody

コンポーネントのボディに関するフェーズです(コンポーネントのボディとは、コンテナのテンプレート内でコンポーネントが占めている部分のことです)。 BeforeRenderBodyフェーズは、コンポーネント自身のボディはスキップするがテンプレート(もしあれば)の残りの部分はレンダリングする、 ということを可能にします。

BeforeRenderBodyでアノテーションされたメソッドが無い場合、デフォルトではボディはレンダリングされます。 これはコンポーネントテンプレート内の<body>要素がある場所で行われ、 またコンポーネントにテンプレートが無い場合(しかし、ボディはある場合)は自動的に行われます。

AfterRenderBody

ボディのレンダリング後に実行されるフェーズです; これはコンポーネントがボディを持っている場合にのみ行われます。

AfterRender

このフェーズはBeginRenderを補うもので、 BeginRenderフェーズ内でレンダリングした開始タグに対応する終了タグをレンダリングするのにしばしば使用されます。 AfterRenderフェーズはCleanupRenderに進むことも(上の例のCountコンポーネントのように)BeginRenderに戻ることもできます。

AfterRenderでアノテーションされたメソッドが無い場合、特別な出力は何も行われず、CleanupRenderフェーズに進みます。

CleanupRender

SetupRenderと対になり、後始末を行うことができます。

アノテーションの代わりにメソッドの命名規約を使う

あなたのメソッドにアノテーションを使うことを望まない場合、メソッドの命名規約に従うことでアノテーションを使わずに済ますことができます。 setupRender(), beginRender() といった具合にメソッド名をアノテーション名と同じにし、最初の文字を小文字にします。 アノテーションによるレンダーフェーズメソッドと同じように、可視性や戻り値、パラメータにはTapestryが適応します。

この方法を使うと、前の例は以下のように書き換えることができます:

package org.example.app.components;

import org.apache.tapestry5.annotations.Parameter;

public class Count
{
    @Parameter
    private int start = 1;

    @Parameter(required = true)
    private int end;

    @Parameter
    private int value;

    private boolean increment;

    void setupRender()
    {
        value = start;

        increment = start < end;
    }

    boolean afterRender()
    {
        if (increment)
        {
            int newValue = value + 1;

            if (newValue <= end)
            {
                value = newValue;
                return false;
            }
        }
        else
        {
            int newValue = value - 1;

            if (newValue >= end)
            {
                value = newValue;
                return false; 
            }
        }

        return true;
    }
}

このやり方にはトレードオフがあります: 利点としては、コードがより簡単に短くなります。そしてあるクラスと別のクラスのメソッド名が一貫したものとなるでしょう。 欠点は、メソッド名がとても一般的であるため、場合によってはアノテーションしたメソッドを使用する場合よりもメソッドの内容を表すのに不十分となるかもしれません (initializeValue()next() の方がメソッドの内容をよく表している考える人もいるでしょう)。

もちろん、あるメソッドはレンダーフェーズメソッドの命名規約に従い、別のメソッドはアノテーションを使用するというように併用することもできます。

Rendering Components

レンダーフェーズメソッドから true または false を返す代わりに、コンポーネントを返すことができます。 そのコンポーネントは Component アノテーションでインジェクトしたものか、パラメータとして渡されたものです。

いずれにせよ、レンダーフェーズメソッドから返されたコンポーネントはキューに入り、現在のコンポーネントの 前に レンダリングされます。

レンダーフェーズメソッドから返されるコンポーネントは、アプリケーション内の全く別のページのものでも構いません。

コンポーネントの再帰的なレンダリングはできません。

この手法によりTapestryのページレンダリングは とても 動的になります。

booleanを返す場合はフェーズを中断しますがコンポーネントのインスタンスを返す場合は中断 しません。 複数のメソッドからコンポーネントを返すことができます(それを推奨はしません -- 狂気のさたです)。

メソッドの衝突と順序

同じレンダーフェーズに複数のメソッドをアノテーションすることができます。 これは同じクラス内のメソッドに対しても、別のクラスから継承したメソッドに対してもあてはまります。

Mixinはコンポーネントより先

コンポーネントがMixinを持っている場合、そのMixinのレンダーフェーズメソッドはコンポーネントのレンダーフェーズメソッドより 前に 実行されます。Mixinがベースクラスから派生している場合、Mixinの親クラスのレンダーフェーズメソッドがサブクラスのメソッドよりも前に実行されます。

Mixin同士の実行順序は今のところ未定義です。

例外: MixinAfter をアノテーションしたMixinクラスは、前ではなく 後に 実行されます。

親クラスは子クラスより先

常に親が先です。親クラスで定義されているメソッドは常に子クラスで定義されているメソッドよりも先に実行されます。

基底クラスのレンダーフェーズメソッドをサブクラスでオーバーライドした場合、そのメソッドは基底クラスの他のメソッドと共に一度だけ実行されます。 サブクラスは、基底クラスのメソッドをオーバーライドすることでその 実装 を変更することはできますが、メソッドが実行される タイミング を変更することはできません。 参照: TAPESTRY-2311

AfterXXX と CleanupRender は順序が逆

AfterXXX フェーズは BeginXXXフェーズや BeforeXXXフェーズと対になる存在です。 (レンダリング中のコンポーネントにボディやテンプレートがある場合には)たいてい、 BeforeXXX フェーズや BeforeXXX フェーズで開始した要素は対応する AfterXXX で終了します(閉じます)。

適切に、そして自然な順序で処理されることを保証するために、 AfterXXX と CleanupRender のレンダーフェーズメソッドは 逆順 に実行されます:

  • サブクラスのメソッド
  • 親クラスのメソッド
  • Mixinのサブクラスのメソッド
  • Mixinの親クラスのメソッド

ひとつのクラス内では

今のところメソッドはアルファベット順にソートされます。 同じ名前のメソッドはパラメータの数によってソートされます。しかし、これは良い方法ではありません ... ただひとつのメソッドを定義し、そのメソッドからあなたが望む順序で他のメソッドを呼び出してください。

フェーズの中断

メソッドが true または false を返す場合、そのフェーズの処理を中断します。 通常なら同じフェーズ内で実行されるはずの他のメソッドが実行されなくなります。

予期せぬ中断により同じフェーズ内の他のメソッドが実行されなくなるのを防ぐため、ほとんどのレンダーフェーズメソッドはvoidを返すべきです。