Amad Zafar Amad Zafar - 18 days ago 10
C# Question

Using dataAnnonation regular expression validation attribute in Wpf Mvvm IdataErrorInfo

I'm newbie to wpf app development using mvvm. So please ignore if I am asking something out of the box. I have a model class where I am validating the data using data annotations.

Here is the code part of model class

/// <summary>
/// The firstname of the person.
/// </summary>
[Required(AllowEmptyStrings = false, ErrorMessage = "First name must not be empty.")]
[MaxLength(20, ErrorMessage = "Maximum of 20 characters is allowed.")]
public string Firstname { get; set; }

/// <summary>
/// The lastname of the person.
/// </summary>
[Required(AllowEmptyStrings = false, ErrorMessage = "Address must not be empt.")]
public string Address { get; set; }

[MaxLength(20, ErrorMessage = "Maximum of 20 characters is allowed.")]
public string PhoneNum { get; set; }


My validation is bound totally fine to the xaml, works fine and shows errors in text boxes in case of "Required and Maxlength Attributes". Now I want to use Regular Expression Attribute with my phone number in model class. Like

[RegularExpression("^[0-9]*$", ErrorMessage = "Phone Num must be numeric")]
[MaxLength(20, ErrorMessage = "Maximum of 20 characters is allowed.")]
public string PhoneNum { get; set; }


Here is the code of IDataErrorInfo in my BaseModel class.

using Annotations;

/// <summary>
/// Abstract base class for all models.
/// </summary>
public abstract class BaseModel : INotifyPropertyChanged, IDataErrorInfo
{
#region constants

private static List<PropertyInfo> _propertyInfos;

#endregion

#region events

/// <summary>
/// Occurs when a property value changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;

#endregion

#region constructors and destructors

/// <summary>
/// Default constructor.
/// </summary>
public BaseModel()
{
InitCommands();
}

#endregion

#region explicit interfaces

/// <summary>
/// Gets an error message indicating what is wrong with this object.
/// </summary>
/// <returns>
/// An error message indicating what is wrong with this object. The default is an empty string ("").
/// </returns>
public string Error => string.Empty;

/// <summary>
/// Gets the error message for the property with the given name.
/// </summary>
/// <returns>
/// The error message for the property. The default is an empty string ("").
/// </returns>
/// <param name="columnName">The name of the property whose error message to get. </param>
public string this[string columnName]
{
get
{
CollectErrors();
return Errors.ContainsKey(columnName) ? Errors[columnName] : string.Empty;
}
}

#endregion

#region methods

/// <summary>
/// Override this method in derived types to initialize command logic.
/// </summary>
protected virtual void InitCommands()
{
}

/// <summary>
/// Can be overridden by derived types to react on the finisihing of error-collections.
/// </summary>
protected virtual void OnErrorsCollected()
{
}

/// <summary>
/// Raises the <see cref="PropertyChanged" /> event.
/// </summary>
/// <param name="propertyName">The name of the property which value has changed.</param>
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

/// <summary>
/// Is called by the indexer to collect all errors and not only the one for a special field.
/// </summary>
/// <remarks>
/// Because <see cref="HasErrors" /> depends on the <see cref="Errors" /> dictionary this
/// ensures that controls like buttons can switch their state accordingly.
/// </remarks>
private void CollectErrors()
{
Errors.Clear();
PropertyInfos.ForEach(
prop =>
{
var currentValue = prop.GetValue(this);
var requiredAttr = prop.GetCustomAttribute<RequiredAttribute>();
var maxLenAttr = prop.GetCustomAttribute<MaxLengthAttribute>();

if (requiredAttr != null)
{
if (string.IsNullOrEmpty(currentValue?.ToString() ?? string.Empty))
{
Errors.Add(prop.Name, requiredAttr.ErrorMessage);
}
}
if (maxLenAttr != null)
{
if ((currentValue?.ToString() ?? string.Empty).Length > maxLenAttr.Length)
{
Errors.Add(prop.Name, maxLenAttr.ErrorMessage);
}
}


// further attributes
});
// we have to this because the Dictionary does not implement INotifyPropertyChanged
OnPropertyChanged(nameof(HasErrors));
OnPropertyChanged(nameof(IsOk));
// commands do not recognize property changes automatically
OnErrorsCollected();
}

#endregion

#region properties

/// <summary>
/// Indicates whether this instance has any errors.
/// </summary>
public bool HasErrors => Errors.Any();

/// <summary>
/// The opposite of <see cref="HasErrors" />.
/// </summary>
/// <remarks>
/// Exists for convenient binding only.
/// </remarks>
public bool IsOk => !HasErrors;

/// <summary>
/// Retrieves a list of all properties with attributes required for <see cref="IDataErrorInfo" /> automation.
/// </summary>
protected List<PropertyInfo> PropertyInfos
{
get
{
return _propertyInfos
?? (_propertyInfos =
GetType()
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(prop => prop.IsDefined(typeof(RequiredAttribute), true) || prop.IsDefined(typeof(MaxLengthAttribute), true))
.ToList());
}
}

/// <summary>
/// A dictionary of current errors with the name of the error-field as the key and the error
/// text as the value.
/// </summary>
private Dictionary<string, string> Errors { get; } = new Dictionary<string, string>();

#endregion
}


}

