Jason Ridge
Jason Ridge

Reputation: 1868

Validation error adornment not clearing when HasError is false

INTRODUCTION

I have created a DecimalTextBox user control which has ValidationRules attached to it to prevent nulls, have a minimum and maximum range and it has event handlers to prevent non-decimal values. I have used

ValidatesOnTargetUpdated="True"

on the bindings because I want the validation to be activated immediately (and I had an issue before where the min and max values were changing but the validation wasn't being reevaluated).

The null validation I have done is dependent on the value of an "AllowNull" dependency property: If the control specifies true then the control is valid even when the value is null. When false then null is not allowed. The default of this property is False


THE PROBLEM

I am setting the AllowNull to true when using it in a certain UserControl. Unfortunately because the ValidatesOnTargetUpdated is set to true, the control is validated before the xaml sets the AllowNull to true, while it is still in its default false setting.

This causes an error before loading, as the binding to the text of the TextBox is also not resolved yet, so before loading it doesn't allow null, and the value of the text is null.

This is all fine and dandy because after the loading the validation is reevaluated with the new AllowNull value (of true) and the error is removed.

However the red validation adornment remains. Not entirely sure how to get rid of it.


THE CODE The xaml for the textbox usercontrol:

<UserControl x:Class="WPFTest.DecimalTextBox"
         x:Name="DecimalBox"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         xmlns:v="clr-namespace:ValidationRules"
         mc:Ignorable="d" 
         d:DesignHeight="25" d:DesignWidth="100" Initialized="DecimalBox_Initialized" >

    <TextBox x:Name="textbox">
        <TextBox.Text>
            <Binding ElementName="DecimalBox" TargetNullValue="" Path="Text" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay" ValidatesOnDataErrors="True" ValidatesOnExceptions="True" NotifyOnValidationError="True">
                <Binding.ValidationRules>
                    <v:DecimalRangeRule  ValidatesOnTargetUpdated="True">
                        <v:DecimalRangeRule.MinMaxRange>
                            <v:MinMaxValidationBindings x:Name="minMaxValidationBindings"/>
                        </v:DecimalRangeRule.MinMaxRange> 
                    </v:DecimalRangeRule>
                    <v:NotEmptyRule  ValidatesOnTargetUpdated="True">
                        <v:NotEmptyRule.AllowNull>
                            <v:AllowNullValidationBinding x:Name="allowNullValidationBindings"></v:AllowNullValidationBinding>
                        </v:NotEmptyRule.AllowNull>
                    </v:NotEmptyRule>
                </Binding.ValidationRules>
            </Binding>
        </TextBox.Text>
    </TextBox>
</UserControl>

The code behind for the control:

    public static DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(textboxcontrol), new PropertyMetadata());
    public static DependencyProperty MinimumProperty = DependencyProperty.Register("Minimum", typeof(decimal), typeof(DecimalTextBox), new PropertyMetadata(0M));
    public static DependencyProperty MaximumProperty = DependencyProperty.Register("Maximum", typeof(decimal), typeof(DecimalTextBox), new PropertyMetadata(0M));
    public static DependencyProperty AllowNullProperty = DependencyProperty.Register("AllowNull", typeof(bool), typeof(DecimalTextBox), new UIPropertyMetadata(false));

    public bool AllowNull
    {
        get { return (bool)GetValue(AllowNullProperty); }
        set { SetValue(AllowNullProperty, value); }
    }
    public decimal Minimum
    {
        get { return (decimal)GetValue(MinimumProperty); }
        set { SetValue(MinimumProperty, value); }
    }
    public decimal Maximum
    {
        get { return (decimal)GetValue(MaximumProperty); }
        set { SetValue(MaximumProperty, value); }
    }
    public string Text
    {
        get
        {
            return (string)GetValue(TextProperty);
        }
        set
        {
            SetValue(TextProperty, value);
        }
    }



    private void DecimalBox_Initialized(object sender, EventArgs e)
    {
        Binding minBinding = new Binding("Minimum");
        minBinding.Source = this;
        Binding maxBinding = new Binding("Maximum");
        maxBinding.Source = this;
        Binding allownullBinding = new Binding("AllowNull");
        allownullBinding.Source = this;

        minMaxValidationBindings.SetBinding(ValidationRules.MinMaxValidationBindings.minProperty, minBinding);
        BindingOperations.SetBinding(minMaxValidationBindings, ValidationRules.MinMaxValidationBindings.maxProperty, maxBinding);
        BindingOperations.SetBinding(allowNullValidationBindings, ValidationRules.AllowNullValidationBinding.allowNullProperty, allownullBinding);
    }

