F#
Wprowadzenie do WPF w F #
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
- FSharp.ViewModule (pod FsXaml)
- Gjallarhorn (ref Próbki)
Māris Krivtežs napisał dwa świetne posty na ten temat:
- Aplikacja F # XAML - MVVM vs MVC, w której wyróżniono zalety i wady obu podejść.
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.
- F # XAML - MVVM sterowany zdarzeniami, jak opisano w powyższym temacie.
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.