LoginSignup
2
2

More than 5 years have passed since last update.

SodiumFRP + WPF(F#) ※ ReactivePropertyとの比較付き

Last updated at Posted at 2019-02-05

概要

 初期値50から開始して45から55の範囲で、「+1」ボタンによりカウントアップ、「-1」ボタンによりカウントダウンするアプリケーションを、SodiumFRPを利用したものと、ReactivePropertyを利用したものの2つのバージョンで作成しました。
- SodiumFRP + WPF + F# による MVVM なスタイル
- ReactiveProperty + WPF + F# による MVVM なスタイル

動作のイメージ

ボタンをクリックすることでカウントアップ、カウントダウンしていきます。
2019-02-05_12h18_58.png

範囲(45~55)を超える数値とならないように、ボタンの有効・無効もICommandCanExecuteを通じて自動で切り替わるようにしていきます。
2019-02-05_12h28_59.png

SodiumFRP

SodiumFRPとは・・・、説明できるほど理解しきれていません。

  • セル(Cell)とは「状態」をモデル化したもの
  • ストリーム(Stream)とは「状態の変化」をモデル化したもの

関数型リアクティブプログラミング (翔泳社) や、Sodium(FRP)を使ってみた@QiitaSodiumでFunctional Reactive Programming (F#)@何でもプログラミングが参考になります。

準備(共通)

F#でWPFアプリケーションを作成するための手順については、F#+WPF+ReactiveProperty@Qiita を参考にさせていただきました。C#のときのように、VisualStudioの「新規プロジェクト作成」からテンプレートを選んで・・・というわけにはいかないようです(VS2017の場合)。

Viewの設計(共通)

  • データコンテキストとして、次セクションで作成するビューモデルMainWindowVM (.fs)を指定
  • カウント値を表示するためのTextBlockを配置して、プロパティCount.Valueをバインディング
  • カウントアップ/ダウンの操作のボタンを配置して、コマンドPlusCommandMinusCommandをバインディング
    • コマンドパラメータは「なし」としておきます。
MainWindow.xaml
<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の戻値はとりあえず破棄
MainWindowVM.fs
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」を追加
SodiumRP.fs
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

SodiumRPSodiumRCを利用したViewModelが、次のMainWindowVM.fsのようになります。

概念図

  • 各コマンドの実行(発火)をストリーム(ssPlusssMinus)としてモデル化しています。
  • カウント値(状態)をセル(cCount)としてモデル化しています。
  • 各コマンドが実行可能か?という状態をセル(cCanExecutePlus,cCanExecuteMinus)としてモデル化しています。

名称未設定-2.png

コード

MainWindowVM.fs
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(カウント範囲の最大値)になるように動作させます。

2019-02-06_12h17_28.png

Viewの修正変更

次のように、CommandParameterによりView上でstring型で増分値(この場合は「+3」)を与えるものとします。また、CommandParameterなしでCommandを実行した場合は、それぞれ「+1」「-1」の増分値が与えられるものとします。

MainWindow.xaml(変更部周辺を抜粋)
<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型を想定したコマンドパラメータを扱うために、ssPlusssMinus を、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)) により、カウント値が上限を超えないように調整しています。
    • とても分かりずらいですが、dがパイプ演算子により前から渡される増分値、vがスナップショットにより取得された現在のカウント値となります。
    • 無題8.png
MainWindowVM.fs
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)

参考資料

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2