サーチ…


前書き

このトピックでは、 WPFアプリケーションFunctional Programmingを活用する方法について説明します 。最初の例は、MārisKrivtežsの投稿(下の備考欄参照)から来ています。このプロジェクトを再訪する理由は2つあります。

モデルは純粋に保たれ、変更は機能的な方法で伝播されます。

2 \ Gjallarhornの実装に簡単に移行できるようになります。

備考

ライブラリデモプロジェクト@GitHub

MārisKrivtežsはこのトピックに関する2つの偉大な記事を書いた:

私はこれらのXAMLアプリケーションスタイルのどれも関数型プログラミングのメリットがないと感じています。理想的なアプリケーションは、イベントを生成するビューとイベントが現在のビュー状態を保持することになると私は想像しています。すべてのアプリケーションロジックは、イベントのフィルタリングと操作、およびモデルの表示によって処理され、出力では、ビューにバインドされた新しいビューモデルが生成されます。

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> 。この場合、購読したいイベントはScoringEventScoringEvent

コントローラは、機能的な方法でイベントを処理します。そのScore -> ScoringEvent -> Scoreは、イベントが発生するたびに、モデルの現在の値が新しい値に変換されることを示しています。ビューモデルはそうではありませんが、これにより私たちのモデルは純粋なままです。

eventHandlerは、ビューの状態を変更することを担当します。 EventViewModelBase<'a>から継承し、 EventValueCommandEventValueCommandCheckedを使用してイベントにコマンドを接続することができます。

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 。それらのコンストラクタでは、最初の引数sourceBindingSource型(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つの異なるプロジェクトによって主導され、懸念の分離を強調する。


Modified text is an extract of the original Stack Overflow Documentation
ライセンスを受けた CC BY-SA 3.0
所属していない Stack Overflow