2

I have a custom TextBox that adds MinMaxValidationRule when both Min and Max properties are set

public class TextBox2 : TextBox
{
    public static readonly DependencyProperty MinProperty = DependencyProperty.Register(
        nameof(Min), typeof(double?), typeof(TextBox2),
        new PropertyMetadata(null, MinMaxChangeCallback));

    public double? Min
    {
        get => (double?)GetValue(MinProperty);
        set => SetValue(MinProperty, value);
    }

    public static readonly DependencyProperty MaxProperty = DependencyProperty.Register(
        nameof(Max), typeof(double?), typeof(TextBox2),
        new PropertyMetadata(null, MinMaxChangeCallback));

    public double? Max
    {
        get => (double?)GetValue(MaxProperty);
        set => SetValue(MaxProperty, value);
    }

    private static void MinMaxChangeCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var textBox2 = (TextBox2)d;
        if (textBox2.Min == null || textBox2.Max == null)
            return;
        var binding = textBox2.GetBindingExpression(TextProperty);
        var validationRules = binding
            .ParentBinding
            .ValidationRules;
        validationRules.Clear();
        var minMaxValidationRule = new MinMaxValidationRule((double)textBox2.Min, (double)textBox2.Max)
        {
            ValidatesOnTargetUpdated = true
        };
        validationRules.Add(minMaxValidationRule);
        binding.UpdateTarget(); // triggers the validation rule
    }
}

The validation rule is defined as follows

public class MinMaxValidationRule : ValidationRule
{
    public double Min { get; }
    public double Max { get; }

    public MinMaxValidationRule(double min, double max)
    {
        Min = min;
        Max = max;
    }

    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        if (double.TryParse((string)value, out var d)
            && d <= Max
            && d >= Min)
            return ValidationResult.ValidResult;

        return new ValidationResult(false, $"Value must be in [{Min},{Max}]");
    }
}

I've created a ItemsControl to display a list of ItemInfo objects

<ItemsControl
    Margin="8"
    ItemsSource="{Binding ItemInfos}">
    <ItemsControl.ItemTemplate>
        <DataTemplate DataType="{x:Type testApp:ItemInfo}">
            <testApp:TextBox2
                Style="{StaticResource MaterialDesignOutlinedTextBox}"
                Width="400"
                Margin="12"
                Padding="8"
                Min="{Binding Min}"
                Max="{Binding Max}"
                Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>
public partial class MainWindow
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = this;
        ItemInfos = new()
        {
            new()
            {
                Min = 0,
                Max = 100,
                Value = 50
            },
            new()
            {
                Min = 200,
                Max = 300,
                Value = 250
            },
        };
    }

    public List<ItemInfo> ItemInfos { get; }
}

public class ItemInfo
{
    public double Min { get; set; }
    public double Max { get; set; }
    public double Value { get; set; }
}

When writing "2" in the first TextBox (changing the value from 50 to 502), WPF is using the validation rule of the second textBox, but I expect that each TextBox uses its own ValidationRule.

Any idea on how to fix this?

Demo of the actual behaviour

1
  • I am guessing that since the validation rule is applied to the Binding of the DataTemplate it is shared by the two TextBoxes but I am not sure how to resolve it. Commented Oct 1, 2023 at 22:00

1 Answer 1

1

This seems to be a known limitation when using Templates. There is a workaround to this, by using another override of Validate of the ValidationRule so that you can access the control and the updated value.

You can edit the MinMaxValidationRule class as follow

public class MinMaxValidationRule : ValidationRule
{
    public MinMaxValidationRule()
    {
        ValidatesOnTargetUpdated = true;
    }
    
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        throw new NotImplementedException();
    }

    public override ValidationResult Validate(object value, CultureInfo cultureInfo, BindingExpressionBase owner)
    {
        // This will be the actual validation (use the BindingExpressionBase to get acess to the TextBox2 control)
        var textBox2 = (TextBox2)owner.Target;
        if (textBox2.Min != null
            && textBox2.Max != null
            && double.TryParse((string)value, out var d) is var b
            && (!b || d > textBox2.Max || d < textBox2.Min)
           )
            return new ValidationResult(false, $"Value must be in [{textBox2.Min},{textBox2.Max}]");
        return ValidationResult.ValidResult;        
    }
}

Now since we read the current value of the Min and Max property, you don't need the MinMaxChangeCallback so you can just add the Validation rule on the loaded event.

public TextBox2()
{
    this.Loaded += TextBox2_Loaded;
}

private void TextBox2_Loaded(object sender, RoutedEventArgs e)
{
    var binding = this.GetBindingExpression(TextProperty);
    var validationRules = binding
        .ParentBinding
        .ValidationRules;
    validationRules.Clear();
    var minMaxValidationRule = new MinMaxValidationRule();
    validationRules.Add(minMaxValidationRule);
    binding.UpdateTarget(); // triggers the validation rule
}
Sign up to request clarification or add additional context in comments.

2 Comments

Great, your solution is different, not like the one suggested by KoenJ.. When setting ValidationStep = ValidationStep.UpdatedValue; in MinMaxValidationRule ctor, the object value will be the BindingExpression (from which you can access the TextBox2 instance), but the issue is that when I want to double.TryParse(textBox2.Text, out var d), the value of textBox2.Text is the old one (i.e 50), not the new one (i.e 502).. This is solved with the override approach you've suggested, Where I'm getting the Min/Max from BindingExpression and the updated text (i.e 502) from object value.
@Muhammad Sulaiman : Well he does mention getting the BindingExpression into the ValidationRule then dealing with its state to make the validation (though I didn't investigate the link with ValidationStep.UpdatedValue). Anyway I am glad that my answer helped you :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.