Sök…


Introduktion

Detta ämne illustrerar hur man utnyttjar funktionell programmering i en WPF-applikation . Det första exemplet kommer från ett inlägg av Māris Krivtežs (ref Kommentarer avsnitt längst ner). Anledningen till att besöka detta projekt är tvåfaldigt:

1 \ Designen stöder separering av oro, medan modellen hålls ren och förändringar sprids på ett funktionellt sätt.

2 \ Likheten kommer att möjliggöra en enkel övergång till Gjallarhorn-implementeringen.

Anmärkningar

Demonstrationsprojekt för bibliotek @GitHub

Māris Krivtežs skrev två bra inlägg om detta ämne:

Jag känner att ingen av dessa XAML-applikationsstilar drar mycket nytta av funktionell programmering. Jag föreställer mig att den ideala applikationen skulle bestå av den vy som producerar händelser och händelser som håller det aktuella visningsläget. All applikationslogik ska hanteras genom att filtrera och manipulera händelser och visningsmodell, och i utgången ska den producera en ny visningsmodell som är bunden tillbaka till vyn.

FSharp.ViewModule

Vår demo-app består av en resultattavla. Poängmodellen är en oföränderlig rekord. Resultattavlan finns i en unionstyp.

namespace Score.Model

type Score = { ScoreA: int ; ScoreB: int }    
type ScoringEvent = IncA | DecA | IncB | DecB | NewGame

Ändringar sprids genom att lyssna på händelser och uppdatera visningsmodellen i enlighet därmed. Istället för att lägga till medlemmar till modelltypen, som i OOP, förklarar vi en separat modul som är värd för de tillåtna operationerna.

[<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 

Vår visningsmodell härstammar från EventViewModelBase<'a> , som har en egenskap EventStream av typen IObservable<'a> . I det här fallet är de händelser vi vill prenumerera på av typen ScoringEvent .

Styrenheten hanterar händelserna på ett funktionellt sätt. Dess signatur Score -> ScoringEvent -> Score visar oss att närhelst en händelse inträffar förvandlas modellens nuvarande värde till ett nytt värde. Detta gör att vår modell kan förbli ren, även om vår synsmodell inte är det.

En eventHandler ansvarar för att mutera visningen. Arv från EventViewModelBase<'a> vi kan använda EventValueCommand och EventValueCommandChecked att ansluta händelserna till kommandona.

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

Koden bakom filen (* .xaml.fs) är där allt sätts ihop, dvs. uppdateringsfunktionen ( controller ) injiceras i MainViewModel .

namespace Score.Views

open FsXaml

type MainView = XAML<"MainWindow.xaml">

type CompositionRoot() =
    member __.ViewModel = Score.ViewModel.MainViewModel(Score.Model.Score.update)

Typen CompositionRoot fungerar som ett omslag som refereras till i XAML-filen.

<Window.Resources>
    <ResourceDictionary>
        <local:CompositionRoot x:Key="CompositionRoot"/>
    </ResourceDictionary>
</Window.Resources>
<Window.DataContext>
    <Binding Source="{StaticResource CompositionRoot}" Path="ViewModel" />
</Window.DataContext>

Jag dyker inte djupare in i XAML-filen eftersom det är grundläggande WPF-grejer, hela projektet kan hittas på GitHub .

Gjallarhorn

Kärntyper i Gjallarhorn biblioteket genomföra IObservable<'a> , vilket kommer att göra genomförandet ser bekant (minns EventStream egendom från FSharp.ViewModule exempel). Den enda verkliga förändringen av vår modell är ordningen på argumenten för uppdateringsfunktionen. Vi använder också termen Meddelande istället för Event .

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

För att bygga ett UI med Gjallarhorn, istället för att göra klasser för att stödja databindning, skapar vi enkla funktioner som kallas en Component . I deras konstruktor det första argumentet source är av typ BindingSource (definieras i Gjallarhorn.Bindable), och används för att kart modellen till vyn, och händelser från vyn tillbaka in meddelanden.

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
        ]

Den nuvarande implementeringen skiljer sig från FSharp.ViewModule-versionen genom att två kommandon inte har CanExecute korrekt implementerat ännu. Listar också applikationens VVS.

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

Vänster med att ställa in den frånkopplade vyn, kombinera MainWindow typen och den logiska applikationen.

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

Detta sammanfattar kärnbegreppen, för ytterligare information och ett mer detaljerat exempel hänvisas till Reed Copseys inlägg . Christmas Trees- projektet belyser ett par fördelar med denna strategi:

  • Lös oss effektivt in från behovet av att (manuellt) kopiera modellen till en anpassad samling visningsmodeller, hantera dem och konstruera modellen manuellt utifrån resultaten.
  • Uppdateringar inom samlingarna görs på ett öppet sätt samtidigt som en ren modell upprätthålls.
  • Logiken och vyn är värd för två olika projekt, som betonar separationen av oro.


Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow