F#
Einführung in WPF in F #
Suche…
Einführung
In diesem Thema wird veranschaulicht, wie die funktionale Programmierung in einer WPF-Anwendung genutzt wird . Das erste Beispiel stammt aus einem Beitrag von Māris Krivtežs (siehe Abschnitt " Anmerkungen " unten). Es gibt zwei Gründe, dieses Projekt erneut zu besuchen:
1 \ Das Design unterstützt die Trennung von Anliegen, während das Modell rein bleibt und Änderungen funktional weitergegeben werden.
2 \ Die Ähnlichkeit ermöglicht einen einfachen Übergang zur Implementierung von Gjallarhorn.
Bemerkungen
Demo-Projekte für Bibliotheken @GitHub
- FSharp.ViewModule (unter FsXaml)
- Gjallarhorn (Referenzbeispiele)
Māris Krivtežs schrieb zwei großartige Beiträge zu diesem Thema:
- F # XAML-Anwendung - MVVM vs. MVC, bei der die Vor- und Nachteile beider Ansätze hervorgehoben werden.
Ich denke, keiner dieser XAML-Anwendungsstile profitiert von der funktionalen Programmierung. Ich kann mir vorstellen, dass die ideale Anwendung aus der Ansicht bestehen würde, bei der Ereignisse erzeugt werden und Ereignisse den aktuellen Ansichtszustand haben. Die gesamte Anwendungslogik sollte durch Filtern und Bearbeiten von Ereignissen und Ansichtsmodell behandelt werden. In der Ausgabe sollte ein neues Ansichtsmodell erstellt werden, das an die Ansicht gebunden ist.
- F # XAML - ereignisgesteuerte MVVM wie in dem obigen Thema überarbeitet.
FSharp.ViewModule
Unsere Demo-App besteht aus einer Anzeigetafel. Das Score-Modell ist ein unveränderlicher Datensatz. Die Ereignisse der Anzeigetafel sind in einem Unionstyp enthalten.
namespace Score.Model
type Score = { ScoreA: int ; ScoreB: int }
type ScoringEvent = IncA | DecA | IncB | DecB | NewGame
Änderungen werden durch Abhören von Ereignissen verbreitet und das Ansichtsmodell entsprechend aktualisiert. Anstatt wie bei OOP Mitglieder zum Modelltyp hinzuzufügen, deklarieren wir ein separates Modul zum Hosten der zulässigen Operationen.
[<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
Unser Ansichtsmodell leitet sich von EventViewModelBase<'a>
, die über die Eigenschaft EventStream
vom Typ IObservable<'a>
. In diesem Fall haben die Ereignisse, die wir abonnieren möchten, den Typ ScoringEvent
.
Der Controller behandelt die Ereignisse funktional. Die Signatur Score -> ScoringEvent -> Score
zeigt uns, dass der aktuelle Wert des Modells bei Auftreten eines Ereignisses in einen neuen Wert umgewandelt wird. Dies ermöglicht, dass unser Modell rein bleibt, unser Sichtmodell jedoch nicht.
Ein eventHandler
ist für die eventHandler
des Ansichtsstatus verantwortlich. Von EventViewModelBase<'a>
wir EventValueCommand
und EventValueCommandChecked
, um die Ereignisse mit den Befehlen zu 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
Im Code hinter der Datei (* .xaml.fs) wird alles zusammengesetzt, dh die Aktualisierungsfunktion ( controller
) wird in das MainViewModel
.
namespace Score.Views
open FsXaml
type MainView = XAML<"MainWindow.xaml">
type CompositionRoot() =
member __.ViewModel = Score.ViewModel.MainViewModel(Score.Model.Score.update)
Der Typ CompositionRoot
dient als Wrapper, auf den in der XAML-Datei verwiesen wird.
<Window.Resources>
<ResourceDictionary>
<local:CompositionRoot x:Key="CompositionRoot"/>
</ResourceDictionary>
</Window.Resources>
<Window.DataContext>
<Binding Source="{StaticResource CompositionRoot}" Path="ViewModel" />
</Window.DataContext>
Ich werde nicht tiefer in die XAML-Datei eintauchen, da es grundlegende WPF-Sachen ist. Das gesamte Projekt ist auf GitHub zu finden.
Gjallarhorn
Die IObservable<'a>
in der Gjallarhorn-Bibliothek implementieren IObservable<'a>
, wodurch die Implementierung bekannt wird (erinnern Sie sich an die EventStream
Eigenschaft aus dem FSharp.ViewModule-Beispiel). Die einzige wirkliche Änderung an unserem Modell ist die Reihenfolge der Argumente der Aktualisierungsfunktion. Außerdem verwenden wir jetzt den Begriff Nachricht anstelle von Ereignis .
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
Um eine Benutzeroberfläche mit Gjallarhorn zu erstellen, erstellen wir anstelle von Klassen zur Unterstützung der Datenbindung einfache Funktionen, die als Component
. In ihrem Konstruktor die erste Argument source
ist vom Typ BindingSource
(definiert in Gjallarhorn.Bindable) und verwendet , um das Modell zu der Ansicht auf der Karte, und Ereignisse aus der Sicht zurück in Nachrichten.
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
]
Die aktuelle Implementierung unterscheidet sich von der Version von FSharp.ViewModule darin, dass CanExecute für zwei Befehle noch nicht ordnungsgemäß implementiert ist. Listet auch die Installation der Anwendung auf.
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
MainWindow
Einrichtung der entkoppelten Ansicht, wobei der MainWindow
Typ und die logische Anwendung kombiniert werden.
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
Dies fasst die Kernkonzepte zusammen. Weitere Informationen und ein ausführlicheres Beispiel finden Sie in Reed Copseys Beitrag . Das Christmas Trees- Projekt zeigt einige Vorteile dieses Ansatzes auf:
- Wir müssen das Modell effektiv (manuell) in eine benutzerdefinierte Sammlung von Ansichtsmodellen kopieren, verwalten und das Modell manuell aus den Ergebnissen erstellen.
- Aktualisierungen innerhalb von Sammlungen werden auf transparente Weise durchgeführt, wobei ein reines Modell erhalten bleibt.
- Die Logik und Sichtweise werden von zwei verschiedenen Projekten gehostet, die die Trennung von Anliegen hervorheben.