Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experiments: Stronger typing, Mixing Dynamic/Static views, Componentization, SwiftUI-like DSL #738

Closed
TimLariviere opened this issue May 10, 2020 · 48 comments
Labels
t/discussion A subject is being debated

Comments

@TimLariviere
Copy link
Member

TimLariviere commented May 10, 2020

DISCLAIMER: All the following points are experiments only and there's no guarantee they will be available anytime in the future.

Index:

In the last few days, I've been experimenting with a couple of ideas.

Introduction

Instead of having all View.XXX() return untyped ViewElement, it should return a typed ViewElement to make the compiler check for invalid uses (like putting a Button as an item of ListView -- ListView only accepts Cell-derivated controls).

  • Diffing per "attribute" instead of per "control"

Currently in Fabulous.XamarinForms, we diff controls by calling the Update method attached to it (e.g. View.Button() will have a link to ViewBuilders.UpdateButton()).

This update method will check each one of the properties of Button (and will call UpdateVisualElement, etc. to update the inherited properties/events).
This is perfectly fine in 99% of the cases, except when dealing with overridden properties or attached properties.

This becomes particularly tricking to always apply the correct behavior and it results in a lot of custom code.

Instead if we went with "attribute"-based diffing like FuncUI, overridden properties could simply be dealt with by storing the overridden attribute updater instead of the base one in an attributes array (e.g. ViewElement.Attributes).
Same with attached properties, there would be no need to be aware that Grid.Row is an attached property applicable only on the Grid inherited Layout<T>.Children items.

  • Merging Fabulous and Fabulous.StaticViews

Currently you need to choose from either Fabulous or Fabulous.StaticViews to write apps. Mixing the 2 is not possible.

Though there's instances where being able to use the 2 makes sense.
Fabulous.StaticViews makes it easy to use 3rd party controls (or custom XAML views) without spending too much time writing wrappers for it.

It would also be way easier to "market" Fabulous, less confusing on which package to use.

  • Allow Fabulous.XamarinForms to run as a component of an existing app, or as an app as today

Fabulous.XamarinForms currently plugs itself in the Xamarin.Forms.Application.MainPage property and acts from there.
It is quite easy to allow it to plug into a ContentView.Content property instead, allowing it to run anywhere, in multiple instances, even in existing C# apps.

  • A SwiftUI-like DSL

I really like the Fabulous DSL, though IDEs make it difficulty to know what's exactly is available when having like 20 optional method parameters.
Elmish.React - Feliz - Avalonia.FuncUI all have great advantages, but also have different issues in my opinion, I will explain why in a comment below.

So I tried various approaches to keep it close to what's Fabulous DSL offers today, while trying to make it IDE-friendly.
After pushing the F# syntax to its limit several times, I went with a SwiftUI-like syntax.
I think it offers the best balance between readability and discoverability, as well as being nice with the IDEs.

I added a little Flutter-like widgets to the mix to make it great with static views code.

Current experiments can be found in https://github.com/TimLariviere/Fabulous/tree/play

Read the comments below for more information.

@TimLariviere TimLariviere added the t/discussion A subject is being debated label May 10, 2020
@TimLariviere
Copy link
Member Author

TimLariviere commented May 10, 2020

Stronger typing & attribute-based diffing

To support those meant to rewrite most of the foundation of Fabulous.

I went the simplest definition of ViewElement as possible

type Attribute =
    | Property of set: (obj * obj -> unit) * unset: (obj -> unit)
    | Event of subscribe: (obj * obj -> unit) * unsubscribe: (obj * obj -> unit)

type ViewElement(targetType: Type, create: unit -> obj, attributes: KeyValuePair<Attribute, obj> array) =
    member x.TargetType = targetType
    member x.Attributes = attributes
    member x.Create() = create()
    
module DynamicViews =
    type DynamicViewElement<'T>(create: unit -> 'T, attributes) =
        inherit ViewElement(typeof<'T>, (create >> box), attributes)

Attributes store a list of Attribute with their new values. The Attribute type contains the function required to apply (or reset) the new value to the real control if the diffing algorithm finds it should be updated.
This logic is currently missing.

In Fabulous.XamarinForms, to check the correct usage of the controls at compile-time, I introduced interfaces mapping to the most interesting sets of controls:

type IBindableObject = interface end
type IPage = inherit IBindableObject
type IView = inherit IBindableObject
type ICell = inherit IBindableObject
type IMenuItem = inherit IBindableObject

Those interfaces are used by types inheriting DynamicViewElement<'T>

type DynamicBindableObject<'T when 'T :> BindableObject>(create, attribs) =
    inherit DynamicViewElement<'T>(create, attribs)
    interface IBindableObject
    
type DynamicPage<'T when 'T :> Page>(create, attribs) =
    inherit DynamicBindableObject<'T>(create, attribs)
    interface IPage
    
type DynamicView<'T when 'T :> View>(create, attribs) =
    inherit DynamicBindableObject<'T>(create, attribs)
    interface IView
    
type DynamicCell<'T when 'T :> Cell>(create, attribs) =
    inherit DynamicBindableObject<'T>(create, attribs)
    interface ICell

And when declaring a control (e.g. View.Label()), it would return the appropriate instance (e.g. DynamicView<Xamarin.Forms.Label>()).

This allows container controls to require a specific subset of controls.
For example, View.Grid(children: IView list) or View.ListView(items: ICell list).

Today, writing this would compile, even though it's invalid for Xamarin.Forms (will throw a runtime exception)

View.ListView([
    View.Button()
])

But with the new types, it would not compile

View.ListView([
    View.Button() // DynamicView<Button> is not compatible with ICell
])

Regarding the Attribute discriminated union, frameworks can introduce their own variants to run specific update logic.

namespace Fabulous.XamarinForms

type Attribute<'T> =
    | Property of set: ('T * obj -> unit) * unset: (obj -> unit)
    | BindableProperty of property: Xamarin.Forms.BindableProperty
    | CollectionProperty of set: ('T * obj -> unit) * unset: (obj -> unit)
    | AttachedProperty of property: Xamarin.Forms.BindableProperty
    
type EventAttribute<'T> =
    | EventHandler of (obj -> IEvent<System.EventHandler<'T>, 'T>)

let mapXFAttributeToAttribute ... = (...)

// usage
let LabelTextProperty = Attribute<_>.BindableProperty Xamarin.Forms.Label.TextProperty

@Happypig375
Copy link

Intellisense can actually tell you optional parameters. It is the right-most filter, then scroll to after let-bindings.

@TimLariviere
Copy link
Member Author

TimLariviere commented May 10, 2020

@Happypig375 Yes. But only in Visual Studio (Windows). It's not the case in Visual Studio for Mac and Rider. Will include examples.

@TimLariviere
Copy link
Member Author

TimLariviere commented May 10, 2020

Merging Fabulous and Fabulous.StaticViews

With the rewrite of ViewElement from above, it allows to declare a StaticViewElement to use static views alongside dynamic views.
The difference is that StaticViewElement has only one attribute, its state (e.g. BindingContext in Xamarin.Forms)

module StaticViews =
    type StaticViewElement<'T>(create: unit -> 'T, setState: (obj * obj -> unit), unsetState: (obj -> unit), state) =
        inherit ViewElement(typeof<'T>, (create >> box), [| KeyValuePair(Property (setState, unsetState), state) |])
        member x.State = state

To keep it compatible with Fabulous.XamarinForms interfaces IView, ICell, etc., there are subsets of this type.

namespace Fabulous.XamarinForms.StaticViews

[<AbstractClass>]  
type StaticView<'T when 'T :> View>() as this =
    inherit StaticViewElement<'T>(this.Create, ignore, ignore, null)
    interface IView
    abstract Create: unit -> 'T
    
[<AbstractClass>]  
type StaticView<'TView, 'TState when 'TView :> View>(state: 'TState) as this =
    inherit StaticViewElement<'TView>(this.Create, StaticHelpers.setBindingContext, StaticHelpers.unsetBindingContext, state)
    interface IView
    abstract Create: unit -> 'TView

With this, we can create both "stateless" static controls (Flutter's StatelessWidget) and "stateful" static controls (Flutter's StatefulWidget) by inheriting from those.

// Stateless, once created, it won't ever change
type MyButton() =
    inherit StaticView<Button>()
    override x.Create() =
        // This could be any Xamarin.Forms instances. Like a whole page or even bigger. Even 3rd party controls
        Xamarin.Forms.Button(Text = "Click me")

// Stateful, it can define bindings to listen to changes inside its BindingContext
type MyStatefulButtonState =
    { Foo: string
      Click: unit -> unit }
    
type MyStatefulButton(foo: string, click: unit -> unit) =
    inherit StaticView<Button, MyStatefulButtonState>({ Foo = foo; Click = click })
    override x.Create() =
        let button = Xamarin.Forms.Button(TextColor = Color.Blue)
        button.SetBinding(Xamarin.Forms.Button.TextProperty, nameof x.State.Foo)
        button.SetBinding(Xamarin.Forms.Button.CommandProperty, nameof x.State.Click)
        button

Once declared, they can be used directly with their dynamic counterparts

let view model dispatch =
    View.ContentPage(
        View.StackLayout([
            View.Label(text = "Hello")
            View.ListView([
                MyCell()
                View.TextCell()
             ])
             MyButton()
             MyStatefulButton(foo = model.Foo, click = fun () -> dispatch Bar)
        ]) 
    )

Note: This will most likely change in the future because I did not check what it meant to diff a user-defined F# record at runtime.

@TimLariviere
Copy link
Member Author

TimLariviere commented May 10, 2020

Running Fabulous as a component

This one is the simplest experiment from all these.
Fabulous already allows to define a Host that will host the controls created by Fabulous (this is done to allow alternative frameworks like Uno).

Fabulous.XamarinForms defines a XamarinFormsHost that takes an instance of Xamarin.Forms.Application and sets its MainPage property.

type XamarinFormsHost(app: Application) =
    interface IHost with
        member __.GetRootView() =
            match app.MainPage with
            | null -> failwith "No root view"
            | rootView -> rootView :> obj 

        member __.SetRootView(rootView) =
            match rootView with
            | :? Page as page -> app.MainPage <- page
            | _ -> failwithf "Incorrect model type: expected a page but got a %O" (rootView.GetType())

module XamarinFormsProgram =    
    let runWith app arg program =
        let host = XamarinFormsHost(app)

        program
        |> Program.runWithFabulous host arg

This is great, but it prevents to use anything else with Fabulous.
So you either use Fabulous or do classic Xamarin.Forms.

But simply defining a second Host setting content of Xamarin.Forms.ContentView instead of Xamarin.Forms.Application, it means we can use Fabulous as an independant component of a larger Xamarin.Forms app.
And even having multiple Fabulous components in a single app.

type ContentViewHost(contentView: ContentView) =
    interface IHost with
        member __.GetRootView() =
            match contentView.Content with
            | null -> failwith "No root view"
            | rootView -> rootView :> obj 

        member __.SetRootView(rootView) =
            match rootView with
            | :? View as view -> contentView.Content <- view
            | _ -> failwithf "Incorrect model type: expected a View but got a %O" (rootView.GetType())

To keep everything consistent, I had to rename the Program to Component.
Instead of doing

Program.mkProgram init update view
|> Program.withConsoleTrace
|> XamarinFormsProgram.run app

You would do either

// Inside an existing app
type MyFabulousControlInXamarinForms() as this =
    inherit Xamarin.Forms.ContentView

    do
        Component.useCmd init update view // It's simply a rename of mkProgram I tried in the experiment
        |> Component.withConsoleTrace
        |> Component.run this

// A full-Fabulous app
type App() as app =
    inherit Xamarin.Forms.Application
    do
        Component.useCmd init update view
        |> Component.withConsoleTrace
        |> Component.runAsApplication app

Note: I think we can even require "component"-Fabulous to ensure view returns a IView (see above) and "application"-Fabulous returns a IPage to be consistent with Xamarin.Forms

@TimLariviere
Copy link
Member Author

TimLariviere commented May 10, 2020

Why should we change the Fabulous DSL?

Overall, I really like the Fabulous DSL. It's:

  • concise and readable

No repeated tokens like most other Elmish DSLs (e.g. Label.label, props.text/props.style, etc.).
It's not necessarily a bad thing (Feliz is a great example), but less noise = better readability.

  • discoverable

You can find all available properties for the control you're writing (except when IDEs go in the way...)

  • shielding you from doing anything invalid

You only have access to available properties, ain't no Text property on an Image.

Bad user experience in most IDEs

What motivated this question is mostly a bad user experience with the various IDEs when writing an app with Fabulous.

An important thing when working with a UI Framework for me is the discoverability of the various properties/events I can set. Only if I don't find anything resembling what I want to do, I go check the documentation.

To allow only declaring the relevant properties, Fabulous makes heavy use of optional method parameters. But most IDEs handle many optional parameters pretty badly.

Visual Studio (macOS)
Screenshot 2020-05-10 at 9 30 45 PM

Visual Studio for Mac is not great when autocompleting optional parameters. When asking for autocomplete, it will show you everything matching what you typed, regardless of the context.

And if you dare put your cursor over the type to see what's the available properties, you're greeted with this tooltip.

Screenshot 2020-05-10 at 9 30 21 PM

It's ok I guess. Except if you were interested in one of the first parameters. Oops, it's off-screen. Better luck next time I guess.

Rider (macOS)
Screenshot 2020-05-10 at 9 25 49 PM

Terser than Visual Studio for Mac (but without colors), Rider's compact tooltip is even harder to read or find anything interesting in it.
It also tends to follow you when navigating the code masking huge section of code requiring you to mash the Esc key constantly.
The IntelliSense-like dropmenu is not much helpful since it shows you the entire Earth of available things matching what you typed, regardless of the context.

Visual Studio (Windows)
image

Visual Studio on Windows fares a little better. At least the tooltip is compact and colored, even if it's not really useful. Intellisense still shows everything, but at least you can filter it (but you have to know it).
And even then, you'll still have a bunch of irrelevant propositions clogging up the menu before the really interesting things.

So overall, user experience in the various IDE goes from kinda bad to terrible.
Can't really blame the IDEs though, not everyday you see types with tens of optional parameters.

A few shortcomings with the current Fabulous DSL

  • Multiple value types for one property

The current Fabulous DSL has a few shortcomings, especially with properties that can have several types set to them (e.g. Xamarin.Forms.Image.Source could either be a path, a URI, a byte array, a stream, font, or any other ImageSource).
When DynamicViews was first introduced, this kind of properties were handled with an obj field, meaning we didn't have any compile-type check. It then changed to a discriminated union but it is somewhat not natural to use.

View.Image(source = ImagePath "path/to/image.png")
View.Label(fontSize = FontSize 10.)
  • Obsoleting attributes

Frameworks like Xamarin.Forms change over time, and what was the way to do one thing might be replaced by something else later. Usually there's a grace period where the framework marks the property/event as obsolete before being removed for good.

Fabulous can't handle that currently because we can't mark a single optional parameter as obsolete.
So if we happen to surface an obsoleted attribute, we will always have warnings that Fabulous is using an obsoleted code, even though we're not using that attribute.

@TimLariviere
Copy link
Member Author

TimLariviere commented May 10, 2020

Why not use Elmish.React / Feliz / Avalonia.FuncUI DSL instead?

DISCLAIMER: What's following are my own opinions. You might like things in those DSLs that I don't. I'm not attacking in any way your preference.

All those DSLs are great, and have pros to them.
There are readable, mostly concise, discoverable to various degrees, and can shield you from doing anything invalid (only applicable to FuncUI).

But they all lack things I like in Fabulous.

Elmish.React / Avalonia.FuncUI repeat keywords that are not necessary, which means more noise, which means less readability.

Elmish.React

let navBrand =
    Navbar.navbar [ Navbar.Color IsWhite ]
        [ Container.container [ ]
            [ Navbar.Brand.div [ ]
                [ Navbar.Item.a [ Navbar.Item.CustomClass "brand-text" ]
                      [ str "SAFE Admin" ] ]
              Navbar.menu [ ]
                  [ Navbar.Start.div [ ]
                      [ Navbar.Item.a [ ]
                            [ str "Home" ]
                        Navbar.Item.a [ ]
                            [ str "Orders" ]
                        Navbar.Item.a [ ]
                            [ str "Payments" ]
                        Navbar.Item.a [ ]
                            [ str "Exceptions" ] ] ] ] ]

What's happening there? You'll only find out if you put cognitive effort into it.

Avalonia.FuncUI

DockPanel.create [
    DockPanel.children [
        Button.create [
            Button.dock Dock.Bottom
            Button.onClick (fun _ -> dispatch Reset)
            Button.content "reset"
        ]
        (...)
    ]
]

That's better, but still. I know we're dealing with a DockPanel/Button, I don't really need to be reminded all the time.

Feliz

Bulma.footer [
    color.isDanger
    prop.style [ style.paddingBottom 48 ]
    prop.children [
        Bulma.columns [
            columns.isCentered
            prop.children [
                Bulma.column [
                    column.isNarrow
                    prop.children [
                        Bulma.image [
                            Html.img [
                                prop.src Assets.logoSvg
                                prop.style [ style.width 200 ]
                            ]
                        ]
                    ]
                ]
            ]
        ]
    ]
]

It's definitely better to read than Elmish.React. Though having prop leading all fields and having non-prop fields mixed into it makes it harder to read and discover.
Also most things return a IReactProperty meaning you can put any property to any control which hurts both discoverability and "making invalid states unrepresentable".

What does it mean to have a <div src="..." />?
Well, it's HTML, so I guess it's arguable. Less so in non-web frameworks.

Overall, they're all great but I still like Fabulous DSL better.

@vshapenko
Copy link

Fabulous as component looks very good. In our project, we use a (kind of) same technique, but we use main loop as a message passing engine. In your implementation they will be fully separated, which is great.
Concept of static view would help us to solve a problem of keeping same instances of view in lists, as i understand. For now, there is a bunch of problems with collection view:

  • wrong recycling algorithm
  • broken refviews in lists
    Hope these changes will help to solve most of these problems.
    Do i understand correctly, that in new paradigm there would not be so much control recreation, but just setting new attributes on it?

@TimLariviere
Copy link
Member Author

TimLariviere commented May 10, 2020

SwiftUI-like DSL

I must say I was quite in awe when first playing with SwiftUI from Apple. Their IDE-integration was so sweet.
The way they declare UI also made a lot of sense to me.

Basically, you have the following:

HStack(alignment = .leading) {
    Label("Hello world")
        .font(.title)
        .color(.grey)

    Image("path/to/image")
}

So to use a control, you have 4 distinct parts.

  • Its name HStack
  • Its most meaningful properties inside the parentheses (either mandatory or optional - default value is fine but there's a high chance I will change the columns of a Grid for example).
  • Optional child/children
  • Optional additional properties I might change like the color, the padding, etc.

The meaningful properties are not available as additional property methods.

Even though F# doesn't have the { } syntax for the children, it could be mimicked by [ ].
Here's a Todo List app using the SwiftUI-like DSL (use F# 4.7 open static classes):

ContentPage(
    StackLayout(spacing = 10.) [
        Label("Fabulous Todo List")
            .font(NamedSize.Title)
            .textColor(Color.Blue)
            .horizontalOptions(LayoutOptions.Center)
            .padding(top = 50., bottom = 20.)
            
        Grid(coldefs = [ Star; Absolute 50 ]) [
            Entry(
                text = model.EntryValue,
                textChanged = fun args -> dispatch (EntryTextChanged args.NewTextValue)
            )
                
            Button("Add", fun () -> dispatch AddTodo)
                .gridColumn(1)
        ]
        
        ListView(model.Todos) (fun item ->
            TextCell(item)
                .contextActions([
                    MenuItem("Delete", fun() -> dispatch (RemoveTodo item))
                ])
        )
    ]
)

The signal-to-noise ratio is pretty good.

You're also immediately aware of what's important and what's not with the most meaningful properties inside the parentheses.

If you use a Label, 100% of the time you'll want a text set to it. So make it mandatory.
Also why specify the field name? e.g. View.Label(text = "Hello World")

If you want a Button, you'll want to set a content and listen for the click so make it mandatory.
If you use a Grid, you'll most likely set columns and rows. Though you might not necessarily do that because the default values are fine. So optional is good but keep it close to the type.

Optional properties that are less-likely to be used are pushed as external methods.
This helps understand the intent, as well as allow overloaded signatures.
For instance, a font size could be an absolute value or a named size. This is possible to have both without having a DU.

// Current
Label(text = "Hello", fontSize = Named NamedSize.Title)
Label(text = "Hello", fontSize = FontSize 10.)

// Versus
Label("Hello").font(NamedSize.Title)
Label("Hello").font(10.)

This also allows us to provide helpers for things done often

// Current
Label(text = "Hello", padding = Thickness(10.))
Label(text = "Hello", padding = Thickness(0., 50., 0., 20.))
Label(text = "Hello", padding = Thickness(20., 30.))

// Versus
Label("Hello").padding(10.)
Label("Hello").padding(top = 50., bottom = 20.)
Label("Hello").padding(leftRight = 20., topBottom = 30.)
Label("Hello").padding() // Could even have a default padding values (e.g. 5px all around)

Controls can have different constructors depending how you want to use them.

// Templated ListView
let items = [ 1; 2; 3; 4 ]
ListView(items) (fun item ->  // knows its `int`
    TextCell(item.ToString())
)

// Flexible ListView
ListView() [
    for i = 0 to 10 ->
        TextCell(i.ToString())
]

Note: This is all super experimental, it is completely based on a "compilable" syntax, not actually ran. A lot of hurdles are to be expected before having a working sample.

@TimLariviere
Copy link
Member Author

It's infinitely better in the IDEs with this new syntax.
Screenshot 2020-05-10 at 11 56 12 PM
Screenshot 2020-05-10 at 11 56 51 PM

@TimLariviere
Copy link
Member Author

Do i understand correctly, that in new paradigm there would not be so much control recreation, but just setting new attributes on it?

@vshapenko No, static views don't mean you'll have the hand on the controls creation.
It's just that you'll be able to define your UI in the framework's language (ie XAML+Bindings in Xamarin.Forms) and still use it with MVU.

Fabulous will still create or destroy controls as it sees fit. (Fabulous won't dive inside a static view though, it'll just create or destroy the instance. What happens inside doesn't concern Fabulous.)

The various issues you mention need to be fixed before that.

@VistianOpenSource
Copy link

@TimLariviere If you are interested we have as yet an unreleased implementation in C# (sorry) but from a coding experience it does work similar to what you have discussed about. We don't have many actual concrete classes, instead we rely on a covariant interface specification together with extension methods using this interface so that code is correctly applying the correct properties and hopefully the IDE shows just the right extension methods. Whilst our base classes don't need a generator app, since they use reflection to build a map of what properties, events & methods are available (e.g. the user could do something like .With(v=> v.Orientation,Orientation.Vertical), we do however have a generator app which then plumbs into using these reflection maps that have been built up.

@TimLariviere
Copy link
Member Author

@VistianOpenSource That would be really interesting if you can share it.

We don't have many actual concrete classes, instead we rely on a covariant interface specification together with extension methods using this interface so that code is correctly applying the correct properties and hopefully the IDE shows just the right extension methods.

Yeah. Initially, I tried that but F# lacks support for covariance. So a IControl<Xamarin.Forms.Button> can't be used as a IControl<Xamarin.Forms.View>, even though Button inherits from View.
The only way I found around that is by having non-generic interfaces like IView, ICell, etc. Then those interfaces have to be implemented by concrete types.
Only then a DynamicView<Button> can be set into an IView list.

Whilst our base classes don't need a generator app, since they use reflection to build a map of what properties, events & methods are available (e.g. the user could do something like .With(v=> v.Orientation,Orientation.Vertical), we do however have a generator app which then plumbs into using these reflection maps that have been built up.

This is a fine solution, but in Fabulous we tend to change the input of some properties/events so they're easier to use in a functional context with MVU.

C# / XAML

<Button Command="{Binding ClickCommand}" />
public ICommand ClickCommand { get; } = new RelayCommand(ExecuteClickCommand, CanExecuteClickCommand);

public void ExecuteClickCommand() { ... }
public bool CanExecuteClickCommand() { ... }

Fabulous

Button(command = (fun () -> ...), commandCanExecute = true)

Also I know developers who will go to great length to avoid reflection in mobile apps (because of performance and linker).
Does it impact your apps?

@VistianOpenSource
Copy link

@TimLariviere On the input types sometimes being different, yes we do that as well. As well as people being able to write their own extension methods , the generator app is quite easy to add new things into since it uses handlebars to generate all of the code, meaning its very easy to drop in a different generator for just say certain properties, just add another template. On the reflection front, I've got a test app on Android that starts up in around 2 secs, its got recycler views, linear lists, edit views and text fields in it. Write regard to the reflection I used to use ReactiveUI so I took some of my learnings from that. With regard to linking the generator creates a small stub inside each extension method to ensure that it links correctly, so I can turn on full linking inside the app and it works fine, but it only pulls in what is required so keeps the size pretty small e.g. 8MB for the simple test app i referenced, or around 12,500KB for same solution with AOT turned on.

@zakaluka
Copy link

@TimLariviere do you think it makes sense (as part of this level of re-organization) to integrate something like FSharp.Data.Adaptive or https://github.com/dsyme/fabulous-adaptive into the project?

@TimLariviere
Copy link
Member Author

@zakaluka Yes, that's something I had in the back of my head. I'd like ViewElement to be the most basic type possible so you can extend it any way you want (DynamicViewElement, StaticViewElement, AdaptiveViewElement, etc.) without blocking every other ones.

So I don't really envision to integrate Fabulous-Adaptive back in this repository, but that's definitely a thing to take into account if we can let an additional Fabulous.XamarinForms.Adaptive NuGet package you could use in an existing Fabulous.XamarinForms app.

(I don't have much knowledge of what adaptive views would require though, so that might be wishful thinking).

@davidglassborow
Copy link

@TimLariviere Don mentioned the other day how intrusive the adaptive stuff can be SaturnFramework/Saturn#228 (comment)

@uxsoft
Copy link

uxsoft commented May 17, 2020

I think the "SwiftUI-like DSL" is definitely the way to go. Not just for Fabulous but also for Fable.React.

@charlesroddie
Copy link

  • The typing of StaticView looks good.
  • Running Fabulous as a component: very important as it would open up existing apps to experiment with Fabulous. Would allow us to consider using Fabulous.
  • It would be nice to just ignore the untyped Xamarin.Forms binding system: button.SetBinding is taking an ungeneric BindableProperty with obj at the core, and a string. Not sure whether MAUI improves the picture here.
  • I prefer setters to fluent syntax as fluent syntax suggests you have an object, and apply an instance property which returns an object of the same type, without being clear about whether it's the same one or a different one. I find "setting a property on an x" or "initializing an x" clearer then "taking an x and returning an x with a property set". Also fluent naming is problematic: the font property of a Label is not actually a font but a misnamed SetFontAndReturnSelf. Note that there are may be init-only properties in future: https://github.com/dotnet/csharplang/blob/master/proposals/init.md

@TimLariviere
Copy link
Member Author

TimLariviere commented May 28, 2020

@charlesroddie I'm still playing a lot with the new DSL and StaticView.
Right now, I have the following

/// This code right here is pure Xamarin.Forms. So could be XAML, C#, or F#.
/// Only the `DeleteButton` type (and constructor) is relevant for Fabulous
type DeleteButton() as target =
    inherit Xamarin.Forms.Button(BackgroundColor = Color.Red)
    do
        target.SetBinding(Xamarin.Forms.Button.TextProperty, Binding("Text"))
        target.SetBinding(Xamarin.Forms.Button.CommandProperty, Binding("Clicked"))
/// The dynamic UI with a reference to the DeleteButton above
/// Basically, there would be 4 main "helpers" representing
/// the 4 categories of controls: StaticView, StaticPage, StaticCell, StaticMenuItem
let view (model: Model) =
    StackLayout(children = [
        Label("Hello")
            .horizontalOptions(LayoutOptions.Center)

        StaticView(DeleteButton, fun dispatch -> {| Text = "Reset"; Clicked = Command.msg dispatch Reset |})
            .horizontalOptions(LayoutOptions.Center) // Since we use StaticView here, we can provide dynamic properties accessible on Xamarin.Forms.View
    ])

This allows to not have to declare a wrapper type before using an existing control, while still benefiting from the (partial) possibility of setting some dynamic properties.

It would be nice to just ignore the untyped Xamarin.Forms binding system: button.SetBinding is taking an ungeneric BindableProperty with obj at the core, and a string. Not sure whether MAUI improves the picture here.

If you're talking about the MyStatefulButton example I gave previously, the Create method is pure Xamarin.Forms code, it could be XAML or anything.
Fabulous isn't directly involved in this.

I prefer setters to fluent syntax as fluent syntax suggests you have an object, and apply an instance property which returns an object of the same type, without being clear about whether it's the same one or a different one. I find "setting a property on an x" or "initializing an x" clearer then "taking an x and returning an x with a property set". Also fluent naming is problematic: the font property of a Label is not actually a font but a misnamed SetFontAndReturnSelf.

Yes, I agree with you on some points. The fluent syntax feels a little wrong to implement, with the passing of an object (struct actually) around.

But I see this fluent syntax more as a descriptive language than proper F# code.
So the ambiguity with the behaviors of object is not that important in my opinion.

From the users point of view, it will just work while providing a nice syntax.

I think users don't really care about the exact thing Fabulous will do behind the scenes.
So font conveys the idea you're associating a font to the control you're describing. SetFontAndReturnSelf, even if more correct strictly speaking, feels clunky and counterproductive.

@VistianOpenSource
Copy link

@TimLariviere I said I would let you know when we were putting our code up on Github, well we have started the process of moving our code based UI library over to Github. I’ve put some notes regarding functionality up there, the code will start appearing there this week. https://github.com/VistianOpenSource/Birch

@7sharp9
Copy link

7sharp9 commented Jun 16, 2020

Although a SwiftUI DSL is indeed warranted, would the SwiftUI like DSL directly use the SwiftUI API its entirely different to usual Xamarin.iOS API calls.

@TimLariviere
Copy link
Member Author

@7sharp9 No, it's more like taking inspiration from the SwiftUI syntax (declarative fluent builders).

SwiftUI is quite different in its usage than Fabulous, it relies a lot on bindings. Fabulous will still use MVU.
Also SwiftUI is currently only available on recent Apple OSes.

And there won't be one "Fabulous DSL".
The DSL is really tied to the backend you want to use. Fabulous.XamarinForms will provide a DSL tailored for XF, Fabulous.MAUI will provide a DSL for MAUI, etc.
So we can't depend on 3rd party APIs.

@7sharp9
Copy link

7sharp9 commented Jun 16, 2020

It would just be good to consume the native API's and controls thats all, without wrapping too much. All iOS devices work at 60fps minimum, things like the iPad will be 120fps so you really want to be using the metal renders like SwiftUI.

@VistianOpenSource
Copy link

Thing is there isn't that 'much' wrapping. In effect its just applying the appropriate values to properties on whatever your target views/controls are.

@TimLariviere
Copy link
Member Author

@7sharp9 For that, you could write a Fabulous.SwiftUI by taking a dependency on the Fabulous NuGet package and map directly to SwiftUI (you'll still need .NET bindings though). Fabulous will only ask for an instance of ViewElement at the end of the view function, which is basically a dictionary of properties/values.

Each time the model changes, Fabulous will provide you the previous and current ViewElements and let you apply the delta how you want.

@TimLariviere
Copy link
Member Author

Following an old discussion on Gitter and a recent discussion on MAUI, I've realized we can include Application with its resources and menus in the view function instead of doing things like #696 (comment)

It works quite well.

let view (model: Model) =
    Application(
       ContentPage(
           (...)
       )
    ).menu(
        MainMenu([
            Menu([
                MenuItem("Quit CounterApp", CloseApp)
                    .accelerator("cmd+q")
            ])
            Menu("Actions", [
                MenuItem("Increment", Increment)
                MenuItem("Decrement", Decrement)
            ])
        ])
    )

let runnerDefinition = Program.AsApplication.useCmdMsg init update view mapCmdMsgToCmd

type CounterApp () as app = 
    inherit Application ()

    let _ =
        App.runnerDefinition
        |> Program.withConsoleTrace
        |> Program.AsApplication.run app

@7sharp9
Copy link

7sharp9 commented Jun 17, 2020

@TimLariviere I know Fabulous is what it is, it would just be good to leverage the whole of SwiftUI, hot reloading, view preview etc.

@uxsoft
Copy link

uxsoft commented Jul 1, 2020

@TimLariviere any way we can help you with the new DSL? I'm actually quite looking forward to it and would be willing to help to speed it up.

@TimLariviere
Copy link
Member Author

TimLariviere commented Jul 2, 2020

@uxsoft Thanks for your proposition. I currently need feedback on the syntax and architecture of the PoC I got working here : https://github.com/TimLariviere/Fabulous/tree/play

So if you want, you could test it.
Write an app or port an existing one (like FabulousContacts?) to validate the concept and architecture.
The CounterApp sample should give you a good look at the new syntax.

If you notice anything or want to discuss about the PoC, you can open an issue on my fork repository.

(Note: if you're on macOS, you'll need to dotnet build the shared CounterApp project and then build (not rebuild!) the iOS/Android projects before you can run them on a device/emulator. That's because it's using F# 5.0 which is not fully supported by MSBuild on macOS. If you're on Windows, Visual Studio should handle it fine by default)

@shirshov
Copy link

shirshov commented Jul 6, 2020

Sorry, I'm late to this. I'm working on an MVU library for Xamarin.Forms that took React + Redux approach: https://github.com/shirshov/laconic. Meaning virtual views, diffing, and touching actual views for changed values only. Maybe you'll find some bits useful.

@TimLariviere wrote:

Attributes store a list of Attribute with their new values. The Attribute type contains the function required to apply (or reset) the new value to the real control if the diffing algorithm finds it should be updated.
This logic is currently missing.

That's pretty much what I did, using BindableProperty fields as keys, and using SetValue and ClearValue for patching. There's no Reflection, no data binding, no attached properties.

Views created with Laconic and views created with XAML can co-exist; Laconic views can be hosted inside XAML views (the inverse is possible too, but ugly).

More of C# note, but, @TimLariviere wrote:

If you use a Label, 100% of the time you'll want a text set to it. So make it mandatory.
Also why specify the field name? e.g. View.Label(text = "Hello World")

I thought about it and abandoned the idea... Sometimes it's hard to decide which attributes are the defining ones for a control. Take BoxView for example: quite often it's used as an invisible spacer, where its presence is all that necessary, no need to set any attribute. Other times it's used as a horizontal divider line, requiring only HeightRequest being set but not WidthRequest. But when it's used as a vertical line it's the opposite. So, in Laconic I just use C# property setters. IntelliSense works well.

@TimLariviere
Copy link
Member Author

I thought about it and abandoned the idea... Sometimes it's hard to decide which attributes are the defining ones for a control.

@shirshov I agree that controls don't all have a clear use case and defining custom constructors for them is not easy.
The way I understand SwiftUI handles your example is by providing semantic controls (e.g. Spacer) rather than technical controls (e.g. BoxView).
It's a valid option I think. It does simplify the reading and understanding of the UI code, even though the internal implementation is a little bit more obscure to the users and you're adding new things on top of the underlying framework (Xamarin.Forms).

Also, having specific constructors and no constructor are not mutually exclusive in my opinion.
You can have defining attributes for controls that have a clear use case (like Label), but none for controls that don't have a clear use case.

@shirshov
Copy link

shirshov commented Jul 6, 2020

@TimLariviere

Have you considered requiring a key for views added to Children? It can speed up diffing tremendously. React does it, here's an explanation why: https://reactjs.org/docs/reconciliation.html#recursing-on-children
(Laconic also requires keys)

@VistianOpenSource
Copy link

As @TimLariviere says, and from own experience, having those properties which more than likely are going to be used not in the constructor just means you end up doing more typing. There will be many controls/views for which it doesn't make sense but for things like labels and text edit controls it does cut down on type amount you type.

@VistianOpenSource
Copy link

@shirshov @TimLariviere on the diffing front either the Myer or the Heckel algoritm for performance of diffs. This does accommodate quite easily issues of inserts / deletes and generally does produce an optimal update for the user interface. If you want implementations of either just let me know,I've got both of them implemented in C#.

@VistianOpenSource
Copy link

FYI both iOS and Android have 'diffable' abilities to update uicollectionview/recyclerview. Androids implementation uses Myers algorithm, not use what iOS uses. Hashing is very important for these implementations for them to work effectively/

@TimLariviere
Copy link
Member Author

TimLariviere commented Jul 7, 2020

@shirshov Yes, we introduced a key property in v0.55 (currently in preview) similar to React. Though it is not required.
Maybe we need to make it required for collection controls like ListView / CollectionView.

@VistianOpenSource Thanks for the algorithm recommendations.
You know the difference between Myers and Heckel? Which one is best in which cases?
We implemented a custom diffing algorithm for list in v0.55 preview, though we have some issues with it.
If you could share your implementations in #772, that would be greatly appreciated.
Thanks.

@deviousasti
Copy link

Even though F# doesn't have the { } syntax for the children, it could be mimicked by [ ].
Here's a Todo List app using the SwiftUI-like DSL (use F# 4.7 open static classes):

We now have implicit yields in 4.7, so you could technically still recreate the syntax with a computation builder.

let items = seq {
    "Implicit"
    "Yields"
    "Are"
    "Great"
}

One advantage is that you can choose not to emit a child, unlike with a list.

let items = seq {
    "Implicit"
    "Yields"
    "Are"
    if false then
        "Not"
    "Great"
    } 

@Happypig375
Copy link

@deviousasti With F# 4.7, lists can do the same.

let items = [
    "Implicit"
    "Yields"
    "Are"
    if false then
        "Not"
    "Great"
]

@TimLariviere
Copy link
Member Author

@deviousasti I thought of using computation expressions, but I find them inconvenient to write.

Also I'd like for the DSL to be as lightweight as possible, both in allocations and dll size in release mode.
And I'm not sure how CEs fare on that side.

Technically, a seq is requested for the children so you could pass a seq instead of a list, you'll just have an additional keyword.

Grid() (seq {
   ...
})

But for practicality, contrary to my example here, I moved the children list has a parameter of Grid()

Grid([
   ...
])

// or if multiple params

Grid(rowdefs = [Auto; Star], children = [
   ...
])

This is better when we want to add additional properties

Grid(rowdefs = [Auto; Star], children = [
   ...
]).alignment(LayoutOptions.Center)

// compared to

(Grid(rowdefs = [Auto; Star]) [
   ...
]).alignment(LayoutOptions.Center)

@deviousasti
Copy link

Right. What was I thinking. I've used that very feature in Elmish. d'oh
But it does present an opportunity to pass along an ambient context to help it with designer synchronization.

@chkn
Copy link

chkn commented Nov 1, 2020

I'm using computation expressions for my SwiftUI binding: chkn/Xamarin.SwiftUI#37

I find they are quite lightweight- as you can see in the disassembly on the PR, the compiler is very good at inlining them in optimized (release) builds.

The only thing I find (mildly) annoying is that, as previously mentioned, you need to wrap the whole expression in parenthesis to add fluent-style properties:

(Button(fun () -> printfn "clicked") {
    Text("1")
    Text("2")
    Text("3")
})
 .Background(Color.Red)
 .Font(...)
// etc

@TimLariviere
Copy link
Member Author

@chkn Pretty impressive work. Thanks for sharing this!

The only thing I find (mildly) annoying is that, as previously mentioned, you need to wrap the whole expression in parenthesis to add fluent-style properties

Yeah. I guess that's something we could get used to and no longer think about after a while.

Maybe another way to handle this would be to abuse the fluent builders, and do something like this:

Button(fun () -> printfn "clicked")
    .Background(Color.Red)
    .Font(...)
{
    Text("1")
    Text("2")
    Text("3")
}

Weirdly, this feels like XAML in the structure. 😄

@chkn
Copy link

chkn commented Nov 5, 2020

@TimLariviere Thanks!

Maybe another way to handle this would be to abuse the fluent builders, and do something like this:

Button(fun () -> printfn "clicked")
    .Background(Color.Red)
    .Font(...)
{
    Text("1")
    Text("2")
    Text("3")
}

Interesting idea! I do think it's more aesthetically pleasing than the parenthetical version, but I'd hesitate to adopt that syntax for a couple reasons:

  1. Unlike XAML platforms, SwiftUI views are immutable. So it's not actually setting properties on Button, but rather wrapping it in lightweight views that set the font, background color, etc on the environment that applies to all child views

  2. The current syntax is close enough to the Swift version that we can hopefully lean on Apple's SwiftUI docs. Maybe this won't pan out, but I can dream :)

@uxsoft
Copy link

uxsoft commented Dec 25, 2020

I implemented Swift-like syntax in another project using just plain old classes and an index accessor that takes a list:

Button(fun () -> printfn "clicked")
    .Background(Color.Red)
    .Font(...)
    .[[
        Text("1")
        Text("2")
        Text("3")
     ]]

It's simple and it works, but it's not as elegant as the fluent builders.

@Dolfik1
Copy link

Dolfik1 commented Jan 2, 2021

Current Fabulous DSL is very pretty. You are right about IDE autocompletion, but I think this is IDE's problem, not Fabulous. We can try to create issue for Rider to add better support for Fabulous like syntax. Their F# team is always open to discuss.

@TimLariviere
Copy link
Member Author

TimLariviere commented Jan 8, 2021

@Dolfik1 Even though Rider could accommodate us, it's not the primary IDE most people use and it's a paid one.

I think the main problem with IntelliSense is that Fabulous only has 1 type ViewElement, meaning all methods and properties are stored on it and the IDE can't really help.
Just returning a typed ViewElement will greatly improve this (eg ButtonViewElement, EntryViewElement).

But after that, like I explained in one of the posts at the top, the current Fabulous DSL is not really flexible and does not allow to easily extend existing controls, since all properties and events are optional constructor parameters.
So if one need to simply extend an existing control like with a custom Button, he has to duplicate all the constructor parameters.

The new proposed DSL will be more flexible with this. Especially with attached properties that are currently quite hard to support correctly in Fabulous today.

Of course, I don't plan to immediately discard the old DSL.
The new DSL will be introduced in v2.0 along the current one.
Only when v3.0 will be in the works that I might consider to continue supporting the existing DSL or not.

@TimLariviere
Copy link
Member Author

Work on Fabulous v2 has started in https://github.com/TimLariviere/Fabulous-new and will include most experiments listed here.
If you're curious what will be part of v2, you can read: https://github.com/TimLariviere/Fabulous-new/blob/main/docs/goals-of-v2.md

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
t/discussion A subject is being debated
Projects
None yet
Development

No branches or pull requests