Zoeken…


Invoering

Dit onderwerp illustreert hoe u functionele programmering in een WPF-toepassing kunt benutten. Het eerste voorbeeld komt uit een bericht van Māris Krivtežs (zie het gedeelte Opmerkingen onderaan). De reden voor het opnieuw bezoeken van dit project is tweeledig:

1 \ Het ontwerp ondersteunt de scheiding van punten van zorg, terwijl het model zuiver wordt gehouden en veranderingen op een functionele manier worden doorgevoerd.

2 \ De gelijkenis zorgt voor een gemakkelijke overgang naar de Gjallarhorn-implementatie.

Opmerkingen

Demo-projecten van bibliotheken @GitHub

Māris Krivtežs schreef twee geweldige berichten over dit onderwerp:

Ik heb het gevoel dat geen van deze XAML-toepassingsstijlen veel baat heeft bij functioneel programmeren. Ik stel me voor dat de ideale toepassing zou bestaan uit het beeld dat gebeurtenissen produceert en gebeurtenissen de huidige beeldstatus hebben. Alle applicatielogica moet worden afgehandeld door gebeurtenissen en het weergavemodel te filteren en te manipuleren, en in de uitvoer moet een nieuw weergavemodel worden geproduceerd dat is gebonden aan het aanzicht.

FSharp.ViewModule

Onze demo-app bestaat uit een scorebord. Het scoremodel is een onveranderlijk record. De scorebordgebeurtenissen zijn opgenomen in een Union Type.

namespace Score.Model

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

Wijzigingen worden doorgevoerd door te luisteren naar gebeurtenissen en het weergavemodel dienovereenkomstig bij te werken. In plaats van leden toe te voegen aan het modeltype, zoals in OOP, verklaren we een afzonderlijke module voor het hosten van de toegestane bewerkingen.

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

Ons weergavemodel is afgeleid van EventViewModelBase<'a> , dat een eigenschap EventStream van het type IObservable<'a> . In dit geval zijn de evenementen waarop we ons willen abonneren van het type ScoringEvent .

De controller verwerkt de gebeurtenissen op een functionele manier. De kenmerkende Score -> ScoringEvent -> Score toont ons dat, telkens wanneer zich een gebeurtenis voordoet, de huidige waarde van het model wordt omgezet in een nieuwe waarde. Dit zorgt ervoor dat ons model puur blijft, hoewel ons kijkmodel dat niet is.

Een eventHandler heeft de leiding over het wijzigen van de status van de weergave. Overnemen van EventViewModelBase<'a> we EventValueCommand en EventValueCommandChecked gebruiken om de gebeurtenissen aan de opdrachten te 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

In de code achter het bestand (* .xaml.fs) wordt alles samengevoegd, dwz dat de updatefunctie ( controller ) wordt geïnjecteerd in het MainViewModel .

namespace Score.Views

open FsXaml

type MainView = XAML<"MainWindow.xaml">

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

Het type CompositionRoot dient als een wrapper waarnaar wordt verwezen in het XAML-bestand.

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

Ik zal niet dieper in het XAML-bestand duiken omdat het basis WPF-dingen zijn, het hele project is te vinden op GitHub .

Gjallarhorn

De kerntypen in de Gjallarhorn-bibliotheek implementeren IObservable<'a> , waardoor de implementatie er vertrouwd uitziet (onthoud de eigenschap EventStream uit het voorbeeld FSharp.ViewModule). De enige echte verandering in ons model is de volgorde van de argumenten van de update-functie. We gebruiken nu ook de term Bericht in plaats van Gebeurtenis .

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

Om een gebruikersinterface met Gjallarhorn te bouwen, in plaats van klassen te maken om gegevensbinding te ondersteunen, maken we eenvoudige functies die een Component . In de constructor eerste argument source is van het type BindingSource (gedefinieerd in Gjallarhorn.Bindable), en gebruikt om het model om de weergave kaart en gebeurtenissen van het weergeven in berichten.

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
        ]

De huidige implementatie verschilt van de FSharp.ViewModule-versie in die zin dat twee opdrachten CanExecute nog niet correct hebben geïmplementeerd. Ook wordt het sanitair van de applicatie vermeld.

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

Links met het instellen van de ontkoppelde weergave, waarbij het type MainWindow en de logische toepassing worden gecombineerd.

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

Dit vat de kernbegrippen samen, voor meer informatie en een meer gedetailleerd voorbeeld raadpleegt u de post van Reed Copsey . Het project Christmas Trees belicht een aantal voordelen van deze aanpak:

  • Ons effectief verlossen van de noodzaak om het model (handmatig) te kopiëren naar een aangepaste verzameling weergavemodellen, deze te beheren en het model handmatig samen te stellen op basis van de resultaten.
  • Updates binnen collecties worden op een transparante manier gedaan, met behoud van een puur model.
  • De logica en weergave worden gehost door twee verschillende projecten, die de scheiding van zorgen benadrukken.


Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow