-
-
Notifications
You must be signed in to change notification settings - Fork 71
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
Comments
@YkTru There are two issues here that seem to be complicating things.
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. ExampleI'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 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 So let's go through each of the arguments that we started with (
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 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 General ProcessGenerally 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 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 |
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 |
@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 |
@xperiandri Do you think it would help the experience to change 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. |
Yes, I propose to backport my changes to Elmish.WPF
And with Fantomas it will be 3 lines minimum, while the old approach allows one line |
Actually my repo has some changes to the |
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) |
However, looking at the difference:
It can even be reduced to :
"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)
|
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.
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.
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, I'll try to look at that in more detail in the next few weeks, as I have time. |
Good point! |
( @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)
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?).
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? |
3- I can share my personal experience if it can be relevant:
|
Yes it is |
@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 are you interested in WPF only or Uno Platform is within your interest too? |
@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)). |
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? |
Me too! I use Syncfusion on Windows |
I agree it is the best development experience |
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. |
[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:
[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):
[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)
The text was updated successfully, but these errors were encountered: