9
votes

I have a WPF textbox defined in XAML like this:

<Window.Resources>        
    <Style x:Key="textBoxInError" TargetType="{x:Type TextBox}">
        <Style.Triggers>
            <Trigger Property="Validation.HasError" Value="true">
                <Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
            </Trigger>
        </Style.Triggers>
    </Style>
</Window.Resources>

<TextBox x:Name="upperLeftCornerLatitudeTextBox" Style="{StaticResource textBoxInError}">
    <TextBox.Text>
        <Binding Path="UpperLeftCornerLatitude" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">
            <Binding.ValidationRules>
                <local:LatitudeValidationRule ValidationStep="RawProposedValue"/>
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

As you can see, my textbox is bound to a decimal property on my business object called UpperLeftCornerLatitude which looks like this:

private decimal _upperLeftCornerLongitude;
public decimal UpperLeftCornerLatitude
{
    get { return _upperLeftCornerLongitude; }
    set
    {
        if (_upperLeftCornerLongitude == value)
        {
            return;
        }

        _upperLeftCornerLongitude = value;
        OnPropertyChanged(new PropertyChangedEventArgs("UpperLeftCornerLatitude"));
    }
}

My user will be entering a latitude value into this textbox and in order to validate that entry, I've created a validation rule that looks like this:

public class LatitudeValidationRule : ValidationRule
{
    public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
    {
        decimal latitude;

        if (decimal.TryParse(value.ToString(), out latitude))
        {
            if ((latitude < -90) || (latitude > 90))
            {
                return new ValidationResult(false, "Latitude values must be between -90.0 and 90.0.");
            }
        }
        else
        {
            return new ValidationResult(false, "Latitude values must be between -90.0 and 90.0.");
        }

        return new ValidationResult(true, null);
    }
}

My textbox initially starts off empty and I have a breakpoint set at the beginning of my validation rule. I enter 1 in the textbox and when my debugger breaks inside of the validation rule, I can see that value = "1". So far so good. Now I continue running and enter a decimal point in the textbox (so we should have "1." now). Again, the debugger breaks inside of the validation rule and, as expected, value = "1.". If I step through the validation rule code, I see that it passes the latitude value check and returns the following:

new ValidationRule(true, null);

However, as soon as the validation rule returns and I step into the next line of code, I find myself on the first line of my UpperLeftCornerLatitude property setter. Mousing over value here reveals that it's a value of "1" instead of "1." as I would expect. So naturally when I continue running my code, I end up back in the textbox staring at a value of "1" instead of "1.". If I remove all of the breakpoints, the effect is that I can't seem to enter a decimal point in the textbox. Is there something obvious that I'm missing here that's causing my setter to end up with a value of "1" even though I have entered "1." in the textbox? Thanks very much!

6
Doesn't have anything to do with the ValidationRule, it has to do with the Converter. When you type "1." it cannot parse that as a decimal so it fallsback on "1"James Sampica

6 Answers

27
votes

Here are a few ways to fix this problem

A. Specify LostFocus (textbox default) for your binding

<Binding Path="UpperLeftCornerLatitude" Mode="TwoWay" UpdateSourceTrigger="LostFocus">
</Binding>

B. Specify a Delay for the binding that will allow for some time for you to type the decimal

<Binding Path="UpperLeftCornerLatitude" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" Delay="1000">
</Binding>

C. Change decimal to string and parse it yourself

D. Write a ValueConverter to override the default conversion process

class DecimalConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        ...
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        ...
    }
}
6
votes

.NET 4.5 UPDATE

In .NET 4.5, Microsoft decided to introduce a breaking change to the way that data is entered into the TextBox control when the binding UpdateSourceTrigger is set to PropertyChanged. A new KeepTextBoxDisplaySynchronizedWithTextProperty property was introduced that was supposed to recreate the previous behaviour... setting it to false should return the previous behaviour:

FrameworkCompatibilityPreferences.KeepTextBoxDisplaySynchronizedWithTextProperty = false;

Unfortunately, although it allows us to enter a numerical separator again, it doesn't quite work as it used to. For example, the separator will still not appear in the TextBox.Text property value until it is followed by another number and this can cause issues if you have custom validation. However, it's better than a slap in the face.

2
votes

This really isn't going to be pretty, since WPF is going to automatically try to convert the string values to decimals as you type; I think this is due to the default Behavior<TextBox>. I think the simplest way for you to resolve this quickly would be to bind your control to a string property and expose another decimal property:

private string _upperLeftCornerLongitudeStr;
public string UpperLeftCornerLatitudeStr
{
    get { return _upperLeftCornerLongitudeStr; }
    set
    {
        if (_upperLeftCornerLongitudeStr == value)                
            return;                

        _upperLeftCornerLongitudeStr = value;
        OnPropertyChanged("UpperLeftCornerLatitudeStr");
    }
}

public decimal? UpperLeftCornerLatitude
{
    get
    {
        decimal val;
        if (decimal.TryParse(_upperLeftCornerLongitudeStr, out val))
            return val;

        return null;
    }
    set { _upperLeftCornerLongitudeStr = value != null ? value.ToString() : null; }
}

That being said, you may want to look into different approaches that would prevent your used from entering invalid characters in the first place:

DecimalUpDown in WPF Toolkit

TextBox Input Behavior - A little more complex

0
votes

For my use-case I have chosen to parse a decimal? as a string. I have used this code on the setter, so that if an invalid entry is entered, it isn't stored:

public string Price {
    get { return this._price.ToString(); }
    set
    {
        if (value != null)
        {
            decimal p;
            if (decimal.TryParse(value, out p))
            {
                this._price = (value);
                this.NotifyPropertyChanged("Price");
            }

        }
    }
}
0
votes

In the validation rule code, return 'False' ValidationResult if the input value (as string) end with the NumberDecimalSeparator. You'll be able to continue typing in the text box ...

-1
votes

I've run into the same issue, and I believe it's caused by the attempt to set "1." to a decimal property. "1." does not parse to a valid decimal value.

Your options are to either

A) Remove the "UpdateSourceTrigger" from your TextBox's Text property binding. This will allow any text to be entered and validation is performed upon the TextBox losing focus;

or

B) Add your decimal point AFTER you've added your decimal values. For example, to enter "1.25", enter "125", then position the cursor between the "1" and the "2" and enter the ".". Tabbing off the control at this point yields a bound value of 1.25, which DOES parse to a valid decimal value.