1
votes

I'm implementing a validation rule for a property of type double bound to a TextBox. The problem is that when I enter a 0 in the decimal digits, if the resulting number is accepted by the rule, WPF erases that last 0 as it understands it to be dummy mathematically. This prevents me from entering a non-0 digit after it.

For example I cannot input 5.101 because when I reach 5.10, WPF erases the 0 and I get back to 5.1.

I can workaround this by returning a failed ValidationResult when I catch 5.10, as in that case WPF does not remove the 0. But this is handled as a failure by the style (red border) and is confusing to the user.

Any idea on a better workaround?

The validation is handled in a class inheriting from ValidationRule, and the Validate method is this.

    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        double dValue = 0.0;
        string sValue;
        string definition = "Enter number in [ " + Min + "; " + Max + "]";

        // Catch non-double, empty, minus sign
        try
        {
            sValue = (string)value;
            if (sValue == "-")
                return new ValidationResult(false, definition);
            else if (sValue.Length > 0)
                dValue = double.Parse(sValue);
            else // Empty entry
                return new ValidationResult(false, definition);
        }
        catch (Exception ex)
        {
            return new ValidationResult(false, "Invalid entry: " + ex.Message);
        }

        // Forbid finishing with dot but return false to allow keyboard input
        if (sValue.EndsWith("."))
            return new ValidationResult(false, "Cannot end with '.'");

        // Check range
        if (dValue < Min || dValue > Max)
            return new ValidationResult(false, definition);
        else
        {
            // Workaround to allow input of 0
            if (sValue.Contains(".") && sValue.EndsWith("0"))
                return new ValidationResult(false, "Accepted");
            else
                return new ValidationResult(true, null);
        }
    }

The problem seems to be connected to feedback from the object. When I change from TwoWay to something else, the validation no longer prevents the input of 0s. Unfortunately I do need the TextBox to display the content of my object the first time I bind it. But after that I would be ok with just OneWayToSource as I can just reset the DataContext to update. Is there a way for the TextBox to be filled with the property value when I attach the object to the DataContext, while in OneWayToSource (not by explicity setting its Text I mean)?

2
Can you post the code you're using please? - Gigi
Sorry about the unreadable code, I'm looking how to display that in a readable way. - sebgur
Just edit your question and use the code formatting option to put the code there. You can't format code in comments. - Gigi
I'm not sure if this helps, but have you tried using a format string in your binding? Like this: stackoverflow.com/a/18362876/983064 - Gigi
Thanks I tried that but the string format forces me to input numbers in that exact format, and has an annoying behavior. If I want to input 5.1, when I press 5 it automatically brings the specified number of 0 digits and if I type .1 it doesn't understand it and type that before those added 0. Quite a troublesome behavior. - sebgur

2 Answers

1
votes

I wish I knew WPF better to know for sure whether there's a better solution than this. But note that you can provide your own IValueConverter to handle the conversion, and in doing so you can avoid the stripping of the trailing zero. For example, this converter converts from the bound property to the control only once, ignoring all subsequent update events:

class DoubleToStringConverter : IValueConverter
{
    private bool _convert = true;

    public object Convert(object value,
        Type targetType, object parameter, CultureInfo culture)
    {
        return _convert ? value.ToString() : Binding.DoNothing;
    }

    public object ConvertBack(object value,
        Type targetType, object parameter, CultureInfo culture)
    {
        string text = value as string;
        double doubleValue;

        if (text != null &&
            targetType == typeof(double) &&
            double.TryParse((string)value, out doubleValue))
        {
            _convert = false;
            return doubleValue;
        }

        return Binding.DoNothing;
    }
}

This allows the control to be updated when your program first initializes the control, but after that it behaves as a one-way binding, only updating the source on changes.

Frankly, it seems a little sneaky/hacky to me. But it does work. :)

0
votes

I faced same issue with validation and Peter's answer was helpful to me. But it disables synchronization from source (model), in many cases it's not acceptable. I'm gone further and upgraded it a little. Be sure converter is markup extension or option x:Shared="False" used, in that case all bindings will use their own converter instances.

public class OptimizedDoubleToStringConverter : ConverterBase
{
   private double _prevValue;

   public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
   {
      if (!(value is double) && !(value is float))
         return null;

      return XMath.Eq(_prevValue, System.Convert.ToDouble(value))
            ? Binding.DoNothing
            : value.ToString();
   }

   public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
   {
      if (!(value is string))
         return null;

      double doubleValue = double.Parse(value.ToString());

      if (!XMath.Eq(_prevValue, doubleValue))
         _prevValue = doubleValue;

      return doubleValue;
   }
}

ConverterBase - base class to "converter as markup extension", XMath.Eq - just comparind floating point numbers with epsilon.