F#
Introduction à WPF en F #
Recherche…
Introduction
Cette rubrique illustre comment exploiter la programmation fonctionnelle dans une application WPF . Le premier exemple provient d'un article de Māris Krivtežs (voir la section Remarques en bas). La raison de revoir ce projet est double:
1 \ La conception prend en charge la séparation des problèmes, tandis que le modèle reste pur et que les modifications sont propagées de manière fonctionnelle.
2 \ La ressemblance permettra une transition facile vers la mise en œuvre de Gjallarhorn.
Remarques
Projets de démonstration de la bibliothèque @GitHub
- FSharp.ViewModule (sous FsXaml)
- Gjallarhorn (réf Samples)
M. Krivtežs a écrit deux excellents articles sur ce sujet:
- Application F # XAML - MVVM vs MVC où les avantages et les inconvénients des deux approches sont mis en évidence.
Je pense qu'aucun de ces styles d'application XAML ne profite beaucoup de la programmation fonctionnelle. J'imagine que l'application idéale serait la vue qui produit des événements et les événements contiennent l'état d'affichage actuel. Toute la logique de l'application doit être traitée en filtrant et en manipulant les événements et le modèle de vue. Dans la sortie, elle doit produire un nouveau modèle de vue lié à la vue.
- F # XAML - MVVM événementiel revisité dans le sujet ci-dessus.
FSharp.ViewModule
Notre application de démonstration consiste en un tableau de bord. Le modèle de score est un enregistrement immuable. Les événements du tableau d'affichage sont contenus dans un type d'union.
namespace Score.Model
type Score = { ScoreA: int ; ScoreB: int }
type ScoringEvent = IncA | DecA | IncB | DecB | NewGame
Les modifications sont propagées en écoutant les événements et en mettant à jour le modèle de vue en conséquence. Au lieu d'ajouter des membres au type de modèle, comme dans la POO, nous déclarons un module distinct pour héberger les opérations autorisées.
[<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
Notre modèle de vue dérive de EventViewModelBase<'a>
, qui possède une propriété EventStream
de type IObservable<'a>
. Dans ce cas, les événements auxquels nous souhaitons souscrire sont de type ScoringEvent
.
Le contrôleur gère les événements de manière fonctionnelle. Sa signature Score -> ScoringEvent -> Score
nous montre que chaque fois qu’un événement se produit, la valeur actuelle du modèle est transformée en une nouvelle valeur. Cela permet à notre modèle de rester pur, même si notre modèle de vue ne l’est pas.
Un eventHandler
est chargé de modifier l'état de la vue. Héritage de EventViewModelBase<'a>
nous pouvons utiliser EventValueCommand
et EventValueCommandChecked
pour connecter les événements aux commandes.
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
Le code derrière le fichier (* .xaml.fs) est l'endroit où tout est mis en place, c'est-à-dire que la fonction de mise à jour ( controller
) est injectée dans MainViewModel
.
namespace Score.Views
open FsXaml
type MainView = XAML<"MainWindow.xaml">
type CompositionRoot() =
member __.ViewModel = Score.ViewModel.MainViewModel(Score.Model.Score.update)
Le type CompositionRoot
sert de wrapper référencé dans le fichier XAML.
<Window.Resources>
<ResourceDictionary>
<local:CompositionRoot x:Key="CompositionRoot"/>
</ResourceDictionary>
</Window.Resources>
<Window.DataContext>
<Binding Source="{StaticResource CompositionRoot}" Path="ViewModel" />
</Window.DataContext>
Je ne vais pas plonger plus profondément dans le fichier XAML car ce sont des choses WPF de base, le projet entier se trouve sur GitHub .
Gjallarhorn
Les principaux types de la bibliothèque Gjallarhorn implémentent IObservable<'a>
, ce qui rendra l’implémentation plus familière (souvenez-vous de la propriété EventStream
de l’exemple FSharp.ViewModule). Le seul changement réel à notre modèle est l'ordre des arguments de la fonction de mise à jour. De plus, nous utilisons maintenant le terme Message au lieu de 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
Afin de créer une interface utilisateur avec Gjallarhorn, au lieu de créer des classes pour prendre en charge la liaison de données, nous créons des fonctions simples appelées Component
. Dans leur constructeur, le premier argument source
est de type BindingSource
(défini dans Gjallarhorn.Bindable) et permet de mapper le modèle à la vue et les événements de la vue dans les messages.
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'implémentation actuelle diffère de la version de FSharp.ViewModule dans la mesure où CanExecute n'est pas encore correctement implémenté dans deux commandes. Liste également la plomberie de l'application.
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
À gauche avec la configuration de la vue découplée, en combinant le type MainWindow
et l'application logique.
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
Cela résume les concepts de base, pour plus d'informations et un exemple plus élaboré, veuillez vous reporter au message de Reed Copsey . Le projet des arbres de Noël met en évidence quelques avantages à cette approche:
- En nous remplaçant efficacement de la nécessité de copier (manuellement) le modèle dans une collection personnalisée de modèles de vue, en les gérant et en reconstruisant manuellement le modèle à partir des résultats.
- Les mises à jour au sein des collections sont effectuées de manière transparente, tout en conservant un modèle pur.
- La logique et la vue sont hébergées par deux projets différents, mettant l’accent sur la séparation des préoccupations.