none
ExceptionValidationRule with a non-default Converter

    Question

  • I'm trying to use the ExceptionsValidationRule with a custom IValueConverter. In some cases the converter is unable to convert-back the given value and must throw an exception. However this exception is not caught by the ExceptionValidationRule and my application fails.

     

    In the documentation, "Data Binding Overview" lists the following steps during a target to source value transfer:

    1. ...

    2. ...

    3. If all of the rules pass, the binding engine then calls the converter, if one exists.

    4. If the converter passes, the binding engine calls the setter of the source property.

    5. If the binding has an ExceptionValidationRule associated with it and an exception is thrown during step 3 or 4, the binding engine checks to see if there is a UpdateSourceExceptionFilter. You have the option to use the UpdateSourceExceptionFilter callback to provide a custom handler for handling exceptions. If an UpdateSourceExceptionFilter is not specified on the Binding, the binding engine creates a ValidationError with the exception and adds it to the Validation.Errors collection of the bound element.

    I reach step 4 and the converter fails. However Step 5 never happens and my application fails.

     

    Here is a sample illustration of my problem, a custom converter to convert an Int32 to a String and back:

    public class MyInt32Converter : IValueConverter
    {
        public object Convert(object value, Type targetType, 
            object parameter, System.Globalization.CultureInfo culture)
        {
            return value.ToString();
        }
    
        public object ConvertBack(object value, Type targetType, 
            object parameter, System.Globalization.CultureInfo culture)
        {
            return Int32.Parse(value.ToString());
        }
    }

    Here is some sample XAML with some TextBox Text bindings set. The first uses the default converter and the ExceptionValidationRule is applied as expected. The second uses MyInt32Converter and fails.

    To experiment, try changing the values in the TextBoxes to non Int32 values, eg "abc". An invalid value in the first TextBox will show a red border, an invalid value in the second TextBox will cause the application to fail.

      <StackPanel>
        <StackPanel.DataContext>
          <x:Array Type="s:Int32" xmlns:s="clr-namespace:System;assembly=mscorlib">
            <s:Int32>0</s:Int32>
            <s:Int32>1</s:Int32>
          </x:Array>
        </StackPanel.DataContext>
        <TextBox Margin="3">
          <TextBox.Text>
            <Binding Path="[0]">
              <Binding.ValidationRules>
                <ExceptionValidationRule />
              </Binding.ValidationRules>
            </Binding>
          </TextBox.Text>
        </TextBox>
        <TextBox Margin="3">
          <TextBox.Text>
            <Binding Path="[1]">
              <Binding.Converter>
                <local:MyInt32Converter xmlns:local="clr-namespace:" />
              </Binding.Converter>
              <Binding.ValidationRules>
                <ExceptionValidationRule />
              </Binding.ValidationRules>
            </Binding>
          </TextBox.Text>
        </TextBox>
      </StackPanel>
    Sunday, April 15, 2007 1:24 PM

All replies

  • A little bit logic in converter might help you Smile

     

    Code Snippet

    [ValueConversion(typeof(String), typeof(Int32))]

    public class MyInt32Converter : ValidationRule, IValueConverter

    {

    public object Convert(object value, Type targetType,

    object parameter, System.Globalization.CultureInfo culture)

    {

    return value.ToString();

    }

    public object ConvertBack(object value, Type targetType,

    object parameter, System.Globalization.CultureInfo culture)

    {

    if (this.Validate(value, culture) == ValidationResult.ValidResult)

    {

    return Int32.Parse(value.ToString());

    }

    return value;

    }

     

     

    public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)

    {

    int i=0;

    return new ValidationResult(Int32.TryParse(value.ToString(),out i),null);

    }

    }

     

    BTW, this makes sense to convert first and then validate the result before displaying it. This way it works

    Sunday, April 15, 2007 2:12 PM
  • So in order to signal a failed conversion we should return the original value (or some other incompatible type)?

     

    Although this makes me wonder how we could signal a failed custom conversion from object to object.

    Sunday, April 15, 2007 2:40 PM
  • That's exacly what happens with default converter. If you'll input wrong value into top textbox, the value will remain there, but Validation error will be fired.

     

    What do you mean by second question?

     

     

    Sunday, April 15, 2007 2:44 PM
  • I mean, if the source type is object how can we signal an invalid conversion.

     

    For example if we replace <x:Array Type="s:Int32" with <x:Array Type="s:Object" in my example above, even though the conversion to Int32 has failed, the string value is assigned to the array.

    Sunday, April 15, 2007 3:00 PM
  • First of all, in order to do anything with converter, you should know source and target type. Huh, at least target type Smile

     IValueConverter accept object as source, so you always can return it as is back, if validation failed.

    Sunday, April 15, 2007 3:04 PM
  •  Tamir Khason wrote:

    First of all, in order to do anything with converter, you should know source and target type. Huh, at least target type

     IValueConverter accept object as source, so you always can return it as is back, if validation failed.

     

    The problem is, although validation fails inside the converter, the value is still assigned and no validation errors show in the UI. Of course I can return the original value, but it doesn't help.

     

    I think as it turns out, the reason your example of returning the original value works to apply the ExceptionValidationRule is because there is a default conversion from string to int32 which is applied after the custom converter fails to produce a compatible type. If the custom converter actually used types for which there are no default conversions, or fails validation in cases where the default converter would pass, this technique would not work.

     

    The documentation actually indicates we should return DependencyProperty.UnsetValue. This works to the extent that the invalid value is not passed on to the source, however this will not apply the ExceptionValidationRule.

    Monday, April 16, 2007 12:44 AM
  • I also have the same problem and I think it is WPF bug.

     

    ExceptionValidationRule documentation says:

    Represents a rule that checks for exceptions that are thrown during the update of the binding source property.

     

    And those exceptions should be also custom converter exceptions.

    Thursday, May 10, 2007 6:55 AM
  • For now I'm just calling the converter twice... once to validate, and once to do the actual conversion. I posted some more details here: http://11011.net/archives/000695.html
    Thursday, May 10, 2007 7:04 AM
  • Here is the code of System.Windows.Data.BindingExpression.UpdateValue:

     

    private void UpdateValue()
    {
        object obj2 = base.Value;
        Collection<ValidationRule> validationRulesInternal = this.ParentBinding.ValidationRulesInternal;
        CultureInfo cultureInfo = base.GetCulture();
        ExceptionValidationRule exceptionValidationRule = null;
        base.UpdateValidationError(null);
        if (validationRulesInternal != null)
        {
            foreach (ValidationRule rule2 in validationRulesInternal)
            {
                ExceptionValidationRule rule3 = rule2 as ExceptionValidationRule;
                if (rule3 != null)
                {
                    exceptionValidationRule = rule3;
                    continue;
                }
                ValidationResult result = rule2.Validate(obj2, cultureInfo);
                if (!result.IsValid)
                {
                    base.UpdateValidationError(new ValidationError(rule2, this, result.ErrorContent, null));
                    return;
                }
            }
        }
    // the following is the problem - should be in try/catch block
        if (this.Converter != null)
        {
            if (!base.UseDefaultValueConverter)
            {
                Type targetType = this.Worker.SourcePropertyType;
                obj2 = this.Converter.ConvertBack(obj2, targetType, this.ParentBinding.ConverterParameter, cultureInfo);
                if (((obj2 != null) && (obj2 != Binding.DoNothing)) && ((obj2 != DependencyProperty.UnsetValue) && !targetType.IsAssignableFrom(obj2.GetType())))
                {
                    obj2 = this.ConvertBackHelper(this.DynamicConverter, obj2, targetType, base.TargetElement, cultureInfo, exceptionValidationRule);
                }
            }
            else
            {
                obj2 = this.ConvertBackHelper(this.Converter, obj2, this.Worker.SourcePropertyType, base.TargetElement, cultureInfo, exceptionValidationRule);
            }
        }
        if (obj2 == DependencyProperty.UnsetValue)
        {
            base.SetStatus(BindingStatus.UpdateSourceError);
        }
        if ((obj2 != Binding.DoNothing) && (obj2 != DependencyProperty.UnsetValue))
        {
            try
            {
                base.BeginSourceUpdate();
                this.Worker.UpdateValue(obj2);
            }
            catch (Exception exception)
            {
                if (CriticalExceptions.IsCriticalException(exception))
                {
                    throw;
                }
                if (TraceData.IsEnabled)
                {
                    TraceData.Trace(TraceEventType.Error, TraceData.WorkerUpdateFailed, this, exception);
                }
                this.ProcessException(exception, exceptionValidationRule);
                base.SetStatus(BindingStatus.UpdateSourceError);
            }
            catch
            {
                if (TraceData.IsEnabled)
                {
                    TraceData.Trace(TraceEventType.Error, TraceData.WorkerUpdateFailed, this);
                }
                base.SetStatus(BindingStatus.UpdateSourceError);
            }
            finally
            {
                base.EndSourceUpdate();
            }
            this.OnSourceUpdated();
        }
        else
        {
            base.EndSourceUpdate();
        }
    }
    

    Thursday, May 10, 2007 7:10 AM
  • Yes thanks for the idea - it seems to be the only way now ...

    My similar approach is to use BadValueValidationRule:

     

    using System;

    using System.Globalization;

    using System.Collections.Generic;

    using System.Windows;

    using System.Windows.Controls;

    using System.Windows.Data;

    namespace Astra92.UI.WPF

    {

    /// <summary>

    /// Validation rule ensuring a value is different then <see cref="BadValueValidationRule.BadValue"/>.

    /// </summary>

    /// <remarks>

    /// Because WPF 1 (.NET 3) does not catch exceptions thrown by custom <see cref="IValueConverter"/> in a data binding, you can use this validation rule in your binding together with a converter throwing an exception or returning <see cref="DependencyProperty.UnsetValue"/> instead - see <see cref="Validate"/>.

    /// </remarks>

    public class BadValueValidationRule : ValidationRule

    {

    public BadValueValidationRule()

    {

    }

    /// <summary>

    /// Constructor.

    /// </summary>

    /// <param name="converter">See <see cref="Converter"/>.</param>

    /// <param name="converterParameter">See <see cref="ConverterParameter"/>.</param>

    /// <param name="converterTargetType">See <see cref="ConverterTargetType"/>.</param>

    public BadValueValidationRule(IValueConverter converter, object converterParameter, Type converterTargetType)

    {

    this.Converter = converter;

    this.ConverterParameter = converterParameter;

    this.ConverterTargetType = converterTargetType;

    }

    /// <summary>

    /// Custom converter used in <see cref="Validate"/>.

    /// </summary>

    /// <value>Default: null.</value>

    public IValueConverter Converter

    {

    get { return _converter; }

    set { _converter = value; }

    }

    /// <summary>

    /// Custom converter parameter used in <see cref="Validate"/>.

    /// </summary>

    /// <value>Default: null.</value>

    public object ConverterParameter

    {

    get { return _converterParameter; }

    set { _converterParameter = value; }

    }

    /// <summary>

    /// Target type for <see cref="Converter"/> used in <see cref="Validate"/>.

    /// </summary>

    /// <value>Default: null.</value>

    public Type ConverterTargetType

    {

    get { return _converterTargetType; }

    set { _converterTargetType = value; }

    }

    /// <summary>

    /// Bad value used in <see cref="Validate"/>.

    /// </summary>

    /// <value>Default: <see cref="DependencyProperty.UnsetValue"/>.</value>

    public object BadValue

    {

    get { return _badValue; }

    set { _badValue = value; }

    }

    /// <summary>

    /// Validates <paramref name="value"/>.

    /// </summary>

    /// <param name="value">Value.</param>

    /// <param name="cultureInfo">Culture.</param>

    /// <returns>

    /// If <see cref="Converter"/> is not null, its <see cref="IValueConverter.ConvertBack"/> with <see cref="ConverterTargetType"/>, <see cref="ConverterParameter"/> and <paramref name="cultureInfo"/> is called to convert <paramref name="value"/> first and if an exception is thrown, invalid result is returned with the exception.

    /// Returns valid result if <paramref name="value"/> is different then <see cref="BadValue"/>.

    /// Equality is determined with <see cref="EqualityComparer{T}.Equals(T, T)"/> for <see cref="object"/> type.

    /// </returns>

    public override ValidationResult Validate(object value, CultureInfo cultureInfo)

    {

    if (this.Converter != null)

    {

    try

    {

    value = this.Converter.ConvertBack(value, this.ConverterTargetType, this.ConverterParameter,

    cultureInfo);

    }

    catch (Exception e)

    {

    return new ValidationResult(false, e);

    }

    }

    return new ValidationResult(!EqualityComparer<object>.Default.Equals(value, this.BadValue), null);

    }

    /// <summary>

    /// See <see cref="Converter"/>.

    /// </summary>

    private IValueConverter _converter;

    /// <summary>

    /// See <see cref="ConverterParameter"/>.

    /// </summary>

    private object _converterParameter;

    /// <summary>

    /// See <see cref="ConverterTargetType"/>.

    /// </summary>

    private Type _converterTargetType;

    /// <summary>

    /// See <see cref="BadValue"/>.

    /// </summary>

    private object _badValue = DependencyProperty.UnsetValue;

    }

    }

    Thursday, May 10, 2007 10:06 AM