概要
初期値50から開始して45から55の範囲で、「+1」ボタンによりカウントアップ、「-1」ボタンによりカウントダウンするアプリケーションを、SodiumFRPを利用したものと、ReactivePropertyを利用したものの2つのバージョンで作成しました。
- SodiumFRP + WPF + F# による MVVM なスタイル
- ReactiveProperty + WPF + F# による MVVM なスタイル
動作のイメージ
ボタンをクリックすることでカウントアップ、カウントダウンしていきます。
範囲(45~55)を超える数値とならないように、ボタンの有効・無効もICommand
のCanExecute
を通じて自動で切り替わるようにしていきます。
SodiumFRP
SodiumFRPとは・・・、説明できるほど理解しきれていません。
- セル(Cell)とは「状態」をモデル化したもの
- ストリーム(Stream)とは「状態の変化」をモデル化したもの
関数型リアクティブプログラミング (翔泳社) や、Sodium(FRP)を使ってみた@Qiita、SodiumでFunctional Reactive Programming (F#)@何でもプログラミングが参考になります。
準備(共通)
F#でWPFアプリケーションを作成するための手順については、F#+WPF+ReactiveProperty@Qiita を参考にさせていただきました。C#のときのように、VisualStudioの「新規プロジェクト作成」からテンプレートを選んで・・・というわけにはいかないようです(VS2017の場合)。
Viewの設計(共通)
- データコンテキストとして、次セクションで作成するビューモデル
MainWindowVM (.fs)
を指定 - カウント値を表示するためのTextBlockを配置して、プロパティ
Count.Value
をバインディング - カウントアップ/ダウンの操作のボタンを配置して、コマンド
PlusCommand
、MinusCommand
をバインディング- コマンドパラメータは「なし」としておきます。
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm ="clr-namespace:xxxxLab.ViewModels;assembly=WindowApp"
Title="{Binding WindowTitle, Mode=OneTime}" Height="85" Width="350">
<Window.DataContext>
<vm:MainWindowVM/>
</Window.DataContext>
<Grid>
<StackPanel Orientation="Horizontal" Margin="10">
<StackPanel.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Width" Value="60"/>
<Setter Property="TextAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="FontSize" Value="18"/>
</Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Width" Value="50"/>
<Setter Property="Height" Value="25"/>
<Setter Property="Margin" Value="5,0"/>
</Style>
</StackPanel.Resources>
<TextBlock Text="{Binding Count.Value}"/>
<Button Content="+1" Command="{Binding PlusCommand}"/>
<Button Content="-1" Command="{Binding MinusCommand}"/>
</StackPanel>
</Grid>
</Window>
ViewModelの設計 ReactivePeroperty版
- Nugetで「ReactiveProperty 5.3.2」を追加
- ReactiveCommand.Subscribeの戻値はとりあえず破棄
namespace xxxxLab.ViewModels
open System
open System.Reactive.Linq
open Reactive.Bindings
type MainWindowVM() =
let initValue = 50
let maxValue = 55
let minValue = 45
// ReactiveProperty Version
let count = new ReactiveProperty<int>(initValue)
let plusCommand = count.Select(fun p -> p < maxValue).ToReactiveCommand()
let minusCommand = count.Select(fun p -> p > minValue).ToReactiveCommand()
do plusCommand.Subscribe (fun _ -> count.Value<-count.Value+1) |> ignore
do minusCommand.Subscribe (fun _ -> count.Value<-count.Value-1) |> ignore
member x.WindowTitle = "F# MVVM ReactiveProperty ver."
member x.Count = count
member x.PlusCommand = plusCommand
member x.MinusCommand = minusCommand
ViewModelの設計 SodiumFRP版
SoduimをReactiveProperty的に使えるようにSodiumRP
、ReactiveCommand的に使えるようにSodiumRC
を定義しました。
- Nugetで「SodiumFRP.FSharp 4.0.5」を追加
namespace xxxLab.ViewModels
open System
open SodiumFRP
open System.ComponentModel
open System.Windows.Input
type SodiumRP<'T>( stateCell:Cell<'T> ) as this =
let ev = new Event<_,_>()
let propChangedNotifier _ = postT <| fun _ -> ev.Trigger(this, PropertyChangedEventArgs("Value"))
let listener:IStrongListener = stateCell
|> listenC propChangedNotifier
member x.Value = Cell.sample stateCell
interface IDisposable with
member __.Dispose() = listener.Unlisten ()
interface INotifyPropertyChanged with
[<CLIEvent>]
member x.PropertyChanged = ev.Publish
type SodiumRC<'T>( commandExecuteStream :StreamSink<Option<'T>> , canExecuteCell:Cell<bool> ) as this =
let ev = new Event<_,_>()
let commandExecute p = StreamSink.send p commandExecuteStream
let canExecuteChangedNotifier _ = postT <| fun _ -> ev.Trigger(this, EventArgs())
let listener:IStrongListener = canExecuteCell |> listenC canExecuteChangedNotifier
interface IDisposable with
member __.Dispose() = listener.Unlisten ()
interface ICommand with
member this.CanExecute obj = Cell.sample canExecuteCell
member this.Execute commandParameter =
let p = match commandParameter with
| null -> None
| x -> Some (x:?>'T)
commandExecute p
[<CLIEvent>]
member this.CanExecuteChanged = ev.Publish
SodiumRP
とSodiumRC
を利用したViewModelが、次のMainWindowVM.fs
のようになります。
概念図
- 各コマンドの実行(発火)をストリーム(
ssPlus
、ssMinus
)としてモデル化しています。 - カウント値(状態)をセル(
cCount
)としてモデル化しています。 - 各コマンドが実行可能か?という状態をセル(
cCanExecutePlus
,cCanExecuteMinus
)としてモデル化しています。
コード
namespace xxxxLab.ViewModels
open System
open SodiumFRP
type MainWindowVM() =
let initValue = 50
let maxValue = 55
let minValue = 45
// SodiumFRP Version
let ssPlus = StreamSink.create<Option<unit>>()
let ssMinus = StreamSink.create<Option<unit>>()
let cCount = Sodium.loopWithNoCapturesC ( fun value ->
let sPlusDelta = ssPlus |> Stream.map (fun _ -> 1 )
let sMinusDelta = ssMinus |> Stream.map (fun _ -> -1)
let sDelta = (sPlusDelta, sMinusDelta) |> Stream.orElse
let sCountUpdate = sDelta |> Stream.snapshot value (+)
sCountUpdate |> Stream.hold initValue )
let cCanExecutePlus = cCount |> mapC (fun p -> p < maxValue)
let cCanExecuteMinus = cCount |> mapC (fun p -> p > minValue)
member x.WindowTitle = "F# MVVM SodiumFRP ver."
member x.PlusCommand = new SodiumRC<unit>(ssPlus,cCanExecutePlus)
member x.MinusCommand = new SodiumRC<unit>(ssMinus,cCanExecuteMinus)
member x.Count = new SodiumRP<int>(cCount)
コマンドパラメータで増分値を指定
次のように「+3」ボタンを追加することを考えます。なお、カウント値が54のときに「+3」が押されても57ではなく55(カウント範囲の最大値)になるように動作させます。
Viewの修正変更
次のように、CommandParameterによりView上でstring型で増分値(この場合は「+3」)を与えるものとします。また、CommandParameterなしでCommandを実行した場合は、それぞれ「+1」「-1」の増分値が与えられるものとします。
<TextBlock Text="{Binding Count.Value}"/>
<Button Content="+1" Command="{Binding PlusCommand}"/>
<Button Content="+3" Command="{Binding PlusCommand}"
CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"/>
<Button Content="-1" Command="{Binding MinusCommand}"/>
ViewModelの修正変更 SodiumFRP版
主な変更点は次のようになります。
- string型を想定したコマンドパラメータを扱うために、
ssPlus
とssMinus
を、StreamSink.create<Option<unit>>()
からStreamSink.create<Option<string>>()
に変更しました。 - ストリーム
ssPlus
にはオプション型(Option<string>
)のデータが流れます。具体的には、コマンドパラメータが指定されてるときはSome("string型のコマンドパラメータ値")
、そうでないときはNone
となります。- データが
None
でなければOption.get
により内容を取り出しています。
- データが
-
Stream.snapshot value (fun d v -> Math.Min(d,maxValue-v))
により、カウント値が上限を超えないように調整しています。
namespace xxxxLab.ViewModels
open System
open SodiumFRP
open System
type MainWindowVM() =
let initValue = 50
let maxValue = 55
let minValue = 45
// SodiumFRP Version
let ssPlus = StreamSink.create<Option<string>>()
let ssMinus = StreamSink.create<Option<string>>()
let cCount = Sodium.loopWithNoCapturesC ( fun value ->
let sPlusDelta = ssPlus
|> Stream.map (fun p -> if p=None then 1 else int <| Option.get p )
|> Stream.snapshot value (fun d v -> Math.Min(d,maxValue-v))
let sMinusDelta = ssMinus
|> Stream.map (fun p -> if p=None then -1 else int <| Option.get p)
let sDelta = (sPlusDelta, sMinusDelta) |> Stream.orElse
let sCountUpdate = sDelta |> Stream.snapshot value (+)
sCountUpdate |> Stream.hold initValue )
let cCanExecutePlus = cCount |> mapC (fun p -> p < maxValue)
let cCanExecuteMinus = cCount |> mapC (fun p -> p > minValue)
member x.WindowTitle = "F# MVVM SodiumFRP ver."
member x.PlusCommand = new SodiumRC<string>(ssPlus,cCanExecutePlus)
member x.MinusCommand = new SodiumRC<string>(ssMinus,cCanExecuteMinus)
member x.Count = new SodiumRP<int>(cCount)
参考資料
- F#+WPF+ReactiveProperty@Qiita
- SodiumでFunctional Reactive Programming (F#)@何でもプログラミング
- 実践F# 関数型プログラミング入門 (技術評論社)
- 関数型リアクティブプログラミング (翔泳社)