4
votes

Overview:

I have a Silverlight 4 app where I am seeing problematic behavior from the ValidationSummary control when used with a ListBox and am hoping someone can help me out.

At a high level, I have a ListBox where each row is defined (via the ItemTemplate) with a textbox named "txtInput". The textbox is bound (TwoWay) to an object property marked with a DataAnnotation.Range attribute. The ListBox is bound to a collection of these objects. In the same parent control, I also have a ValidationSummary control.

Scenario:

Imagine a case where there are two or more objects in the item collection. The user would see the ListBox with multiple rows, each containing a textbox. If the user types invalid data into the first textbox, a ValidationException is thrown as expected, and the ValidationSummary control shows the error, as expected. The textbox also gets the validation error styling (red border).

Then, if the user enters invalid data into the second row's text box (without fixing the data in the first textbox), the second textbox also throws a ValidationException and gets the validation error styling (red border), as expected, HOWEVER, the ValidationSummary control shows only one instance of the error message.

Then, if the user fixes either one (but not both) of the invalid text entries, the fixed textbox has the validation styling (red border) removed, and the ValidationSummary box goes away (meaning it thinks all validation errors have been resolved and has .HasErrors set to false). The second (still invalid) textbox still has the validation error styling (red border) applied.

My expectation is that the still remaining invalid textbox would cause the ValidationSummary control to continue to be displayed. My assumption is that the ValidationSummary control is just keeping track of failures by property name and once there is a successful attempt to set a property of that name, it clears the error marker (ie: it doesn't account for the case where multiple instances of the same name occur).

Wanted Result:

Ultimately, what I'm trying to do is prevent the user from clicking the "Save" button on the screen when there is invalid data. I am currently doing this by binding the IsEnabled property of the button to the HasErrors property of the ValidationSummary, but this doesn't work if the ValidationSummary shows the above behavior.

Can anyone tell me either a way to get the ValidationSummary control to respect multiple failures of the same (repeated) textbox, or provide a viable alternative way to disable the Save button when these failures exist? (note: in my actual app, each row has multiple input controls, so any solution would need to consider that)

XAML snippet:

    <sdk:ValidationSummary x:Name="valSummary" />
    <ListBox ItemsSource="{Binding DomainObjectCollection, Mode=TwoWay, ValidatesOnDataErrors=True, ValidatesOnNotifyDataErrors=True, ValidatesOnExceptions=true, NotifyOnValidationError=true}" >
        <ListBox.ItemTemplate>
            <DataTemplate>
                <TextBox Name="txtInput" Text="{Binding DecimalValue, Mode=TwoWay, ValidatesOnDataErrors=True, ValidatesOnNotifyDataErrors=True, ValidatesOnExceptions=true, NotifyOnValidationError=true}"/>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

    <Button x:Name="btnSave" Content="Save" Command="{Binding SaveButtonCommand}"  IsEnabled="{Binding HasErrors, ElementName=valSummary, Converter={StaticResource NotBoolConverter}}" />

Domain Object classes:

[System.Runtime.Serialization.CollectionDataContractAttribute()]
public partial class DomainObjectCollection : System.Collections.ObjectModel.ObservableCollection<DomainObject>
{
}

[System.Runtime.Serialization.DataContractAttribute()]
public partial class DomainObject : System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.IDataErrorInfo, System.ComponentModel.INotifyDataErrorInfo
{

    private int DomainObjectId_BackingField;

    private decimal DecimalValue_BackingField;

    private System.Collections.Generic.Dictionary<string, System.Collections.Generic.List<object>> _errors;

    [System.Runtime.Serialization.DataMemberAttribute()]
    public virtual int DomainObjectId
    {
        get { return this.DomainObjectId_BackingField; }
        set 
        {
            if (!DomainObjectId_BackingField.Equals(value))
            {
                this.DomainObjectId_BackingField = value;
                this.RaisePropertyChanged("DomainObjectId");
            }
        }
    }

    [System.Runtime.Serialization.DataMemberAttribute()]
    [System.ComponentModel.DataAnnotations.RangeAttribute(typeof(decimal), "0", "100", ErrorMessage = "Value must be from 0 to 100.")]
    public virtual decimal DecimalValue
    {
        get { return this.DecimalValue_BackingField; }
        set
        {
            if (!DecimalValue_BackingField.Equals(value))
            {
                this.DecimalValue_BackingField = value;
                this.RaisePropertyChanged("DecimalValue");
            }
        }
    }

    string System.ComponentModel.IDataErrorInfo.Error
    {
        get { return string.Empty; }
    }

    string System.ComponentModel.IDataErrorInfo.this[string propertyName]
    {
        get
        {
            var results = Validate(propertyName);
            return results.Count == 0 ? null : string.Join(System.Environment.NewLine, results.Select(x => x.ErrorMessage));
        }
    }

    private System.Collections.Generic.Dictionary<string, System.Collections.Generic.List<object>> Errors
    {
        get
        {
            if (_errors == null)
                _errors = new System.Collections.Generic.Dictionary<string, System.Collections.Generic.List<object>>();

            return _errors;
        }
    }

    bool System.ComponentModel.INotifyDataErrorInfo.HasErrors
    {
        get { return Errors.Count > 0; }
    }

    public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

    public event System.EventHandler<System.ComponentModel.DataErrorsChangedEventArgs> ErrorsChanged;

    protected internal void RaisePropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
        }
    }

    private void Raise(string propertyName)
    {
        if (ErrorsChanged != null)
            ErrorsChanged(this, new System.ComponentModel.DataErrorsChangedEventArgs(propertyName));
    }

    System.Collections.IEnumerable System.ComponentModel.INotifyDataErrorInfo.GetErrors(string propertyName)
    {
        System.Collections.Generic.List<object> propertyErrors;

        if (Errors.TryGetValue(propertyName, out propertyErrors))
            return propertyErrors;

        return new System.Collections.Generic.List<object>();
    }

    public void AddError(string propertyName, object error)
    {
        System.Collections.Generic.List<object> propertyErrors;

        if (!Errors.TryGetValue(propertyName, out propertyErrors))
        {
            propertyErrors = new System.Collections.Generic.List<object>();
            Errors.Add(propertyName, propertyErrors);
        }

        if (propertyErrors.Contains(error))
            return;

        propertyErrors.Add(error);
        Raise(propertyName);
    }

    public void RemoveError(string propertyName)
    {
        Errors.Remove(propertyName);
        Raise(propertyName);
    }

    public virtual System.Collections.Generic.List<System.ComponentModel.DataAnnotations.ValidationResult> Validate(string propertyName)
    {
        var results = new System.Collections.Generic.List<System.ComponentModel.DataAnnotations.ValidationResult>();
        var propertyInfo = GetType().GetProperty(propertyName);

        if (propertyInfo == null)
            return results;

        RemoveError(propertyName);

        var context = new System.ComponentModel.DataAnnotations.ValidationContext(this, null, null)
        {
            MemberName = propertyName
        };

        if (!System.ComponentModel.DataAnnotations.Validator.TryValidateProperty(propertyInfo.GetValue(this, null), context, results))
        {
            foreach (var validationResult in results)
                AddError(propertyName, validationResult.ErrorMessage);
        }

        return results;
    }
}
1

1 Answers

3
votes

I ran into this issue awhile back and figured out it happens because the ValidationSummary control uses the name of the control to see if it already has the error in its collection of errors. I was working on a solution and came up with the following behavior. We ended up going a different route due to some issues I ran into because a lot of UI is generated on the fly.

You can take a look and give it a try to see if it might fix your issue:

public class ValidationSummaryCountFixBehavior : Behavior<ValidationSummary>
{
    private Dictionary<string, ValidationSummaryItem> _validationErrors = new Dictionary<string, ValidationSummaryItem>();

    protected override void OnAttached()
    {
        base.OnAttached();

        AssociatedObject.Loaded += new RoutedEventHandler(AssociatedObject_Loaded);
    }

    void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
    {
        var target = AssociatedObject.Target as FrameworkElement ?? VisualTreeHelper.GetParent(AssociatedObject) as FrameworkElement;
        if (target != null)
        {
            target.BindingValidationError += new EventHandler<ValidationErrorEventArgs>(target_BindingValidationError);
        }
        AssociatedObject.Loaded -= new RoutedEventHandler(AssociatedObject_Loaded);
    }

    void target_BindingValidationError(object sender, ValidationErrorEventArgs e)
    {
        FrameworkElement inputControl = e.OriginalSource as FrameworkElement;

        if (((e != null) && (e.Error != null)) && ((e.Error.ErrorContent != null) && (inputControl != null)))
        {
            string message = e.Error.ErrorContent.ToString();
            string goodkey = inputControl.GetHashCode().ToString(CultureInfo.InvariantCulture);
            goodkey = goodkey + message;

            if (e.Action == ValidationErrorEventAction.Added && ValidationSummary.GetShowErrorsInSummary(inputControl))
            {
                string messageHeader = null;
                ValidationSummaryItem item = new ValidationSummaryItem(message, messageHeader, ValidationSummaryItemType.PropertyError, new ValidationSummaryItemSource(messageHeader, inputControl as Control), null);
                _validationErrors[goodkey] = item;
            }
            else
            {
                _validationErrors.Remove(goodkey);
            }
        }

        UpdateDisplayedErrors();
    }

    private void UpdateDisplayedErrors()
    {
        AssociatedObject.Errors.Clear();
        foreach (ValidationSummaryItem item in _validationErrors.Values)
        {
            AssociatedObject.Errors.Add(item);
        }
    }
}