수색…


소개

이 항목에서는 WPF 응용 프로그램 에서 Functional Programming 을 활용하는 방법을 설명합니다. 첫 번째 예는 Māris Krivtežs (하단의 비고 섹션 참조)의 게시물에서 가져온 것입니다. 이 프로젝트를 다시 방문하는 이유는 두 가지입니다.

1 \ 디자인은 관심사 분리를 지원하지만 모델은 순수하게 유지되고 변경은 기능적인 방식으로 전파됩니다.

2 \ 비슷한 점이 Gjallarhorn 구현으로 쉽게 전환 할 수 있습니다.

비고

라이브러리 데모 프로젝트 @GitHub

Māris Krivtežs는이 주제에 대해 두 가지 훌륭한 글을 썼습니다.

이러한 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> 에서 파생됩니다. EventViewModelBase<'a> 에는 IObservable<'a> 유형의 EventStream 속성이 있습니다. 이 경우 우리가 구독하고자하는 이벤트는 ScoringEvent 유형 ScoringEvent .

컨트롤러는 기능적으로 이벤트를 처리합니다. 그것의 서명 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)은 모든 것이 함께 저장되는 곳입니다. 즉, 업데이트 기능 ( controller )이 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> 구현하므로 구현이 익숙해집니다 (FSharp.ViewModule 예제의 EventStream 속성 기억). 우리 모델의 유일한 실제 변경은 업데이트 기능의 인수 순서입니다. 또한 이제 Event 대신 Message 라는 용어를 사용합니다.

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 라고하는 간단한 함수를 만듭니다. 생성자에서 첫 번째 인수 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
        ]

현재 구현은 FSharp.ViewModule 버전과는 달리 두 명령이 아직 제대로 구현되지 않은 CanExecute를 가지고 있지 않습니다. 또한 응용 프로그램의 배관을 나열합니다.

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의 게시물을 참조하십시오. 크리스마스 트리 프로젝트는이 접근법에 대한 몇 가지 이점을 강조합니다.

  • 모델을 뷰 모델의 사용자 정의 컬렉션으로 (수동으로) 복사하고, 관리하고, 결과에서 모델을 수동으로 다시 구성해야하는 필요성을 효과적으로 줄여줍니다.
  • 컬렉션 내의 업데이트는 순수한 모델을 유지하면서 투명한 방식으로 수행됩니다.
  • 논리와 견해는 두 가지 다른 프로젝트에 의해 주최되며, 관심사의 분리를 강조합니다.


Modified text is an extract of the original Stack Overflow Documentation
아래 라이선스 CC BY-SA 3.0
와 제휴하지 않음 Stack Overflow