Buscar..


Introducción

Este tema ilustra cómo explotar la programación funcional en una aplicación WPF . El primer ejemplo proviene de un post de Māris Krivtežs (ref. Sección de Comentarios en la parte inferior). La razón para volver a visitar este proyecto es doble:

1 \ El diseño admite la separación de preocupaciones, mientras que el modelo se mantiene puro y los cambios se propagan de manera funcional.

2 \ La semejanza facilitará la transición a la implementación de Gjallarhorn.

Observaciones

Biblioteca de proyectos de demostración @GitHub

Māris Krivtežs escribió dos publicaciones excelentes sobre este tema:

Siento que ninguno de estos estilos de aplicación XAML se beneficia mucho de la programación funcional. Me imagino que la aplicación ideal consistiría en la vista que produce eventos y los eventos mantienen el estado de vista actual. Toda la lógica de la aplicación debe manejarse filtrando y manipulando los eventos y el modelo de vista, y en la salida debe producir un nuevo modelo de vista que esté vinculado de nuevo a la vista.

FSharp.ViewModule

Nuestra aplicación de demostración consiste en un marcador. El modelo de puntuación es un registro inmutable. Los eventos del marcador están contenidos en un Tipo de Unión.

namespace Score.Model

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

Los cambios se propagan escuchando eventos y actualizando el modelo de vista en consecuencia. En lugar de agregar miembros al tipo de modelo, como en OOP, declaramos un módulo separado para alojar las operaciones permitidas.

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

Nuestro modelo de vista se deriva de EventViewModelBase<'a> , que tiene una propiedad EventStream de tipo IObservable<'a> . En este caso, los eventos a los que queremos suscribirnos son de tipo ScoringEvent .

El controlador maneja los eventos de una manera funcional. Su Score -> ScoringEvent -> Score distintivo Score -> ScoringEvent -> Score nos muestra que, cada vez que ocurre un evento, el valor actual del modelo se transforma en un nuevo valor. Esto permite que nuestro modelo permanezca puro, aunque nuestro modelo de vista no lo sea.

Un eventHandler se encarga de mutar el estado de la vista. Al heredar de EventViewModelBase<'a> podemos usar EventValueCommand y EventValueCommandChecked para conectar los eventos a los comandos.

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

El código detrás del archivo (* .xaml.fs) es donde se pone todo junto, es decir, la función de actualización ( controller ) se inyecta en MainViewModel .

namespace Score.Views

open FsXaml

type MainView = XAML<"MainWindow.xaml">

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

El tipo CompositionRoot sirve como un contenedor al que se hace referencia en el archivo XAML.

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

No profundizaré más en el archivo XAML ya que es un elemento básico de WPF, el proyecto completo se puede encontrar en GitHub .

Gjallarhorn

Los tipos de núcleo en la biblioteca Gjallarhorn implementan IObservable<'a> , lo que hará que la implementación parezca familiar (recuerde la propiedad EventStream del ejemplo FSharp.ViewModule). El único cambio real en nuestro modelo es el orden de los argumentos de la función de actualización. Además, ahora usamos el término Mensaje en lugar de 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

Con el fin de crear una interfaz de usuario con Gjallarhorn, en lugar de crear clases para admitir el enlace de datos, creamos funciones simples denominadas Component . En su constructor, el source del primer argumento es de tipo BindingSource (definido en Gjallarhorn.Bindable), y se utiliza para asignar el modelo a la vista, y los eventos de la vista vuelven a aparecer en los mensajes.

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
        ]

La implementación actual difiere de la versión FSharp.ViewModule en que dos comandos aún no han implementado correctamente CanExecute. También se enumeran las tuberías de la aplicación.

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

Se fue con la configuración de la vista desacoplada, combinando el tipo MainWindow y la aplicación lógica.

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

Esto resume los conceptos básicos, para obtener información adicional y un ejemplo más detallado, consulte la publicación de Reed Copsey . El proyecto de los árboles de Navidad destaca algunos beneficios de este enfoque:

  • Redimiéndonos efectivamente de la necesidad de (manualmente) copiar el modelo en una colección personalizada de modelos de vista, administrándolos y construyendo manualmente el modelo a partir de los resultados.
  • Las actualizaciones dentro de las colecciones se realizan de manera transparente, mientras se mantiene un modelo puro.
  • La lógica y la vista están alojadas en dos proyectos diferentes, que enfatizan la separación de preocupaciones.


Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow