Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF validation rule preventing decimal entry in textbox?

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!

like image 828
bmt22033 Avatar asked Jan 08 '14 19:01

bmt22033


3 Answers

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)
    {
        ...
    }
}
like image 84
James Sampica Avatar answered Oct 29 '22 19:10

James Sampica


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

like image 6
Sheridan Avatar answered Oct 29 '22 21:10

Sheridan


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

like image 2
Matt Avatar answered Oct 29 '22 21:10

Matt