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

[Statically-Typed VM] - Missing proper documentation (samples/examples/best practices..) #606

Open
YkTru opened this issue May 31, 2024 · 21 comments

Comments

@YkTru
Copy link

YkTru commented May 31, 2024

[Context] Hi, maybe it's all clear to most of you since I seem to be one of the only ones asking these kind of questions (I've been trying to learn this library since last November, and F# about 10 months ago, so it's all still “new” to me),

[Problem] but honestly, I'm having a really hard time figuring out how to use the statically type VM, since there are almost no samples/examples of how to “convert” the untyped bindings in all the other samples, and I could really use some help (I'm a little desperate right now, honestly).

Almost all the samples (as far as I know) use untyped bindings with the exception of this one), and a very limited, short example in ElmishWPF documentation/bindings reference.


I really like the XAML experience I get from the StaticVm binding helpers I'm able to use, but honestly I'm spending hours trying to “convert” (i.e. end up with horrible “signature hacks”) samples like this with no success:

"Entities" |> Binding.subModelSeq
    ( fun m -> m.Entities
    , fun e -> e.Id
    , fun () -> [
      "Name" |> Binding.oneWay (fun (_, e) -> e.Name)
      "IsSelected" |> Binding.twoWay ((fun (_, e) -> e.IsSelected), (fun isSelected (_, e) -> SetIsSelected (e.Id, isSelected)))
      "SelectedLabel" |> Binding.oneWay (fun (_, e) -> if e.IsSelected then " - SELECTED" else "")
    ] )

[Questions] I must say that I think I understand untyped "normal" ElmishWPF bindings well, since I'm able to use option more easily and subModelSeq, but how to convert these to StaticVM bindings? Should I use a SubModelSeqT (which doesn't exist), or should it be SubModelSeqKeyedT?

