F#
Inleiding tot WPF in F #
Zoeken…
Invoering
Dit onderwerp illustreert hoe u functionele programmering in een WPF-toepassing kunt benutten. Het eerste voorbeeld komt uit een bericht van Māris Krivtežs (zie het gedeelte Opmerkingen onderaan). De reden voor het opnieuw bezoeken van dit project is tweeledig:
1 \ Het ontwerp ondersteunt de scheiding van punten van zorg, terwijl het model zuiver wordt gehouden en veranderingen op een functionele manier worden doorgevoerd.
2 \ De gelijkenis zorgt voor een gemakkelijke overgang naar de Gjallarhorn-implementatie.
Opmerkingen
Demo-projecten van bibliotheken @GitHub
- FSharp.ViewModule (onder FsXaml)
- Gjallarhorn (ref monsters)
Māris Krivtežs schreef twee geweldige berichten over dit onderwerp:
- F # XAML-applicatie - MVVM versus MVC waar de voor- en nadelen van beide benaderingen worden benadrukt.
Ik heb het gevoel dat geen van deze XAML-toepassingsstijlen veel baat heeft bij functioneel programmeren. Ik stel me voor dat de ideale toepassing zou bestaan uit het beeld dat gebeurtenissen produceert en gebeurtenissen de huidige beeldstatus hebben. Alle applicatielogica moet worden afgehandeld door gebeurtenissen en het weergavemodel te filteren en te manipuleren, en in de uitvoer moet een nieuw weergavemodel worden geproduceerd dat is gebonden aan het aanzicht.
- F # XAML - event driven MVVM zoals herzien in bovenstaand onderwerp.
FSharp.ViewModule
Onze demo-app bestaat uit een scorebord. Het scoremodel is een onveranderlijk record. De scorebordgebeurtenissen zijn opgenomen in een Union Type.
namespace Score.Model
type Score = { ScoreA: int ; ScoreB: int }
type ScoringEvent = IncA | DecA | IncB | DecB | NewGame
Wijzigingen worden doorgevoerd door te luisteren naar gebeurtenissen en het weergavemodel dienovereenkomstig bij te werken. In plaats van leden toe te voegen aan het modeltype, zoals in OOP, verklaren we een afzonderlijke module voor het hosten van de toegestane bewerkingen.
[<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
Ons weergavemodel is afgeleid van EventViewModelBase<'a>
, dat een eigenschap EventStream
van het type IObservable<'a>
. In dit geval zijn de evenementen waarop we ons willen abonneren van het type ScoringEvent
.
De controller verwerkt de gebeurtenissen op een functionele manier. De kenmerkende Score -> ScoringEvent -> Score
toont ons dat, telkens wanneer zich een gebeurtenis voordoet, de huidige waarde van het model wordt omgezet in een nieuwe waarde. Dit zorgt ervoor dat ons model puur blijft, hoewel ons kijkmodel dat niet is.
Een eventHandler
heeft de leiding over het wijzigen van de status van de weergave. Overnemen van EventViewModelBase<'a>
we EventValueCommand
en EventValueCommandChecked
gebruiken om de gebeurtenissen aan de opdrachten te EventValueCommandChecked
.
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
In de code achter het bestand (* .xaml.fs) wordt alles samengevoegd, dwz dat de updatefunctie ( controller
) wordt geïnjecteerd in het MainViewModel
.
namespace Score.Views
open FsXaml
type MainView = XAML<"MainWindow.xaml">
type CompositionRoot() =
member __.ViewModel = Score.ViewModel.MainViewModel(Score.Model.Score.update)
Het type CompositionRoot
dient als een wrapper waarnaar wordt verwezen in het XAML-bestand.
<Window.Resources>
<ResourceDictionary>
<local:CompositionRoot x:Key="CompositionRoot"/>
</ResourceDictionary>
</Window.Resources>
<Window.DataContext>
<Binding Source="{StaticResource CompositionRoot}" Path="ViewModel" />
</Window.DataContext>
Ik zal niet dieper in het XAML-bestand duiken omdat het basis WPF-dingen zijn, het hele project is te vinden op GitHub .
Gjallarhorn
De kerntypen in de Gjallarhorn-bibliotheek implementeren IObservable<'a>
, waardoor de implementatie er vertrouwd uitziet (onthoud de eigenschap EventStream
uit het voorbeeld FSharp.ViewModule). De enige echte verandering in ons model is de volgorde van de argumenten van de update-functie. We gebruiken nu ook de term Bericht in plaats van Gebeurtenis .
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
Om een gebruikersinterface met Gjallarhorn te bouwen, in plaats van klassen te maken om gegevensbinding te ondersteunen, maken we eenvoudige functies die een Component
. In de constructor eerste argument source
is van het type BindingSource
(gedefinieerd in Gjallarhorn.Bindable), en gebruikt om het model om de weergave kaart en gebeurtenissen van het weergeven in berichten.
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
]
De huidige implementatie verschilt van de FSharp.ViewModule-versie in die zin dat twee opdrachten CanExecute nog niet correct hebben geïmplementeerd. Ook wordt het sanitair van de applicatie vermeld.
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
Links met het instellen van de ontkoppelde weergave, waarbij het type MainWindow
en de logische toepassing worden gecombineerd.
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
Dit vat de kernbegrippen samen, voor meer informatie en een meer gedetailleerd voorbeeld raadpleegt u de post van Reed Copsey . Het project Christmas Trees belicht een aantal voordelen van deze aanpak:
- Ons effectief verlossen van de noodzaak om het model (handmatig) te kopiëren naar een aangepaste verzameling weergavemodellen, deze te beheren en het model handmatig samen te stellen op basis van de resultaten.
- Updates binnen collecties worden op een transparante manier gedaan, met behoud van een puur model.
- De logica en weergave worden gehost door twee verschillende projecten, die de scheiding van zorgen benadrukken.