XAML WPF Validation

I am really struggling to find a WPF validation pattern that is easily maintainable. I have been doing some web work, and I was really impressed at how easy validation is in AngularJS. So I may have unrealistic expectations of what WPF will do. The solution I have now seems like it has a bunch of garbage. Here is the XAML I have right now. I don't like having to create the DataResource, and it seems far too verbose. Any suggestions?

<local:DataResource x:Key="RequireFcpaGovernmentRelationsText" BindingTarget="{Binding Vendor.RequireFcpaGovernmentRelationsText}" />

<TextBox MaxLength="50" Width="350" HorizontalAlignment="Left">
    <Binding Path="Vendor.FcpaGovernmentRelationsText" UpdateSourceTrigger="PropertyChanged">
        <Binding.ValidationRules>
            <local:RequiredFieldRule ValidationStep="UpdatedValue" RequireIf="{local:DataResourceBinding DataResource={StaticResource RequireFcpaGovernmentRelationsText}}" RequiredMessage="Please specify government relations."/>
        </Binding.ValidationRules>
    </Binding>
</TextBox>

Solution 1:

Unfortunately, WPF is very verbose by nature. It isn't really fair to compare it to a web technology. The DataResource is needed because of the way that WPF manages the DataContext. We have started doing more and more web project because WPF just doesn't keep up.

Solution 2:

This question comes up regularly, and things also change in the .NET world, so let's come up with a current solution that, unlike many snippets here and there, works and is easily maintanable. It will use INotifyDataErrorInfo that's appeared in .NET 4.5.

Make sure you have a base class that you inherit all your data classes from. Make this base class implement INotifyDataErrorInfo:

public abstract class ObjectBase<T> : INotifyDataErrorInfo, INotifyPropertyChanged {
  public event PropertyChangedEventHandler PropertyChanged;
  public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
  private readonly Dictionary<string, List<string>> StoredErrors = new();

  protected void OnPropertyChanged(string propertyName) {
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    if (!string.IsNullOrEmpty(propertyName))
      Validate(propertyName);
  }

Using INotifyPropertyChanged is not strictly required but you probably want to use that, anyway, and it's easy to combine both.

protected void OnErrorsChanged(string propertyName) =>
  ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));

public bool HasErrors => StoredErrors.Any();

public IEnumerable GetErrors(string propertyName) => StoredErrors.GetValueOrDefault(propertyName);

protected void AddError(string propertyName, string error) {
  if (!StoredErrors.ContainsKey(propertyName))
    StoredErrors[propertyName] = new List<string>();

  if (!StoredErrors[propertyName].Contains(error)) {
    StoredErrors[propertyName].Add(error);
    OnErrorsChanged(propertyName);
  }
}

protected void ClearErrors(string propertyName) {
  if (StoredErrors.ContainsKey(propertyName)) {
    StoredErrors.Remove(propertyName);
    OnErrorsChanged(propertyName);
  }
}

You have two approaches for the actual validation logic, you can select either or even combine them. You can add code to your properties that check for your validation conditions and set the errors if those conditions are not met. Or, you can use attributes on your properties to make it automatic. Only add these extra functions if you plan to use the second approach as well:

public void ValidateAll() {
  foreach (var prop in GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static))
    Validate(prop.Name);
}

protected void Validate(string propertyName) {
  ClearErrors(propertyName);
  var prop = GetType().GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);
  var validationAttributes = prop?.GetCustomAttributes(typeof(ValidationAttribute)) ?? new List<Attribute>();
  if (validationAttributes.Any()) {
    var validationResults = new List<ValidationResult>();
    object value = prop.GetValue(this);
    if (!Validator.TryValidateProperty(value, new ValidationContext(this, null, null) { MemberName = propertyName }, validationResults)) {
      foreach (var attributeValidationResult in validationResults)
        AddError(propertyName, attributeValidationResult.ErrorMessage);
    }
  }
}

That's a bit of a boilerplate all right, but it only needs to go into your base class and the actual data classes will use it very simply:

public class Person : ObjectBase<Person> {

  private string name;
  [Required(ErrorMessage = "Name is required")]
  public string Name {
    get => name;
    set {
      name = value;
      OnPropertyChanged(nameof(Name));
    }
  }

  private string address;
  public string Address {
    get => address;
    set {
      address = value;
      OnPropertyChanged(nameof(Address));
      ClearErrors(nameof(Address));
      if (address.IsEmpty()) //your own functions
        AddError(nameof(Address), "Address is required");
      if (address.IsInvalid()) //your own functions
        AddError(nameof(Address), "Invalid address");
    }
  }

You can observe the two approaches here. Either you use attributes like Required, StringLength, etc (WPF has quite a few of them) or you can create your own by inheriting ValidationAttribute and providing an IsValid() override. Or you can use simple conditional logic to set errors. Or you can mix those approaches, both in the same data class, or even in the same property -- whichever you find easier to use and more maintainable.

This is already mostly automatic, but using fields in dialogs might require a little bit more plumbing. Use a CanExecute callback with your OK button command handler:

<CommandBinding Command="{StaticResource DoOk}" Executed="Ok" CanExecute="CanOk" />

private void CanOk(object sender, CanExecuteRoutedEventArgs e) =>
  e.CanExecute = !YourData?.HasErrors ?? false;

private void Ok(object sender, ExecutedRoutedEventArgs e) {
  YourData.ValidateAll();
  if (!YourData.HasErrors) {
    DialogResult = true;
  }
}

This only allows OK to be enabled if there are no validation errors, and makes sure that the dialog can't be finished even if it's enabled but there are errors (which is usually the case right after opening the dialog but before actually editing the fields).