Reputation: 993
I'm trying to implement INotifyDataErrorInfo and my model has some custom types that require different validation based on their use. I'm not sure how to implement this validation.
I tried to create a simple example below that will show what I'm trying to accomplish. I'm not looking for suggestions on changes to the model since my actual model is much more complex.
My example model is for a media event where there will be presenters and guests. When scheduling a media event, the user will enter a name, min and max presenters, and min and max guests. As a rule, a media has to have at least 1 presenter and no more than 5, and has to have at least 10 guests and no more than 50.
I have the following class, taken from an online example, that's used as the base for my model classes.
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace NotifyDataErrorInfo
{
public class ValidatableModel : INotifyDataErrorInfo, INotifyPropertyChanged
{
public ConcurrentDictionary<string, List<string>> _errors = new ConcurrentDictionary<string, List<string>>();
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
ValidateAsync();
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public void OnErrorsChanged(string propertyName)
{
var handler = ErrorsChanged;
if (handler != null)
{
handler(this, new DataErrorsChangedEventArgs(propertyName));
}
}
public IEnumerable GetErrors(string propertyName)
{
if (propertyName == null) return null;
List<string> errorsForName;
_errors.TryGetValue(propertyName, out errorsForName);
return errorsForName;
}
public bool HasErrors
{
get
{
return _errors.Any(kv => kv.Value != null && kv.Value.Count > 0);
}
}
public Task ValidateAsync()
{
return Task.Run(() => Validate());
}
private object _lock = new object();
public void Validate()
{
lock (_lock)
{
var validationContext = new ValidationContext(this, null, null);
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(this, validationContext, validationResults, true);
foreach (var kv in _errors.ToList())
{
if (validationResults.All(r => r.MemberNames.All(m => m != kv.Key)))
{
List<string> outLi;
_errors.TryRemove(kv.Key, out outLi);
OnErrorsChanged(kv.Key);
}
}
var q = from r in validationResults
from m in r.MemberNames
group r by m into g
select g;
foreach (var prop in q)
{
var messages = prop.Select(r => r.ErrorMessage).ToList();
if (_errors.ContainsKey(prop.Key))
{
List<string> outLi;
_errors.TryRemove(prop.Key, out outLi);
}
_errors.TryAdd(prop.Key, messages);
OnErrorsChanged(prop.Key);
}
}
}
}
}
Because I'm using min and max values in two places, I created the following class to store the min and max values. This is the oversimplified part of my example but should get the point across.
namespace NotifyDataErrorInfo
{
public class MinMaxValues : ValidatableModel
{
private int min;
private int max;
public int Min
{
get
{
return min;
}
set
{
if (!min.Equals(value))
{
min = value;
RaisePropertyChanged(nameof(Min));
OnErrorsChanged(nameof(Min));
}
}
}
public int Max
{
get
{
return max;
}
set
{
if (!max.Equals(value))
{
max = value;
RaisePropertyChanged(nameof(Max));
OnErrorsChanged(nameof(Max));
}
}
}
public MinMaxValues()
{
Min = 0;
Max = 0;
}
}
}
This is my MediaEvent class, which you can see is using the MinMaxValues class for the MinMaxPresenters and MinMaxGuests.
using System.ComponentModel.DataAnnotations;
namespace NotifyDataErrorInfo
{
public class MediaEvent: ValidatableModel
{
private string name;
private MinMaxValues minMaxPresenters;
private MinMaxValues minMaxGuests;
public MediaEvent()
{
name = string.Empty;
minMaxPresenters = new MinMaxValues();
minMaxGuests = new MinMaxValues();
this.Validate();
this.minMaxPresenters.Validate();
this.minMaxGuests.Validate(); }
}
[Required]
[StringLength(10, MinimumLength = 5)]
public string Name
{
get
{
return name;
}
set
{
if(!name.Equals(value))
{
name = value;
RaisePropertyChanged(nameof(Name));
}
}
}
public MinMaxValues MinMaxPresenters
{
get
{
return minMaxPresenters;
}
set
{
if (!minMaxPresenters.Equals(value))
{
minMaxPresenters = value;
RaisePropertyChanged(nameof(MinMaxPresenters));
}
}
}
public MinMaxValues MinMaxGuests
{
get
{
return minMaxGuests;
}
set
{
if (!minMaxGuests.Equals(value))
{
minMaxGuests = value;
RaisePropertyChanged(nameof(MinMaxGuests));
}
}
}
}
}
This is the XAML for my MainWindow
<Window
x:Class="NotifyDataErrorInfo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:NotifyDataErrorInfo"
mc:Ignorable="d"
Title="MainWindow"
Height="209" Width="525"
ResizeMode="NoResize">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="42*"/>
<RowDefinition Height="43*"/>
<RowDefinition Height="42*"/>
<RowDefinition Height="43*"/>
</Grid.RowDefinitions>
<Label
Content="Meeting Name: "
Grid.Row="0" Grid.Column="0"/>
<TextBox
Text="{Binding Name}"
Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="3"/>
<Label
Content="Min Presenters: "
Grid.Row="1" Grid.Column="0"/>
<TextBox
Text="{Binding MinMaxPresenters.Min}"
Grid.Row="1" Grid.Column="1"/>
<Label
Content="Max Presenters: "
Grid.Row="1" Grid.Column="2"/>
<TextBox
Text="{Binding MinMaxPresenters.Max}"
Grid.Row="1" Grid.Column="3"/>
<Label
Content="Min Guests: "
Grid.Row="2" Grid.Column="0"/>
<TextBox
Text="{Binding MinMaxGuests.Min}"
Grid.Row="2" Grid.Column="1"/>
<Label
Content="Max Guests: "
Grid.Row="2" Grid.Column="2"/>
<TextBox
Text="{Binding MinMaxGuests.Max}"
Grid.Row="2" Grid.Column="3"/>
<Button
x:Name="TestButton"
Content="TEST"
Click="TestButton_Click"
Grid.Row="3" Grid.Column="3"/>
</Grid>
</Window>
Which is loaded in App.xaml.cs using
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var mainWindow = new MainWindow();
var mediaEvent = new MediaEvent();
mainWindow.DataContext = mediaEvent;
mainWindow.Show();
}
In the MediaEvent class I decorated the Name property with [Required] and [StringLength(10, MinimumLength = 5)] attributes. These work as expected. When a Name is shorter than 5 chars or longer than 10 chars is entered, I can see a red box around the Name TextBox to show there's an error.
Now I'm not sure how to do the validation for MinMaxPresenters.Min, MinMaxPresenters.Max, MinMaxGuests.Min, and MinMaxGuests.Max
If I decorate the Min property in the MinMaxValues class with something like [Range(1, 5)], I can confirm the validation is happening and the UI is update accordingly.
The issue is that the validation applies to the Min value for presenters and guests. I need to validate different Min values for presenters and guests.
In MediaEvent I hooked into the PropertyChanged event of minMaxPresenters. In that event handler, I tried validating the Min and Max values based on the rules for presenters (range = 1 to 5). If the validation fails, I tried adding to the _errors collection.
In my constructor I added
minMaxPresenters.PropertyChanged += MinMaxPresenters_PropertyChanged;
and then created the following
private void MinMaxPresenters_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "Min")
{
if (minMaxPresenters.Min < 1)
{
_errors.TryAdd("MinMaxPresenters.Min", new List<string> { "A media event requires at least 1 presenter" });
OnErrorsChanged("MinMaxPresenters.Min");
}
}
else if (e.PropertyName == "Max")
{
if (minMaxPresenters.Max <= minMaxPresenters.Min)
{
_errors.TryAdd("MinMaxPresenters.Max", new List<string> { "The max presenters must be greater than the min" });
OnErrorsChanged("MinMaxPresenters.Max");
}
else if (minMaxPresenters.Max > 5)
{
_errors.TryAdd("MinMaxPresenters.Max", new List<string> { "A media event can't have more than 5 presenters" });
OnErrorsChanged("MinMaxPresenters.Max");
}
}
}
When I enter min and max values that are outside of the range for presenters I can see my errors are being added to the _errors collection in my model but my view doesn't indicate that there's any errors.
Am I close? Am I going about this all wrong?
I also have the need to validate values based on other property values so doing the custom validation and adding the errors through code will be needed. An example would be in the validation of the Max value above. The Max for presenters needs to be less than 5 but it also has to be greater than the value entered for Min.
You can ignore the button in MainWindow. It was just something to click and break in the code behind so I can see what error are in the collection.
Also, in case someone comments on making _errors public, that was just a quick way to try adding the errors. I ideally I would create AddError and RemoveError methods.
Upvotes: 3
Views: 2220
Reputation: 32182
Your problem is here
_errors.TryAdd("MinMaxPresenters.Min", new List<string>
{ "A media event requires at least 1 presenter" });
You are adding the error to the parent object but WPF bindings look for the errors on the last object in the property chain. It's a major headache with validation and WPF. With your model you should do
MinMaxPresenters._errors.TryAdd("Min", new List<string>
{ "A media event requires at least 1 presenter" });
Then the errors will get picked up by the UI.
In my framework that I have developed I am able to do what you originally tried but I parse the error string "MinMaxPresenters.Min" and then look for properties with the name "MinMaxPresenters" and automatically forward the validation error to the sub objects.
My implementation of AddErrors is
public void AddErrors(string path, IEnumerable<Exception> errors, bool nest = true)
{
var exceptions = errors as IList<Exception> ?? errors.ToList();
var nestedPath = path.Split('.').ToList();
if (nestedPath.Count > 1 && nest)
{
var tail = string.Join(".", nestedPath.Skip(1));
// Try and get a child property as Maybe<INotifyDataExceptionInfo>
// and if it exists pass the error
// downwards after stripping off the first part of
// the path.
var notifyDataExceptionInfo = this.TryGet<INotifyDataExceptionInfo,INotifyDataExceptionInfo>(nestedPath[0]);
if(notifyDataExceptionInfo.IsSome)
notifyDataExceptionInfo.Value.AddErrors(tail, exceptions);
}
_Errors.RemoveKey(path);
foreach (var error in exceptions)
{
_Errors.Add(path, error);
}
RaiseErrorEvents(path);
}
** TryGet is a method to get a property value by refelection
** Full implementation can be found at this location.
Upvotes: 2