Challenge Engineer Life !

エンジニア人生を楽しみたい!仕事や趣味で学んだ技術的なことを書いていくブログです。

Faces Flowで即時にフローを開始したい

今かかわっている案件、画面数は少ないのですが、業務的には画面間を行き来することが多いような画面となっています(なら画面分けるのやめてSPAに、というのは置いておいて…)

開発はJava EE 7でJSFを使うのですが、ViewScopeだとちょっと微妙で、候補としてはSessionScope、ConversationScope、そしてJSF 2.2で入ったFlowScopeのいずれかかなと。

Conversationはあまり良い思い出がないので、FlowScopeことFaces Flowを初めて実戦投入してみようかな…と思っています。

Faces Flowについては2年前のJava EE Advent Calendar 2013で書いてました。

で、今回のシステムは、他のベンダさんが提供してるWebシステムから呼びされるもので、呼び出された瞬間にフローを開始したいです(下図)。

f:id:kikutaro777:20150209225201j:plain

スタブのページ用意して、URLリンクで普通にフローを呼び出してみたのですが、フローの開始にはなりませんでした。

色々なサンプルをみたのですが、ほとんどがindex.xhtmlのようなページがあって、そこからoutcome属性でフローに入って…という流れのものばかり…。

悩みで近いのは次の2つでした。


jsf 2.2 - How to enter a JSF 2.2 flow with faces-redirect - Stack Overflow


jsf 2 - Starting faces flow - Stack Overflow

2つ目のコードをみると、ConfigurableNavigationHandlerのperformNavigation()にてoutcomeと同じハンドリングができるようなので、1枚ダミーのページをはさんでリダイレクトする形に……(下図)。

f:id:kikutaro777:20150209231417j:plain

これでフロー開始ができました。が、なんかダサい…。良いやり方ないのかな。

JSFでactionListener内の判定結果に応じてactionの画面遷移を止める方法 ~PrimeFaces版~

昨日のエントリの続きです。

昨日の動作はPrimeFacesのコンポーネントでも同じことができます(^^)

一応書きかえてみると

JSFxhtml
<!-- 普通に画面遷移するボタン -->
<p:commandButton id="saveBtn0" ajax="false" value="普通に画面遷移する"
                action="#{beforeRedirectBean.redirectPage()}" />

<!-- actionListenerで画面遷移を止めたボタン -->
<p:commandButton id="saveBtn1" ajax="false" value="プログラムで画面遷移を止める"
                actionListener="#{beforeRedirectBean.thinkAction()}"
                action="#{beforeRedirectBean.redirectPage()}"/>

基本的には、hタグをpタグに変えただけです。
あと、画面遷移なので、pタグのcommandButtonはajax=falseを指定しています。

BackingBeanの抜粋
public String redirectPage(){
    //遷移先のページを返す
    return "afterRedirect.xhtml";
}

public void thinkAction(){
    FacesContext context = FacesContext.getCurrentInstance();

    //画面コンポーネントを取得して setActionExpressionでnullを指定
    CommandButton btn = (CommandButton)context.getViewRoot().findComponent(":beforeFrm:saveBtn1");
    btn.setActionExpression(null);
}

こちらもほぼ昨日のコードと同じです。コンポーネントがPrimeFacesに変わったのでimport文を

  • import javax.faces.component.UICommand;

から

  • import org.primefaces.component.commandbutton.CommandButton;

に変えています。

これでPrimeFaces版の完成です。実行すると昨日作ったものと同じ挙動となります。
実行すると、昨日のとはちょっと違った見え方のボタンが出て

f:id:kikutaro777:20130328214443j:plain

左のボタンを押下すると

f:id:kikutaro777:20130327235106j:plain

遷移します。

右のボタンを押下すると

f:id:kikutaro777:20130328214443j:plain

そのまま留まります。

まあ確認するまでもなく当たり前な挙動なのですが(^^;

JSFでactionListener内の判定結果に応じてactionの画面遷移を止める方法

現在開発している画面の仕様でこんなのがありました。

  • 保存かつ画面遷移するボタンがあって
  • ボタン押下すると、あるロジックが走って、その判定結果に応じて
    • そのまま保存して画面遷移するパターン
    • 画面遷移せずに留まって、確認ダイアログを出すパターン

の2つに分岐します。さらに確認ダイアログが出るパターンでは

  • 確認ダイアログで「はい」を押したら保存して画面遷移する
  • 確認ダイアログで「いいえ」を押したら保存せずに画面に留まる

という分岐が…。

WindowsFormじゃなくてWebなんだけどなぁ…とボヤきつつ、そもそもJSFで画面遷移を条件に応じて動的に止めるとかできるの??と疑問が。

ちなみに上記ボタンは画面遷移するボタンなので、基本的にAjaxは使いません。

調べたら、綺麗な方法がありました(^^;

Andrejus Baranovskis's Blog
How To Disable Action Conditionally from Action Listener

actionListenerで判定して、その結果、actionの画面遷移を有効・無効にしてます。
なるほどー。

というわけで、自分で試した結果は以下の通りです。

JSFxhtml
<!-- 普通に画面遷移するボタン -->
<h:commandButton id="saveBtn0" value="普通に画面遷移する"
                action="#{beforeRedirectBean.redirectPage()}" />

<!-- actionListenerで画面遷移を止めたボタン -->
<h:commandButton id="saveBtn1" value="プログラムで画面遷移を止める"
                actionListener="#{beforeRedirectBean.thinkAction()}"
                action="#{beforeRedirectBean.redirectPage()}" />
BackingBeanの抜粋
public String redirectPage(){
    //遷移先のページを返す
    return "afterRedirect.xhtml";
}

public void thinkAction(){
    FacesContext context = FacesContext.getCurrentInstance();

    //画面コンポーネントを取得して setActionExpressionでnullを指定
    UICommand btn = (UICommand)context.getViewRoot().findComponent(":beforeFrm:saveBtn1");
    btn.setActionExpression(null);
}

実行すると以下のような画面がでて

f:id:kikutaro777:20130327235100j:plain

左のボタンを押下すると、以下のように次のページに遷移しますが

f:id:kikutaro777:20130327235106j:plain

右のボタンを押下すると、そのままとどまっています。

f:id:kikutaro777:20130327235111j:plain

上記プログラムは無条件でnullを指定していますが、ここを条件分岐して遷移しない場合はnullをしていすれば動的な制御が可能です。

ちなみにPrimeFacesのコンポでも同様のことが実現できました。

ただ最初に書いたダイアログ呼び出しまで含めると上記では上手くいきませんでした(ダイアログが消えてしまうので)
これはまた微妙なやり方で解決したので、別途まとめたいと思います。

しかし、いいのかな、こんなゴリゴリやって(-_-;

PrimeFacesのCommandLinkでajax属性を悩ましく感じたケース

PrimeFacesは非常に便利なコンポーネントが揃っていますが、たまにハマります。
今日ハマったのは、CommandLink(CommandButtonも多分同じですが)とダイアログ周りの動きです。

以前に書いた「PrimeFacesでManaged Bean内の判定結果に応じてダイアログの表示有無を制御をする方法」で、リンクをクリックした場合にManagedBeanの内部状態によって表示ダイアログを切り替える、ことをやりました。

今回もベースとなるシチュエーション(DataTable内のCommandLink)は同じで、やりたい処理は「リンクをクリックしてダイアログが出る場合もあるし、そのままCommandLinkのactionを実行して画面遷移する場合もある」という割と単純なものです。

が、PrimeFacesを触った方はわかると思いますが、CommandButtonやCommandLinkはajax属性がtrue(これがデフォルト)だと画面遷移ができません。これ自体、意外と最初にハマるのですが(^^;

なので、画面遷移する際は

  • ajax属性をfalseにして使う
  • redirectを使う

のが良いとされています(PrimeFacesのUsers GuideドキュメントのFAQ No.6とか)

今の開発では基本的に前者の方針を取っているのですが、今回やろうとしていることは

「リンクをクリックしてダイアログが出る場合もあるし、そのままCommandLinkのactionを実行して画面遷移する場合もある」

なので

  • ダイアログを出すためにはajax=trueじゃないといけない
  • 画面遷移するためにはajax=falseじゃないといけない

という矛盾がありました。試しにプログラム内でajaxをtrue/falseとスイッチしたりしましたが駄目でした(^^;

解決方法は「redirectを使う」しかなさそうで以下のような書き方をしました。

JSFxhtml

<p:commandLink id="lnkEdit" value="編集" ajax="true"
    actionListener="#{xxxBean.権限があるのか確認するメソッド(xxxList.xxx)}"
    action="#{xxxBean.画面遷移するメソッド()}"
    oncomplete="handleEditDialog(xhr, status, args)"/>
                        
                       
<script type="text/javascript">
    function handleEditDialog(xhr, status, args) {
        if(!args.hasAuth){
            accessDlg.show();
         }
    }
</script>

ManagedBean(CDIの)

public void 権限があるのか確認するメソッド(XXXXX xxx){
    boolean hasAuth = false;
        
    //中略 ~権限があるかチェックなど~ 
        
    RequestContext context = RequestContext.getCurrentInstance();
    context.addCallbackParam("hasAuth", hasAuth);
}
public String 画面遷移するメソッド(){

    RequestContext context = RequestContext.getCurrentInstance();
    Map<String,Object> map = context.getCallbackParams();
    boolean auth = (boolean)map.get("hasAuth");
    if(!auth){
        return null;
    }
        
    return "./画面遷移先ページ" + "?faces-redirect=true";
}

上記記述で、明細のリンクをクリックして
f:id:kikutaro777:20130306211342j:plain
ManagedBeanで権限チェック処理とか実行して、権限がなければ以下のようにダイアログ表示して、権限があれば、そのまま編集画面に進む、みたいな挙動が実装できました。
f:id:kikutaro777:20130306211346j:plain

以下のPrimeFaces Community Forumで全く同じことを質問している人がいたため助かりました(^^;
JSF Navigation and AJAX

redirectを普段使わないので、「矛盾してるの、どーやるの!」と地味にハマりました。

続・画面遷移に悩む

少し前に「画面遷移に悩む」を書いたのですが、そこからパートナーと一緒に色々探った結果、最終的には自前でCDIのViewScopeを作るのが最も自分たちの理想に近い動きをすることがわかりました。

参考にしたのはこちらのサイト
http://www.verborgh.be/articles/2010/01/06/porting-the-viewscoped-jsf-annotation-to-cdi/

Stackoverflowでも色々議論されていて
http://stackoverflow.com/questions/11832666/jsf-2-0-cdi-scopes-and-best-practises
http://stackoverflow.com/questions/4865047/view-scope-in-cdi-weld
等など、他にもたくさん出てきました。やはり皆、どういうスコープを使うのが良いのか悩んでいる様子。

MyFaceのCODIを使う、とか、JBoss Seam3を使う等の解決策もありましたが、自分達はまだそういったものを扱えるほどのレベルではないので積極的になれませんでした。
で、投稿回答の中に、Seamを使ってなくて普通のJSFとPrimefacesを使っている、と言う人が(今回自分たちが参考にした)上記のサイトをあげていて、実装してみたら理想に近い形となりました。

結局の所、単純にJSFの@ViewScopeをCDIで使えればいい、というものなので、なぜCDIの標準としてViewScopeがないのか疑問です。。。

それにしても割と単純なWebシステムのプロトタイプを作るだけなのに、学習コストが高いなぁという気がします。
ASP.NETならば、わりと悩まない話かと思います。

画面遷移に悩む

現在開発しているプロトタイプでは、画面xhtmlの裏にいるバックビーンはCDIの管理Beanを使用することで統一しました。

@Named(value = "xxxBean")...

Beanは画面で共通的に持つフィールド変数(画面IDやユーザ情報クラスなど)は抽象クラスで括りだし、実装しています。

public abstract class AbstractBaseBean implements Serializable{
    
    /**
     * 画面ID
     */
    private String screenId;

    ...
}

これを継承したCDI管理Beanを各画面に対して1対1となるように定義する形です。

 
@Named(value = "xxxBean")
@RequestScoped
public class XxxBean extends AbstractBaseBean implements Serializable{

   各画面の定義

}

で、悩ましいのがスコープと画面遷移です。

当初は単純にRequestScopeにしていました。画面遷移は自画面のBeanの関数を呼び出して、そこから抽象クラスの画面遷移処理へ遷移先の画面IDを渡して呼び出します。抽象クラスでは、ログ処理を行い、画面IDからURLを返す処理をします。

RequestScopeなので、画面遷移の際に自画面のBeanのコンストラクタ(@PostConstruct)が呼ばれます。
これはASP.NET的に簡単にポストバック判定ができるものと思っていました。
FacesContextにisPostbackなる関数もあったので。

しかしながら、記述してみると、なぜか上手く判定してくれません。なぜだ…。これは要詳細調査事項として保留中です。

仕方ないからSessionScopeにするか、と安易に考えそうですが、業務系で使うシステムなのでメモリ肥大は性能問題等に絡んでくるため好ましくないです。

CDIにはConversationScopeという便利なスコープもあるので、これを採用して、コンストラクタでBeginして、画面遷移でEndするようにしました。
しかし、ブラウザの戻るボタンが絡んでくると上手く動かないケースがあり、微妙です。

上記でやっていることは、JSFのViewScopeと同じなので、ViewScopeが欲しいのですが、CDIには標準ではないようです。なぜ…。

そんなこんなで、現在どのようにスコープを設定するか、中々悩ましい状況となっています。
みんなどうやっているんだろうか…。結構調べたのですが、なかなか「これだ!」という情報がないなぁ、という感じです。

にほんブログ村 IT技術ブログへ
にほんブログ村 にほんブログ村 IT技術ブログ Javaへ
にほんブログ村