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!
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)
{
...
}
}
.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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With