Is this the right way of doing and displaying validation on WPF, Prism, MVVM?

409 Views Asked by At

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:

enter image description here

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 IValueConverters:

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?

0

There are 0 best solutions below