Skip to content

Commit

Permalink
added support for multiple validation errors #314
Browse files Browse the repository at this point in the history
  • Loading branch information
Tyson Williams committed Dec 30, 2020
1 parent db20825 commit 60d71cc
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 57 deletions.
2 changes: 1 addition & 1 deletion src/Elmish.WPF.Tests/ViewModelTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ module Helpers =
name |> createBinding (TwoWayValidateData {
Get = get >> box
Set = unbox<'a> >> set
Validate = validate
Validate = validate >> ValueOption.toList
WrapDispatch = id
})

Expand Down
121 changes: 102 additions & 19 deletions src/Elmish.WPF/Binding.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
}

Expand Down Expand Up @@ -565,6 +565,33 @@ type Binding private () =
} |> createBinding


/// <summary>
/// Creates a two-way binding with validation using
/// <c>INotifyDataErrorInfo</c>.
/// </summary>
/// <param name="get">Gets the value from the model.</param>
/// <param name="set">Returns the message to dispatch.</param>
/// <param name="validate">
/// Returns the validation messages from the updated model.
/// </param>
/// <param name="wrapDispatch">
/// Wraps the dispatch function with additional behavior, such as
/// throttling, debouncing, or limiting.
/// </param>
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


/// <summary>
/// Creates a two-way binding with validation using
/// <c>INotifyDataErrorInfo</c>.
Expand All @@ -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

Expand All @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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


/// <summary>
/// Creates a two-way binding to an optional value with validation using
/// <c>INotifyDataErrorInfo</c>. The binding automatically converts between
/// the optional source value and an unwrapped (possibly <c>null</c>) value
/// on the view side.
/// </summary>
/// <param name="get">Gets the value from the model.</param>
/// <param name="set">Returns the message to dispatch.</param>
/// <param name="validate">
/// Returns the validation messages from the updated model.
/// </param>
/// <param name="wrapDispatch">
/// Wraps the dispatch function with additional behavior, such as
/// throttling, debouncing, or limiting.
/// </param>
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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -1965,6 +2021,33 @@ module Extensions =
} |> createBinding


/// <summary>
/// Creates a two-way binding with validation using
/// <c>INotifyDataErrorInfo</c>.
/// </summary>
/// <param name="get">Gets the value from the model.</param>
/// <param name="set">Returns the message to dispatch.</param>
/// <param name="validate">
/// Returns the validation messages from the updated model.
/// </param>
/// <param name="wrapDispatch">
/// Wraps the dispatch function with additional behavior, such as
/// throttling, debouncing, or limiting.
/// </param>
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


/// <summary>
/// Creates a two-way binding with validation using
/// <c>INotifyDataErrorInfo</c>.
Expand All @@ -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

Expand All @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
35 changes: 14 additions & 21 deletions src/Elmish.WPF/ViewModel.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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> = {
Expand Down Expand Up @@ -284,7 +284,7 @@ and [<AllowNullLiteral>] internal ViewModel<'model, 'msg>
let errorsChanged = DelegateEvent<EventHandler<DataErrorsChangedEventArgs>>()

/// Error messages keyed by property name.
let errors = Dictionary<string, string>()
let errorsByBindingName = Dictionary<string, string list>()


let withCaching b = Cached { Binding = b; Cache = ref None }
Expand All @@ -309,24 +309,17 @@ and [<AllowNullLiteral>] 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 _
Expand Down Expand Up @@ -831,9 +824,9 @@ and [<AllowNullLiteral>] internal ViewModel<'model, 'msg>
[<CLIEvent>]
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 "<null>")
match errors.TryGetValue propName with
| true, err -> upcast [err]
match errorsByBindingName.TryGetValue propName with
| true, errs -> upcast errs
| false, _ -> upcast []
38 changes: 33 additions & 5 deletions src/Samples/Validation.Views/MainWindow.xaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Window x:Class="Elmish.WPF.Samples.Validation.MainWindow"
<Window x:Class="Elmish.WPF.Samples.Validation.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Expand All @@ -11,7 +11,7 @@
mc:Ignorable="d"
d:DataContext="{x:Static vm:Program.designVm}">
<Window.Resources>
<Style x:Key="textBoxInError" TargetType="Control">
<Style x:Key="textBoxInErrorSingle" TargetType="Control">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
Expand All @@ -34,9 +34,37 @@
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="textBoxInErrorMultiple" TargetType="Control">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<DockPanel>
<TextBlock DockPanel.Dock="Left" Foreground="Red" FontWeight="Bold">*</TextBlock>
<ItemsControl DockPanel.Dock="Bottom" ItemsSource="{Binding ElementName=placeholder, Path=AdornedElement.(Validation.Errors)}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ErrorContent}" Foreground="Red"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Border BorderBrush="Red" BorderThickness="2">
<AdornedElementPlaceholder x:Name="placeholder"/>
</Border>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<StackPanel Margin="0,25,0,0">
<TextBox Style="{StaticResource textBoxInError}" Text="{Binding RawValue, UpdateSourceTrigger=PropertyChanged}" Width="150" Margin="0,5,0,25" />
<Button Command="{Binding Submit}" Content="Submit" Margin="0,5,10,5" Width="50" />
<StackPanel HorizontalAlignment="Center" Margin="0,25,0,0">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Value: "/>
<TextBox Style="{StaticResource textBoxInErrorSingle}" Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}" Width="150" Margin="0,5,0,25" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Password: "/>
<TextBox Style="{StaticResource textBoxInErrorMultiple}" Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}" Width="150" Margin="0,5,0,25" />
</StackPanel>
<Button Command="{Binding Submit}" Content="Submit" Margin="0,40,10,5" Width="50" />
</StackPanel>
</Window>
Loading

0 comments on commit 60d71cc

Please sign in to comment.