elmish / Elmish.WPF

Static WPF views for elmish programs
Other
429 stars 71 forks source link

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

Open YkTru opened 4 months ago

YkTru commented 4 months ago

[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)

marner2 commented 4 months ago

@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).

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 commented 4 months ago

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 commented 4 months ago

@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 commented 4 months ago

@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 commented 4 months ago

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 commented 4 months ago

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 commented 4 months ago

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 commented 3 months ago

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.

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


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 commented 3 months ago

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 commented 3 months ago

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 commented 3 months ago

( @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 commented 3 months ago

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:

xperiandri commented 3 months ago

Yes it is

YkTru commented 2 months ago

@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.

xperiandri commented 2 months ago

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

YkTru commented 2 months ago

@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 commented 2 months ago

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 commented 2 months ago

I need controls like TreeListControl

Me too! I use Syncfusion on Windows

xperiandri commented 2 months ago

WPF is quite stable and isn't about to disappear

I agree it is the best development experience

xperiandri commented 2 months ago

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 commented 2 months ago

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.