Szukaj…


Wprowadzenie

W tym temacie pokazano, jak wykorzystać programowanie funkcjonalne w aplikacji WPF . Pierwszy przykład pochodzi z postu Mārisa Krivtežsa (patrz sekcja Uwagi na dole). Powód ponownej wizyty w tym projekcie jest dwojaki:

1 \ Projekt obsługuje separację problemów, podczas gdy model jest utrzymywany w czystości, a zmiany są propagowane w sposób funkcjonalny.

2 \ Podobieństwo ułatwi przejście do implementacji Gjallarhorn.

Uwagi

Projekty demonstracyjne bibliotek @GitHub

Māris Krivtežs napisał dwa świetne posty na ten temat:

Wydaje mi się, że żaden z tych stylów aplikacji XAML nie korzysta z programowania funkcjonalnego. Wyobrażam sobie, że idealna aplikacja składałaby się z widoku, który generuje zdarzenia i zdarzenia utrzymują bieżący stan widoku. Cała logika aplikacji powinna być obsługiwana przez filtrowanie i manipulowanie zdarzeniami oraz model widoku, a na wyjściu powinien wytworzyć nowy model widoku, który jest powiązany z widokiem.

FSharp.ViewModule

Nasza aplikacja demo składa się z tablicy wyników. Model punktacji jest niezmiennym zapisem. Wydarzenia w tablicy wyników są zawarte w typie unijnym.

namespace Score.Model

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

Zmiany są propagowane przez nasłuchiwanie zdarzeń i odpowiednią aktualizację modelu widoku. Zamiast dodawać członków do typu modelu, jak w OOP, deklarujemy oddzielny moduł do obsługi dozwolonych operacji.

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

Nasz model widoku pochodzi z EventViewModelBase<'a> , która ma właściwość EventStream typu IObservable<'a> . W tym przypadku zdarzenia, które chcemy subskrybować, są typu ScoringEvent .

Kontroler obsługuje zdarzenia w sposób funkcjonalny. Jego podpis Score -> ScoringEvent -> Score pokazuje nam, że ilekroć wystąpi zdarzenie, bieżąca wartość modelu zostaje przekształcona w nową wartość. To pozwala naszemu modelowi pozostać czystym, chociaż nasz model poglądowy nie jest.

eventHandler odpowiada za mutowanie stanu widoku. Dziedzicząc z EventViewModelBase<'a> możemy użyć EventValueCommand i EventValueCommandChecked do połączenia zdarzeń z poleceniami.

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

Kod za plikiem (* .xaml.fs) jest miejscem, w którym wszystko jest razem, tzn. Funkcja aktualizacji ( controller ) jest wstrzykiwana do MainViewModel .

namespace Score.Views

open FsXaml

type MainView = XAML<"MainWindow.xaml">

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

Typ CompositionRoot służy jako opakowanie, do którego odwołuje się plik XAML.

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

Nie będę zagłębiał się głębiej w plik XAML, ponieważ jest to podstawowy plik WPF, cały projekt można znaleźć na GitHub .

Gjallarhorn

Podstawowe typy w bibliotece Gjallarhorn implementują IObservable<'a> , dzięki czemu implementacja wygląda znajomo (pamiętaj właściwość EventStream z przykładu FSharp.ViewModule). Jedyną prawdziwą zmianą w naszym modelu jest kolejność argumentów funkcji aktualizacji. Ponadto używamy teraz terminu Wiadomość zamiast Zdarzenia .

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

Aby zbudować interfejs użytkownika za pomocą Gjallarhorn, zamiast tworzyć klasy do obsługi wiązania danych, tworzymy proste funkcje określane jako Component . W ich konstruktorze pierwsze source argumentu jest typu BindingSource (zdefiniowane w Gjallarhorn.Bindable) i służy do mapowania modelu na widok oraz zdarzenia z widoku z powrotem na wiadomości.

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
        ]

Obecna implementacja różni się od wersji FSharp.ViewModule tym, że dwie komendy nie mają jeszcze poprawnie zaimplementowanej CanExecute. Wymienia również hydraulikę aplikacji.

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

Po lewej stronie konfigurowanie oddzielonego widoku, łącząc typ MainWindow i logiczną aplikację.

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

Podsumowuje to podstawowe pojęcia, dodatkowe informacje i bardziej szczegółowy przykład można znaleźć w poście Reeda Copseya . Projekt choinek podkreśla kilka zalet tego podejścia:

  • Skutecznie odkupia nas od konieczności (ręcznego) kopiowania modelu do niestandardowej kolekcji modeli widoków, zarządzania nimi i ręcznego konstruowania modelu z wyników.
  • Aktualizacje w ramach kolekcji są wykonywane w przejrzysty sposób, przy zachowaniu czystego modelu.
  • Logika i widok są hostowane przez dwa różne projekty, kładąc nacisk na rozdzielenie obaw.


Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow