fsprojects / Avalonia.FuncUI

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

Passing args to ViewBuilder.Create #131

Closed JordanMarr closed 2 years ago

JordanMarr commented 4 years ago

Is it possible to pass in args to the ViewBuilder to initialize the view with?

AngelMunoz commented 4 years ago

I think there's support for that, you should be able to pass any property declared in Avalonia's ContentControl class image

AngelMunoz commented 4 years ago

for "parameter like" kind of props... not sure cc @JaggerJo

JaggerJo commented 4 years ago

@JordanMarr can you give a concrete example of what you’re trying to do ?

JordanMarr commented 4 years ago

After working on a Fable React app for the last year or so, I have gotten very used to creating self-contained components that have their own dispatch loops, which eliminates the need to coordinate messages between components.

Now I am trying to find a windows UI framework that will allow something similar. I see that you have the ability to do this via ViewBuilder.Create / HostControl. The only thing it seems to be missing is that I don't see a way to pass model / args to it. So in other words, it seems to be completely isolated in that regard.

In React, you can create a props entity for a component. When calling it, you can pass in some info that can be used or stored in the model.

For example:

module UsersPage
...
let view (model: Model) dispatch =
    ...
    let props = { UserId: state.SelectedUser.Id }
    ViewBuilder.CreateWithProps<EditUserForm.Host>(props)

Then in my EditUserForm control, I can do something like this:

module UserEditForm
...
type Props = { UserId: int }

let init (props: Props) = 
    { User = None }, Cmd.OfAsync.perform Data.loadUser props.UserId OnUserLoaded

...

type Host() as this =
    inherit Hosts.HostControl()
    do
        let startFn () =
            init (this.Props) 
        Elmish.Program.mkProgram startFn update view
        |> Program.withHost this
        |> Program.run
JaggerJo commented 4 years ago

After working on a Fable React app for the last year or so, I have gotten very used to creating self-contained components that have their own dispatch loops, which eliminates the need to coordinate messages between components.

Now I am trying to find a windows UI framework that will allow something similar. I see that you have the ability to do this via ViewBuilder.Create / HostControl. The only thing it seems to be missing is that I don't see a way to pass model / args to it. So in other words, it seems to be completely isolated in that regard.

FuncUI currently only supports controls that don't take constructor arguments (this could change, should be relatively simple). This means that you can provide some outer state to a component, but it's not there when the control is instanciated.

The LazyView (and LazyView DSL) does something like this.

In React, you can create a props entity for a component. When calling it, you can pass in some info that can be used or stored in the model.

For example:

module UsersPage
...
let view (model: Model) dispatch =
    ...
    let props = { UserId: state.SelectedUser.Id }
    ViewBuilder.CreateWithProps<EditUserForm.Host>(props)

Then in my EditUserForm control, I can do something like this:

module UserEditForm
...
type Props = { UserId: int }

let init (props: Props) = 
    { User = None }, Cmd.OfAsync.perform Data.loadUser props.UserId OnUserLoaded

...

type Host() as this =
    inherit Hosts.HostControl()
    do
        let startFn () =
            init (this.Props) 
        Elmish.Program.mkProgram startFn update view
        |> Program.withHost this
        |> Program.run

What do you expect to happen when the props change ?

Might be good to add some features to FuncUI so this is easily possible.

JordanMarr commented 4 years ago

What do you expect to happen when the props change ?

Might be good to add some features to FuncUI so this is easily possible.

I would expect the control to be updated when the passed-in props changed (which is the way function component props work in React).

I did a little experimenting to try passing props to the Host c'tor (passing into the Activator.CreateInstance). It became immediately obvious that passing props into the c'tor would not work because the control is not re-instantiated on each update (thankfully!). So I guess there would need to be a generic HostControl<'props> that had a "Props" property, and then the props could be taken into consideration in the diff process.

JaggerJo commented 4 years ago

https://github.com/AvaloniaCommunity/Avalonia.FuncUI/pull/133

JaggerJo commented 4 years ago

@JordanMarr still needs testing and an example component implementation. I'm open to suggestions 🙂.

AngelMunoz commented 4 years ago

@JaggerJo I got you covered with the sample, I experimented a bit with this change and pushed a branch with a small sample

https://github.com/AvaloniaCommunity/Avalonia.FuncUI/blob/constructor-parameters-sample/src/Examples/Examples.ViewBuilderProps/Form.fs https://github.com/AvaloniaCommunity/Avalonia.FuncUI/blob/constructor-parameters-sample/src/Examples/Examples.ViewBuilderProps/Shell.fs

One of the first things that come to my mind is that callback functions are passed into the props object often in places like react, and I guess it could apply the same for Fable not sure how will that affect diffing of the constructor params (since functions cannot be compared?)

Here's the relevant sample: https://github.com/AvaloniaCommunity/Avalonia.FuncUI/blob/constructor-parameters-sample/src/Examples/Examples.ViewBuilderProps/Shell.fs#L54,L69

also having the obj array as params kind of makes it unsafe if the params array do not match the constructor params it fails with an exception which may be hard to figure out

System.MissingMethodException: Constructor on type 'Examples.ViewBuilderProps.Form+Host' not found.
  at at System.RuntimeType.CreateInstanceImpl(BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture)
  at at System.Activator.CreateInstance(Type type, BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes)
  at at System.Activator.CreateInstance(Type type, Object[] args)...

I think this covers passing props down to children and display the values, but I'm a bit unsure about the passing changed values up

picture
JordanMarr commented 4 years ago

also having the obj array as params kind of makes it unsafe if the params array do not match the constructor params it fails with an exception which may be hard to figure out

I wonder if it would make sense to pass in a strongly typed createView function instead of using Activator.CreateInstance (and possibly getting a run-time exception):

TabItem.create [
        TabItem.header "Form"
        TabItem.content (ViewBuilder.Create([], Form.Host, formProps)) ]
 ]
static member Create<'view, 'props>(attrs: IAttr<'view> list, createView: 'props -> 'view , props: ‘props) : IView<'view> =
    {
        View.viewType = typeof<'view>
        View.createView = createView
        View.viewConstructorParams = props
        View.attrs = attrs
    }

Then the VirtualDom.Patcher could just call this:

viewElement.viewConstructorParams
|> viewElement.createView
|> Utils.cast<IControl>
JordanMarr commented 4 years ago

One of the first things that come to my mind is that callback functions are passed into the props object often in places like react, and I guess it could apply the same for Fable not sure how will that affect diffing of the constructor params (since functions cannot be compared?)

React documentation points out that it creates a new function on each re-render: "Using Function.prototype.bind in render creates a new function each time the component renders, which may have performance implications (see below)." Doesn't seem to be an issue in your sample, but it's something to look out for.

I will try to do some other testing tomorrow.

JordanMarr commented 4 years ago

This is what I was imagining: Pull #135

JaggerJo commented 2 years ago

(see comment in PR above)