From 60d71cca7d88f326b86673747b2fc6595303c662 Mon Sep 17 00:00:00 2001 From: Tyson Williams Date: Wed, 30 Dec 2020 10:56:11 -0600 Subject: [PATCH] added support for multiple validation errors #314 --- src/Elmish.WPF.Tests/ViewModelTests.fs | 2 +- src/Elmish.WPF/Binding.fs | 121 ++++++++++++++++--- src/Elmish.WPF/ViewModel.fs | 35 +++--- src/Samples/Validation.Views/MainWindow.xaml | 38 +++++- src/Samples/Validation/Program.fs | 43 +++++-- 5 files changed, 182 insertions(+), 57 deletions(-) diff --git a/src/Elmish.WPF.Tests/ViewModelTests.fs b/src/Elmish.WPF.Tests/ViewModelTests.fs index 8641f868..b419a502 100644 --- a/src/Elmish.WPF.Tests/ViewModelTests.fs +++ b/src/Elmish.WPF.Tests/ViewModelTests.fs @@ -153,7 +153,7 @@ module Helpers = name |> createBinding (TwoWayValidateData { Get = get >> box Set = unbox<'a> >> set - Validate = validate + Validate = validate >> ValueOption.toList WrapDispatch = id }) diff --git a/src/Elmish.WPF/Binding.fs b/src/Elmish.WPF/Binding.fs index 0b9cc516..987ec995 100644 --- a/src/Elmish.WPF/Binding.fs +++ b/src/Elmish.WPF/Binding.fs @@ -60,7 +60,7 @@ type internal TwoWayData<'model, 'msg, 'a> = { type internal TwoWayValidateData<'model, 'msg, 'a> = { Get: 'model -> 'a Set: 'a -> 'model -> 'msg - Validate: 'model -> string voption + Validate: 'model -> string list WrapDispatch: Dispatch<'msg> -> Dispatch<'msg> } @@ -565,6 +565,33 @@ type Binding private () = } |> createBinding + /// + /// Creates a two-way binding with validation using + /// INotifyDataErrorInfo. + /// + /// Gets the value from the model. + /// Returns the message to dispatch. + /// + /// Returns the validation messages from the updated model. + /// + /// + /// Wraps the dispatch function with additional behavior, such as + /// throttling, debouncing, or limiting. + /// + static member twoWayValidate + (get: 'model -> 'a, + set: 'a -> 'model -> 'msg, + validate: 'model -> string list, + ?wrapDispatch: Dispatch<'msg> -> Dispatch<'msg>) + : string -> Binding<'model, 'msg> = + TwoWayValidateData { + Get = get >> box + Set = unbox<'a> >> set + Validate = validate + WrapDispatch = defaultArg wrapDispatch id + } |> createBinding + + /// /// Creates a two-way binding with validation using /// INotifyDataErrorInfo. @@ -587,7 +614,7 @@ type Binding private () = TwoWayValidateData { Get = get >> box Set = unbox<'a> >> set - Validate = validate + Validate = validate >> ValueOption.toList WrapDispatch = defaultArg wrapDispatch id } |> createBinding @@ -614,7 +641,7 @@ type Binding private () = TwoWayValidateData { Get = get >> box Set = unbox<'a> >> set - Validate = validate >> ValueOption.ofOption + Validate = validate >> Option.toList WrapDispatch = defaultArg wrapDispatch id } |> createBinding @@ -641,7 +668,7 @@ type Binding private () = TwoWayValidateData { Get = get >> box Set = unbox<'a> >> set - Validate = validate >> ValueOption.ofError + Validate = validate >> ValueOption.ofError >> ValueOption.toList WrapDispatch = defaultArg wrapDispatch id } |> createBinding @@ -670,7 +697,7 @@ type Binding private () = TwoWayValidateData { Get = get >> ValueOption.map box >> ValueOption.toObj Set = ValueOption.ofObj >> ValueOption.map unbox<'a> >> set - Validate = validate + Validate = validate >> ValueOption.toList WrapDispatch = defaultArg wrapDispatch id } |> createBinding @@ -699,7 +726,7 @@ type Binding private () = TwoWayValidateData { Get = get >> ValueOption.map box >> ValueOption.toObj Set = ValueOption.ofObj >> ValueOption.map unbox<'a> >> set - Validate = validate >> ValueOption.ofOption + Validate = validate >> Option.toList WrapDispatch = defaultArg wrapDispatch id } |> createBinding @@ -728,7 +755,36 @@ type Binding private () = TwoWayValidateData { Get = get >> ValueOption.map box >> ValueOption.toObj Set = ValueOption.ofObj >> ValueOption.map unbox<'a> >> set - Validate = validate >> ValueOption.ofError + Validate = validate >> ValueOption.ofError >> ValueOption.toList + WrapDispatch = defaultArg wrapDispatch id + } |> createBinding + + + /// + /// Creates a two-way binding to an optional value with validation using + /// INotifyDataErrorInfo. The binding automatically converts between + /// the optional source value and an unwrapped (possibly null) value + /// on the view side. + /// + /// Gets the value from the model. + /// Returns the message to dispatch. + /// + /// Returns the validation messages from the updated model. + /// + /// + /// Wraps the dispatch function with additional behavior, such as + /// throttling, debouncing, or limiting. + /// + static member twoWayOptValidate + (get: 'model -> 'a option, + set: 'a option -> 'model -> 'msg, + validate: 'model -> string list, + ?wrapDispatch: Dispatch<'msg> -> Dispatch<'msg>) + : string -> Binding<'model, 'msg> = + TwoWayValidateData { + Get = get >> Option.map box >> Option.toObj + Set = Option.ofObj >> Option.map unbox<'a> >> set + Validate = validate WrapDispatch = defaultArg wrapDispatch id } |> createBinding @@ -757,7 +813,7 @@ type Binding private () = TwoWayValidateData { Get = get >> Option.map box >> Option.toObj Set = Option.ofObj >> Option.map unbox<'a> >> set - Validate = validate + Validate = validate >> ValueOption.toList WrapDispatch = defaultArg wrapDispatch id } |> createBinding @@ -786,7 +842,7 @@ type Binding private () = TwoWayValidateData { Get = get >> Option.map box >> Option.toObj Set = Option.ofObj >> Option.map unbox<'a> >> set - Validate = validate >> ValueOption.ofOption + Validate = validate >> Option.toList WrapDispatch = defaultArg wrapDispatch id } |> createBinding @@ -815,7 +871,7 @@ type Binding private () = TwoWayValidateData { Get = get >> Option.map box >> Option.toObj Set = Option.ofObj >> Option.map unbox<'a> >> set - Validate = validate >> ValueOption.ofError + Validate = validate >> ValueOption.ofError >> ValueOption.toList WrapDispatch = defaultArg wrapDispatch id } |> createBinding @@ -1965,6 +2021,33 @@ module Extensions = } |> createBinding + /// + /// Creates a two-way binding with validation using + /// INotifyDataErrorInfo. + /// + /// Gets the value from the model. + /// Returns the message to dispatch. + /// + /// Returns the validation messages from the updated model. + /// + /// + /// Wraps the dispatch function with additional behavior, such as + /// throttling, debouncing, or limiting. + /// + static member twoWayValidate + (get: 'model -> 'a, + set: 'a -> 'msg, + validate: 'model -> string list, + ?wrapDispatch: Dispatch<'msg> -> Dispatch<'msg>) + : string -> Binding<'model, 'msg> = + TwoWayValidateData { + Get = get >> box + Set = fun p _ -> p |> unbox<'a> |> set + Validate = validate + WrapDispatch = defaultArg wrapDispatch id + } |> createBinding + + /// /// Creates a two-way binding with validation using /// INotifyDataErrorInfo. @@ -1987,7 +2070,7 @@ module Extensions = TwoWayValidateData { Get = get >> box Set = fun p _ -> p |> unbox<'a> |> set - Validate = validate + Validate = validate >> ValueOption.toList WrapDispatch = defaultArg wrapDispatch id } |> createBinding @@ -2014,7 +2097,7 @@ module Extensions = TwoWayValidateData { Get = get >> box Set = fun p _ -> p |> unbox<'a> |> set - Validate = validate >> ValueOption.ofOption + Validate = validate >> Option.toList WrapDispatch = defaultArg wrapDispatch id } |> createBinding @@ -2041,7 +2124,7 @@ module Extensions = TwoWayValidateData { Get = get >> box Set = fun p _ -> p |> unbox<'a> |> set - Validate = validate >> ValueOption.ofError + Validate = validate >> ValueOption.ofError >> ValueOption.toList WrapDispatch = defaultArg wrapDispatch id } |> createBinding @@ -2070,7 +2153,7 @@ module Extensions = TwoWayValidateData { Get = get >> ValueOption.map box >> ValueOption.toObj Set = fun p _ -> p |> ValueOption.ofObj |> ValueOption.map unbox<'a> |> set - Validate = validate + Validate = validate >> ValueOption.toList WrapDispatch = defaultArg wrapDispatch id } |> createBinding @@ -2099,7 +2182,7 @@ module Extensions = TwoWayValidateData { Get = get >> ValueOption.map box >> ValueOption.toObj Set = fun p _ -> p |> ValueOption.ofObj |> ValueOption.map unbox<'a> |> set - Validate = validate >> ValueOption.ofOption + Validate = validate >> Option.toList WrapDispatch = defaultArg wrapDispatch id } |> createBinding @@ -2128,7 +2211,7 @@ module Extensions = TwoWayValidateData { Get = get >> ValueOption.map box >> ValueOption.toObj Set = fun p _ -> p |> ValueOption.ofObj |> ValueOption.map unbox<'a> |> set - Validate = validate >> ValueOption.ofError + Validate = validate >> ValueOption.ofError >> ValueOption.toList WrapDispatch = defaultArg wrapDispatch id } |> createBinding @@ -2157,7 +2240,7 @@ module Extensions = TwoWayValidateData { Get = get >> Option.map box >> Option.toObj Set = fun p _ -> p |> Option.ofObj |> Option.map unbox<'a> |> set - Validate = validate + Validate = validate >> ValueOption.toList WrapDispatch = defaultArg wrapDispatch id } |> createBinding @@ -2186,7 +2269,7 @@ module Extensions = TwoWayValidateData { Get = get >> Option.map box >> Option.toObj Set = fun p _ -> p |> Option.ofObj |> Option.map unbox<'a> |> set - Validate = validate >> ValueOption.ofOption + Validate = validate >> Option.toList WrapDispatch = defaultArg wrapDispatch id } |> createBinding @@ -2215,7 +2298,7 @@ module Extensions = TwoWayValidateData { Get = get >> Option.map box >> Option.toObj Set = fun p _ -> p |> Option.ofObj |> Option.map unbox<'a> |> set - Validate = validate >> ValueOption.ofError + Validate = validate >> ValueOption.ofError >> ValueOption.toList WrapDispatch = defaultArg wrapDispatch id } |> createBinding diff --git a/src/Elmish.WPF/ViewModel.fs b/src/Elmish.WPF/ViewModel.fs index 74f2d161..97022215 100644 --- a/src/Elmish.WPF/ViewModel.fs +++ b/src/Elmish.WPF/ViewModel.fs @@ -205,7 +205,7 @@ type internal TwoWayBinding<'model, 'msg, 'a> = { type internal TwoWayValidateBinding<'model, 'msg, 'a> = { Get: 'model -> 'a Set: 'a -> 'model -> unit - Validate: 'model -> string voption + Validate: 'model -> string list } type internal CmdBinding<'model, 'msg> = { @@ -284,7 +284,7 @@ and [] internal ViewModel<'model, 'msg> let errorsChanged = DelegateEvent>() /// Error messages keyed by property name. - let errors = Dictionary() + let errorsByBindingName = Dictionary() let withCaching b = Cached { Binding = b; Cache = ref None } @@ -309,24 +309,17 @@ and [] internal ViewModel<'model, 'msg> let raiseCanExecuteChanged (cmd: Command) = cmd.RaiseCanExecuteChanged () - let setError error propName = - match errors.TryGetValue propName with - | true, err when err = error -> () - | _ -> - log "[%s] ErrorsChanged \"%s\"" propNameChain propName - errors.[propName] <- error - errorsChanged.Trigger([| box this; box <| DataErrorsChangedEventArgs propName |]) - - let removeError propName = - if errors.Remove propName then - log "[%s] ErrorsChanged \"%s\"" propNameChain propName - errorsChanged.Trigger([| box this; box <| DataErrorsChangedEventArgs propName |]) - let rec updateValidationError model name = function | TwoWayValidate { Validate = validate } -> - match validate model with - | ValueNone -> removeError name - | ValueSome error -> setError error name + let oldErrors = + match errorsByBindingName.TryGetValue name with + | (true, errors) -> errors + | (false, _) -> [] + let newErrors = validate model + if oldErrors <> newErrors then + log "[%s] ErrorsChanged \"%s\"" propNameChain name + errorsByBindingName.[name] <- newErrors + errorsChanged.Trigger([| box this; box <| DataErrorsChangedEventArgs name |]) | OneWay _ | OneWayLazy _ | OneWaySeq _ @@ -831,9 +824,9 @@ and [] internal ViewModel<'model, 'msg> [] member __.ErrorsChanged = errorsChanged.Publish member __.HasErrors = - errors.Count > 0 + errorsByBindingName.Count > 0 member __.GetErrors propName = log "[%s] GetErrors %s" propNameChain (propName |> Option.ofObj |> Option.defaultValue "") - match errors.TryGetValue propName with - | true, err -> upcast [err] + match errorsByBindingName.TryGetValue propName with + | true, errs -> upcast errs | false, _ -> upcast [] diff --git a/src/Samples/Validation.Views/MainWindow.xaml b/src/Samples/Validation.Views/MainWindow.xaml index 14042104..aafb5559 100644 --- a/src/Samples/Validation.Views/MainWindow.xaml +++ b/src/Samples/Validation.Views/MainWindow.xaml @@ -1,4 +1,4 @@ - - + - - -