F#
Introducción a WPF en F #
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
- FSharp.ViewModule (bajo FsXaml)
- Gjallarhorn (muestras de referencia)
Māris Krivtežs escribió dos publicaciones excelentes sobre este tema:
- Aplicación F # XAML: MVVM vs MVC, donde se destacan los pros y los contras de ambos enfoques.
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.
- F # XAML: MVVM controlado por evento como revisado en el tema anterior.
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.