How do you also handle a field in the type of this collection that is an option (e.g. “Middle Name”) using StaticVM bindings? (@marner2 you kindly tried to help me in this post, but honnestly I tried many hours and never was able to deal with an option using StaticVM bindings, I still dont get why not all "normal bindings" helpers (which seems to me to cover many more cases) don't have a clear StaticVM binding version

@marner2 I know you advise using composition instead of writing helpers for each specific case, but is there an example/sample or documentation showing properly do this in various (common) specific cases?


..and even more daunting to me, this one (recursive bindings):

let rec subtreeBindings () : Binding<Model * SelfWithParent<RoseTree<Identifiable<Counter>>>, InOutMsg<RoseTreeMsg<Guid, SubtreeMsg>, SubtreeOutMsg>> list =
    let counterBindings =
      Counter.bindings ()
      |> Bindings.mapModel (fun (_, { Self = s }) -> s.Data.Value)
      |> Bindings.mapMsg (CounterMsg >> LeafMsg)

    let inMsgBindings =
      [ "CounterIdText" |> Binding.oneWay(fun (_, { Self = s }) -> s.Data.Id)
        "AddChild" |> Binding.cmd(AddChild |> LeafMsg)
        "GlobalState" |> Binding.oneWay(fun (m, _) -> m.SomeGlobalState)
        "ChildCounters"
          |> Binding.subModelSeq (subtreeBindings, (fun (_, { Self = c }) -> c.Data.Id))
          |> Binding.mapModel (fun (m, { Self = p }) -> p.Children |> Seq.map (fun c -> m, { Self = c; Parent = p }))
          |> Binding.mapMsg (fun (cId, inOutMsg) ->
            match inOutMsg with
            | InMsg msg -> (cId, msg) |> BranchMsg
            | OutMsg msg -> cId |> mapOutMsg msg |> LeafMsg)
      ] @ counterBindings
      |> Bindings.mapMsg InMsg

    let outMsgBindings =
      [ "Remove" |> Binding.cmd OutRemove
        "MoveUp" |> Binding.cmdIf moveUpMsg
        "MoveDown" |> Binding.cmdIf moveDownMsg
      ] |> Bindings.mapMsg OutMsg

    outMsgBindings @ inMsgBindings 

[Request for help] Could you share with me the code of what would be a correct "conversion" of these 2 samples (only the bindings part + an optional “MiddleName” field under the field “Name” in the first one) into StaticVM bindings? I promise that once everything is figured out, I'll make a cheatsheet/doc showing the equivalent for each of the “normal” untyped ElmishWPF bindings, and StaticVM bindings (and I'm sure anyone coming from MVVM C# will really really really appreciate it). Thank you very much, from a lost but devoted soul🥲

( @xperiandri I know you're using Elmish.Uno, but please feel free to share your code/insights if you've figured out how to convert these samples (and all untyped "normal" bindings <-> staticVM bindings), and what new “T bindings” you might have added to make the job easier)

@YkTru YkTru changed the title [Statically-Typed VM] - Missing samples/examples/best practices [Statically-Typed VM] - Missing proper documentation (samples/examples/best practices..) May 31, 2024
@marner2
Copy link
Collaborator

marner2 commented Jun 1, 2024

@YkTru There are two issues here that seem to be complicating things.

  1. Moving from Dynamic View Models to Static View Models.
  2. Moving away from the premade helpers (for example, Binding.cmd, Binding.subModelSeq, etc) and towards inlining the composition of the underlying functions in the modules (for example, Binding.Cmd.id >> Binding.mapModel (fun m -> m.Foo), Binding.SubModelSeqKeyed.id, etc).
    • The helpers I'm wanting to deprecate always start with a lowercase letter, and are always replaced by their implementation in Binding.fs:570-4096. Once they are replaced by their implementation there (essentially blindly inline the function, watching the arguments carefully), you will end up with a binding that starts with Binding.<Module>.bar. This is what has a very close proximity to Binding.<ModuleT>.bar.

There isn't an easy rote 1-1 static vs dynamic drop-in replacement for each and every function. There is, however, a fairly rote process that you can follow in order to convert everything from dynamic to static.

Example

I'll go over the conversion process that we used in our fairly large project here:

In your first example above, you'll need to find the overload of Binding.subModelSeq that was being used. In that case, it looks like it was the overload at Binding.fs:2,678. This one has an implementation of the following:

  static member subModelSeq
      (getSubModels: 'model -> #seq<'subModel>,
       getId: 'subModel -> 'id,
       bindings: unit -> Binding<'model * 'subModel, 'msg> list)
      : string -> Binding<'model, 'msg> =
    Binding.SubModelSeqKeyed.create
      (fun args -> DynamicViewModel<'model * 'subModel, 'msg>(args, bindings ()))
      IViewModel.updateModel
      (snd >> getId)
      (IViewModel.currentModel >> snd >> getId)
    >> Binding.mapModel (fun m -> getSubModels m |> Seq.map (fun sub -> (m, sub)))
    >> Binding.mapMsg snd

You can see from there that this overload uses the keyed version of SubModelSeq, so therefore we need to use Binding.SubModelSeqKeyedT.id.

So let's go through each of the arguments that we started with (getSubModels, getId, and bindings).

  • getId already exists on Binding.SubModelSeqKeyedT.id, which we can use directly. We'll ignore the tuple handling because we're not using it anywhere.
  • getSubModels is called inside Binding.mapModel getSubModels, so that is just a direct composition. Again we'll ignore the tuple handling because we're not using it anywhere.
  • bindings is the old-style binding list. That means we need to replace whatever goes into here with a new static view model that has the 3 bindings inside of it. Here is a rote way of doing it:
    1. Create the new view model type
    2. Copy all of the old style bindings into it (quotes and all)
    3. Remove the quotes from around the binding names
    4. Add member _. in front of the member names
    5. Replace the |> immediately after the member name with base.Get()
    6. Surround the entirety of the binding with parentheses
    7. Handle the edge cases
      • Extract TwoWay bindings into a let at the top of the type and reuse them in the get,set property accessors.
      • Use set-only property syntax if you have a one way to source binding.
      • Remove all _, 's since we aren't using the tuple (as mentioned above in the getId and getSubModels arguments).
fun () -> [
      "Name" |> Binding.oneWay (fun (_, e) -> e.Name)
      "IsSelected" |> Binding.twoWay ((fun (_, e) -> e.IsSelected), (fun isSelected (_, e) -> SetIsSelected (e.Id, isSelected)))
      "SelectedLabel" |> Binding.oneWay (fun (_, e) -> if e.IsSelected then " - SELECTED" else "")
    ]

-> transforms to ->

type EntityViewModel(args) =
  inherit ViewModelBase<Entity, Msg>(args)
  
  let isSelectedTwoWayBinding = Binding.twoWay ((fun (e) -> e.IsSelected), (fun isSelected (e) -> SetIsSelected (e.Id, isSelected)))
  
  member _.Name = base.Get() (Binding.oneWay (fun (e) -> e.Name))
  member _.IsSelected
    with get() = base.Get() isSelectedTwoWayBinding
    and set(v) = base.Set(v) isSelectedTwoWayBinding
  member _.SelectedLabel = base.Get() (Binding.oneWay (fun (e) -> if e.IsSelected then " - SELECTED" else ""))

Now that we have an EntityViewModel, we can use its constructor as the createVm argument of Binding.SubModelSeqKeyedT.id. And finally, since I don't want to convert the whole object graph in my entire application at once, we can use Binding.boxT at the end which boxes the view model into obj, making it work seamlessly with the bindings list.

This leaves us with (assuming I didn't miss any syntax):

"Entities" |> Binding.SubModelSeqKeyedT.id EntityViewModel (fun e -> e.Id)
           |> Binding.mapModel (fun m -> m.Entities)
           |> Binding.boxT

Actually I see from putting this into Visual Studio that I also need to get rid of the integer from the msg, since that is different between them as well. So simply adding |> Binding.mapMsg (fun (_i,m) -> m) will fix that issue as well.

General Process

Generally you want to convert the simple subModels and subModelSeqs first (which always have a binding list than can be converted into a view model). Once you have a few of those, you can go in and change the basic bindings one at a time, experimenting as you go. Also you can remove the Binding.boxT's once the parent type of the SubModelT or SubModelSeqKeyedT bindings are also static.

In the recursive view model case, there's nothing special that needs to be done. Just make sure the underlying model is recursive with a list of itself, then make the view model recursive with a SubModelSeqKeyedT binding that uses the underlying model list and the view model constructor again.

@xperiandri
Copy link

2. Moving away from the premade helpers (for example, Binding.cmd, Binding.subModelSeq, etc) and towards inlining the composition of the underlying functions in the modules

I don't think that it is a good idea. It adds too much verbosity to the code. I use that for non-trivial modifications only

@YkTru
Copy link
Author

YkTru commented Jun 2, 2024

@marner2 Thanks a lot for your explanations, it clarifies a lot of things. I will definitely try this week. I personnally think such informations should be added to the documentation, should a PR be created?

@xperiandri What would recommend instead concretely? Would you provide some samples please? Thank you

@marner2
Copy link
Collaborator

marner2 commented Jun 3, 2024

@xperiandri Do you think it would help the experience to change Binding.SubModelT etc to BindingT.SubModel? This might improve the auto-complete situation.

I'm a bit unsure about committing to 3,500 lines of helpers that all overload each other in weird, non-functional (as in FP) ways. However, looking at the difference:

"Entities" |> Binding.SubModelSeqKeyedT.id EntityViewModel (fun e -> e.Id)
           |> Binding.mapModel (fun m -> m.Entities)

vs

"Entities" |> Binding.subModelSeqT ( fun m -> m.Entities, fun e -> e.Id, EntityViewModel )

I can definitely see the argument about verbosity. Also it probably makes the most sense to specify the model mapping before the binding type and id function, not after.

I'll look into improving the user experience for composing.

@xperiandri
Copy link

xperiandri commented Jun 3, 2024

Do you think it would help the experience to change Binding.SubModelT etc to BindingT.SubModel? This might improve the auto-complete situation.

Yes, I propose to backport my changes to Elmish.WPF
https://github.com/eCierge/Elmish.Uno/blob/eCierge/src/Elmish.Uno/BindingT.fs

I can definitely see the argument about verbosity. Also it probably makes the most sense to specify the model mapping before the binding type and id function, not after.

And with Fantomas it will be 3 lines minimum, while the old approach allows one line

@xperiandri
Copy link

Actually my repo has some changes to the Binding module too to reduce even more boilerplate regarding the requirement of 'model -> 'message -> 'something to have overrides with 'message -> something and put Cmd DU case right away

@xperiandri
Copy link

This is my addition that is not present in the repo

namespace eCierge.Elmish

open System

[<AutoOpen>]
module Dispatching =

    open Elmish
    open R3

    let asDispatchWrapper<'msg> (configure : Observable<'msg> -> Observable<'msg>) (dispatch : Dispatch<'msg>) : Dispatch<'msg> =
        let subject = new Subject<_> ()
        (subject |> configure).Subscribe dispatch |> ignore
        fun msg -> async.Return (subject.OnNext msg) |> Async.Start

    let throttle<'msg> timespan =
        /// Ignores elements from an observable sequence which are followed by another element within a specified relative time duration.
        let throttle (dueTime : TimeSpan) (source : Observable<'Source>) : Observable<'Source> = source.ThrottleLast (dueTime)
        throttle timespan |> asDispatchWrapper<'msg>

    [<Literal>]
    let DefaultThrottleTimeout = 500.0

    [<Literal>]
    let HalfThrottleTimeout = 250.0

open Elmish.Uno

module Binding =

    open Validus

    /// <summary>
    ///  Adds validation to the given binding using <c>INotifyDataErrorInfo</c>.
    /// </summary>
    /// <param name="validate">Returns the errors associated with the given model.</param>
    /// <param name="binding">The binding to which validation is added.</param>
    let addValidusValidation
        (map : 'model -> 't)
        (validate : 't -> ValidationResult<'t>)
        (binding : Binding<'model, 'msg, 't>)
        : Binding<'model, 'msg, 't> =
        binding
        |> Binding.addValidation (fun model ->
            match model |> map |> validate with
            | Ok _ -> []
            | Error e -> e |> ValidationErrors.toList
        )


open System.Runtime.InteropServices

[<AbstractClass; Sealed>]
type BindingT private () =

    static member twoWayThrottle
        (get : 'model -> 'a, setWithModel : 'a -> 'model -> 'msg, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWay (get, setWithModel = setWithModel)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayThrottle
        (
            get : 'model -> 'a,
            setWithModel : 'a -> 'model -> 'msg,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayThrottle (get, setWithModel, (TimeSpan.FromMilliseconds timeout))

    static member twoWayThrottle (get : 'model -> 'a, set : 'a -> 'msg, timespan) : string -> Binding<'model, 'msg, 'a> =
        BindingT.twoWay (get, set)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayThrottle
        (get : 'model -> 'a, set : 'a -> 'msg, [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayThrottle (get, set, (TimeSpan.FromMilliseconds timeout))

    static member twoWayOptThrottle
        (getOpt : 'model -> 'a option, setWithModel : 'a option -> 'model -> 'msg, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOpt (getOpt, setWithModel = setWithModel)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayOptThrottle
        (
            getOpt : 'model -> 'a option,
            setWithModel : 'a option -> 'model -> 'msg,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptThrottle (getOpt, setWithModel, (TimeSpan.FromMilliseconds timeout))

    static member twoWayOptThrottle
        (get : 'model -> 'a option, set : 'a option -> 'msg, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOpt (get, set)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayOptThrottle
        (
            get : 'model -> 'a option,
            set : 'a option -> 'msg,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptThrottle (get, set, (TimeSpan.FromMilliseconds timeout))

    static member twoWayOptThrottle
        (getVOpt : 'model -> 'a voption, setWithModel : 'a voption -> 'model -> 'msg, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOpt (getVOpt = getVOpt, setWithModel = setWithModel)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayOptThrottle
        (
            getVOpt : 'model -> 'a voption,
            setWithModel : 'a voption -> 'model -> 'msg,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptThrottle (getVOpt, setWithModel, (TimeSpan.FromMilliseconds timeout))

    static member twoWayOptThrottle
        (get : 'model -> 'a voption, set : 'a voption -> 'msg, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOpt (get, set)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayOptThrottle
        (
            get : 'model -> 'a voption,
            set : 'a voption -> 'msg,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptThrottle (get, set, (TimeSpan.FromMilliseconds timeout))

    static member twoWayValidateThrottle
        (get : 'model -> 'a, setWithModel : 'a -> 'model -> 'msg, validate : 'model -> string list, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayValidate (get = get, setWithModel = setWithModel, validate = validate)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayValidateThrottle
        (
            get : 'model -> 'a,
            setWithModel : 'a -> 'model -> 'msg,
            validate : 'model -> string list,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayValidateThrottle (get, setWithModel, validate, (TimeSpan.FromMilliseconds timeout))

    static member twoWayValidateThrottle
        (get : 'model -> 'a, set : 'a -> 'msg, validate : 'model -> string list, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayValidate (get, set, validate)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayValidateThrottle
        (
            get : 'model -> 'a,
            set : 'a -> 'msg,
            validate : 'model -> string list,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayValidateThrottle (get, set, validate, (TimeSpan.FromMilliseconds timeout))

    static member twoWayValidateThrottle
        (get : 'model -> 'a, setWithModel : 'a -> 'model -> 'msg, validate : 'model -> string voption, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayValidate (get, setWithModel = setWithModel, validate = validate)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayValidateThrottle
        (
            get : 'model -> 'a,
            setWithModel : 'a -> 'model -> 'msg,
            validate : 'model -> string voption,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayValidateThrottle (get, setWithModel, validate, (TimeSpan.FromMilliseconds timeout))

    static member twoWayValidateThrottle
        (get : 'model -> 'a, set : 'a -> 'msg, validate : 'model -> string voption, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayValidate (get, set, validate)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayValidateThrottle
        (
            get : 'model -> 'a,
            set : 'a -> 'msg,
            validate : 'model -> string voption,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayValidateThrottle (get, set, validate, (TimeSpan.FromMilliseconds timeout))

    static member twoWayValidateThrottle
        (get : 'model -> 'a, setWithModel : 'a -> 'model -> 'msg, validate : 'model -> string option, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayValidate (get, setWithModel = setWithModel, validate = validate)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayValidateThrottle
        (
            get : 'model -> 'a,
            setWithModel : 'a -> 'model -> 'msg,
            validate : 'model -> string option,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayValidateThrottle (get, setWithModel, validate, (TimeSpan.FromMilliseconds timeout))

    static member twoWayValidateThrottle
        (get : 'model -> 'a, set : 'a -> 'msg, validate : 'model -> string option, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayValidate (get, set, validate)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayValidateThrottle
        (
            get : 'model -> 'a,
            set : 'a -> 'msg,
            validate : 'model -> string option,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayValidateThrottle (get, set, validate, (TimeSpan.FromMilliseconds timeout))

    static member twoWayValidateThrottle
        (get : 'model -> 'a, setWithModel : 'a -> 'model -> 'msg, validate : 'model -> Result<'ignored, string>, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayValidate (get, setWithModel = setWithModel, validate = validate)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayValidateThrottle
        (
            get : 'model -> 'a,
            setWithModel : 'a -> 'model -> 'msg,
            validate : 'model -> Result<'ignored, string>,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayValidateThrottle (get, setWithModel, validate, (TimeSpan.FromMilliseconds timeout))

    static member twoWayValidateThrottle
        (get : 'model -> 'a, set : 'a -> 'msg, validate : 'model -> Result<'ignored, string>, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayValidate (get, set, validate)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayValidateThrottle
        (
            get : 'model -> 'a,
            set : 'a -> 'msg,
            validate : 'model -> Result<'ignored, string>,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayValidateThrottle (get, set, validate, (TimeSpan.FromMilliseconds timeout))

    static member twoWayOptValidateThrottle
        (getVOpt : 'model -> 'a voption, setWithModel : 'a voption -> 'model -> 'msg, validate : 'model -> string list, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidate (getVOpt = getVOpt, setWithModel = setWithModel, validate = validate)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayOptValidateThrottle
        (
            getVOpt : 'model -> 'a voption,
            setWithModel : 'a voption -> 'model -> 'msg,
            validate : 'model -> string list,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidateThrottle (getVOpt, setWithModel, validate, (TimeSpan.FromMilliseconds timeout))

    static member twoWayOptValidateThrottle
        (get : 'model -> 'a voption, set : 'a voption -> 'msg, validate : 'model -> string list, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidate (get, set, validate)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayOptValidateThrottle
        (
            get : 'model -> 'a voption,
            set : 'a voption -> 'msg,
            validate : 'model -> string list,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidateThrottle (get, set, validate, (TimeSpan.FromMilliseconds timeout))

    static member twoWayOptValidateThrottle
        (get : 'model -> 'a voption, setWithModel : 'a voption -> 'model -> 'msg, validate : 'model -> string voption, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidate (get, setWithModel = setWithModel, validate = validate)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayOptValidateThrottle
        (
            get : 'model -> 'a voption,
            setWithModel : 'a voption -> 'model -> 'msg,
            validate : 'model -> string voption,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidateThrottle (get, setWithModel, validate, (TimeSpan.FromMilliseconds timeout))

    static member twoWayOptValidateThrottle
        (get : 'model -> 'a voption, set : 'a voption -> 'msg, validate : 'model -> string voption, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidate (get, set, validate)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayOptValidateThrottle
        (
            get : 'model -> 'a voption,
            set : 'a voption -> 'msg,
            validate : 'model -> string voption,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidateThrottle (get, set, validate, (TimeSpan.FromMilliseconds timeout))

    static member twoWayOptValidateThrottle
        (get : 'model -> 'a voption, setWithModel : 'a voption -> 'model -> 'msg, validate : 'model -> string option, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidate (get, setWithModel = setWithModel, validate = validate)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayOptValidateThrottle
        (
            get : 'model -> 'a voption,
            setWithModel : 'a voption -> 'model -> 'msg,
            validate : 'model -> string option,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidateThrottle (get, setWithModel, validate, (TimeSpan.FromMilliseconds timeout))

    static member twoWayOptValidateThrottle
        (get : 'model -> 'a voption, set : 'a voption -> 'msg, validate : 'model -> string option, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidate (get, set, validate)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayOptValidateThrottle
        (
            get : 'model -> 'a voption,
            set : 'a voption -> 'msg,
            validate : 'model -> string option,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidateThrottle (get, set, validate, (TimeSpan.FromMilliseconds timeout))

    static member twoWayOptValidateThrottle
        (
            get : 'model -> 'a voption,
            setWithModel : 'a voption -> 'model -> 'msg,
            validate : 'model -> Result<'ignored, string>,
            timespan
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidate (get, setWithModel = setWithModel, validate = validate)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayOptValidateThrottle
        (
            get : 'model -> 'a voption,
            setWithModel : 'a voption -> 'model -> 'msg,
            validate : 'model -> Result<'ignored, string>,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidateThrottle (get, setWithModel, validate, (TimeSpan.FromMilliseconds timeout))

    static member twoWayOptValidateThrottle
        (get : 'model -> 'a voption, set : 'a voption -> 'msg, validate : 'model -> Result<'ignored, string>, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidate (get, set, validate)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayOptValidateThrottle
        (
            get : 'model -> 'a voption,
            set : 'a voption -> 'msg,
            validate : 'model -> Result<'ignored, string>,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidateThrottle (get, set, validate, (TimeSpan.FromMilliseconds timeout))

    static member twoWayOptValidateThrottle
        (get : 'model -> 'a option, setWithModel : 'a option -> 'model -> 'msg, validate : 'model -> string list, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidate (get, setWithModel = setWithModel, validate = validate)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayOptValidateThrottle
        (
            get : 'model -> 'a option,
            setWithModel : 'a option -> 'model -> 'msg,
            validate : 'model -> string list,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidateThrottle (get, setWithModel, validate, (TimeSpan.FromMilliseconds timeout))

    static member twoWayOptValidateThrottle
        (get : 'model -> 'a option, set : 'a option -> 'msg, validate : 'model -> string list, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidate (get, set, validate)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayOptValidateThrottle
        (
            get : 'model -> 'a option,
            set : 'a option -> 'msg,
            validate : 'model -> string list,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidateThrottle (get, set, validate, (TimeSpan.FromMilliseconds timeout))

    static member twoWayOptValidateThrottle
        (get : 'model -> 'a option, setWithModel : 'a option -> 'model -> 'msg, validate : 'model -> string voption, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidate (get, setWithModel = setWithModel, validate = validate)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayOptValidateThrottle
        (
            get : 'model -> 'a option,
            setWithModel : 'a option -> 'model -> 'msg,
            validate : 'model -> string voption,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidateThrottle (get, setWithModel, validate, (TimeSpan.FromMilliseconds timeout))

    static member twoWayOptValidateThrottle
        (get : 'model -> 'a option, set : 'a option -> 'msg, validate : 'model -> string voption, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidate (get, set, validate)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayOptValidateThrottle
        (
            get : 'model -> 'a option,
            set : 'a option -> 'msg,
            validate : 'model -> string voption,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidateThrottle (get, set, validate, (TimeSpan.FromMilliseconds timeout))

    static member twoWayOptValidateThrottle
        (get : 'model -> 'a option, setWithModel : 'a option -> 'model -> 'msg, validate : 'model -> string option, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidate (get, setWithModel = setWithModel, validate = validate)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayOptValidateThrottle
        (
            get : 'model -> 'a option,
            setWithModel : 'a option -> 'model -> 'msg,
            validate : 'model -> string option,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidateThrottle (get, setWithModel, validate, (TimeSpan.FromMilliseconds timeout))

    static member twoWayOptValidateThrottle
        (get : 'model -> 'a option, set : 'a option -> 'msg, validate : 'model -> string option, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidate (get, set, validate)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayOptValidateThrottle
        (
            get : 'model -> 'a option,
            set : 'a option -> 'msg,
            validate : 'model -> string option,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidateThrottle (get, set, validate, (TimeSpan.FromMilliseconds timeout))

    static member twoWayOptValidateThrottle
        (
            get : 'model -> 'a option,
            setWithModel : 'a option -> 'model -> 'msg,
            validate : 'model -> Result<'ignored, string>,
            timespan
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidate (get, setWithModel = setWithModel, validate = validate)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayOptValidateThrottle
        (
            get : 'model -> 'a option,
            setWithModel : 'a option -> 'model -> 'msg,
            validate : 'model -> Result<'ignored, string>,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidateThrottle (get, setWithModel, validate, (TimeSpan.FromMilliseconds timeout))

    static member twoWayOptValidateThrottle
        (get : 'model -> 'a option, set : 'a option -> 'msg, validate : 'model -> Result<'ignored, string>, timespan)
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidate (get, set, validate)
        >> Binding.alterMsgStream (throttle timespan)

    static member twoWayOptValidateThrottle
        (
            get : 'model -> 'a option,
            set : 'a option -> 'msg,
            validate : 'model -> Result<'ignored, string>,
            [<Optional; DefaultParameterValue(DefaultThrottleTimeout)>] timeout : float
        )
        : string -> Binding<'model, 'msg, 'a>
        =
        BindingT.twoWayOptValidateThrottle (get, set, validate, (TimeSpan.FromMilliseconds timeout))

and this is how the code look like

namespace rec eCierge.Console.Logic.AutoSuggestAddress

open System

open Elmish
open Elmish.Uno

open eCierge.Console.Domain
open eCierge.Console.Services
open eCierge.Elmish

type Model = {
    SuggestedAddresses : Address list
    SelectedAddress : Address voption
    Street : string
    SuggestedCities : City list
    City : string
    SuggestedStates : string list
    State : string
    ZipCode : string
} with

    static member Initial = {
        SuggestedAddresses = []
        SelectedAddress = ValueNone
        Street = ""
        SuggestedCities = []
        City = ""
        SuggestedStates = []
        State = ""
        ZipCode = ""
    }

    static member OfAddress address = {
        SuggestedAddresses = []
        SelectedAddress = ValueSome address
        Street = address.Street
        SuggestedCities = []
        City = address.City
        SuggestedStates = []
        State = address.State
        ZipCode = address.Zip
    }

    member m.ToAddress () =
        match m.SelectedAddress with
        | ValueSome a -> a
        | _ -> { Street = m.Street; City = m.City; State = m.State; Zip = m.ZipCode }

type Msg =
    | StreetChanged of string
    | CityChanged of string
    | StateChanged of string
    | ZipCodeChanged of string
    | AddressesFound of Address list
    | AddressSelected of Address
    | CitiesFound of City list
    | CitySelected of City
    | StatesFound of string list
    | StateSelected of string

type public Program (addressService : IAddressService) =

    let findAddressesAsync value = task { return [] }

    let findCitiesAsync value = task { return [] }

    let findStatesAsync value = task { return [] }

    member p.Init () = Model.Initial, Cmd.none

    member p.Update msg (m : Model) =
        match msg with
        | StreetChanged s ->
            { m with Street = s; SelectedAddress = ValueNone }, Cmd.OfTask.perform findAddressesAsync s AddressesFound
        | CityChanged s -> { m with City = s; SelectedAddress = ValueNone }, Cmd.none
        | StateChanged s -> { m with State = s; SelectedAddress = ValueNone }, Cmd.none
        | ZipCodeChanged s -> { m with ZipCode = s; SelectedAddress = ValueNone }, Cmd.none
        | AddressesFound addresses -> { m with SuggestedAddresses = addresses }, Cmd.none
        | AddressSelected address ->
            {
                m with
                    SelectedAddress = ValueSome address
                    Street = address.Street
                    City = address.City
                    State = address.State
                    ZipCode = address.Zip
            },
            Cmd.none
        | CitiesFound cities -> { m with SuggestedCities = cities }, Cmd.none
        | CitySelected city -> { m with City = city.Name; State = city.State }, Cmd.none
        | StatesFound states -> { m with SuggestedStates = states }, Cmd.none
        | StateSelected state -> { m with State = state }, Cmd.none

module Bindings =

    let private viewModel = Unchecked.defaultof<AutoSuggestAddressViewModel>

    let suggestedAddressesBinding =
        BindingT.oneWaySeq (_.SuggestedAddresses, (=), id) (nameof viewModel.SuggestedAddresses)

    let suggestedCitiesBinding =
        BindingT.oneWaySeq (_.SuggestedCities, (=), id) (nameof viewModel.SuggestedCities)

    let suggestedStatesBinding =
        BindingT.oneWaySeq (_.SuggestedStates, (=), id) (nameof viewModel.SuggestedStates)

    let streetBinding = BindingT.twoWayThrottle (_.Street, StreetChanged) (nameof viewModel.Street)
    let cityBinding = BindingT.twoWayThrottle (_.City, CityChanged) (nameof viewModel.City)
    let stateBinding = BindingT.twoWayThrottle (_.State, StateChanged) (nameof viewModel.State)
    let zipCodeBinding = BindingT.twoWayThrottle (_.ZipCode, ZipCodeChanged) (nameof viewModel.ZipCode)

    let searchAddressCommandBinding =
        let canExecute street m =
            String.IsNullOrWhiteSpace street
            && String.Equals (street, m.Street, StringComparison.InvariantCultureIgnoreCase)
        BindingT.cmdParamIf (StreetChanged, canExecute) (nameof viewModel.SearchAddressCommand)

    let addressSelectedCommandBinding =
        let canExecute address m = ValueOption.fold (fun _ a -> a <> address) true m.SelectedAddress
        BindingT.cmdParamIf (AddressSelected, canExecute) (nameof viewModel.AddressSelectedCommand)

    let searchCityCommandBinding =
        let canExecute city m =
            String.IsNullOrWhiteSpace city
            && String.Equals (city, m.City, StringComparison.InvariantCultureIgnoreCase)
        BindingT.cmdParamIf (CityChanged, canExecute) (nameof viewModel.SearchCityCommand)

    let citySelectedCommandBinding =
        let canExecute city m =
            not
            <| (String.Equals (city.Name, m.City, StringComparison.InvariantCultureIgnoreCase)
                && String.Equals (city.State, m.State, StringComparison.InvariantCultureIgnoreCase))
        BindingT.cmdParamIf (CitySelected, canExecute) (nameof viewModel.CitySelectedCommand)

    let searchStateCommandBinding =
        let canExecute state m =
            String.IsNullOrWhiteSpace state
            && String.Equals (state, m.State, StringComparison.InvariantCultureIgnoreCase)
        BindingT.cmdParamIf (StateChanged, canExecute) (nameof viewModel.SearchStateCommand)

    let stateSelectedCommandBinding =
        let canExecute state m =
            not
            <| String.Equals (state, m.State, StringComparison.InvariantCultureIgnoreCase)
        BindingT.cmdParamIf (StateSelected, canExecute) (nameof viewModel.StateSelectedCommand)

type AutoSuggestAddressViewModel (args) =
    inherit ViewModelBase<Model, Msg> (args)

    member _.SuggestedAddresses = base.Get (Bindings.suggestedAddressesBinding)
    member _.SuggestedCities = base.Get (Bindings.suggestedCitiesBinding)
    member _.SuggestedStates = base.Get (Bindings.suggestedStatesBinding)

    member _.Street
        with get () = base.Get<string> (Bindings.streetBinding)
        and set (value) = base.Set<string> (Bindings.streetBinding, value)

    member _.City
        with get () = base.Get<string> (Bindings.cityBinding)
        and set (value) = base.Set<string> (Bindings.cityBinding, value)

    member _.State
        with get () = base.Get<string> (Bindings.stateBinding)
        and set (value) = base.Set<string> (Bindings.stateBinding, value)

    member _.ZipCode
        with get () = base.Get<string> (Bindings.zipCodeBinding)
        and set (value) = base.Set<string> (Bindings.zipCodeBinding, value)

    member _.SearchAddressCommand = base.Get (Bindings.searchAddressCommandBinding)
    member _.AddressSelectedCommand = base.Get (Bindings.addressSelectedCommandBinding)
    member _.SearchCityCommand = base.Get (Bindings.searchCityCommandBinding)
    member _.CitySelectedCommand = base.Get (Bindings.citySelectedCommandBinding)
    member _.SearchStateCommand = base.Get (Bindings.searchStateCommandBinding)
    member _.StateSelectedCommand = base.Get (Bindings.stateSelectedCommandBinding)

@YkTru
Copy link
Author

YkTru commented Jun 5, 2024

However, looking at the difference:

"Entities" |> Binding.SubModelSeqKeyedT.id EntityViewModel (fun e -> e.Id)
           |> Binding.mapModel (fun m -> m.Entities)

vs

"Entities" |> Binding.subModelSeqT ( fun m -> m.Entities, fun e -> e.Id, EntityViewModel )

I can definitely see the argument about verbosity. Also it probably makes the most sense to specify the model mapping before the binding type and id function, not after.

I'll look into improving the user experience for composing.

  • I agree that the second version seems more “logical” (model.coll, submodel.id, submodelvm); I don't dislike the piping in the first one though since it may help to understand what's going on for beginners, although I suppose the more I use the library, the more I'll want to use the second version to reduce the “boilerplate”, in fact I think I'd use it right now if it were available.

It can even be reduced to : “Entities” |> Binding.subModelSeqT ( _.Entities, _.Id, EntityViewModel )


"Entities" |> Binding.SubModelSeqKeyedT.id EntityViewModel (fun e -> e.Id)
           |> Binding.mapModel (fun m -> m.Entities)
           |> Binding.boxT

instead of:

    member _.Entities =
        base.Get
            ()
            (Binding.SubModelSeqKeyedT.id EntityViewModel (_.Id)
             >> Binding.mapModel (_.Entities)
             >> Binding.mapMsg snd)

Is this equivalent?

In fact I ended up getting this for the conversion, all is good? (Binding.CmdT.modelAlways was the only overload that worked):

[<AllowNullLiteral>]
type AppViewModel(args) =
    inherit ViewModelBase<Model, Msg>(args)

    let selectRandomExec =
        fun m -> m.Entities.Item(Random().Next(m.Entities.Length)).Id |> (fun id -> SetIsSelected(id, true))

    new() = AppViewModel(init () |> ViewModelArgs.simple)

    member _.SelectRandom = base.Get () (Binding.CmdT.modelAlways (selectRandomExec))

    member _.DeselectAll = base.Get () (Binding.CmdT.setAlways DeselectAll)

    member _.Entities =
        base.Get
            ()
            (Binding.SubModelSeqKeyedT.id EntityViewModel (_.Id)
             >> Binding.mapModel (_.Entities)
             >> Binding.mapMsg snd)

@marner2
Copy link
Collaborator

marner2 commented Jun 19, 2024

I'm actually not very fond of the overload strategy that was used. It causes a bunch of confusion especially when you're passing in multiple selectors.

@marner2 Why are you using:

"Entities" |> Binding.SubModelSeqKeyedT.id EntityViewModel (fun e -> e.Id)
      |> Binding.mapModel (fun m -> m.Entities)
      |> Binding.boxT

instead of:

member _.Entities =
   base.Get
       ()
       (Binding.SubModelSeqKeyedT.id EntityViewModel (_.Id)
        >> Binding.mapModel (_.Entities)
        >> Binding.mapMsg snd)

Is this equivalent?

I was illustrating how that you can start in the middle and do a partial conversion. So the change is not "viral" in that it doesn't force you to change everything everywhere all at once. So yes, they are (at least as far as I can tell) equivalent.

In fact I ended up getting this for the conversion, all is good? (Binding.CmdT.modelAlways was the only overload that worked):

And now you're exposing the weakness with trying to name all of the different functions, as they either get very cryptic or very verbose. In this particular case, modelAlways refers to the fact that it passes in the model (as opposed to the parameter, both, or nothing), and "always" canExecute (as opposed to looking at the model, parameter or both to disable the command). We need a better naming scheme (starting with using BindingT). In fact, I'm tempted to remove the model ones as you can always introduce it with a call to Bindings.mapMsgWithModel and just disregard the original msg (which is the obj parameter).

I'll try to look at that in more detail in the next few weeks, as I have time.

@xperiandri
Copy link

In fact, I'm tempted to remove the model ones as you can always introduce it with a call to Bindings.mapMsgWithModel and just disregard the original msg (which is the obj parameter).

Good point!

@YkTru
Copy link
Author

YkTru commented Jun 26, 2024

( @marner2 First I'd like to say that now all my code using ElmishWPF staticVM works everywhere HUGE thanks for your patience and help, so now my questions are focused on getting "best/better practices" and a better understanding)
.

I'm actually not very fond of the overload strategy that was used.

1- By "overload strategy", are you referring only to "modelAlways" and "setAlways" (etc.) or also to other things/cases, and does elmishWPF's vm static class force (so far) the use of such strategies?

Because one thing that confuses me is why this [sample] (https://github.com/elmish/Elmish.WPF/blob/master/src/Samples/SubModelStatic.Core/Program.fs) uses many such overloads, while you seem to advise against them (am I not understanding something?).


It causes a bunch of confusion especially when you're passing in multiple selectors.

2- Could you provide a concrete example + a code sample where such confusion occurs, so that I can compare the two approaches in such a situation?

@YkTru
Copy link
Author

YkTru commented Jun 26, 2024

And now you're exposing the weakness with trying to name all of the different functions, as they either get very cryptic or very verbose

3- I can share my personal experience if it can be relevant:

  • Coming from C#+MVVM (and knowing little about F#/FP principles eight months ago), how to use StaticVM + overloads has been for me the most confusing/frustrating/scary part of the last couple of months: I had no choice but to try to decipher the source code (still in progress), and ask you tons of questions. I wasted many hours trying to "hack signatures" by trying almost randomly the various overloads + conversions, until I made this post and many things became much clearer thanks to your advice.

  • I don't know if removing/discouraging the "overload strategy" is the way to go, as I'm guessing @xperiandri prefers this approach because he knows the inner workings of the library very well and don't need "explicit" (ie noisy/boilerplate) code. This approach can certainly help achieve cleaner code IMO, but would you @marner2 advise that such "helpers/overloads" should only be created according to the user's specific needs (is it easy/recommended to extend Elmish.WPF in such ways?)?

    (@xperiandri, is "cleaner" code the main reason you have so many overloads? Are there other major benefits?)

  • Also, the use of SubModel wasn't obvious at first; why not call it "SubViewModel", or "ChildViewModel"? I think this might help newcomers, unless I'm misunderstanding something.

@xperiandri
Copy link

Yes it is

@YkTru
Copy link
Author

YkTru commented Jul 9, 2024

@marner2 No pressure, but have you had time to think about it?

(I also intend to post a sample of a complete abstract project structure + abstract MVU using static VM eventually following this discussion do you think you have time/interest for that? Should I post it as a PR, at the end of this discussion or as a new "Issue"? (@TysonMN ?))

Thank you.

@YkTru YkTru closed this as completed Jul 20, 2024
@YkTru YkTru reopened this Jul 20, 2024
@xperiandri
Copy link

@YkTru are you interested in WPF only or Uno Platform is within your interest too?

@YkTru
Copy link
Author

YkTru commented Jul 22, 2024

@xperiandri I stick with WPF because I make exclusively desktop applications, and mostly because I use Devexpress which has a lot of amazing controls that aren't available for MAUI/UNO/WinUI3 etc. (though I would certainly like to have "x:Bind" and other great stuff that were added from UWP and others)

Also, WPF is quite stable and isn't about to disappear (WinUI3 was supposed to “revolutionize” everything; now it's simply dead (I know Uno is open source and not tied to MS but still, I need controls like TreeListControl (i.e. TreeList + Grid) which are pretty hard to implement)).

@YkTru
Copy link
Author

YkTru commented Jul 22, 2024

Elmish.WPF is by far the best .Net paradigm/library I've used so far; to follow Prism obliged a mess in the folder structure + extremely redundant boilerplate code, ReactiveUI was also messy in many aspects (although I liked the source generator attributes + the fluent approach), and I first tried JavaFX too (which was the most terrible IMO (MVC-based)).

But I'm really sad to realize that, although @marner2 and @TysonMN are very generous, patient and kind, the Elmish.WPF community as a whole seems largely dead/asleep (at least the “philosophical”, “quest for the best Elmish.WPF paradigmatic approaches” part (eg to use StaticVM + overload or not and how, as we shortly discussed here)). And I honnestly understand many just don't have time for that anymore.. or maybe my questions/insights/propositions aren't considered relevant enough.

I would have loved to participate in such great discussions when @cmeeren and @TysonMN were more active/present.

Do you know where else I could find an F# community interested in such a discussions (even though they don't necessarily use specifically Elmish.WPF's)? MAUI users thought they would have MVU but it never happened (and many are quite angry/desperate), I guess it would have been a great community to discuss paradigmatic approaches to folder structure, scaling and specific XAML related stuff (like best way to use or not to use DataTemplateSelectors, converters, behaviors, dialogs etc.).

I spend a lot of time learning from what Richard Feldman does, proposes, encourages, which helps++, even if sometimes I don't have enough knowledge/intuition to see how it would translate with Elmish.WPF (eg Elm's "extensible records"), or if what he wrote 7 years ago still makes sense (like his famous spa-example hasn't been revised in over 5 years.)

This guy has recently proposed an “updated” version that doesn't seem to follow at all (as I understand it) some of Feldman's recommended approaches (perhaps for good reasons I can't pinpoint right now), nor the folder structure (although, as he explained, he deliberately chose to do so).

Do you have any examples of folder structures you can share? We could start a new discussion (similar to #93 ) on this subject in another thread?

@xperiandri
Copy link

I need controls like TreeListControl

Me too! I use Syncfusion on Windows

@xperiandri
Copy link

WPF is quite stable and isn't about to disappear

I agree it is the best development experience

@xperiandri
Copy link

Do you have any examples of folder structures you can share? We could start a new discussion (similar to #93) on this subject in another thread?

Yes I do have

@YkTru
Copy link
Author

YkTru commented Jul 23, 2024

Do you have any examples of folder structures you can share? We could start a new discussion (similar to #93) on this subject in another thread?

Yes I do have

Alright! I'll try to start a thread somewhere in the next few weeks (I still have some thinking/revision to do before sharing my ElmishWPF project structures), unless you want start one yourself until then please don't hesistate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants