1
votes

I have a tricky problem with a WPF data grid and validation error tooltips not updating when validation messages change. This is with .Net 4 code, so I can't use INotifyDataErrorInfo.

I have an ObservableCollection bound to a datagrid. The object type in the collection implements IDataErrorInfo so that we can support validation and highlight fields that have invalid values. This works fine in most cases. However, there are issues with the message that is displayed in the tooltip in the following scenario:

  1. Field A has two rules Rule 1 and Rule S (a shared rule)
  2. Field B has one rule Rule S (the shared rule)
  3. Rule S is a shared rule than references both Field A and Field B
  4. If Rule 1 and Rule S are both invalid, we get the following validation tooltips shown for each field, which is the behaviour we want:

    Field A < "Rule 1 is invalid. Rule S is invalid"
    Field B < "Rule S is invalid"
    
  5. If we now edit field B to make Rule S valid. We want both the tooltip messages to update as follows:

    Field A < "Rule 1 is invalid."
    Field B < (valid - no tooltip)
    

Note, the validation state of field A has not changed (Validation.HasError does not change value), only the message bound to the tooltip.

What we actually see is:

Field A < "Rule 1 is invalid. Rule S is invalid"
Field B < (valid - no tooltip)

NB the underlying ValidationError data on the class instance is correct at this point.

It appears that the UI will not update the tooltip text for field A unless we force it to requery the state and call IDataErrorInfo.this[string columnName] again. The only way I have found to force this to happen is to manually raise the property changed event for field A. However, I don't want to have to do this since the value of field A hasn't actually changed, only the bound error messages. While this solution works the extra and unnecessary property changed events play havok with the performance with lots of data.

What can I do to force IDataErrorInfo.this[string columnName] to be called for field B without having to go as far as raising a property changed event?

NB here's the error data template we use to display the validation message.

    <!-- ERROR HANDLING Data Template -->
    <Style x:Key="controlBaseStyle"
           TargetType="{x:Type Control}">

        <Setter Property="Validation.ErrorTemplate">
            <Setter.Value>
                <ControlTemplate>
                    <Border BorderBrush="Red" 
                            BorderThickness="2"
                            Visibility="{Binding ElementName=placeholder, Path=AdornedElement.IsVisible, Converter={StaticResource BooleanToVisibilityConverter}}">
                        <AdornedElementPlaceholder x:Name="placeholder"/>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>

        <Setter Property="ToolTipService.ShowOnDisabled" Value="true"/>

        <Style.Triggers>
            <Trigger Property="Validation.HasError" Value="true">
                <Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors).CurrentItem, Converter={StaticResource ErrorContentConverter}}"/>
            </Trigger>


            <!--We don't want to see the validation if the control is disabled.  This doesn't affect it if the control is read only. -->
            <Trigger Property="IsEnabled" Value="False">
                <Setter Property="Validation.ErrorTemplate">
                    <Setter.Value>
                        <ControlTemplate>
                            <AdornedElementPlaceholder x:Name="placeholder"/>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Trigger>
        </Style.Triggers>

    </Style>
2

2 Answers

0
votes

You simply have to raise PropertyChanged for both properties in each setter, i.e.:

  • Raise PropertyChanged for A in the setter of A and B
  • Raise PropertyChanged for B in the setter of A and B

That's the only way I am aware of.

0
votes

I usually include an IsValid property on my objects that will go through the validation rules and return true/false if the object is valid or not.

Typically my code looks like this. Your solution may look different depending on how you implement IDataErrorInfo

public class MyObject : ValidatingObject
{
    public MyObject()
    {
        // Add Properties to Validate here
        this.ValidatedProperties.Add("FieldA");
        this.ValidatedProperties.Add("FieldB");
    }

    // Implement validation rules here
    public override string GetValidationError(string propertyName)
    {
        if (ValidatedProperties.IndexOf(propertyName) < 0)
        {
            return null;
        }

        string s = null;

        switch (propertyName)
        {
            case "FieldA":
            case "FieldB":
                if (FieldA <= FieldB)
                    s = "FieldA must be greater than FieldB";
                break;
        }

        return s;
    }

}

And my ValidatingObject base class which implements IDataErrorInfo usually contains the following:

#region IDataErrorInfo & Validation Members

/// <summary>
/// List of Property Names that should be validated
/// </summary>
protected List<string> ValidatedProperties = new List<string>();

public abstract string GetValidationError(string propertyName);

string IDataErrorInfo.Error { get { return null; } }

string IDataErrorInfo.this[string propertyName]
{
    get { return this.GetValidationError(propertyName); }
}

public bool IsValid
{
    get
    {
        return (GetValidationError() == null);
    }
}

public string GetValidationError()
{
    string error = null;

    if (ValidatedProperties != null)
    {
        foreach (string s in ValidatedProperties)
        {
            error = GetValidationError(s);
            if (error != null)
            {
                return error;
            }
        }
    }

    return error;
}

#endregion // IDataErrorInfo & Validation Members

Then you just trigger the validation by calling IsValid whenever you want, such as

if (SomeObject.IsValid)
    CanSave = true;

It's also not uncommon for me to have a PropertyChange notification that re-evaluates a command's CanSave() when a property changes, such as

if (e.PropertyName == "FieldA" || e.PropertyName == "FieldB")
    ((DelegateCommand)SaveCommand).RaiseCanExecuteChanged();

and

void CanSave()
{
    return SomeObject.IsValid;
}

But with all that said, if the only time you will be re-evaluating if a field is valid or not is when another field changes, then it's probably best to just go with what Daniel said and raise a PropertyChange notification for FieldB when FieldA changes.