fsprojects / Avalonia.FuncUI

Develop cross-plattform GUI Applications using F# and Avalonia!
https://funcui.avaloniaui.net/
MIT License
957 stars 74 forks source link

Is it possible to use itemTemplate and dataTemplate #19

Closed bradphelan closed 5 years ago

bradphelan commented 5 years ago

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.

bradphelan commented 5 years ago

Maybe I was too quick... If you set recycling true on the itemTemplate ie

    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 )

then it is super fast but doesn't render the items correctly. ie: if I delete an item in the middle of the list only the item at the end is deleted.

If I do

    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
            ) ,false )

Then it is not so snappy but it renders correctly. I guess I'm not so sure about what Avalonia is doing in this case.

JaggerJo commented 5 years ago

First, this is super interesting. The current version had no focus at all on templates and virtualisation support.

I just ran your example.

It seems like the dataTemplate needs to subscribe to its DataContext and manually apply changes. This is not a good way for FuncUI.

FuncDataTemplate

I have plans to get this right in vNext - but currently have nothing concrete. My idea is basically to wrap each item template automatically with a HostControl and the (also in the background) subscribe to changes. This would be super convenient ( - and could also support further optimisations).

JaggerJo commented 5 years ago

What are you trying to do with FuncUI ?

I personally use it to build 'Flink' my 2D Editor and seeing other use cases is super interesting. I have big plans for FuncUI.

I hope once Avalonia gets more traction this will be the thing F# devs happily use to write apps for all platforms and devices.

bradphelan commented 5 years ago

Actually. I'm not trying to build anything. I'm a C++ dev by day and I had a lazy sunday. It's been a while since i hacked on F# and it's a super elegant language. We do have a need at work for platform independant UI's ( linux, windows ) and I thought maybe I'd look at Avalonia but XAML is so boring to work with so I thought I'd try some F# with it.

However understanding how to write dispatchers and updates for heavily nested structures with lists is non-trivial or at least less trivial than using XAML binders with mutable structures. There is certainly some duality between the dispatch logic and the update logic that is asking for a set of abstractions to make this obvious to deal with but I can't quite figure out what they should be.

bradphelan commented 5 years ago

I'm no web developer but probably looking at how React handles virtualizing long lists might assist. https://github.com/bvaughn/react-window

bradphelan commented 5 years ago

I had an idea. Have you looked into using optics / lenses. You could create binders that look very much like standard XAML binders but still working with immutable data.

https://github.com/eiriktsarpalis/TypeShape/blob/master/samples/TypeShape.Samples/lens.fs

Imagine binders/optics that have the behaviour of XAML binders ( not the same as the above link )

let personView binder = [
    Views.stackLayout [
        StackLayout.children [
            Views.TextBox [ TextBox.Text (binder >>= <@ fun c -> c.name @>) ]
            Views.TextBox [ TextBox.Text (binder >>= <@ fun c -> c.age @>) ]
            Views.TextBox [ TextBox.Text (binder >>= <@ fun c -> c.address @>) ]
            Views.TextBox [ TextBox.Text (binder >>= <@ fun c -> c.starSign @>) ]
        ]
    ]
]

Notice that the binders are composable based on optics technology. You would have one root mutable object which I assume you have in FuncUI already. Then you create optics/binders that index into the model. Essentially a binder is a pair IObservable + IObserver but with knowledge of the path indexed to get to it's object and composable so that with application of further indexing you can get deeper into the structure.

It's kind of a compromise between mutable binders and a fully immutable data store and for simple cases easier to manage that a pure functional system.

Just an idea.

JaggerJo commented 5 years ago

Haven't done a lot with lenses so far to be honest.

I'm currently implementing lazy views and I am pretty sure the same concept works fine for virtualisation. I'll let you know when I have something to play around with

image
JaggerJo commented 5 years ago

I now have a working prototype!

backing data is a list from 1 to 100.000

2019-10-04 20 13 26

bradphelan commented 5 years ago

Awsome :)

On Fri., 4 Oct. 2019, 20:19 Josua Jäger, notifications@github.com wrote:

backing data is a list from 1 to 100.000

[image: 2019-10-04 20 13 26] https://user-images.githubusercontent.com/13090415/66230106-b4fdb000-e6e3-11e9-8e5c-2c8698b29d99.gif

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/JaggerJo/Avalonia.FuncUI/issues/19?email_source=notifications&email_token=AAAEJ4SKR37GT7QXUFKCEZTQM6CJ3A5CNFSM4I3TNPUKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEAMPUZQ#issuecomment-538507878, or mute the thread https://github.com/notifications/unsubscribe-auth/AAAEJ4URSUHGSVXJBEFPUWTQM6CJ3ANCNFSM4I3TNPUA .