Ricerca…


introduzione

Questo argomento illustra come sfruttare la programmazione funzionale in un'applicazione WPF . Il primo esempio proviene da un post di Māris Krivtežs (sezione Remarks ref in basso). La ragione per rivisitare questo progetto è duplice:

1 \ Il design supporta la separazione delle preoccupazioni, mentre il modello è mantenuto puro e le modifiche sono propagate in modo funzionale.

2 \ La somiglianza faciliterà la transizione all'implementazione di Gjallarhorn.

Osservazioni

Progetti demo della biblioteca @ GitHub

Māris Krivtežs ha scritto due fantastici post su questo argomento:

Ritengo che nessuno di questi stili di applicazione XAML tragga un grande beneficio dalla programmazione funzionale. Immagino che l'applicazione ideale consista nella vista che produce eventi ed eventi che mantengono lo stato attuale della vista. Tutta la logica applicativa dovrebbe essere gestita filtrando e manipolando eventi e modelli di vista, e nell'output dovrebbe produrre un nuovo modello di vista che è legato alla vista.

FSharp.ViewModule

La nostra app demo è composta da un tabellone. Il modello del punteggio è un record immutabile. Gli eventi segnapunti sono contenuti in un Tipo Unione.

namespace Score.Model

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

Le modifiche vengono propagate ascoltando gli eventi e aggiornando il modello di visualizzazione di conseguenza. Invece di aggiungere membri al tipo di modello, come in OOP, dichiariamo un modulo separato per ospitare le operazioni consentite.

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

Il nostro modello di visualizzazione deriva da EventViewModelBase<'a> , che ha una proprietà EventStream di tipo IObservable<'a> . In questo caso gli eventi a cui vogliamo iscriversi sono di tipo ScoringEvent .

Il controller gestisce gli eventi in modo funzionale. Il suo Score -> ScoringEvent -> Score firma Score -> ScoringEvent -> Score ci mostra che, ogni volta che si verifica un evento, il valore corrente del modello viene trasformato in un nuovo valore. Ciò consente al nostro modello di rimanere puro, sebbene il nostro modello di vista non lo sia.

Un eventHandler compito di modificare lo stato della vista. Ereditando da EventViewModelBase<'a> possiamo usare EventValueCommand ed EventValueCommandChecked per collegare gli eventi ai comandi.

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

Il codice dietro il file (* .xaml.fs) è dove tutto è messo insieme, cioè la funzione di aggiornamento ( controller ) viene iniettata nel MainViewModel .

namespace Score.Views

open FsXaml

type MainView = XAML<"MainWindow.xaml">

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

Il tipo CompositionRoot funge da wrapper a cui viene fatto riferimento nel file XAML.

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

Non approfondirò più nel file XAML dato che è roba di base di WPF, l'intero progetto può essere trovato su GitHub .

Gjallarhorn

I tipi fondamentali nella biblioteca Gjallarhorn implementano IObservable<'a> , che renderà l'attuazione aspetto familiare (ricordate EventStream proprietà dall'esempio FSharp.ViewModule). L'unica vera modifica al nostro modello è l'ordine degli argomenti della funzione di aggiornamento. Inoltre, ora usiamo il termine Messaggio anziché Evento .

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

Per costruire un'interfaccia utente con Gjallarhorn, invece di creare classi per supportare l'associazione dati, creiamo semplici funzioni denominate Component . Nel loro costruttore la prima source argomenti è di tipo BindingSource (definita in Gjallarhorn.Bindable) e utilizzata per associare il modello alla vista e gli eventi dalla vista ai messaggi.

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
        ]

L'implementazione corrente differisce dalla versione di FSharp.ViewModule in quanto due comandi non hanno ancora correttamente implementato CanExecute. Elenca anche l'impianto idraulico dell'applicazione.

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

A sinistra con l'impostazione della vista disaccoppiata, combinando il tipo MainWindow e l'applicazione logica.

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

Questo riassume i concetti fondamentali, per ulteriori informazioni e un esempio più elaborato si prega di fare riferimento al post di Reed Copsey . Il progetto Alberi di Natale evidenzia un paio di vantaggi a questo approccio:

  • Ci riscattiamo efficacemente dalla necessità di copiare (manualmente) il modello in una collezione personalizzata di modelli di vista, gestendoli e ricostruendo manualmente il modello dai risultati.
  • Gli aggiornamenti all'interno delle raccolte vengono eseguiti in modo trasparente, pur mantenendo un modello puro.
  • La logica e la vista sono ospitate da due diversi progetti, sottolineando la separazione delle preoccupazioni.


Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow