[Gradle] http経由でファイルをダウンロードするスクリプト

WordPressがgist埋め込み対応したということで、試してみたかっただけです。

 

広告

Scrumのプラクティスっぽくサッカーを説明する

自分、アジャイルは理論派な方です!
(実践がほとんど伴っていないの意)

昨日、Scrumのセミナーを受講したので何か書こうと思ったのだけど、五輪も始まるし、Scrumのプラクティスっぽくサッカーを説明するという思考実験をしてみることにした。自分の中では両者はかなり類似していると思うので。

サッカーというのは、足という比較的ニブイ部位でイレギュラーバウンドしうるグラウンド上のボールをコントロールするという、不確実性てんこ盛りの競技である。しかも自分たちを打ち負かそうとする相手もいて、常に自分たちの裏をかこうと我々が予測もしないようなプレーをしかけてくる。ソフトウェアの開発と同様に、予測不可能な状況の変化の頻繁な発生に対して、チーム全員で対処する。時にはリスク覚悟でのチャレンジも勝利のためには必要な場合もあるという、ソフトウェア開発を90分に凝縮したようなスポーツだと思う。(ソフトウェア開発でのチャレンジって何だろうか。例えばチームが初めて使う技術で機能を実装するとかもそうだし、そもそもプロジェクトというのは既にチャレンジだし。Scrumを導入するというのもそうかもしれない。リスクやその対価として得る効果の大小は様々だけど、開発にはいろんなチャレンジが含まれてると思う)

Scrumチームについて

自己組織化

サッカーは試合が開始したら、監督の指示を待ってから選手がプレーをするようなスポーツではない。刻一刻と変化する状況に迅速に対処するには、誰かの指示を待っていては遅すぎる。ピッチで起きた問題を一番最初に知るのは、いつでもそこに居る選手である。だから選手は目の前の問題に対し、自分で考え自分で対処する。
チームは選手たちの得手不得手をお互いにカバーしあうことになる。誰かが相手のディフェンスを打開できなかったら、パス交換したり、まわりの選手がスペースに走りこんだりして「チームで」対処する。

役割はあるが、水平で対等

サッカーにおいては「スクラムマスター」のような役割は「監督」や「コーチ」に近く、「選手」は「開発者」にあてはまるかもしれない。
選手の中にもFW、MFのようなポジションや、わかりやすく言えば「司令塔」のようなロール(役割)があり、各選手は自分の役割を担いゲームに臨む。
ロールはあくまでロールであり、司令塔が一番えらいとか、GKはえらくないとか(またはその逆とか)はない。あくまで対等である。

責任をもつ

チームですることにチームが責任を持つ。サッカーは、試合に負ければチーム全員が敗者である。「自分はGKではないので失点したのは自分のせいではない。自分は敗者ではない」などと言っても仕方がないのであり、負けたのは「自分たちのチーム」である。
もうひとつ、Scrumにはいちいち書いていないと思うが、チームのメンバー個人にも、自分の役割をはたす責任はある。たぶん、これは欧米ではあたりまえだから書いていないのではないかと思うが、一昔前の日本人選手は(いや今でもたまに)、ゴール前でGKと1対1の状況というチャンスなのに横パスに「逃げる」というプレーが頻繁に見られた。
日本人は命令型のシステムで、上司や先生の指示に従うことで、つまり「言われたとおりにすること」によって、自分に責任がなく指示者が責任を負うということに慣れすぎているせいで、自己組織化されたチームでの役割を担うことに対し、それによって負う責任を重く感じてしまい、それから咄嗟に逃げようとしてしまった結果ではないかという分析をしている人もいる。他にも、失敗に対する寛容さだとかいろいろな要素はあるのだろうが、いずれにしろ各メンバーが自身の役割をまっとうしなければ、チームが機能するわけがない。

機能横断的(クロスファンクショナル)

一昔前のサッカーは、ソフトウェア開発もかつてはそうだったように、FWは攻撃専門、DFは守備専門といったスペシャリストによる完全分業が最も効率よい戦略とされていた。
しかし 1974年のワールドカップでオランダ代表が用いたトータルフットボールと呼ばれる、ポジションが流動的で全員攻撃・全員守備をする戦略がサッカー界に大きな影響を与えた。この戦術はチーム全員が複数のポジションの役割を担うことができ、チーム全体を掌握して自身がどのようにプレーすべきかという監督のような視点を持っていることが求められるため、戦術として理想ではあるが実現が非常に難しいものである。
現代のサッカー戦術にも大きく影響を与えている。例えば現代サッカーのFWは、相手にボールを奪われたときの一番最初の守備者でなくてはならない。相手DFが持つボールを、別のFWや攻撃的MFと協調して追い立てることを求められる。(スタミナを犠牲に、相手ゴールに近い位置でボールを奪うためのチャレンジをする)
日本サッカーも数年前までは、中央の攻撃的MFのポジション(トップ下などと呼ばれたりもする)の選手を「司令塔」と呼びゲームコントロールの役割を担っていたが、そのうち守備的MF(ボランチとも呼ばれたりする)がゲームを組み立てる役割を持つようになり(ボランチとはポルトガル語で「ハンドル(舵)」の意)、いまやセンターバックのDFの選手も、相手FWが追い立てるのをいなすことができる足元のテクニックが求められている。GKにも足元が求められるとさえ言う監督もいる。
ソフトウェア開発も、完全分業からクロスファンクショナルに移り変わっているのは、ハードルは高いものの、チームの戦略として最も効率が良く理想的であるからだと思う。

ところで、トータルフットボールのような、全員が連携して動くサッカーを「スペクタクル」とか「美しい」サッカーと表現されたりする。美しいサッカーはある程度のリスクを負って、それを選手の高いスキルによって低減し、高いレベルで効率よくプレーする。
それとは逆に、8~9人で守りを固めて、ボールを奪ったらロングボールを蹴って前線に残った1人に攻撃させるようなサッカーもある。「実践的」サッカーなどといわれ、リスクを最小限にし、勝つためというより負けないサッカーである。日本の高校サッカーのようなトーナメント方式だと、結構な割合で「実践的な」サッカーをするチームが上位に来る。もちろんどちらか一方が絶対的に優れているというわけではないが、顧客(オーナーやサポーター)が求めているのが負けないことなのかチャレンジなのか、あるいは美しさなのかということも大事な要素である。

一人の選手が複数の役割をこなすメリットは、このようなものがあるのではないかと思う。
ある選手がケガなどで試合に出られないときに、代わりをする選手がその役割を担うことができること。
ソフトウェア開発でも欠勤や研修で不在となるメンバーの代わりができる程度のバッファは必要であるが、専業分業スタイルのバックアップメンバーを用意するとしたら、完全に倍の人数が必要になってしまう。まぁこれはお金で何とか解決できないこともないかもしれないが、それとは別に、試合中の戦術変更を選手交代なしに行うことができるというメリットもある。(サッカーでは、選手交代は3人までというルールの試合が多い)
もうひとつ思うのは、Scrumの説明としてはどこにも書いてないかもしれないが、メンバーが固定であると、ぬるま湯というか現状肯定し更なるステップアップをしなくなってしまうリスクがあるので、それを回避する効果もあるのではないかと思う。ある人が専業的に1つの役割を担っていたりすると、もうこれ以上改善する余地がないと思い込んでしまってそのままになってしまうことはありうる。そんなときにメンバーを流動的にすると、まったく別の視点や方法から改善の余地が見えてくることもあるだろうと。

とりあえずScrumチームになぞらえてサッカーのチームについて語ってみた。中にはScrumのプラクティスの用語ではないものを使っているかもしれない。セミナー資料ベースで考えたので(汗 という逃げ。。
他のプラクティスも何か通ずるものがあるかもしれないので、思いついたら書くかもしれない。

最後に、サッカーの戦術もトレンドがあり、また選手たちのフィジカルが向上したり、シューズなどの道具の進化もあるだろうしトレーニング手法の進化などによって、戦術自体も可能性が広がり進化を続けている。ソフトウェア開発も同じように進化しつづけるだろう。
10年前のJリーグと今のJリーグとでまったく違うサッカーに進化しているように、数年後の日本のソフトウェア開発もスペクタクルで美しいものに進化していることを望むし、微力でもその助力となりたいと思う。

[gradle] TaskとAction

自分がGradleでハマッたポイントを晒してみる

task copyTaskA(type: Copy) {
  from 'src/main/webapp'
  into 'build/explodedWar'
}

task copyTaskB {
  copy {
    from 'src/main/webapp'
    into 'build/explodedWar'
  }
}

上のcopyTaskAとcopyTaskBは同じだと思って何気なく書いていたのだが、しばらくしてcopyTaskBのコピーが、依存タスクに関係なく、いつでも実行されてしまっていることに気づいた。

結論をいうと、copyTaskAと同等のタスクにするには << が必要だった。

task copyTaskA(type: Copy) {
  from 'src/main/webapp'
  into 'build/explodedWar'
}

task copyTaskB << {
  copy {
    from 'src/main/webapp'
    into 'build/explodedWar'
  }
}

こたえはここに書いていた。
Example 48.1. Single project build
つまりこういうことらしい

task configured {
  // ここは、タスク自体のプロパティを設定するところ
  // gradleの実行時にすべてのタスクで評価が行われる
}

task actionAdded << {
  // ここは、actionAddedタスクが直接的に呼び出されたり、依存元タスクが呼びされれたりすると実行される
}

では、taskに << {}するとは何なのか。ここでGradle DSLのドメインモデルを。


Gradleでは、Project、Task、Actionのような階層になっていて、Taskは Actionオブジェクトのシーケンスを持ってる。<<演算子は、そのシーケンスにActionを追加する。また、Taskの doFirstとdoLastメソッドでActionを追加することも可能。たとえばcompileの前にファイルコピーをしたかったらこのように書く。

compile.doFirst {
  copy {
    from 'otherdir/othersrc'
    into 'src/main/java'
  }
}

ちなみにここのcopyは Projectクラスのメソッドであり CopyTask (一番最初にcopyTaskA(type: Copy)と書いたもの)とはまったく関係ない。Projectクラスのメソッドは build.gradle内のどこからでも呼べることになっている(たしか。原典みつからず)というルールで実現されている。task myTask {…} の taskも同じ。

話はそれるが、~の前にしたいコピーをタスクにして依存関係を持たせることもできる。compileタスクが実行される前にmyCopyが実行される点は同じだが、myCopy単独で実行もできる(Taskなので。)

task myCopy << {
  copy {
    from 'src/main/webapp'
    into 'build/explodedWar'
  }
}
compile.dependsOn(myCopy)

参考までに

[gradle] Eclipse Gradle STS Support と gradle-gae-plugin

はじめに

Gradleが気になっていたので使ってみたメモ。
Eclipseと統合されているのがよいので、Eclipseの拡張機能(Gradle STS Support)を試してみた。
ついでにGoogle App Engineのアプリケーションをgradleからデプロイする方法も試してみた
今回試したのは下記のバージョン。

セットアップ

Gradleのサイトによると、SpringSourceのSTS(Spring Tools Suite)にGradleサポート機能があり、Gradle機能だけを個別にインストールできるとのこと。Springを使うわけではないのに(いや、使うにしても)重量級のSTSを入れなくてもすむのは助かる。

Gradle STS Supportのインストール方法はGradle STS Support — 2.9.0 — Installation(英語)の通り。一緒に gradle-1.0-mileston-7 もプラグインとしてインストールされるので、gradle自体のインストールは不要(※単なるJavaプロジェクトならこれでも十分だと思うが、実際は gradle-gae-pluginが動かなくて結局別のバージョンを使った。詳しくは後述する)

まずはeclipseでプロジェクト構成を作る。デフォルトのディレクトリ構造にしておくと(もちろん変更できるが)面倒が少ない。webアプリケーションのファイル(jspなど)はwarプラグインのデフォルトディレクトリ構造にしておく。

で、build.gradleを書いて、プロジェクトのディレクトリ直下に置く。GAEとslim3を使う場合はこのような例に。

build.gradle
apply plugin: 'java'

def gaeVersion = '1.6.4'
def slim3Version = '1.0.15'

repositories {
    mavenCentral()
    mavenRepo(url: 'http://maven.seasar.org/maven2/')
}

dependencies {
    compile 'javax.servlet:servlet-api:2.5'
    compile 'javax.servlet:jsp-api:2.0'
    compile "com.google.appengine:appengine-api-1.0-sdk:$gaeVersion"
    compile "com.google.appengine:appengine-api-labs:$gaeVersion"
    compile ("org.slim3:slim3:$slim3Version") {
        exclude group: 'com.google.appengine', module: 'appengine-api-1.0-sdk'
    }
}

Gradle STS Support — 2.9.0 — Tutorial(英語)に使い方は書いてあるが、下記に簡単に書く

  • (チュートリアルではImportを使っているが、)プロジェクトの右クリックメニュー→[Configure]→[Convert to Gradle Project]をすると、Gradleプロジェクトとして認識される。チュートリアルに出てくるような「G」マークがプロジェクトに付くようになる
  • build.gradleを右クリックメニュー→[Gradle]→[Enable Dependency Management]を選ぶと、ビルドパスに[Gradle Dependencies(persisted)]というLibraryが追加され、build.gradleに書いた依存関係にあるjar(dependencies {…}に書いたslim3-1.0.15.jarなど)が追加される。
    • なお、build.gradleのdependenciesの記述を更新しても自動的には反映されない。手動で[Gradle]->[Refresh Dependencies]を実行する必要がある
  • Gradle Tasks Viewを開き、[Project]を選択すると、タスク一覧が表示される。たとえば compileJava をダブルクリックすればGradleによってコンパイルが動作する
  • もちろん、Eclipseのビルドパスにjarが追加されているのでEclipseでのコンパイルもできる

Google Eclipse Pluginでのデプロイには、warディレクトリを構成しなくてはいけないので、そういうCopyなどのタスクを定義するというやり方もあるだろう。
しかしもう少しスマートに gradleから Google App Engineに Deployする方法を書く。この方法だとGoogle Eclipse Pluginは特に不要である。

gradle-gae-plugin

gradleは汎用的に設計されたビルドツールであり、例えば javaをビルドしたいときは apply plugin: 'java' と記述して、 java プラグインを適用する。javaやscalaなどの標準で組み込まれているプラグインが多数あるが Google App Engineのプラグインは標準にはない。今回使用するgradle-gae-pluginはbmuschko氏のサードパーティ製プラグインである。
使い方は簡単で、readmeにあるとおり下記をそれぞれ追加するだけである。(7/7 0.7.3時点のreadmeに修正。今はグループIDがorg.gradle…になり、maven central repositoryから取得可能となった)

pluginを指定する

apply plugin: 'gae'

buildscriptを記述する

buildscript {
    repositories {
        mavenCentral()
    }

    dependencies {
        classpath 'org.gradle.api.plugins:gradle-gae-plugin:0.7.3'
    }
}
ちなみに:

buildscriptdependenciesには、このgradleビルドスクリプトを動作させるために必要なライブラリへの依存関係を解決するためのもので、サードパーティのプラグインをここで定義する。先に述べたdependenciesのcompileなどに定義しても動くと思うが、compileに定義したjarは war/WEB-INF/libの下に含まれてしまう。gradle pluginや、例えば slim3-gen.jarのようなビルドスクリプト動作時にのみ必要なライブラリはここに書くのがよい。

さてここで EclipseのGradle Tasks Viewを更新する。(矢印が回転しているアイコンのボタンをクリック。ツールチップに「Refresh task list」とでる)
すると、下記のエラーになって止まってしまう。

org.gradle.internal.service.ServiceRegistry
Could not fetch model of type 'EclipseProject' using Gradle distribution 'http://repo.gradle.org/gradle/distributions/gradle-1.0-milestone-7-bin.zip'.

ここでEclipseのメニューバー[Window]→[Preferences]→[Gradle]のGradle DistributionのURIに

と入力したところ、うまく動作した。

現象的にはGRADLE-2185の問題に近いと思われる。
実際、いろいろ調べている間、milestone-9で上記同様の指定をしたのだがうまく動かなかった。そのうちrc-1がリリースされて試してみたらうまくいった。

Gradle Tasks Viewに gaeXxxというタスクが並んで見えるようになるはず。
デプロイの場合は、その中のgaeUploadというタスクを実行し、appspotのログインID(emailアドレス)やパスワードを入れるとデプロイができる。他のタスクについては、readmeを参照のこと。

gaeDownloadSdkについて

Google App EngineのSDKをどこかに展開している人は、環境変数のAPPENGINE_HOME または システムプロパティの google.appengine.sdk にパスをしていしておけばよいが、gradle-gae-pluginにはSDKをダウンロードしてくるタスクもある。これでどの環境でも同じSDKのバージョンを使用することになるし、SDKのバージョンアップもGradleのビルドスクリプトだけで完結する。

依存関係として、gaeSdk configurationに、GAEのSDKを指定する。

dependencies {

    //compile .......
    //  ..... 

    gaeSdk "com.google.appengine:appengine-java-sdk:$gaeVersion"
}

gae.downloadSdk = trueを指定する

gae {
    downloadSdk = true
}

それから、gradle-gae-pluginのバグなのか、二回目以降のビルドで gaeDownloadSdkがUP-TO-DATEの時にSDKパスがAPPENGINE_HOME にも google.appengine.sdkにも指定されていないというエラーとなってしまうので、自分はこのように設定している
(7/7訂正 0.7.3で確認したところsystem propertiesの設定は不要になっていた。バグだったのがFIXされた模様)

gae {
    downloadSdk = true
    System.properties['appengine.sdk.root'] = gaeDownloadSdk.explodedSdkDirectory.getPath() + File.separator + "appengine-java-sdk-$gaeVersion"
}

4/18追記
gaeUploadの途中でemailとpasswordを聞かれ、Eclipseのコンソールに入力すると動作していたのだが、最近うまく動かなくなったので回避策を。

build.gradleのgae{}にappcfg{}を定義して、emailとpassIn=trueを設定。
※ここにpassword=xxxxxxxと入れてもよいが、build.gradleはリポジトリに入れてるので外だしにする。

gae {
    appcfg {
        email = 'your_email_address@gmail.com'
        passIn = true
    }

    downloadSdk = true
    System.properties['appengine.sdk.root'] = gaeDownloadSdk.explodedSdkDirectory.getPath() + File.separator + "appengine-java-sdk-$gaeVersion"
}

~/.gradleディレクトリ(Windows 7の場合は C:\Users\ryo-murai\.gradleなど)にgradle.propertiesファイルを置いて、下記のように書く。

gradle.properties

gaePassword=your_google_password

これで、指定したemail/passwordが使用される模様。
追記おわり

まとめ

  • EclipseとGradleの統合に、EclipseプラグインであるGradle STS Supportを使ってみた
  • Google App Engineへのデプロイにgradle-gae-pluginを使ってみた
    • Gradle STS Support同梱の gradle-1.0-milestone-7では動かないので、1.0-rc-1を指定する
  • gaeDownloadSdk使ってもSDKへのパス設定が必要だった。(オレだけ?)

補足

この記事のリンクは
gradle-1.0-rc-1のマニュアルやGradle STS Support 2.9.0のドキュメントを参照しているが、GradleはURLの /docs/1.0-rc-1/userguide//docs /current/userguide/に、またSTSは/sts/docs/2.9.0/reference//sts/docs/latest/reference/に変更すると、最新のバージョンのドキュメントへのURLとなる。

[WPF][MVVM][Prism] InteractionRequest

はじめに

MVVMのViewModelの処理で、処理の途中にユーザへの問い合わせをして以降の処理を分岐させたい(例:「警告:重複したデータがありますが上書きしますか?」→「はい」を選択したら継続し、「いいえ」を選択したら終了する)ような場合、ViewModelから確認ダイアログを表示すると、Viewへの依存を持つことになり、また自動テストもできなくなってしまう。Prismではそういったユーザ問い合わせを抽象化した仕組みがInteractionRequest名前空間にあるクラス・インタフェース群に存在するので使ってみる

準備

プロジェクトの参照に、次の2つのアセンブリを追加する

  • Microsoft.Practices.Prism.Interactivity.dll
    • PrismをインストールしたディレクトリのSource\bin\Desktopにあるはず
    • 他のPrismアセンブリと同様に、Library.Desktopにコピーしてプロジェクトから参照する
  • System.Windows.Interactivity.dll
    • Expression Blend 4をインストールしているPCであればおそらく.NETのコンポーネントとして一覧に表示されるはず
    • 自分はExpression Blendをインストールしていない(!)ので、PrismをインストールしたディレクトリのSource\Lib\Desktopにあったものを使ってみる

実装

まず、ViewModelにInteractionRequestをプロパティとして保持する
FileListViewModel.cs

public class FileListViewModel : NotificationObject, IConfirmNavigationRequest
{
    private readonly InteractionRequest<Confirmation> confirmNavigationRequest =
        new InteractionRequest<Confirmation>();

    public IInteractionRequest ConfirmNavigationRequest
    {
        get
        {
            return this.confirmNavigationRequest;
        }
    }
}

xamlにSystem.Windows.Interactivityを使えるように、名前空間を追加する

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

Interaction.Triggersを定義する

<UserControl x:Class="WindowsExplorerish.Views.FileListView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:prism="http://www.codeplex.com/prism"
             xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <i:Interaction.Triggers>
        <prism:InteractionRequestTrigger
            SourceObject="{Binding ConfirmNavigationInteractionRequest}">
            <!-- TriggerActionをここに定義 -->
        </prism:InteractionRequestTrigger>
    </i:Interaction.Triggers>

    <!-- (略) -->

</UserControl>

<prism:InteractionRequestTrigger>は、ConfirmNavigationInteractionRequestプロパティ(InteractionRequest型)のRaisedイベント発火によって発生するトリガを定義する。
このトリガが発生したときのTriggerAction(今回は確認ダイアログを出すAction)を定義するのだが、後述する。

戻ってViewModelで、Interactionを発生させるコードを書く。
FileListViewModel.cs

        public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
        {
            if (this.IsNavigationLocked)
            {
                this.confirmNavigationRequest.Raise(
                            new Confirmation() { Title = "Confirm", Content = "Navigationがロックされていますが、強制的に開きますか?" },
                            c => continuationCallback(c.Confirmed));
            }
            else
            {
                continuationCallback(true);
            }
        }

InteractionRequest.Raise()に、確認内容を抽象化したオブジェクト(Confirm)を渡す。2つ目のパラメータは確認結果を渡してcontinuationCallbackをコールするコールバックを渡している(ややこしい)

continuationCallbackをそのままRaise()に渡したいところだが、メソッドシグネチャが違うためできない。Callbackの形式をとっているのは非同期的な処理を扱うためだろうか。やってみないとあまり納得感がないが。

この状態で実行すると、Navigationが常にキャンセルされる。

                this.confirmNavigationRequest.Raise(
                            new Confirmation() { Title = "Confirm", Content = "Navigationがロックされていますが、強制的に開きますか?" },
                            c => continuationCallback(c.Confirmed));

デバッガで確認すると、Confirmation.Confirmedプロパティが常にfalseを応答しているようだ。

では確認ダイアログを表示するカスタムTriggerActionを実装する。

SilverlightだとPopupChildWindowActionなどというActionが存在するのだが、WPFにはない。確認ダイアログの見た目や選択肢は様々なのでデフォルトのアクションを用意しても仕方ないという判断なのかもしれないが。

新規にShowMessageBoxActionクラスをViewsフォルダに追加する

namespace WindowsExplorerish.Views
{
    public class ShowMessageBoxAction : TriggerAction<FrameworkElement>
    {
        protected override void Invoke(object parameter)
        {
            InteractionRequestedEventArgs e = parameter as InteractionRequestedEventArgs;
            if (e != null)
            {
                var title = e.Context.Title;
                var message = e.Context.Content.ToString();

                if (e.Context is Confirmation)
                {
                    var result = MessageBox.Show(message, title, MessageBoxButton.YesNo, MessageBoxImage.Question);
                    ((Confirmation)e.Context).Confirmed = result == MessageBoxResult.Yes;
                }
                else
                {
                    MessageBox.Show(message, title, MessageBoxButton.OK, MessageBoxImage.Information);
                }

                e.Callback();
            }
        }
    }
}

TriggerAction.Invoke()にはInteractionRequestTrigger.SourceObjectに定義したオブジェクト(ConfirmNavigationInteractionRequestプロパティ)がパラメータで渡される。この実装では Confirmationか Notificationかでメッセージダイアログの選択肢とアイコンを変更するようにした。
そしてユーザとのInteractionを終えたことを知らせるためにe.Callback()をコールする

次にこのActionをxamlに定義する。

<UserControl x:Class="WindowsExplorerish.Views.FileListView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:prism="http://www.codeplex.com/prism"
             xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
             xmlns:localview="clr-namespace:WindowsExplorerish.Views"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <i:Interaction.Triggers>
        <prism:InteractionRequestTrigger
            SourceObject="{Binding ConfirmNavigationInteractionRequest}">
            <localview:ShowMessageBoxAction />
        </prism:InteractionRequestTrigger>
    </i:Interaction.Triggers>

    <!-- (略) -->

</UserControl>

これで実行すると Navigationのたびに確認ダイアログが表示され、「はい」を押すとNavigationが実行され、「いいえ」を押すと元のままである。
Confirm

まとめ

  • ユーザへの確認や通知ダイアログをViewModelトリガで行う場合は Interactivityを使う
  • ViewModelにIInteractionRequestをプロパティを実装し、ViewのInteractionTrigger.SourceObjectとdatabindする
  • MessageBoxを表示するTriggerActionはWPFのデフォルトにはない(Silverlightにはある)ので実装する
    • Interactionが完了したことを知らせるために、callbackを確認結果を渡して呼び出す

参考:
Developer’s Guide to Microsoft Prism Chapter 8: Navigation Confirming or Cancelling Navigation

これまでの目次

[WPF][Prism] Navigationのキャンセル

はじめに

Prismでは、Navigationが発生したときに、現在アクティブなViewの状態に応じて、それをキャンセル(Navigationがなかったことに)することができる。今回はそれについて説明する。

実現方法

まずはIConfirmNavigationRequestインタフェースをViewまたはViewModelに実装する。(どちらでも良い)
この例では MVVM的にViewModelが状態やキャンセルするかしないかを判断するようにしてみる。
NavigationのRequestが発生した時、Navigation対象のRegionでActiveなViewのConfirmNavigationRequest()が呼び出されるようだ。

FileListViewModel.cs

public class FileListViewModel : NotificationObject, IConfirmNavigationRequest

IConfirmNavigationRequestインタフェースの4つのメソッドを実装する。(うち3つは以前の記事「Navigation」で説明したINavigationAwareのメソッドなので説明を省略する)

public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
{
    bool canNavigate = this.IsNavigationLocked == false;
    continuationCallback(canNavigate);
}

continuationCallbackのパラメータにNavigationが継続可能かどうかをboolで渡せばよい。
なぜコールバックなのか。(あるいは、なぜ boolConfirmNavigationRequest()メソッドが返却しないのか)については、よくわかっていない。

まとめ

  • NavigationのキャンセルができるのはIConfirmNavigationRequestインタフェースを実装したViewまたはViewModel
  • Navigation対象のRegionで現在ActiveなView(またはそのViewModel)のConfirmNavigationRequest()が呼ばれる
  • continuationCallbackのパラメータにNavigationが継続可能かどうかをboolで渡す

これまでの目次

[WPF][MVVM][Prism] Navigation

はじめに

様々なビューが1つのWindowに統合されているCompositeな複合UIでは、Window全体ではなく区画ごとに画面が切り替わるようなUIが望ましい。Prismでは、Regionごとに画面を遷移させることができる機能がある。
画面遷移のAPIはいくつかあるが、ここでは遷移先の画面をURIで指定する方法を使う。

Navigation の実行


var regionManager = container.Resolve<IRegionManager>();
regionManager.RequestNavigate("RegionName", "NewView");

URIの文字列"NewView"を指定すると、Unityにその名前で登録されているViewを取得してNavigationを実行する仕様なので、事前に"NewView"の登録が必要

container.RegisterType<object, NewView>("NewView");

URIなのでパラメータを指定することもできる

regionManager.RequestNavigate("RegionName", "NewView?id=1&name=new");

またRequestNavigatestringだけでなくUriを指定できるメソッドもある。

Navigation 先の処理

ViewまたはViewModelパラメータを受け取るには、INavigationAwareインタフェースを実装する。Viewでもよいし、ViewModelでも良い。(PrismではNavigationのときに、Viewに実装されていなければ、ViewModelに実装されているかをチェックしている)。Navigationが発生すると、遷移先のViewでOnNavigatedTo()が呼び出される。メソッドのパラメータでNavigationContextを受け取り、ParametersでURIのパラメータ部分を取得してViewに反映させる。
他の2つメソッドはそれぞれ、
OnNavigatedFrom() : Navigationが発生し、新しいViewが現れる前のアクティブなViewで呼ばれる。データを保存したりする。
IsNavigationTarget() : 既に開いているViewに遷移したいときに、Trueを応答すると、そのViewがアクティブ化される
もうひとつ。INavigationAwareを拡張した、IConfirmNavigationRequestインタフェースというものがある。

public interface IConfirmNavigationRequest : INavigationAware

これのConfirmNavigationRequestメソッドを実装すると、Navigationをキャンセルできる。(例:変更は保存されませんがよろしいでしょうか?→[いいえ]を押されたとき)
Navigationのキャンセルについては別の機会に。

前回までに実装したWindowsエクスプローラ風のビューに実装

Navigationを前回までに実装したWindowsエクスプローラ風のビューに実装してみる。
フォルダツリーで選択されたフォルダをファイル一覧に表示する。(ファイル一覧の表示区域(“MainRegion”)だけが切り替わる)

まずは、URIで指定できるようにUnityに登録する
WindowsExplorerishModule.cs

public class WindowsExplorerishModule : IModule
{
    private readonly IUnityContainer container;
    private readonly IRegionManager regionManager;

    public WindowsExplorerishModule(IUnityContainer container, IRegionManager regionManager)
    {
        this.container = container;
        this.regionManager = regionManager;
    }

    public void Initialize()
    {
        this.container.RegisterType<object, FolderTreeView>("FolderTreeView");
        this.container.RegisterType<object, FileListView>("FileListView");

        this.regionManager.RequestNavigate("LeftRegion", "FolderTreeView");
        this.regionManager.RequestNavigate("MainRegion", "FileListView");
    }
}

初期表示として画面遷移APIを呼び出す

    public void Initialize()
    {
        this.container.RegisterType<object, FolderTreeView>("FolderTreeView");
        this.container.RegisterType<object, FileListView>("FileListView");

        this.regionManager.RequestNavigate("LeftRegion", "FolderTreeView");
        this.regionManager.RequestNavigate("MainRegion", "FileListView");
    }

前回、フォルダツリーの選択状態をViewModelのプロパティにdatabindingしたので、そのプロパティ変更処理で画面遷移を実行する

FolderTreeViewModel.cs

public class TreeNode : NotificationObject
{
    private readonly DirectoryInfo dirInfo
    private bool isSelected = false;

    public bool IsSelected
    {
        get
        {
            return this.isSelected;
        }

        set
        {
            if (this.isSelected != value)
            {
                this.isSelected = value;
                if (this.isSelected)
                {
                    var regionManager = ServiceLocator.Current.GetInstance<IRegionManager>();
                    regionManager.RequestNavigate("MainRegion", "FileListView?path=" + this.dirInfo.FullName);
                }

                this.RaisePropertyChanged(() => this.IsSelected);
            }
        }
    }

    // 他のコードは変更なしのため省略
}

URIはこのような形でNavigation実行

FileListView?path=c:\work

他の画面遷移の方法として、AddToRegion()RegisterViewWithRegion()といったAPIもあるが、ViewModelから View を参照してしまうため、MVVMの原則に合致しない問題がある。

var regionManager = container.Resolve<IRegionManager>();

regionManager.AddToRegion("RegionName", new NewView());
regionManager.RegisterViewWithRegion("RegionName", typeof(NewView));

FileListViewModel はのプロパティはread-onlyで、常に Directory.GetCurrentDirectory()の内容を表示していた。今回は、指定されたパスのファイル一覧を表示するため、プロパティを変更可能にする。Pathプロパティが変更されるとき、変更通知を発生させてViewに変更を伝播させる。
FileListViewModel.cs

public class FileListViewModel : NotificationObject, INavigationAware
{
    private string path = null;

    public string Path
    {
        get
        {
            return this.path;
        }

        set
        {
            if (this.path != value)
            {
                this.path = value;

                // raise property changed notifications
                this.RaisePropertyChanged(() => this.Files);
            }
        }
    }

    public IEnumerable<FileViewModel> Files
    {
        get
        {
            try
            {
                return Directory
                        .EnumerateFiles(this.Path)
                        .Select(s => new FileViewModel(s));
            }
            catch (Exception)
            {
                return null;
            }
        }
    }
}

INavigationAwareインタフェースを実装する。
FileListViewModel.cs

public class FileListViewModel : NotificationObject, INavigationAware

3つのメソッドを実装する。Navigationが発生すると、ViewとViewModelが生成され、その後にOnNavigatedTo()が呼ばれるため、URIパラメータからパスを取得し、Pathプロパティに設定する。
FileListViewModel.cs

public bool IsNavigationTarget(NavigationContext navigationContext)
{
    var targetPath = navigationContext.Parameters["path"];
    return this.Path == targetPath;
}

public void OnNavigatedFrom(NavigationContext navigationContext)
{
    // do nothing
}

public void OnNavigatedTo(NavigationContext navigationContext)
{
    if (this.Path == null)
    {
        var newPath = navigationContext.Parameters["path"];

        this.Path = newPath ?? Directory.GetCurrentDirectory();
    }
}

この状態でアプリケーションを実行して、ツリービューのフォルダをクリックすると、ファイルリストが切り替わる。
これだと良くわからないので、次回はファイルリストの表示区域(”MainRegion”)をタブ化してみる。

まとめ

  • Navigationには Region名と遷移先画面を特定するURIを指定して実行する
  • 画面の URIは Unityへの登録名で決まる
  • URIにパラメータを指定することで、遷移時に画面へのパラメータを渡すことが出来る
  • Navigation先の画面はINavigationAwareインタフェースを実装することでパラメータを受け取り、画面に反映させる

これまでの目次