How can I add regular expression attribute in my Basemodel class? Any help will be appreciated. Thanks

Answer

Instead of adding more or clauses - || - to your property you could just get all attributes derived from ValidationAttribute. All DataAnnotation attributes are derived from this class:

/// <summary>
/// Retrieves a list of all properties with attributes required for <see cref="IDataErrorInfo" /> automation.
/// </summary>
protected List<PropertyInfo> PropertyInfos
{
    get
    {
        return _propertyInfos
               ?? (_propertyInfos =
                   GetType()
                       .GetProperties(BindingFlags.Public | BindingFlags.Instance)
                       .Where(prop => prop.IsDefined(typeof(ValidationAttribute), true))
                       .ToList());
    }
}

If you don't like this approach you could then add an || clause by each attribute type you want to handle:

protected List<PropertyInfo> PropertyInfos
{
    get
    {
        return _propertyInfos
                ?? (_propertyInfos =
                    GetType()
                        .GetProperties(BindingFlags.Public | BindingFlags.Instance)
                        .Where(prop => 
                            prop.IsDefined(typeof(RequiredAttribute), true) || 
                            prop.IsDefined(typeof(MaxLengthAttribute), true) ||
                            prop.IsDefined(typeof(RegularExpressionAttribute), true) )
                        .ToList());
    }
}

As per your comment, I think that you need a generic way of validating your attributes or your CollectErrors method will get ugly pretty soon.

Give it a try to this approach taken from a project I developed using Prism. This code should go into your BaseModel class.

private bool TryValidateProperty(PropertyInfo propertyInfo, List<string> propertyErrors)
{
    var results = new List<ValidationResult>();
    var context = new ValidationContext(this) { MemberName = propertyInfo.Name };
    var propertyValue = propertyInfo.GetValue(this);

    // Validate the property
    var isValid = Validator.TryValidateProperty(propertyValue, context, results);

    if (results.Any()) { propertyErrors.AddRange(results.Select(c => c.ErrorMessage)); }

    return isValid;
}

/// <summary>
/// Is called by the indexer to collect all errors and not only the one for a special field.
/// </summary>
/// <remarks>
/// Because <see cref="HasErrors" /> depends on the <see cref="Errors" /> dictionary this
/// ensures that controls like buttons can switch their state accordingly.
/// </remarks>
private void CollectErrors()
{
    Errors.Clear();
    PropertyInfos.ForEach(
        prop =>
        {
            //Validate generically
            var errors = new List<string>();
            var isValid = TryValidateProperty(prop, errors);
            if (!isValid)
                //As you're using a dictionary to store the errors and the key is the name of the property, then add only the first error encountered. 
                Errors.Add(prop.Name, errors.First());
        });
    // we have to this because the Dictionary does not implement INotifyPropertyChanged            
    OnPropertyChanged(nameof(HasErrors));
    OnPropertyChanged(nameof(IsOk));
    // commands do not recognize property changes automatically
    OnErrorsCollected();
}
Comments