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

M. Krivtežs a écrit deux excellents articles sur ce sujet:

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.

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.


Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow