Поиск…


Вступление

В этом разделе показано, как использовать функциональное программирование в приложении WPF . Первый пример - это сообщение от Māris Krivtežs (см. Раздел « Примечания » внизу). Причина пересмотра этого проекта двоякая:

1 \ Конструкция поддерживает разделение проблем, в то время как модель поддерживается чистой, а изменения распространяются функционально.

2 \ Сходство приведет к легкому переходу на реализацию Gjallarhorn.

замечания

Демо-проекты библиотеки @GitHub

Марис Кривтеж написал две большие посты на эту тему:

Я чувствую, что ни один из этих стилей приложений XAML не выигрывает от функционального программирования. Я предполагаю, что идеальное приложение будет состоять из представления, которое создает события и события, имеющие текущее состояние представления. Вся прикладная логика должна обрабатываться путем фильтрации и манипулирования событиями и моделью представления, а на выходе она должна создать новую модель представления, которая привязана к представлению.

FSharp.ViewModule

Наше демо-приложение состоит из табло. Модель оценки - неизменная запись. События табло содержатся в типе Союза.

namespace Score.Model

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

Изменения распространяются путем прослушивания событий и соответственно изменения модели представления. Вместо добавления членов в тип модели, как и в ООП, мы объявляем отдельный модуль для размещения разрешенных операций.

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

Наша модель представления EventViewModelBase<'a> из EventViewModelBase<'a> , которая имеет свойство EventStream типа IObservable<'a> . В этом случае события, которые мы хотим подписаться, имеют тип ScoringEvent .

Контроллер управляет событиями в функциональном режиме. Его подпись Score -> ScoringEvent -> Score показывает нам, что всякий раз, когда происходит событие, текущее значение модели преобразуется в новое значение. Это позволяет нашей модели оставаться чистым, хотя наша модель взглядов не является.

eventHandler отвечает за изменение состояния представления. Наследуя от EventViewModelBase<'a> мы можем использовать EventValueCommand и 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

Код файла (* .xaml.fs) - это место, где все сведено вместе, т.е. функция обновления ( controller ) вводится в MainViewModel .

namespace Score.Views

open FsXaml

type MainView = XAML<"MainWindow.xaml">

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

Тип CompositionRoot является оберткой, на которую ссылается файл XAML.

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

Я больше не буду погружаться в файл XAML, поскольку это базовый материал WPF, весь проект можно найти на GitHub .

Gjallarhorn

Основные типы библиотеки Gjallarhorn реализуют IObservable<'a> , что сделает реализацию знакомой (помните свойство EventStream из примера FSharp.ViewModule). Единственное реальное изменение нашей модели - это порядок аргументов функции обновления. Кроме того, теперь мы используем термин « Сообщение вместо события» .

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

Чтобы создать пользовательский интерфейс с Gjallarhorn, вместо того, чтобы создавать классы для поддержки привязки данных, мы создаем простые функции, называемые Component . В их конструкторе первый source аргумента имеет тип BindingSource (определенный в Gjallarhorn.Bindable) и используется для сопоставления модели с представлением и событий из представления обратно в сообщения.

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
        ]

Текущая реализация отличается от версии FSharp.ViewModule тем, что две команды не имеют CanExecute должным образом реализованы. Также перечисление сантехники приложения.

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 и логическое приложение.

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

Это подводит итог основным концепциям, дополнительной информации и более сложному примеру, пожалуйста, обратитесь к сообщению Рида Копси . Проект « Рождественские деревья » подчеркивает пару преимуществ такого подхода:

  • Эффективно избавляя нас от необходимости (вручную) копировать модель в пользовательскую коллекцию моделей взглядов, управлять ими и вручную конструировать модель из результатов.
  • Обновления в коллекциях выполняются прозрачным образом, сохраняя чистую модель.
  • Логика и представление организованы двумя разными проектами, что подчеркивает разделение проблем.


Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow