F#
Introduzione a WPF in F #
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
- FSharp.ViewModule (sotto FsXaml)
- Gjallarhorn (campioni di riferimento)
Māris Krivtežs ha scritto due fantastici post su questo argomento:
- Applicazione F # XAML - MVVM vs MVC in cui vengono evidenziati i pro e i contro di entrambi gli approcci.
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.
- F # XAML - MVVM basato sugli eventi come rivisitato nell'argomento sopra.
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.