コマンド¶
System.Windows.Input.ICommand¶
まずWPFにおけるICommand( System.Windos.Input.ICommand )について簡単におさらいしておきましょう。
ICommandはメニューやボタンから実行するコマンドを表すインターフェースです。
public interface ICommand
{
event EventHandler CanExecuteChanged;
bool CanExecute(object parameter);
void Execute(object parameter);
}
コマンドを実行するExecuteおよびコマンドが実行可能かどうかを判定するCanExecuteメソッドを持ちます。 また、CanExecuteChangedイベントを持ち、コマンドが実行可能かどうか変更があった場合に通知を発生させなければなりません。
MoNo.RAILによるコマンドの生成¶
MoNo.RAILではICommand実装インタンスを生成する関数がいくつか用意されています。
Wpf.Command.createWpfCommand¶
まず最も基本的なものはMoNo.Wpf.Command.createWpfCommandです。シグネチャを確認してみましょう。
Wpf.Command.createWpfCommand : canExecute:IReactive<bool> option -> execute:('a -> unit) -> System.Windows.Input.ICommand
引数としてIReactive<bool> option型のcanExecuteと関数executeを渡し、それぞれICommandのCanExecuteとExecuteに対応付けられたICommand実装クラスが生成されます。 コマンドの実行結果としては単に関数executeが実行されます。
通常、ICommandの実装にはCanExecuteメソッドとCanExecuteChangedイベントを適切に実装する必要があります。 MoNo.RAILに用意されている一連のコマンド生成関数ではReactive<bool>型を渡すことで実行可能かどうかを決定します。 また、実行可能かどうかの変更通知もReactiveの仕組みを利用して通知するためValプロパティの値を変更するだけです。
なおcanExecuteはoptionです。Noneの場合は常に実行可能なコマンドが生成されます。
Wpf.Cont.toWpfCommand¶
次に引数に「実行される関数」ではなく前項で解説した「継続モナド」を渡してコマンド化する関数を見てみましょう。
Wpf.Cont.toWpfCommand : runner:IContRunner -> canExecute:#IReactive<bool> option -> cont:Cont<unit> -> System.Windows.Input.ICommand
第1引数にIContRunnerというものを渡しています。これは本項では解説しませんが継続モナドを活かすためのインターフェースです。 サンプルプログラムではViewModelがその実装クラスであるContRunnerを継承し、自身を渡すようにしています。
第3引数で関数の代わりに継続モナドを渡しています。 これにより継続モナドを呼び出すICommand実装インスタンスが生成できます。
Wpf.Command.toWpfCommand¶
最後は引数にMoNo.Commandのインスタンスを渡してコマンド化する関数です。
Wpf.Command.toWpfCommand : runner:IContRunner -> command:Command<'a,unit> -> System.Windows.Input.ICommand
この関数では第2引数にCommand<’a,unit>を渡します。 これはMoNo.RAILで定義されているレコード型で、
type Command<'a, 'b> = {
CanExecute : IReactive<bool>
Execute : 'a -> Cont<'b>
}
上記の定義の通り、IReactive<bool>型のCanExecuteと’aを引数に取り’bの継続モナドを返すExecute関数を持つレコードです。 それぞれICommandのCanExecuteとExecuteに対応するので説明の必要はないでしょう。
サンプルプログラム CommandSample¶
さて、ここまでを踏まえてサンプルプログラムを見てみましょう。
サンプルプログラムは MoNo.RAIL
の下記フォルダに含まれています:
MoNo.RAIL.Samples/WpfMvvmSamples/CommandSample
外観はこのような感じです。
サンプルプログラム CommandSampleの仕様は以下の通りです。
- Incrementボタンを押すと値が1増えます。
- Decrementボタンを押すと値が1減ります。ただし値が1以上の時でないと実行できません。
- Doubleボタンを押すと値が倍になります。
- Div By 2ボタンを押すと値が半分になります。ただし値が10以上の時でないと実行できません。
- 10 timesボタンを押すと3秒間待った後、値が10倍になります。待機時間中にAbortボタンが有効になり、これを押すと処理が中断されます。
- 20 timesボタンを押すとプログレスバーが表示され3秒間待った後、値が20倍になります。こちらも待機時間中にAbortボタンが有効になり、これを押すと処理が中断されます。
まずModel.fsを見てみましょう。
type Model () =
let value = Reactive 0
member __.Value = value
...
シンプルなモデルクラスです。本質的にはReactive<int>型の値を一つ持っているだけです。
次にMainWindow.xamlを見てみましょう。
...
<Label Content="{Binding Path=Value.Val}"/>
<Button Command="{Binding Path=IncrementCommand}">Increment</Button>
<Button Command="{Binding Path=DecrementCommand}">Decrement</Button>
<Button Command="{Binding Path=DoubleCommand}">Double</Button>
<Button Command="{Binding Path=DivBy2Command}">Div By 2</Button>
<Button Command="{Binding Path=TenTimesCommand}">10 times</Button>
<Button Command="{Binding Path=TwentyTimesCommand}">20 times</Button>
...
ラベルにViewModelのValue.Valが、各ボタンにViewModelクラスが持つコマンドがそれぞれBindされています。 それではViewModel.fsで実装されている各コマンドをそれぞれ詳しく見ていきましょう。
IncrementCommand¶
member val IncrementCommand =
Wpf.Command.createWpfCommand None (fun _ -> model.Increment ())
Wpf.Command.createWpfCommand
に「値をインクリメントする関数」を渡してコマンドを作成しています。
第1引数はNoneなので常に実行可能なコマンドです。
DecrementCommand¶
member val DecrementCommand =
Wpf.Command.createWpfCommand (Some model.CanDecrement) (fun _ -> model.Decrement ())
同様に Wpf.Command.createWpfCommand
に「値をデクリメントする関数」を渡してコマンドを作成しています。
IncrementCommandと異なるのは第1引数です。実行可能かどうかはmodelのCanDecrementプロパティの値によって決定されます。
member val CanDecrement = value |> Reactive.map (fun x -> x >= 1)
値が1以上の場合にのみ実行可能であることがわかります。
DoubleCommand¶
member val DoubleCommand =
cont { model.Double() }
|> Wpf.Cont.toWpfCommand this None
modelの値を2倍する継続モナドを Wpf.Cont.toWpfCommand
に渡してコマンドを作成しています。
第1引数にはContRunnerを継承しているthisを渡しています。
第2引数にはNoneを渡していますので基本的には常に実行可能なコマンドです。
が、実は実行可能でなくなるケースがあります。後述するCancelCommandの箇所で解説します。
DivBy2Command¶
member val DivBy2Command =
model.DivBy2Command
|> Wpf.Command.toWpfCommand this
modelのMoNo.Command型プロパティDivBy2Commandを Wpf.Command.toWpfCommand
に渡してコマンドを作成しています。
第1引数にはやはりContRunnerであるthisを渡しています。
ではmodelのDivBy2Commandをのぞいてみましょう。
member val DivBy2Command =
cont { value.Val <- value.Val / 2 }
|> Command.ofCont
|> Command.guardBy (value |> Reactive.map (fun x -> x >= 10))
Command.ofCont
は継続モナドを受け取りコマンド(MoNo.Command)を作成する関数です。この関数で作成されるコマンドは常に実行可能なコマンドとなります。
さらにここではコマンドを Command.guardBy
関数に渡しています。
この関数はコマンドに実行可能な条件を付け加える関数です。ここでは値が10以上の場合に実行可能となるように条件を付加しています。
結局、DivBy2Commandは「値が10以上の場合に実行可能な、値を2で割るコマンド」となることがわかります。
TenTimesCommand¶
member val TenTimesCommand =
cont {
let x = model.Value.Val
let! y = this.ByAsync (async {
do! Async.Sleep 3000
return 10 * x
})
model.Value.Val <- y
}
|> Wpf.Cont.toWpfCommand this None
DoubleCommandと同様、 Wpf.Cont.toWpfCommand
に継続モナドを渡してコマンドを作成しています。
ここでは「継続モナド」の項、「ContRunnerの利用」で説明したContRunnerの ByAsync
メソッドを使っています。非同期で3秒(=3000ミリ秒)待機した後、値を10倍しています。
TwentyTimesCommand¶
member val TwentyTimesCommand =
cont {
let x = model.Value.Val
let! y = this.ByProgress (fun token ->
for i = 1 to 100 do
System.Threading.Thread.Sleep 30
token.Notify 0.01
20 * x)
model.Value.Val <- y
}
|> Wpf.Cont.toWpfCommand this None
やはり Wpf.Cont.toWpfCommand
に継続モナドを渡してコマンドを作成しています。
ここでは値を20倍する際に ByProgress
というメソッドを使って継続モナドを作成しています。
IContRunner.ByProgrsss : computation:Func<IProgressToken,'b> -> Cont<ISession,'b>
この関数は IProgressToken
を引数に取る関数を渡して継続モナドを作る関数です。
このIProgressTokenインターフェースを通して関数内での進捗を通知することができます。
具体的には Notify
メソッドで進捗割合を「加算」するよう通知することができます。合計が1.0に達すると進捗率が100%ということになります。
つまりByProgressによって30ミリ秒毎に進捗率を1%ずつ進める処理を100回繰り返した後、元の値を20倍して返す継続モナドを作成していることになります。
また、 通知された進捗率はByProgressメソッドを呼び出したContRunnerの ProgressRunner.CurrentProgressPercentage
に格納されます。その名の通り%で表した値が格納されます。
MainWindow.xamlをもう一度見て下さい。
...
<ProgressBar DockPanel.Dock="Bottom" Height="20"
Value="{Binding Path=ProgressRunner.CurrentProgressPercentage, Mode=OneWay}"/>
...
プログレスバーにViewModelのProgressRunner.CurrentProgressPercentageがBindされていることが確認できます。 TwentyTimesCommandの進捗具合がこのプログレスバーに表示されるようになっているのです。
CancelCommand¶
member val CancelCommand =
Wpf.Command.createWpfCommand (Some this.IsInSession) this.AbortSession
最後にCancelCommandです。
これは IsInSession
がtrueの値を持つときに実行可能で、AbortSession
関数を実行するコマンドということがわかります。
ではIsInSessionやAbortSessionとは何でしょうか。 IsInSessionは継承元であるContRunnerの持つプロパティで、セッション中であることを表します。 ByAsyncに渡されたasyncコンピューテーション式の実行中や、ByProgressに渡された関数の実行中などに値がtrueになります。
そしてAbortSessionは、セッション中の継続モナドを中止する関数です。
これでCancelCommandはセッション中の場合にのみ実行可能で、セッションを中止するコマンドであることがわかりました。
ところで「10 Times」ボタンや「20 Times」ボタンを押した後、完了待ちの間にいくつかのコマンドが実行できなくなっていたことにお気づきでしょうか?
DoubleCommand、DivBy2Command、TenTimesCommand、TwentyTimesCommandが実行できなくなっています。 これらのコマンドは、「常に実行可能なコマンド」として作成したはずです。 それに対して、IncrementCommandやDecrementCommandはセッション中でも実行可能になっています。 これは一体なぜでしょうか?
セッション中に実行できなくなったコマンドに共通するのは、コマンドを作成する関数にContRunnerを渡す関数でコマンドを作成したことです。
実はContRunnerを渡す関数で作成されたコマンドが実行可能かどうかは「引数で渡した実行可能フラグがtrueかどうか」だけでなく、 さらに「引数で渡したContRunnerがセッション中でない」という条件が暗黙的に付加されます。 このためセッション中はDoubleCommand、DivBy2Command、TenTimesCommand、TwentyTimesCommandが実行できなくなるというわけです。
この仕組みをうまく活用することで、重い処理を行っている最中に余計なコマンドを実行できないようにすることができます。
逆にCancelCommandを
member val CancelCommand =
cont {
this.AbortSession()
}
|> Wpf.Cont.toWpfCommand this (Some this.IsInSession)
と実装してしまうと、「セッション中」かつ「セッション中でない」場合に実行可能なコマンド、つまりは「常に実行できない」コマンドが実装されてしまうことになります。