I have a sample where I bind a view model's properties with some TextBox
controls, including validation rules. In most cases, this works fine. But when I try to include the IsFocused
property of the bound TextBox
, I am having trouble in the case when an invalid number is entered in the control.
When I input the wrong number in the TextBox
controls that are bound directly to the view model's property, the errors are shown as expected (red border around the TextBox
). But in the TextBox
that is bound with a MultiBinding
that includes both the view model property and the IsFocused
property of the TextBox
, the error is not shown and the value gets reset to the previous valid value.
For example, if a number less than 10 is invalid, and I input 3, when the TextBox
loses focus, a red border normally would appear in the TextBox
signaling the error. But in the TextBox
which includes IsFocused
as a source for its binding, the value changes back to the previous valid value (if there was a 39 before I entered 3, the TextBox
changes back to 39).
Using the code below you can reproduce the issue:
TestViewModel.cs
public class TestViewModel
{
public double? NullableValue { get; set; }
}
MainWindow.xaml
<Window x:Class="TestSO34204136TextBoxValidate.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:TestSO34204136TextBoxValidate"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<l:TestViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="Nullable: "/>
<TextBox VerticalAlignment="Top" Grid.Column="1">
<TextBox.Text>
<MultiBinding Mode="TwoWay">
<Binding Path="NullableValue"/>
<Binding Path="IsFocused"
RelativeSource="{RelativeSource Self}"
Mode="OneWay"/>
<MultiBinding.ValidationRules>
<l:ValidateIsBiggerThanTen/>
</MultiBinding.ValidationRules>
<MultiBinding.Converter>
<l:TestMultiBindingConverter/>
</MultiBinding.Converter>
</MultiBinding>
</TextBox.Text>
</TextBox>
<TextBox VerticalAlignment="Top" Grid.Column="2"/>
</Grid>
</Window>
TestMultiBindingConverter.cs
public class TestMultiBindingConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values[0] != null)
return values[0].ToString();
return DependencyProperty.UnsetValue;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
if (value != null)
{
double doubleValue;
var stringValue = value.ToString();
if (Double.TryParse(stringValue, out doubleValue))
{
object[] values = { doubleValue };
return values;
}
}
object[] values2 = { DependencyProperty.UnsetValue };
return values2;
}
}
ValidateIsBiggerThanTen.cs
public class ValidateIsBiggerThanTen : ValidationRule
{
private const string errorMessage = "The number must be bigger than 10";
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
var error = new ValidationResult(false, errorMessage);
if (value == null)
return new ValidationResult(true, null);
var stringValue = value.ToString();
double doubleValue;
if (!Double.TryParse(stringValue, out doubleValue))
return new ValidationResult(true, null);
if (doubleValue <= 10)
return error;
return new ValidationResult(true, null);
}
}
Why are the errors not showing for the TextBox
in the above example?
The cause of the behavior you're seeing is specifically that you've bound the TextBox
's IsFocused
property in your MultiBinding
. This directly has the effect of forcing an update of the target of the binding when the focus changes.
In the scenario where validation fails, there is a very brief moment when the validation rule has fired, the error is set, but the focus hasn't actually been changed yet. But this all happens too fast for a user to see. And since validation failed, the source of the binding is not updated.
So when the IsFocused
property value changes, after the validation and rejection of the entered value happens, the next thing to happen is that the binding is re-evaluated (because one of the source properties changed!) to update the target. And since the actual source value never changed, the target (the TextBox
) reverts from whatever you typed back to whatever was stored in the source.
How should you fix this? It depends on the exact behavior desired. You have three basic options:
IsFocused
, and add UpdateSourceTrigger="PropertyChanged"
. This will keep the basic current behavior of copying the old value back when focus is lost, but will at least provide the user with immediate validation feedback as the value is edited.IsFocused
altogether. Then the target of the binding won't depend on that, and won't be re-evaluated when focus changes. Problem solved. :)IsFocused
, and add logic so that the interaction with validation does not result in copying a stale value back to the TextBox
.Based on our comments back and forth, it seems that the third option above is the preferred one for your scenario, as you desire to format the text representation of the value differently when the control has focus vs. when it does not.
I am skeptical of the wisdom of a user interface that formats data differently depending on whether the control is focused or not. Of course, it makes complete sense for focus changes to affect the overall visual presentation, but that would generally involve things like underlining, highlighting, etc. Displaying a completely different string depending on whether the control is focused seems likely to interfere with user comprehension and possibly annoy them as well.
But I'm in agreement that this is a subjective point, and clearly in your case you have this specific behavior that is desirable for your specification and needs to be supported. So with that in mind, let's look at how you can accomplish that behavior…
If you want to be able to bind to the IsFocused
property, but not have changes to focus copy over the current contents of the control if the source has not actually been updated yet (i.e. if a validation error prevented that from happening), then you can also bind to the Validation.HasError
property, and use that to control the converter's behavior. For example:
class TestMultiBindingConverter : IMultiValueConverter
{
private bool _hadError;
public object Convert(object[] values,
Type targetType, object parameter, CultureInfo culture)
{
bool? isFocused = values[1] as bool?,
hasError = values[2] as bool?;
if ((hasError == true) || _hadError)
{
_hadError = true;
return Binding.DoNothing;
}
if (values[0] != null)
{
return values[0].ToString() + (isFocused == true ? "" : " (+)");
}
return DependencyProperty.UnsetValue;
}
public object[] ConvertBack(object value,
Type[] targetTypes, object parameter, CultureInfo culture)
{
if (value != null)
{
double doubleValue;
var stringValue = value.ToString();
if (Double.TryParse(stringValue, out doubleValue))
{
object[] values = { doubleValue };
_hadError = false;
return values;
}
}
object[] values2 = { DependencyProperty.UnsetValue };
return values2;
}
}
The above adds a field _hadError
that "remembers" what's happened recently to the control. If the converter is called while validation is detecting an error, the converter returns Binding.DoNothing
(which has the effect its name suggests :) ), and sets the flag. Thereafter, no matter what happens, as long as that flag is set the converter will always do nothing.
The only way that the flag will get cleared is if the user eventually enters text that is valid. Then the converter's ConvertBack()
method will be called to update the source, and in doing so it can clear the _hadError
flag. This ensures that the control contents will never get overwritten due to binding updates, except when there has been no error since the last time the source was updated.
Here's the XAML example above updated to use the additional binding input:
<Window x:Class="TestSO34204136TextBoxValidate.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:TestSO34204136TextBoxValidate"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<l:TestViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="Nulleable: "/>
<TextBox x:Name="textBoxWrapper" Grid.Column="1" VerticalAlignment="Top">
<TextBox.Text>
<MultiBinding x:Name="TextBoxBinding" Mode="TwoWay"
UpdateSourceTrigger="PropertyChanged">
<Binding Path="NulleableValue"/>
<Binding Path="IsFocused"
RelativeSource="{RelativeSource Self}"
Mode="OneWay"/>
<Binding Path="(Validation.HasError)"
RelativeSource="{RelativeSource Self}"
Mode="OneWay"/>
<MultiBinding.ValidationRules>
<l:ValidateIsBiggerThanTen/>
</MultiBinding.ValidationRules>
<MultiBinding.Converter>
<l:TestMultiBindingConverter/>
</MultiBinding.Converter>
</MultiBinding>
</TextBox.Text>
</TextBox>
<TextBox VerticalAlignment="Top" Grid.Column="2"/>
</Grid>
</Window>
I should point out, in case it's not obvious: the _hadError
field is for the converter itself. For the above to work correctly, you'll need a separate instance of the converter for each binding to which it's applied. There are alternative ways to track such a flag for each control uniquely, but I feel an extended discussion of the options in that respect are outside the scope of this question. Feel free to explore on your own, and post a new question regarding that aspect if you are unable to address the issue adequately on your own.