Search code examples
c#wpfvalidationicommand

How do I run IDataErrorInfo validation code when my IValueConverter throws an exception?


I am having trouble with validation for integers in a WPF window. My business object has an Age:int property. I want to validate that Age >= 5. I see how to do this with IDataErrorInfo and setting ValidatesOnDataErrors to true. That works if I enter in a number. But if I enter an empty string or "abc", the validation code isn't getting called. This is a problem because I have a button that I want to be enabled only if the validation code succeeds.

According to my Output Window, the problem is that the default converter is throwing a FormatException in the ConvertBack method. This prevents the code from calling the IDataErrorInfo.this[string columnName] method. (I'm currently using the default int converter)

I've read about a couple of solutions to this, but they either don't work, or seem hacky:

  1. Use a Nullable<int>. This doesn't prevent the exception.
  2. Use a string instead of an int. This would work, but it doesn't allow me to take advantage of built-in WPF features. It feels like a hack.
  3. I thought I might be able to use my own IValueConverter to intercept the ConvertBack call, but I need to give the converter access to the my business object so that it can set some kind of IsValid:bool property. I don't know how to give the converter a reference to my business object.

How can I validate my business object when the ConvertBack throws an exception (e.g., user entering an empty string)?


Here's my business object:

class DContext : INotifyPropertyChanged, IDataErrorInfo
{
    public DContext()
    {
        this._submit = new Commands.btnSubmit(this);
    }

    private readonly ICommand _submit;
    public ICommand Submit { get { return this._submit; } }

    private int _age;
    public int Age
    {
        get { return this._age; }
        set
        {
            if (this._age != value)
            {
                this._age = value;
                this.OnPropertyChanged();
            }
        }
    }

    private string _error;
    public string Error
    {
        get { return this._error; }
    }

    public string this[string columnName]
    {
        get 
        {
            this._error = null;
            if (columnName == "Age" && this._age < 5)
                this._error = "Age must be >= 5";

            return this._error;
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberNameAttribute]string propertyName = "")
    {
        if (this.PropertyChanged != null)
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

Here's my WPF:

<StackPanel Orientation="Horizontal" Height="20" VerticalAlignment="Top">
    <TextBlock Text="Age" />
    <TextBox Width="100" Text="{Binding Age,ValidatesOnDataErrors=True}" />
    <Button Content="Submit" Command="{Binding Submit}" />
</StackPanel>

Interestingly, I noticed that the ICommand.CanExecute method is getting called before the IDataErrorInfo.this[string columnName] method. This means that the ICommand is working with values that have not yet been validated. This means that I'll have to duplicate my error detection code.


Solution

  • I think the solution has to be either #2 or #3. I would lean towards #3 because it does not introduce any clutter into your view model. Have the "ConvertBack" method attempt to parse the input, and return an invalid value (maybe 0 or -1) if the parse fails.

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            string input = value as string;
            int validNumber;
            if (input != null && int.TryParse(input, out validNumber))
                return validNumber;
            else
                return 0;
        }
    

    Your validation should work with this with no other changes. The only reason I could see for using #2 instead would be if you wanted different error messages like "The age field is required" and "The age field must be a valid number".

    This means that the ICommand is working with values that have not yet been validated. This means that I'll have to duplicate my error detection code.

    I'm not sure why that would be necessary. Can't you just raise "Command.CanExecuteChanged" whenever your validation runs? If not, then you may want to consider using INotifyDataErrorInfo instead of IDataErrorInfo, as it allows you to raise an "ErrorsChanged" event to trigger a validation.