サーチ…
前書き
このトピックでは、 WPFアプリケーションでFunctional Programmingを活用する方法について説明します 。最初の例は、MārisKrivtežsの投稿(下の備考欄参照)から来ています。このプロジェクトを再訪する理由は2つあります。
モデルは純粋に保たれ、変更は機能的な方法で伝播されます。
2 \ Gjallarhornの実装に簡単に移行できるようになります。
備考
ライブラリデモプロジェクト@GitHub
- FSharp.ViewModule ( FsXamlの下)
- Gjallarhorn (refサンプル)
MārisKrivtežsはこのトピックに関する2つの偉大な記事を書いた:
- F#XAMLアプリケーション -両方のアプローチの賛否両論が強調表示されているMVVM対MVC
私はこれらのXAMLアプリケーションスタイルのどれも関数型プログラミングのメリットがないと感じています。理想的なアプリケーションは、イベントを生成するビューとイベントが現在のビュー状態を保持することになると私は想像しています。すべてのアプリケーションロジックは、イベントのフィルタリングと操作、およびモデルの表示によって処理され、出力では、ビューにバインドされた新しいビューモデルが生成されます。
- F#XAML -上記のトピックで再訪されたイベント駆動型MVVM
FSharp.ViewModule
デモアプリはスコアボードで構成されています。スコアモデルは不変レコードです。スコアボードイベントはユニオンタイプに含まれています。
namespace Score.Model
type Score = { ScoreA: int ; ScoreB: int }
type ScoringEvent = IncA | DecA | IncB | DecB | NewGame
イベントをリッスンし、それに応じてビューモデルを更新することによって、変更が伝播されます。 OOPのように、モデルタイプにメンバーを追加する代わりに、許可された操作をホストする別のモジュールを宣言します。
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module Score =
let zero = {ScoreA = 0; ScoreB = 0}
let update score event =
match event with
| IncA -> {score with ScoreA = score.ScoreA + 1}
| DecA -> {score with ScoreA = max (score.ScoreA - 1) 0}
| IncB -> {score with ScoreB = score.ScoreB + 1}
| DecB -> {score with ScoreB = max (score.ScoreB - 1) 0}
| NewGame -> zero
私たちのビューモデルから派生EventViewModelBase<'a>
不動産あり、 EventStream
タイプのIObservable<'a>
。この場合、購読したいイベントはScoringEvent
型ScoringEvent
。
コントローラは、機能的な方法でイベントを処理します。そのScore -> ScoringEvent -> Score
は、イベントが発生するたびに、モデルの現在の値が新しい値に変換されることを示しています。ビューモデルはそうではありませんが、これにより私たちのモデルは純粋なままです。
eventHandler
は、ビューの状態を変更することを担当します。 EventViewModelBase<'a>
から継承し、 EventValueCommand
とEventValueCommandChecked
を使用してイベントにコマンドを接続することができます。
namespace Score.ViewModel
open Score.Model
open FSharp.ViewModule
type MainViewModel(controller : Score -> ScoringEvent -> Score) as self =
inherit EventViewModelBase<ScoringEvent>()
let score = self.Factory.Backing(<@ self.Score @>, Score.zero)
let eventHandler ev = score.Value <- controller score.Value ev
do
self.EventStream
|> Observable.add eventHandler
member this.IncA = this.Factory.EventValueCommand(IncA)
member this.DecA = this.Factory.EventValueCommandChecked(DecA, (fun _ -> this.Score.ScoreA > 0), [ <@@ this.Score @@> ])
member this.IncB = this.Factory.EventValueCommand(IncB)
member this.DecB = this.Factory.EventValueCommandChecked(DecB, (fun _ -> this.Score.ScoreB > 0), [ <@@ this.Score @@> ])
member this.NewGame = this.Factory.EventValueCommand(NewGame)
member __.Score = score.Value
コードビハインドファイル(* .xaml.fs)はすべてがまとめられていMainViewModel
。つまり、更新機能( controller
)がMainViewModelに挿入されていMainViewModel
。
namespace Score.Views
open FsXaml
type MainView = XAML<"MainWindow.xaml">
type CompositionRoot() =
member __.ViewModel = Score.ViewModel.MainViewModel(Score.Model.Score.update)
CompositionRoot
型は、XAMLファイルで参照されるラッパーとして機能します。
<Window.Resources>
<ResourceDictionary>
<local:CompositionRoot x:Key="CompositionRoot"/>
</ResourceDictionary>
</Window.Resources>
<Window.DataContext>
<Binding Source="{StaticResource CompositionRoot}" Path="ViewModel" />
</Window.DataContext>
基本的なWPFのように、私はXAMLファイルを詳しく調べず、プロジェクト全体をGitHubで見つけることができます。
Gjallarhorn
GjallarhornライブラリのコアタイプはIObservable<'a>
実装しているため、実装はEventStream
ます(FSharp.ViewModuleの例のEventStream
プロパティを覚えておいてEventStream
)。モデルの唯一の実際の変更は、更新関数の引数の順序です。また、 イベントの代わりにメッセージという用語を使用します。
namespace ScoreLogic.Model
type Score = { ScoreA: int ; ScoreB: int }
type ScoreMessage = IncA | DecA | IncB | DecB | NewGame
// Module showing allowed operations
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module Score =
let zero = {ScoreA = 0; ScoreB = 0}
let update msg score =
match msg with
| IncA -> {score with ScoreA = score.ScoreA + 1}
| DecA -> {score with ScoreA = max (score.ScoreA - 1) 0}
| IncB -> {score with ScoreB = score.ScoreB + 1}
| DecB -> {score with ScoreB = max (score.ScoreB - 1) 0}
| NewGame -> zero
GjallarhornでUIを構築するために、データバインディングをサポートするクラスを作成するのではなく、 Component
と呼ばれる単純な関数を作成しComponent
。それらのコンストラクタでは、最初の引数source
はBindingSource
型(Gjallarhorn.Bindableで定義されている)であり、モデルをビューにマップし、ビューからのイベントをメッセージに戻します。
namespace ScoreLogic.Model
open Gjallarhorn
open Gjallarhorn.Bindable
module Program =
// Create binding for entire application.
let scoreComponent source (model : ISignal<Score>) =
// Bind the score to the view
model |> Binding.toView source "Score"
[
// Create commands that turn into ScoreMessages
source |> Binding.createMessage "NewGame" NewGame
source |> Binding.createMessage "IncA" IncA
source |> Binding.createMessage "DecA" DecA
source |> Binding.createMessage "IncB" IncB
source |> Binding.createMessage "DecB" DecB
]
現在の実装は、2つのコマンドがCanExecuteを適切に実装していないという点で、FSharp.ViewModuleバージョンとは異なります。また、アプリケーションの配管を列挙します。
namespace ScoreLogic.Model
open Gjallarhorn
open Gjallarhorn.Bindable
module Program =
// Create binding for entire application.
let scoreComponent source (model : ISignal<Score>) =
let aScored = Mutable.create false
let bScored = Mutable.create false
// Bind the score itself to the view
model |> Binding.toView source "Score"
// Subscribe to changes of the score
model |> Signal.Subscription.create
(fun currentValue ->
aScored.Value <- currentValue.ScoreA > 0
bScored.Value <- currentValue.ScoreB > 0)
|> ignore
[
// Create commands that turn into ScoreMessages
source |> Binding.createMessage "NewGame" NewGame
source |> Binding.createMessage "IncA" IncA
source |> Binding.createMessageChecked "DecA" aScored DecA
source |> Binding.createMessage "IncB" IncB
source |> Binding.createMessageChecked "DecB" bScored DecB
]
let application =
// Create our score, wrapped in a mutable with an atomic update function
let score = new AsyncMutable<_>(Score.zero)
// Create our 3 functions for the application framework
// Start with the function to create our model (as an ISignal<'a>)
let createModel () : ISignal<_> = score :> _
// Create a function that updates our state given a message
// Note that we're just taking the message, passing it directly to our model's update function,
// then using that to update our core "Mutable" type.
let update (msg : ScoreMessage) : unit = Score.update msg |> score.Update |> ignore
// An init function that occurs once everything's created, but before it starts
let init () : unit = ()
// Start our application
Framework.application createModel init update scoreComponent
MainWindow
タイプと論理アプリケーションを結合して、デカップリングビューを設定します。
namespace ScoreBoard.Views
open System
open FsXaml
open ScoreLogic.Model
// Create our Window
type MainWindow = XAML<"MainWindow.xaml">
module Main =
[<STAThread>]
[<EntryPoint>]
let main _ =
// Run using the WPF wrappers around the basic application framework
Gjallarhorn.Wpf.Framework.runApplication System.Windows.Application MainWindow Program.application
これは、コアコンセプトをまとめたものです。詳細については、 Reed Copseyの記事を参照してください。 クリスマスツリープロジェクトでは、このアプローチにいくつかの利点があります。
- モデルをビューモデルのカスタムコレクションに(手動で)コピーし、それらを管理し、結果からモデルを手動で構築する必要から、私たちを効果的に交換します。
- コレクション内の更新は、純粋なモデルを維持しながら、透過的に行われます。
- 論理と見解は、2つの異なるプロジェクトによって主導され、懸念の分離を強調する。