10

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!

bmt22033
  • 6,880
  • 14
  • 69
  • 98
  • 1
    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" – jamesSampica Jan 08 '14 at 20:40

6 Answers6

29

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)
    {
        ...
    }
}
Jai
  • 8,165
  • 2
  • 21
  • 52
jamesSampica
  • 12,230
  • 3
  • 63
  • 85
  • Option B seems to be working pretty well for me. NICE! – Gary Feb 05 '14 at 16:38
  • 9
    Thanks for this great answer. Just wanted to point out that while option B works pretty nicely, it's a bit of a risky approach. Imagine the user is a little distracted, he started typing "1.", in the meantime he's checking some papers, and then goes back to it, typing "23". But in the meantime the delay passed and the decimal point was removed, so what should be "1.23" is now "123". When money is involved, this is totally not desirable. :) – Gigi Dec 03 '14 at 13:35
6

.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.

Sheridan
  • 68,826
  • 24
  • 143
  • 183
2

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

Community
  • 1
  • 1
Matt
  • 2,682
  • 1
  • 17
  • 24
0

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");
            }

        }
    }
}
Tunaki
  • 132,869
  • 46
  • 340
  • 423
user3791372
  • 4,445
  • 6
  • 44
  • 78
0

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 ...

Dinidu Hewage
  • 2,169
  • 6
  • 40
  • 51
-2

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.

Jonathan Shay
  • 916
  • 8
  • 12