Giter Site home page Giter Site logo

fsprojects / avalonia.funcui Goto Github PK

View Code? Open in Web Editor NEW
922.0 34.0 73.0 20.54 MB

Develop cross-plattform GUI Applications using F# and Avalonia!

Home Page: https://funcui.avaloniaui.net/

License: MIT License

F# 99.46% CSS 0.25% HTML 0.20% JavaScript 0.09%
avalonia avaloniaui elmish fsharp dotnet mvu ui gui

avalonia.funcui's People

Contributors

albert-du avatar angelmunoz avatar avestura avatar beyon avatar dsyme avatar excpt avatar foggyfinder avatar happypig375 avatar houstonhaynes avatar iminashi avatar jaggerjo avatar jl0pd avatar jordanmarr avatar kmutagene avatar marklam avatar matachi avatar mathias-brandewinder avatar melursus23 avatar miticcio avatar nicoviii avatar numpsy avatar picolino avatar ryushiaok avatar sandeepc24 avatar scotthutchinson avatar shalokshalom avatar silkyfowl avatar sleepyfran avatar thecentury avatar uxsoft avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

avalonia.funcui's Issues

Is it possible to use itemTemplate and dataTemplate

I've tested adding 1000 items to a stack panel using the naive implementation and the UI grinds. However I spent a bit of time figuring out how to get DataTemplates working in FuncUI way and it seems to work and the UI becomes super snappy even for 1000 element lists.

The code for the working mini app is

https://gist.github.com/bradphelan/06d2e2250facfcf01b848ee71fda4064

The critical line is a helper

    let itemTemplate view dispatch =  
        Avalonia.Controls.Templates.FuncDataTemplate<(int*'state)>( ( fun (id,state) -> 
            let viewElement = (view state id dispatch)
            let view =  viewElement |> VirtualDom.createView
            let delta = Avalonia.FuncUI.VirtualDom.Delta.ViewDelta.From viewElement
            VirtualDom.Patcher.patch(view, delta)
            view
            ) ,true )

and can be used to render lists efficiently like below

module PersonsModule =
    type PersonsMsg = 
        | Update of IndexedMessage<PersonModule.PersonMsg>
        | Delete of int

    let update (personsMsg:PersonsMsg) state =
        match personsMsg with
        | Update (id, msg) -> pvSet id (PersonModule.update msg (pvGet id state)) state 
        | Delete id -> pvDel id state 


    let itemView (person:PersonModule.PersonState) (id:int) dispatch : View =
        // Set up a dispatcher for a person at a specific id
        let dispatchPerson  id = (fun msg -> dispatch(Update(id,msg)))

        Views.dockpanel [
            Attrs.children [
                Views.button [
                    Attrs.content "X"
                    Attrs.onClick ( fun sender args -> dispatch (PersonsMsg.Delete  id) )
                ]
                PersonModule.view person (dispatchPerson id)
            ]
        ]

    let view (state:PersonModule.PersonState PersistentVector) (dispatch) : View =
        Views.scrollViewer [
            Attrs.content (
                Views.listBox [
                    Attrs.itemTemplate (itemTemplate itemView dispatch )
                    Attrs.items (state |> Seq.indexed |> PersistentVector.ofSeq )
                ]
            )
        ]

Maybe I'm telling you something you already know but it seemed like a non-obvious trick and I had to pull some code out of the API to make it work. Hope it is useful for vNext as you think about it.

Question: Possibilities with local state.

I've finally learned enough FUNCUI to get working what I wanted.

image

The text parsing error in my current example does not have to be stored in the global state store. It is local to the textbox view and is reset if the the outer global state changes. So we can think of it as a temporary local program that gets a job done and then is discarded.

Some pre-comments. A Redux<'a> at it's most basic is an object with Get and Set members. It is a merging of state and dispatch. When you call Set a message is dispatched to update the data that this redux points to. With that understanding we can look at the example.

The example demonstrates the capability of a new subclass of HostControl called LocalStateControl. I took the code from LazyView and added the ability for the view to have it's own local state with a lifetime not greater than when a difference is detected in the incoming outer state.

The original code is at

https://github.com/bradphelan/FUNCUI.Samples/blob/master/src/Examples/ValidatingTextBoxApp/Main.fs

namespace BGS

open Avalonia.FuncUI.DSL
open XTargets.Elmish
open System
open Avalonia.Controls
open Avalonia.Layout
open FSharpx
open Avalonia
open Avalonia.FuncUI.Types

module Data =

    // Create a simple model with two int fields
    type Item = {
        value0: int 
        value1: int 
    } with
        // Provide lenses for focusing on seperate fields
        static member value0' = (fun o->o.value0),(fun v o -> {o with value0 = v})
        static member value1' = (fun o->o.value1),(fun v o -> {o with value1 = v})

        // Provide an initializer
        static member init = { value0 = 0; value1 = 1 }


module ItemView =

    // the view recieves a `Redux` or a pointer to the data it needs to 
    // render and update. The `Redux` object has Get and Set methods.
    // Calling `Set` fires the dispatcher with a message that knows 
    // how to do the update on the root data. 
    let view (item:Redux<Data.Item>) = 

        // Get a redux for each sub property by using the lens combinators
        let value0:Redux<int> = (item >-> Data.Item.value0')
        let value1:Redux<int> = (item >-> Data.Item.value1')

        // Generate a form field for a specific property
        let inline formField label (value:Redux<int>)  = 
            StackPanel.create [
                StackPanel.orientation Orientation.Horizontal
                StackPanel.children [

                    TextBlock.create [
                        TextBlock.text label
                        TextBlock.width 150.0
                    ]

                    LocalStateView.create [

                        // Set state so that the patch algorithm knows to rerender if the value changes
                        LocalStateView.state value.Get

                        // Set the view function for rendering. The view function should
                        // take 1 parameter being a Redux<'a> when 'a : equality. In this
                        // case we want the errHandler to be our local state and we
                        // want `string option` though it could be almost anything we want
                        LocalStateView.viewFunc ( fun (errHandler:Redux<string option>) -> 
                            TextBox.create [
                                // Render the current value for the text
                                TextBox.text (string value.Get)
                                // Convert the Redux<int> to Redux<string> via a two way value converter.
                                // The setter of a Redux<string option> is passed to collect any parsing 
                                // errors. Notice that `errHandler` is the local state that is passed
                                // into the view. It doesn't not propagate out of this view. It will
                                // always be reset to the default value if the state propery is updated
                                let stringValue:Redux<string> = value.Convert ValueConverters.StringToInt32 errHandler.Set

                                // Bind the Set and Get methods of the stringValue to the TextBox. See bindText
                                yield! stringValue |> TextBox.bindText

                                // Collect the current parse errors and store them in the errors field
                                // of the textbox
                                let parseErrors = 
                                    errHandler.Get 
                                    |> Option.toArray
                                    |> Seq.cast<obj> 
                                TextBox.errors parseErrors

                                TextBox.width 150.0
                            ] :> IView
                            
                        )
                    ]

                ]
            ]

        StackPanel.create [
            StackPanel.orientation Orientation.Vertical
            StackPanel.children [
                formField "Value 0" value0 
                formField "Value 1" value1 
            ]
        ]


Question: How to do validation?

If I have a textbox that needs parsing to an int and I would like that textbox to show errors if the parsing fails this is easy to do with Avalonia proper. One just needs to set the errors property of the textbox.

However ( and forgetting about the lens stuff I'm working on ) How would you handle validation if you had the following complex domain model

However it is tedious to have to reflect the parsing errors into the immutable model tree. In fact the model should not be updated if if the parsing fails. This is a local failure. It feels like I should be able to do something like this in FUNCUI.

```fsharp
    type Company = {
        id: int
        name: string
        business: string
        employees: Person array
        revenue: int32
    }

    type State {
         companies Company array
         selectedCompany: int
    }

So there are two levels. Let's say we want to edit the Company.revenue field and this requires validation. There are two types of validation. 1 easy and 1 hard(er).

The first is validation of the revenue:int32 As this is part of the data model we can just perform some validation during the view function and render some message out.

The second is parsing validation. The user should only be allowed to enter in valid integers and if they don't then a message should be displayed. However strings that don't parse to an integer can't be put in the model and won't be available on the next render pass. This means there needs to be extra storage for this error data which seems a bit tedious though maybe there is no other way.

Do you have any suggestions on this?

vNext 0.2.0

The current / 1.x version of FuncUI is basically a proof of concept that escalated a bit. I started using it for my projects and a lot of my assumptions on "how to do x" were right, but some were wrong. The main issues that will be addressed with the next version are:

Domain Specific Language for Views

FuncUI 1.x has a pretty readable DSL for defining views in a type checked manner. But there are a few downsides that need to be addressed:

Reflection

Reflection is used everywhere. This has several downsides (Performance, AOT Limitations, ..). The vNext will not use reflection AT ALL.

Definition and Naming changes

The DSL currently uses Statically Resolved Type Constraints. This does not scale well (ask for details). The new DSL will use a different and in my opinion overall better approach. Static E
extension methods (or other members) that are attached to the control type. This also leads to a better browsable DSL.

TextBox.create [
    TextBox.text state.Name
    TextBox.onTextChanged (fun text -> dispatch ..)
]

Type Safety

Currently there is no way of knowing what control a View will create. This is no problem in most cases, but makes it harder to provide a good API in some cases. For example when a control takes another control of a certain type as an attribute (Popup / Tooltip / ...).

v1.x

let btn : View = Views.button [ ... ]

vNext

let btn : IView<Button> = Views.button [ ... ]
let btn : IView = Views.button [ ... ]

Virtual Dom

The current virtual Dom implementation is super primitive. This was good in the beginning but there are some thing that need to change.

New Event Handling

In Avalonia all Properties, RoutedEvents and Events are (with a bit of work) Observable. They will be treated all the same and you will finally be able to subscribe to text changed notifications.

Custom Equality implementations for Avalonia Types

This is needed because for example the Grid Control throws an error if the ColumnDefinitions are set more than one time and ColumnDefinitons don't implement value equality.

This will also bring some performance improvements because currently bitmaps are also created on each state change.

Configurable

It will also be possible to configure the LazyViewCache and stuff like that.

SelectedItem doesn't show value sometimes

In my app I saw something that looks rather odd: the selected item is showed only on second click. I tried to create MCVE and notice that behaviour is different depending on State.

I can't exclude that I'm missing something obvious though.

Anyway, here is a MCVE:

open Avalonia.Controls
open Avalonia.FuncUI.DSL
open Avalonia.FuncUI.Components
open Avalonia.Layout

module Sample =
    type State(v) =
        member __.Value = v
        override s.ToString() = s.Value.ToString()

    let init() = State(1)
    type Msg = 
        | Select of int

    let update msg _ : State =
        match msg with
        | Msg.Select v -> State(v)

    let data = [ 1..10 ] |> List.map State
    let dataTemplate (v:State) = 
        TextBlock.create [
            v.Value |> string |> TextBlock.text
        ]

    let view (state: State) (dispatch) =
        StackPanel.create [
            StackPanel.verticalAlignment VerticalAlignment.Center
            StackPanel.children [
                ComboBox.create [
                    ComboBox.dataItems data
                    ComboBox.minWidth 120.0
                    ComboBox.minHeight 50.0
                    ComboBox.selectedItem state
                    ComboBox.onSelectedItemChanged (fun v ->
                        if v <> null then
                            v
                            |> unbox<State>
                            |> fun state -> state.Value
                            |> Msg.Select
                            |> dispatch
                    )

                    ComboBox.itemTemplate 
                        (DataTemplateView<State>.create dataTemplate)
                ]
            ]
        ]

Proposal to add something like reacts 'useEffect

I thought about adding something like Reacts useEffect to simplify interop with some existing Avalonia Controls (CefGlue for Example)

Browser.create [
    Browser.startUrl "duckduckgo.com"
    // not sure how to trigger
    Browser.useEffect (fun browser -> browser.GoBack())
]

Implementing this feature is not complicated, maybe adding more 'hook' for attach und detach could also be helpful for controls that need to be specifically initialised.

Maybe effects should also be located in a new namespace like Avalonia.FuncUI.DSL.Effects, because they introduce side effects to an otherwise pure DSL.

@uxsoft @AngelMunoz @AngelMunoz
opinions ?

A better name

Fable and Fabulous are great sounding names. FuncUI is ok, but not as great.

If you have suggestions please comment below.

TextInput does not raise events.

I've tried

module FooView =
  
    type FooState = {
        text : string
    }

    let initialState = {
        text = "Yo ho ho"
    }

    type FooMsg = 
    | Edit of string

    let update (msg:FooMsg ) (state:FooState) : FooState =
        match msg with
        | Edit text -> { state with text = text}

    let view (state:FooState) (dispatch) : View =
        Views.stackPanel [
            Attrs.children [
                Views.textBox [
                    Attrs.text state.text
                    Attrs.onTextInput(fun sender args -> dispatch (Edit args.Text))
                ]
                Views.textBox [
                    Attrs.text state.text
                ]
            ]
        ]

and the two text boxes render and I can add text but the onTextInput never fires. Is this a bug or am I doing something wrong?

Question: How to give all available space to ListBoxItem and its descendants?

In the MusicPlayer example, this represents song in the playlist.

let private songTemplate (song: Types.SongRecord) (dispatch: Msg -> unit) =
        StackPanel.create [
            StackPanel.spacing 8.0
            StackPanel.onDoubleTapped ((fun _ -> dispatch (PlaySong song)), SubPatchOptions.OnChangeOf song)
            StackPanel.onKeyUp (fun keyargs ->
                match keyargs.Key with
                | Key.Enter -> dispatch (PlaySong song)
                /// eventually add other shortcuts to re-arrange songs or something alike
                | _ -> ()
            , SubPatchOptions.OnChangeOf song)
            StackPanel.children [
                TextBlock.create [
                    TextBlock.text song.name
                ]
            ]
        ]

In the app itself, onDoubleTapped does not fire if the TextBlock does not pressed because StackPanel is the same size as it. I tried to change it by using Grid and DockPanel by they were like that too. I saw this stackoverflow page and tried that by adding

<Style Selector="ListBoxItem">
        <Setter Property="HorizontalContentAlignment" Value="Stretch"></Setter>
    </Style>

to Styles.xaml but to no avail. Couldn't add it to ListBox because it would throw error saying that property is not available in ListBox.

When i added background property to Grid, like this

Grid.create [
        Grid.rowDefinitions "*"
        Grid.columnDefinitions "*"
        Grid.background Brushes.Aqua
        Grid.children [

, it somehow got all available space though. Why is this happening and is there another way to stretch these panels all over ListBoxItem?

Adding children/child to a button?

Is it possible to add children or a single child to a button? For example to create buttons like those in the sidebar of this app:
image

So, ideally I'd do something like this:

Button.create [
    Button.child [
        StackPanel.create [ (* ... *) ]
    ]
    Button.fontSize 20.0
    // ...
]

Is that possible or is there another way to achieve this sort of thing?

Consider grouping the possible UI elements under one module

Hello @JaggerJo,

This project looks really good and I really like the API used for the view elements. It looks like it was loosely based on fabulous-simple-elements. If that the case, I would like to suggest something that I regret not adding to that library -> not being able to see which elements you can use!

Let me explain: if the user is a beginner, then there is no way to tell which elements are available in the framework. You have to go through the docs and search for elements you need. Of course once you have found them, it becomes easy to find their properties, but only after you have searched the docs.

I propose to add a module, maybe named Ava that contains the constructor for all possible UI elements. Instead of

Button.create [
    Button.onClick (fun _ -> dispatch Increment)
    Button.content "click to increment"
]

I suggest writing:

Ava.button [
    Button.onClick (fun _ -> dispatch Increment)
    Button.content "click to increment"
]

This way the beginner user has a nice "entry point" to the possible UI elements that can be used and from there the search for elements becomes even easier. The reason for the name Ava is that the formatting works nicely when the propery list is indented with 4 spaces such that the property modules become aligned with the constructor function. This is how it is done in both Mui module from Fable.MaterialUI and the Ant module from Fable.AntDesign.

It would be a plus if the property module was also lower-case (but would introduce a breaking change)

Ava.button [
    button.onClick (fun _ -> dispatch Increment)
    button.content "click to increment"
]

What do you think?

Add link to docs to the ReadMe

I think it makes sense to add link to the docs (or alternatively replace one that leads to the wiki) in the ReadMe under Getting started section.

Is this the correct way to write subviews?

I have a basic view that just ensure that one text box is synchronised with another textbox view the model.

module FooView =
  
    type FooState = {
        text : string
    }

    let initialState = {
        text = "Yo ho ho"
    }

    type FooMsg = 
    | Edit of string

    let update (msg:FooMsg ) (state:FooState) : FooState =
        match msg with
        | Edit text -> { state with text = text}

    let view (state:FooState) (dispatch) : View =
        Views.stackPanel [
            Attrs.children [
                Views.textBox [
                    Attrs.text state.text
                    Attrs.onKeyUp(fun sender args -> 
                        dispatch (Edit (sender :?> TextBox).Text) 
                    )
                ]
                Views.textBox [
                    Attrs.text state.text
                ]
            ]
        ]

And then my main model has two copies of this

module ParentView =

    // The model holds data that you want to keep track of while the application is running
    type ParentState = {
        foo1 : FooView.FooState
        foo2 : FooView.FooState
    }

    //The initial state of of the application
    let initialState = {
        foo1 = { text = "foo1"}
        foo2 = { text = "foo1"}
    }

    // The Msg type defines what events/actions can occur while the application is running
    // the state of the application changes *only* in reaction to these events
    type CounterMsg =
    | Foo1Msg of FooView.FooMsg
    | Foo2Msg of FooView.FooMsg

    // The update function computes the next state of the application based on the current state and the incoming messages
    let update (msg: CounterMsg) (state: ParentState) : ParentState =
        match msg with
        | Foo1Msg msg -> { state with foo1 = FooView.update msg state.foo1 }
        | Foo2Msg msg -> { state with foo2 = FooView.update msg state.foo2 }
    
    // The view function returns the view of the application depending on its current state. Messages can be passed to the dispatch function.
    let view (state: ParentState) (dispatch): View =
        Views.dockpanel [
            Attrs.children [
                Views.uniformGrid[
                    Attrs.children [
                        FooView.view state.foo1 (Foo1Msg >> dispatch)
                        FooView.view state.foo2 (Foo2Msg >> dispatch)
                    ]
                ]
            ]
        ]       

image

As this is my first go at FuncUI it feels a bit boilerplatey. Maybe there is a better way to do this but composition of the states and messages into a hierarchy does feel natural. Maybe you can add this example to the readme as a basic example of what to do or not do.

Improve list diffing/patching performance

FuncUI currently implements super simple list diffing that works just fine in most cases.

There are cases where a list contains complex or a lot of different items this can be problematic. Especially when items are inserted at index 1 - because this currently will result in rebuilding all items.

Implementing a 2nd list diffing strategy that utilises keys to reduce the required patch work could speed up list diffing by a lot. (here is how react does it)

Adding a Key to Avalonia Controls is easily possible using AttachedProperties. (We actually implicitly attache properties to controls for internal subscription handling - IIRC)

StackPanel.children [
    TextBlock.create [
        TextBlock.key "item 1"
        TextBlock.text "item 1"
        ...
    ]
    ...
]

[Feature suggestion] Add Dotnet template(s)

Adding dotnet templates for Avalonia.FuncUI will make starting from scratch easier than copying example files to new projects.

I would start creating a template from the elmish counter example. This would be consistent with other templates like the SAFE-Stack template.

Should this be included in the main repo (e.g. a templates folder in the root of this repo) or be its own project?

Does FuncUI really need the MSG type DU's. Can't dispatch just use functions

Here's your counter example rewritten just using functions instead of DU's. It removes the need for a centralised msg handler.

namespace CounterElmishSample

open System
open Avalonia.Controls
open Avalonia.Controls
open Avalonia.FuncUI.DSL
open Avalonia.FuncUI.Components
open Avalonia.FuncUI.Components
open Avalonia.FuncUI.DSL
open Avalonia.FuncUI.DSL
open Avalonia.FuncUI.Types
open Avalonia.Layout

type CustomControl() =
    inherit Control()

    member val Text: string = "" with get, set

[<AutoOpen>]
module ViewExt =
    ()

module Counter =
    open Avalonia.FuncUI.DSL
    
    type CounterState = {
        count : int
        numbers: int list
    }

    let init = {
        count = 0
        numbers = [0 .. 100_000]
    }

    let increment state = {state with count = state.count + 1}    
    let decrement state = {state with count = state.count - 1}
    let specific number_state = {state with count = number }
    let remove_number = { state with numbers = List.except [number] state.numbers }
    
    let view (state: CounterState) (dispatch) =
        DockPanel.create [
            DockPanel.children [
                ListBox.create [
                    ListBox.items state.numbers
                    ListBox.itemTemplate (
                        TemplateView.create(fun data ->
                            let data = data :?> int 
                            DockPanel.create [
                                DockPanel.children [
                                    Button.create [
                                        Button.content "delete"
                                        Button.dock Dock.Right
                                        Button.width 50.0
                                        Button.tag data
                                        Button.onClick (fun args ->
                                            let number = (args.Source :?> Button).Tag :?> int
                                            dispatch remove_number number
                                        )
                                    ]                                    
                                    TextBlock.create [
                                        TextBlock.text (sprintf "%A" data)
                                        TextBlock.width 100.0
                                    ]                                    
                                ]
                            ]
                            |> generalize 
                        )                  
                    )
                ]
                (*
                TextBox.create [
                    TextBox.dock Dock.Bottom
                    TextBox.text (sprintf "%i" state.count)
                    TextBox.onTextChanged (fun text ->
                        printfn "new Text: %s" text
                     )
                ]
                TextBlock.create [
                    TextBlock.dock Dock.Top
                    TextBlock.fontSize 48.0
                    TextBlock.foreground "blue"
                    TextBlock.verticalAlignment VerticalAlignment.Center
                    TextBlock.horizontalAlignment HorizontalAlignment.Center
                    TextBlock.text (string state.count)
                ]
                LazyView.create [
                    LazyView.args dispatch
                    LazyView.state state.count
                    LazyView.viewFunc (fun state dispatch ->
                        let view = 
                            TextBlock.create [
                                TextBlock.dock Dock.Top
                                TextBlock.fontSize 48.0
                                TextBlock.foreground "green"
                                TextBlock.verticalAlignment VerticalAlignment.Center
                                TextBlock.horizontalAlignment HorizontalAlignment.Center
                                TextBlock.text (string state)
                            ]
                            
                        view |> fun a -> a :> IView
                    )
                ]
                *)
            ]
        ]       

Is there an advantage to using the DU's here that just plain function composition can't do? For simple functions it is even possible to inline the operations.

Instead of

 Button.onClick (fun args ->
   let number = (args.Source :?> Button).Tag :?> int
   dispatch remove_number number
  )

you could write

 Button.onClick (fun args ->
   let number = (args.Source :?> Button).Tag :?> int
   dispatch (fun state -> { state with numbers = List.except [number] state.numbers })
  )

Get window width?

First, I'd like to say thank you for working on this project! It's wonderful! Keep up the great work! ๐Ÿ˜ƒ

So I'm wondering, is there a way to get the width of the window in a view function? I'd like to get the window's width so I can create a sidebar that always has a width of, say, 1/3 of that width, but I'm not sure if there's a way to get the window width. (If that be achieved another way without knowing the window width please let me know.)

Description for Repo

Well, this is not a real issue but...
Can you please provide a description for the repo?
image

I have an OCD of Good-Repos-should-have-descriptions. ๐Ÿ˜…

Dispatching a message from TextBox.onTextChanged causes endless message dispatch

This is the stock Counter template project changed like this:

module Counter =
    
    type State = { value : string }
    let init = { value = "" }

    type Msg = | SetValue of string

    let update (msg: Msg) (state: State) : State =
        match msg with
        | SetValue v -> { state with value = v }

    let view (state: State) (dispatch) =
                
        TextBox.create [
            TextBox.text "123"
            TextBox.onTextChanged (fun t -> dispatch (SetValue t))
        ]

With withConsoleTrace on running the program gives

New message:: SetValue "123"
Updated state:: { v = "123" }

printed repeatedly. netcoreapp3.1 on Windows, latest FuncUI and Avalonia packages. I don't get any problems with the FuncUI control catalog project.

Grid doesn't work properly

MCVE

module Sample =
    open Avalonia.Controls

    [<RequireQualifiedAccess>]
    type State = 
        | First
        | Second

    let update state _ = state
    let init() = State.First

    let first dispatch = 
        Grid.create [
            Grid.rowDefinitions "Auto"
            Grid.columnDefinitions "Auto"
            Grid.children [
                Button.create [
                    Grid.column 0
                    Grid.row 0 
                    Button.content "Second"
                    Button.onClick (fun _ -> State.Second |> dispatch)
                ]
            ]
        ]

    let second dispatch =
        Grid.create [
            Grid.rowDefinitions "Auto"
            Grid.children [
               Button.create [
                   Grid.row 0
                   Button.content "First"
                   Button.onClick (fun _ -> State.First |> dispatch)
               ]
            ]
        ]   

    let view (state: State) (dispatch) =
        match state with
        | State.First -> first dispatch :> IView
        | State.Second -> second dispatch :> IView
Elmish.Program.mkSimple Sample.init Sample.update Sample.view

Expected behaviour:

Click on btn navigates to next page

1

Actual behaviour:

2

Click on button changes state but view looks the same.

Without specifyingGrid.columnDefinitions "Auto" and Grid.column 0 for the first view it works fine.

I don't like title but I wasn't able to come to anything better.

Question: How to send a message to another view

I've just run into a problem I don't know how to solve.

I have a view module for rendering a single item in a list. The view for the single item doesn't know about the list itself. However double clicking on the item needs to update a field on the item. Given that it is all immutable I need to tell the parent view or the parent of the parent view to do this. How to bubble up msgs to where they can be handled?

html docs ?

Hi @JaggerJo are the HTML docs for this project published anywhere?

I'm adding a "Use F# for Desktop Apps" to http://fsharp.org and making this a main entry, just wondering what to link to.

GridSplitter possible issue

Hi,

I have an issue where the GridSplitter will stop working if I change the elements in ListView in the adjacent grid cells.

If I selectively render the GridSplitter via a button - remove and then add - it will work again
Repo can be accessed here: https://github.com/sharp-fsh/funcui_gridsplitter_issue

Example gif:
GridSplitterIssue

`
open Avalonia.Controls
open Avalonia.FuncUI.DSL
open Avalonia.Layout
open Avalonia.FuncUI.Components
open Avalonia.Controls.Primitives

type FixGridSplitIssue =
| RenderGridSplitter
| DoNotRenderGridSplitter

type State = { 
    ListOneItems : string list 
    ListTwoItems : string list 
    FixGridSplitIssue : FixGridSplitIssue
    }

let generateStrings total word = 
    [
        for i in 0..total-1 do
            sprintf "%s: %i" word (i+1)
    ]

let rowDefinition1 = "35, 100*, 5, 400*"

let listViewA1 = generateStrings 20 "List A V1" 
let listViewB1 = generateStrings 30 "List B V1" 

let listViewA2 = generateStrings 5 "List A V2" 
let listViewB2 = generateStrings 15 "List B V2" 

let init = { 
    ListOneItems = listViewA1
    ListTwoItems = listViewB1
    FixGridSplitIssue = RenderGridSplitter
}

type Msg = 
| ChangeItems
| ToggleFix

let update (msg: Msg) (state: State) : State =
    match msg with
    | ChangeItems -> { state with ListOneItems = if state.ListOneItems = listViewA1 then listViewA2 else listViewA1
                                  ListTwoItems = if state.ListTwoItems = listViewB1 then listViewB2 else listViewB1}
    | ToggleFix -> { state with FixGridSplitIssue =  if state.FixGridSplitIssue = RenderGridSplitter then  DoNotRenderGridSplitter else RenderGridSplitter }
    

let buttonPanel gridPosition state dispatch = 
    StackPanel.create [
        gridPosition
        StackPanel.orientation Orientation.Horizontal
        StackPanel.children [
            Button.create [
                Button.content "Change Lists"
                Button.width 120.
                Button.height 35.
                Button.onClick (fun _ -> dispatch ChangeItems)
            ]
            Button.create [
                Button.content (if state.FixGridSplitIssue = RenderGridSplitter then "Click Twice To Fix (1 of 2)" else "Click Again To Fix (2 of 2)")
                Button.width 120.
                Button.height 35.
                Button.onClick (fun _ -> dispatch ToggleFix)
            ]
        ]
    ]
    
let genericListBox (items : string list) = 
    ListBox.create  [
            ListBox.dataItems items
            ListBox.itemTemplate (
                DataTemplateView<string>.create (fun (text) ->
                    TextBlock.create [ TextBlock.text text ]
                )
            )
        ]

let verticalScrollerWithItems gridPostion items =
    ScrollViewer.create     [
        gridPostion
        ScrollViewer.verticalScrollBarVisibility ScrollBarVisibility.Auto
        ScrollViewer.content (             
           genericListBox items
        )
    ]

let gridSplitterComponent gridPosition = 
    GridSplitter.create [
        gridPosition
        GridSplitter.horizontalAlignment HorizontalAlignment.Stretch 
    ]
    
let view (state: State) (dispatch) =
    Grid.create [
        Grid.rowDefinitions rowDefinition1
        Grid.showGridLines true
        Grid.children [
            buttonPanel (Grid.row 0) state dispatch
            verticalScrollerWithItems (Grid.row 1) state.ListOneItems
            match state.FixGridSplitIssue with
            | RenderGridSplitter -> gridSplitterComponent (Grid.row 2)
            | DoNotRenderGridSplitter -> StackPanel.create [ (Grid.row 2) ]
            verticalScrollerWithItems (Grid.row 3) state.ListTwoItems
        ]
    ]

`

Something's wrong with Views.grid

I tried modifying your counter example code to use a grid instead of a dock panel:

    let view (state : CounterState) (dispatch) : View =
        Views.grid [
            Attrs.rowDefinitions (RowDefinitions "1*,1*")
            Attrs.columnDefinitions (ColumnDefinitions "1*, 1*")
            Attrs.children [
                Views.button [
                    // Attrs.dockPanel_dock Dock.Bottom
                    Attrs.grid_row 1
                    Attrs.grid_column 0
                    Attrs.onClick (fun sender args -> dispatch Decrement)
                    Attrs.content "-"
                ]
                Views.button [
                    // Attrs.dockPanel_dock Dock.Bottom
                    Attrs.grid_row 1
                    Attrs.grid_column 1
                    Attrs.onClick (fun sender args -> dispatch Increment)
                    Attrs.content "+"
                ]
                Views.textBlock [
                    // Attrs.dockPanel_dock Dock.Top
                    Attrs.grid_row 0
                    Attrs.grid_column 0
                    Attrs.grid_columnSpan 2
                    Attrs.fontSize 48.0
                    Attrs.verticalAlignment VerticalAlignment.Center
                    Attrs.horizontalAlignment HorizontalAlignment.Center
                    Attrs.text (string state.count)
                ]
            ]
        ]

The state is not updated anymore - the update function always gets the initial state for some reason.

The issue is with these two lines:

            Attrs.rowDefinitions (RowDefinitions "1*,1*")
            Attrs.columnDefinitions (ColumnDefinitions "1*, 1*")

I get this error:

testafb3 Error: 0 : Unable to process the message: Increment: System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.NotSupportedException: Reassigning RowDefinitions not yet implemented.
   at Avalonia.Controls.Grid.set_RowDefinitions(RowDefinitions value) in D:\a\1\s\src\Avalonia.Controls\Grid.cs:line 131
   --- End of inner exception stack trace ---
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Reflection.RuntimePropertyInfo.SetValue(Object obj, Object value, BindingFlags invokeAttr, Binder binder, Object[] index, CultureInfo culture)
   at System.Reflection.RuntimePropertyInfo.SetValue(Object obj, Object value, Object[] index)
   at Avalonia.FuncUI.VirtualDom.Patcher.AttrPatcher.setValue@373(IControl view, PropertyAttrDelta attr, Object value)
   at Avalonia.FuncUI.VirtualDom.Patcher.AttrPatcher.patchProperty(IControl view, PropertyAttrDelta attr)
   at Avalonia.FuncUI.VirtualDom.Patcher.patch(IControl view, ViewDelta viewElement)
   at Avalonia.FuncUI.Hosts.HostWindow.Avalonia-FuncUI-Hosts-IViewHost-UpdateView(View viewElement)
   at [email protected](model state, FSharpFunc`2 dispatch)
   at [email protected](msg msg) in /Users/eugene/sources/elmish/elmish/src/program.fs:line 142

Is there a way to avoid this issue?

Question: What is a proper way to deal with OpenFileDialog etc?

Currently I use something like this:

Button.create [
    Button.content "Some text"

    Button.onClick (fun _ ->
        let ctx = System.Threading.SynchronizationContext()
        async {
            let ofd = OpenFolderDialog()
            let window = getWindow()
            let! path = 
                ofd.ShowAsync(window) 
                |> Async.AwaitTask
            do! Async.SwitchToContext ctx
            path
            |> Msgs.SomeMsg
            |> dispatch
        } |> Async.Start
    )
]

but I'm not sure whether this is a good approach or not.

DataTemplateView doesn't use MatchFunc

Hi @JaggerJo!

Found that DataTemplateView doesn't use its MatchFunc except for the comparison with other object.

Its Match func looks like this:

member this.Match (data : obj) : bool =
    match data with
    | :? 'data as _data -> true
    | _ -> false

Upd: here is the fix: #100

Creating a custom control via inheritance requires re-implementing all helpers.

For example

type ValidatingTextBox() =
    inherit Avalonia.Controls.TextBox()

and

module ValidatingTextBox = 
    let inline create<'a when 'a : equality> (parser:Epimorphism<'a,string,string>) (data:Image<'a>) (errors:string option->unit) attrs =
        Builder.ViewBuilder.Create<ValidatingTextBox> [
        //TextBox.create [
            yield! attrs
        ]

One would assume that I could use ValidatingTextBox and TextBox interchangably but it doesn't work. None of the TextBox.* attributes seem to do anything

 ValidatingTextBox.create  [
   TextBox.width 150.0
   TextBox.text state.text
]

does not generate a textbox with width 150 and the text set but if I change the commented out text to TextBox then it does work.

module ValidatingTextBox = 
    let inline create<'a when 'a : equality> (parser:Epimorphism<'a,string,string>) (data:Image<'a>) (errors:string option->unit) attrs =
        //Builder.ViewBuilder.Create<ValidatingTextBox> [
        TextBox.create [
            yield! attrs
        ]

Is this a fundamental problem that cannot be solved?

improve Wiki

The Wiki currently contains a few articles that need to be reviewed. Code Examples are for FuncUI 0.1 and therefor don't work anymore (breaking change from 0.1 -> current).

Bug - subscription callback doesn't update

Hi @JaggerJo, please take a look at repro of this bug โ€” Thecentury@07df113
Button is expected to increase count by 1 continuously, but works only once.

And in the next commit there is a fix: Thecentury@245d864

Bug happens because in the repro new subscription is equal to the previous one.
I added func into a list of members which are used to compare two subscriptions.
I think another possible fix is to always consider two subscriptions to different if at least one of them captures state.
Cannot estimate which solution is better.

What do you think of it?

Update with async function

I am building an application with Avalonia FuncUI, but can run functions with heavy computations and http requests only synchronously, i.e. blocking the main(UI) thread. Could someone please show me a way how to update with a function that runs async and returns the new state on e.g. Button.onClick?

Style DSL

I had a go at hacking a style DSL. It seems to work. I'm sure you could pick it apart but maybe it's a start for some ideas. XAML really should die!!

module Control =
    open Avalonia.Styling
    open Avalonia.Controls
    open FSharpx
    let styling stylesList = 
        let styles = Styles()
        for style in stylesList do
            styles.Add style
        Control.styles styles


    let style (selector:Selector->Selector) (setters:IAttr<'a> seq) =
        let s = Style(fun x -> selector x )
        for attr in setters do
            match attr.Property with
            | Some p -> 
                match p.accessor with
                | InstanceProperty x -> failwith "Can't support instance property" 
                | AvaloniaProperty x -> s.Setters.Add(Setter(x,p.value))
            | None -> ()
        s

and then in my view

  let private binFileTemplate (indexedBinFile: IndexedBinFile) (binFileViewer:BinFile->unit) (dispatch: Msg -> unit) =
        let (id, binFile) = indexedBinFile
        let foreground = 
            match binFile.eq_status with
            | DEGREE_EQUAL                                             -> "green" 
            | DEGREE_DIFFERENT                                         -> "red"
            | DEGREE_EXCEPTION_0|DEGREE_EXCEPTION_1|DEGREE_EXCEPTION_2 -> "yellow"
            | DEGREE_SIMILAR|DEGREE_EQUAL_IN_TOLERANCE                 -> "darkgreen"
            | _                                                        -> "brightred"

        (* create row for name, eq_status, deviation *)
        StackPanel.create [
            StackPanel.orientation Orientation.Horizontal
            StackPanel.background "black"
            StackPanel.onDoubleTapped (fun _ -> ViewBinFile(binFileViewer, binFile) |> dispatch )
            let style = [
                TextBlock.width columnWidth
                TextBlock.foreground foreground
                TextBlock.horizontalAlignment HorizontalAlignment.Left
            ]
            StackPanel.children [
                TextBlock.createFromSeq <| seq {
                    yield TextBlock.text binFile.name
                    yield! style
                } 
                TextBlock.createFromSeq <| seq {
                    TextBlock.text (binFile.eq_status |> DU.toString )
                    yield! style
                }
                TextBlock.createFromSeq <| seq {
                    TextBlock.text (binFile.deviation |> sprintf "%g") 
                    yield! style
                }
            ] 
        ]

Not update the view if model didn't change

Hi @JaggerJo!

What do you think about the idea of not generating the new view and not patching the UI when model hasn't changed after the update function?

Sometimes it is quite tedious to create comparers for all FuncUI properties to eliminate the need to patch the value in the UI, and this change can help to fix problems when repeated setting of the same property with the same value nevertheless changes something under the hood.

Question: Ellipses inside uniformgrid?

I'm creating a program to try to simulate a lab chip that moves liquid with electrodes. I used the game of life example as a bit of a jumping off point to make the electrode grid, and id like to now make droplets of liquid that move on top of this uniform grid. Ive been messing around a bit with trying to create a canvas with ellipses but im feeling a bit lost.
ill paste in my view method with the canvas part commented out.

let view (grid: GridModel) (dispatch: Msg -> unit) : IView =
        DockPanel.create[
            DockPanel.children[
                Button.create [
                            Button.dock Dock.Bottom
                            Button.background "#d35400"
                            Button.onClick ((fun _ -> ImportProcedure (fullPath)  |> dispatch), SubPatchOptions.Always)
                            Button.content "Import Procedure"
                ]|> generalize
                UniformGrid.create [
                    UniformGrid.columns grid.Width
                    UniformGrid.rows grid.Height
                    UniformGrid.children (
                        grid.Electrodes
                        |> Array2D.flati
                        |> Array.map (fun (x, y, electrode) ->
                            let electrodePosition = { x = x; y = y }
                            
                            Button.create [
                                match electrode.ChemList with
                                | [] ->
                                    yield Button.onClick ((fun _ -> AddChem (electrodePosition,("test",1.1)) |> dispatch), SubPatchOptions.OnChangeOf electrodePosition)
                                    yield Button.background "gray"
                                | _ ->
                                    yield Button.onClick ((fun _ -> RemoveChem (electrodePosition,("test",1.1))  |> dispatch), SubPatchOptions.OnChangeOf electrodePosition)
                                    yield Button.background "green"
                                
                            ] |> generalize                     
                        )
                        |> Array.toList
                        |> List.append (List.map (GridModel.DropletValues >> (fun (chems,x,y,r) -> 
                        
                        Canvas.create [
                        Canvas.background "#2c3e50"
                        Canvas.children[
                            Ellipse.create[
                                Ellipse.top (float x)
                                Ellipse.left (float y)
                                Ellipse.width r
                                Ellipse.height r
                                Ellipse.fill "#ecf0f1"
                            ] 
                        ] 
                        ] |> generalize)) grid.Droplets)
                    )        
        ]
        ]]
        |> generalize

I apologize if the question is a bit vague, feel free to ask for additional information. An older version of the program(without the droplets added) is here: https://github.com/rasmusmm/DMBSim.
*edited with a slightly closer version...

How can I make a TextBox input mask?

Hi, can you suggest a way I can provide an input mask for TextBox, e.g. so that it only accepts numeric input? I've tried modifying the input data in the OnTextInput and OnKeyUp event handlers and also by simply setting the text property as part of the msg/model/view process but none of that works. This is on .Net Core/Windows. Also I am new to Avalonia.

Thanks for bringing Elmish to Avalonia btw!

Project setup guidance

Hi @JaggerJo ,

Very interesting project! While you provide samples, I still feel kinda lost creating a new project from scratch. For example, what program are you using here? Is it required?

image

Whats the ideal project template to start from? Is it related to the Avalonia UI .NET core template? (e.g. dotnet new avalonia.app -o MyApp)

Can you guide me through some basic setup steps? I could do a PR adding the basic steps to your README when i then managed to get a new project running.

Thanks!

How to use with FSharp.Control.Reactive library.

I am using Observables to retrieve market feeds (tick data) and simply would like to dispatch an update to the UI state on changes.

let tickObserver (_state: LiveMarketMonitor.State) =
        let sub (dispatch: LiveMarketMonitor.Msg -> unit) =
            marketFeedObservable
            |> Observable.filter (fun event -> event.MarketPair = MarketPair.Btcaud)
            |> Observable.subscribe (fun event ->
                dispatch <|
                    LiveMarketMonitor.Msg.UpdateMarketTick
                        { TimeUpdatedUtc = event.Timestamp.Value
                          LastPrice = event.LastPrice
                          BestAsk = event.BestAsk
                          BestBid = event.BestBid })
            |> ignore

        Cmd.ofSub sub

    do
        base.Title <- "FinFlow.Desktop.App"
        base.Width <- 400.0
        base.Height <- 400.0

        this.VisualRoot.VisualRoot.Renderer.DrawFps <- true
        this.VisualRoot.VisualRoot.Renderer.DrawDirtyRects <- true

        Elmish.Program.mkSimple (fun _ -> LiveMarketMonitor.init) LiveMarketMonitor.update LiveMarketMonitor.view
        |> Program.withHost this
        |> Program.withSubscription tickObserver
        |> Program.withConsoleTrace
        |> Program.run

The dispatch appears to called in the subscribe method. But I am not seeing any changes in the UI. If I put a breakpoint on the UI view method, I can see the correct state, but the UI for some reason has only displays the initial state?

Consider allowing adaptive views as an option

This is about potentially allowing the use of adaptive view specifications, see https://github.com/fsprojects/FSharp.Data.Adaptive

From fabulous-dev/Fabulous#258 (comment)

@JaggerJo The FuncUI implementation is really good - this file is impressive https://github.com/dsyme/Avalonia.FuncUI/blob/master/src/Avalonia.FuncUI/Core/VirtualDom.fs

That said, it's still using view-reevaluation - for example if there are 10K data points in a chart and one is removed then the view is re-evaluated and, in the absence of other hacks, this will involve a significant amount of work to spot the minimal diff.

FuncUI can, I think, be adapted to work with adaptive data relatively easily. This would mean that the 'view' functions are not re-executed on update (except where necessary for incremental DOM maintenance). The diffs in the view would flow out of the adaptive data, rather than having to diff an old and new view like you do here

There's a sample showing how to define a tree of adaptive view-like data and perform incremental maintenance on a mutable HTMLElement data strucutre here: https://github.com/dsyme/FSharp.Data.Adaptive/blob/dom-node/src/FSharp.Data.Adaptive.Tests/DomUpdater.fs. The variation FuncUI would need would be a bit different - for example FuncUI could define a ViewReader for the AdaptiveView type, producing a ViewDelta (and then patch is called on that).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.