I'm new to WPF, Prism, and MVVM and I'm finding very little information out there. When it comes to validation and displaying validation errors, I read quite a few blog posts and articles and none seemed to work anymore.
What I'm trying to achieve is this:
The ViewModel (minus other fields for simplification) seems to have a lot of boilerplate, maybe I'm missing something? Am I reinventing the wheel the way I'm storing and handling errors:
namespace Configurator.ViewModels {
public class RegistrationViewModel : BindableBase, INotifyDataErrorInfo {
private string _email;
public string Email {
get { return _email; }
set { SetProperty(ref _email, value); }
}
public DelegateCommand RegisterCommand { get; private set; }
public RegistrationViewModel() {
RegisterCommand = new DelegateCommand(Register);
ClearAllErrors();
}
private void Register() {
ClearAllErrors();
if (String.IsNullOrWhiteSpace(Email)) {
SetError("Email", "We need your email address to register you.");
}
}
private readonly Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>();
public Dictionary<string, List<string>> Errors {
get { return _errors; }
}
public void SetError(string propertyName, string errorMessage) {
if (!_errors.ContainsKey(propertyName)) {
_errors.Add(propertyName, new List<string>());
}
_errors[propertyName].Add(errorMessage);
RaiseErrorsChanged(propertyName);
}
protected void ClearError(string propertyName) {
if (_errors.ContainsKey(propertyName)) {
_errors.Remove(propertyName);
}
RaiseErrorsChanged(propertyName);
}
protected void ClearAllErrors() {
var errors = _errors.Select(error => error.Key).ToList();
foreach (var propertyName in errors)
ClearError(propertyName);
}
public void RaiseErrorsChanged(string propertyName) {
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
RaisePropertyChanged("Errors");
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged = delegate { return; };
public IEnumerable GetErrors(string propertyName) {
if (String.IsNullOrEmpty(propertyName) || !_errors.ContainsKey(propertyName)) {
return null;
}
return _errors[propertyName];
}
public bool HasErrors {
get { return _errors.Any(x => x.Value != null && x.Value.Count > 0); }
}
}
}
When it comes to the view, I had to add this:
<UserControl.Resources>
<local:ErrorFormatter x:Key="ErrorFormatter" />
<local:ErrorPresent x:Key="ErrorPresent" />
</UserControl.Resources>
Will every view require it? Then the field itself looks like this:
<Label Grid.Column="0" Grid.Row="4" Content="Email:" HorizontalContentAlignment="Right" Margin="6"/>
<TextBox Grid.Column="1" Grid.Row="4" x:Name="email" Margin="6" Text="{Binding Email}"/>
<TextBlock Grid.Column="1" Grid.Row="5" Margin="6,0,6,6" Foreground="Red"
Text="{Binding Errors, Converter={StaticResource ErrorFormatter}, ConverterParameter=Email}"
Visibility="{Binding Errors, Converter={StaticResource ErrorPresent}, ConverterParameter=Email}"/>
Is binding to Errors
the right thing? I've seen examples in which it was binding to Errors[Email]
but that would raise an exception when that key doesn't exist.
This mean that I had to create two IValueConverter
s:
public sealed class ErrorFormatter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
var allErrors = (Dictionary<string, List<string>>)value;
var fieldName = (string)parameter;
if (allErrors.ContainsKey(fieldName) && allErrors[fieldName].Count > 0) {
foreach (string error in allErrors[fieldName]) {
Console.WriteLine(error);
}
return String.Join("\n", allErrors[fieldName]).Trim();
} else {
return null;
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
and
class ErrorPresent : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
var allErrors = (Dictionary<string, List<string>>)value;
var fieldName = (string)parameter;
if (allErrors.ContainsKey(fieldName) && allErrors[fieldName].Count > 0) {
return "Visible";
} else {
return "Collapsed";
};
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
It feels like anybody who needs to display validation errors would have to write these as well, so, am I missing something? I've noticed some expressions using BooleanToVisibilityConverter
but I couldn't get them to work the way I'm storing errors?
What feels like a smell is that the fields are marked with the red border automatically which I believe it's due to INotifyDataErrorInfo
but then I couldn't find any further automatic behavior to wire properly. Am I missing something big?