And the Validation Rules (#Note: They are inside the ValidationRules namespace):

public class NotEmptyRule : ValidationRule
{

    public NotEmptyRule()
    {
    }
    private AllowNullValidationBinding _allowNullBinding;

    public AllowNullValidationBinding AllowNull
    {
        get { return _allowNullBinding; }
        set { _allowNullBinding = value; }
    }

    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        if (!_allowNullBinding.AllowNull)
            if (string.IsNullOrEmpty((string)value))
                return new ValidationResult(false,
                  "Value cannot be null or empty.");
            else
                return new ValidationResult(true, null);

        else
           return new ValidationResult(true, null);

    }
}

public class DecimalRangeRule : ValidationRule
{
    private MinMaxValidationBindings _bindableMinMax;
    public MinMaxValidationBindings MinMaxRange
    {
        get { return _bindableMinMax; }
        set
        {
            _bindableMinMax = value;

        }
    }


    public DecimalRangeRule()
    {

    }
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {

        decimal number = 0;

        if(decimal.TryParse((string)value,out number))
            if (_bindableMinMax.Min != _bindableMinMax.Max || _bindableMinMax.Min != 0)
            {
                if ((number < _bindableMinMax.Min) || (number > _bindableMinMax.Max))
                {
                    return new ValidationResult(false,
                      "Please enter an decimal in the range: " + _bindableMinMax.Min + " - " + _bindableMinMax.Max + ".");
                }
                else
                {
                    return new ValidationResult(true, null);
                }
            }
            else
                return new ValidationResult(true, null);
        else
            return new ValidationResult(true, null);
    }
}

public class AllowNullValidationBinding:FrameworkElement
{
     public static readonly DependencyProperty allowNullProperty = DependencyProperty.Register(
        "AllowNull", typeof(bool), typeof(AllowNullValidationBinding), new UIPropertyMetadata(false));

    public bool AllowNull
    {
        get{return (bool)GetValue(allowNullProperty);}
        set{SetValue(allowNullProperty,value);}
    }
    public AllowNullValidationBinding()
    {}
}

public class MinMaxValidationBindings : FrameworkElement
{
    public static readonly DependencyProperty minProperty = DependencyProperty.Register(
        "Min", typeof(decimal), typeof(MinMaxValidationBindings), new UIPropertyMetadata(0.0m));

    public static readonly DependencyProperty maxProperty = DependencyProperty.Register(
        "Max", typeof(decimal), typeof(MinMaxValidationBindings), new UIPropertyMetadata(0.0m));

    public decimal Min
    {
        get { return (decimal)GetValue(minProperty); }
        set { SetValue(minProperty, value); }
    }

    public decimal Max
    {
        get { return (decimal)GetValue(maxProperty); }
        set { SetValue(maxProperty, value); }
    }

    public MinMaxValidationBindings() { }

}

The FrameworkElement bindings are used so my ValidationRules can have dependency properties to bind to. This allows me to specify a min and max value outside the control.


SUMMARY

I've checked the HasError by using the Validation.GetHasError(DecimalBox) (for both the control itself as well as it's inner TextBox) after load and it produces false.

I know that if I remove the ValidatesOnTargetUpdated="True" The red wont appear, but I need it. So why is the validation being reevaluated but the red border adornment not dissapearing?

I don't know much about the Validation class or its static methods, but is there something in there to remove the adornment. The ClearInvalid method isn't gonna help cause I don't have an error to supply it.

Any ideas?

u_u


EDIT

I've done some more investigating and found the following things:

  1. If I change the text after the load to to an amount greater then the maximum and then change it back the error adorner dissapears
  2. If I change the value of the Text dependency property inside the controls load event programically to an amount greater then the maximum and change it back, the adorner is still there.
  3. If I change the text after the load to a null value and then change it back the adorner is still there.
  4. If I change the value of the viewmodel's property bound to the text inside the constructor of the view model, the adorner is still there
  5. If I change the value of the viewmodel's property bound to the text inside the constructor of the view model to a value greater than the maximum, and then change it back, the adorner is still there.
  6. If I change the value of the viewmodel's property bound to the text with a button to a different value, and then change it back, the adorner dissapears
  7. If I change the value of the viewmodel's property bound to the text with a button to a value greater than the maximum, and then change it back, the adorner dissapears

I'm still fairly stumped. Ive tried methods like UpdateLayout() and tried moving the adorner to different controls and moving it back using the Validation.SetValidationAdornerSite. Ima keep trying but I dont know what to do really.

u_u


2ND EDIT

Ok what I had done in the meantime was place an AdornerDecorator around the TextBox and then in the textboxes load event change the maximum to 1 and the value to 2, then change it back in order to get the textbox to refresh.

This was working, but I hated the idea cause its horrible code.

However this solution is no longer viable. I had some code that was doing stuff on the property changed of one of the properties bound to one of these DecimalTextBoxes. Then because the property was being changed and changed back in the load event, the other code was also being run and causing errors. I have to find a better solution then this.

Does anyone know how to refresh the validation adorner?

u_u

Upvotes: 1

Views: 3257

Answers (3)

zastrowm
zastrowm

Reputation: 8243

I had an issue similar to this where the adorner was not going away even though the underlying error did.

The ultimate workaround that I discovered was to force the layout to be updated by calling

Control.UpdateLayout()

which somehow forced WPF to sync-back up. I did the Update on a Control_Loaded event handler, but it might fix the issue at other times as well.

Upvotes: 0

Rhys Bevilaqua
Rhys Bevilaqua

Reputation: 2167

Here are a couple of workarounds for the problem as I've encountered it and been unable to find anything on it, I suspect that it's a bug somewhere in the framework caused by a race condition but couldn't find anything to back that up.

Reflection on private fields (yuck)

Because you know that your field doesn't have errors you can do this to iterate the controls and kill the adorners

var depPropGetter = typeof (Validation).GetField("ValidationAdornerProperty", BindingFlags.Static | BindingFlags.NonPublic);
var validationAdornerProperty = (DependencyProperty)depPropGetter.GetValue(null);
var adorner = (Adorner)DateActionDone.GetValue(validationAdornerProperty);

if (adorner != null && Validation.GetHasError(MyControl))
{
    var adorners = AdornerLayer.GetAdornerLayer(MyControl).GetAdorners(MyControl);
    if (adorners.Contains(adorner))
        AdornerLayer.GetAdornerLayer(MyControl).Remove(adorner);
}

Alternatively you can call the Validation.ShowAdornerHelper method through reflection, which I hadn't directly tried so haven't bothered writing code for.

Refreshing ALL bindings forcefully

We can take advantage of your finding that making the binding invalid and then valid again will clear the adorner down for us.

This is the solution i've decided to go with, and happens to be quite effective.

Because I'm using IDataErrorInfo in my base view model I can do something along the lines of this, depending on how you handle your validation you might have more trouble getting these to revalidate.

    string IDataErrorInfo.this[string columnName]
    {
        get
        {
            if (_refreshing) return "Refreshing";
            return ValidationEngine.For(this.GetType()).GetError(this, columnName);
        }
    }

    bool _refreshing = false;
    public void RefreshValidation()
    {
        _refreshing = true;
        this.NotifyOfPropertyChange(string.Empty);
        _refreshing = false;
        this.NotifyOfPropertyChange(string.Empty);
    }

Upvotes: 2

Try with to remove the error on event LayoutUpdated of your control (put a flag on the event to do it only once)

Validation.ClearInvalid(SystemCode.GetBindingExpression(TextBox.TextProperty));

and then re-evaluate your Validations Rules (refreshing your bindings).

var dp = SystemCode.GetBindingExpression(TextBox.TextProperty);
dp.UpdateSource();

Upvotes: 0

Related Questions