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

Māris Krivtežs schrieb zwei großartige Beiträge zu diesem Thema:

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.

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.


Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow