Question

I have a class called 'CustomerDataContract' wich has three attributes: MobilePhone, OfficePhone and PrivatePhone. In order to get in touch with the customer at least one of the attributes has to be set.

I created a Validator-Class and added a custom rule called 'CheckForAnyPhoneNumber' to check wheter or not at least one phone number has been provided. This almost solves my problem. The only problem I have is, that if I clear for example the MobilePhone-Attribute it gets evaluated and is in error state. If I now enter a OfficePhone number, the MobilePhone-Attribute does not get reevaluated and stays in error-mode.

Any idea how to solve this problem correctly? I also tried creating a RuleSet but without success.

Here's the (partial) CustomerDataContract:

public partial class CustomerDataContract
{

    public CustomerDataContract Clone()
    {
        return (CustomerDataContract) MemberwiseClone();
    }

    public override ValidationResult SelfValidate()
    {
        return ValidationHelper.Validate<CustomerDataContractValidator, CustomerDataContract>(this);
    }

}

The Base Class of all DataContracts:

[Serializable]
public abstract class BDataModel : IDataErrorInfo, INotifyPropertyChanged
{

    [field: NonSerialized]
    private ValidationResult _currentState = new ValidationResult();

    public ValidationResult CurrentValidationState
    {
        get { return _currentState; }
        set 
        { 
            _currentState = value;
            OnPropertyChanged("CurrentValidationState");
            OnPropertyChanged("IsValid");
            OnPropertyChanged("Error");
        }
    }

    #region INotifyPropertyChanged

    [field: NonSerialized]
    public event PropertyChangedEventHandler PropertyChanged;

    public void OnPropertyChanged(params string[] propertyName)
    {
        propertyName.ToList().ForEach(x =>
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(x));
        });
    }

    #endregion

    public virtual ValidationResult SelfValidate()
    {
        return new ValidationResult();
    }

    public bool IsValid
    {
        get
        {
            if (CurrentValidationState == null)
                CurrentValidationState = SelfValidate();
            return CurrentValidationState.IsValid;
        }
    }

    #region IDataErrorInfo

    public string Error
    {
        get { return ValidationHelper.GetError(CurrentValidationState); }
    }

    public string this[string columnName]
    {
        get
        {
            CurrentValidationState = SelfValidate();
            if (_currentState == null) return string.Empty;
            var columnResults = _currentState.Errors.FirstOrDefault(x => string.Compare(x.PropertyName, columnName, true) == 0);
            return columnResults != null ? columnResults.ErrorMessage : string.Empty;
        }
    }

    #endregion

}

Here's the Validator class:

public class CustomerDataContractValidator : AbstractValidator<CustomerDataContract>
{
    private readonly ILanguageManager _languageManager = LanguageManager.Instance;

    public CustomerDataContractValidator()
    {
        RuleFor(val => val.Lastname).NotEmpty().WithMessage(_languageManager.LanguageData["Global.Validation.ProvideLastname"]);
        RuleFor(val => val.Firstname).NotEmpty().WithMessage(_languageManager.LanguageData["Global.Validation.ProvideFirstname"]);
        RuleFor(val => val.Active).NotEmpty().WithMessage(_languageManager.LanguageData["Global.Validation.ProvideActive"]);
        RuleFor(val => val.Gender).NotEmpty().WithMessage(_languageManager.LanguageData["Global.Validation.ProvideGender"]);
        RuleFor(val => val.Mail).EmailAddress().When(val => !String.IsNullOrEmpty(val.Mail)).WithMessage(_languageManager.LanguageData["Global.Validation.ProvideValidEmail"]);

        // At least one phone number has to be filled in order to contact the customer
        RuleFor(val => val.MobilePhone).Must(CheckForAnyPhoneNumber).WithMessage(_languageManager.LanguageData["Global.Validation.ProvideAnyPhone"]);
        RuleFor(val => val.OfficePhone).Must(CheckForAnyPhoneNumber).WithMessage(_languageManager.LanguageData["Global.Validation.ProvideAnyPhone"]);
        RuleFor(val => val.PrivatePhone).Must(CheckForAnyPhoneNumber).WithMessage(_languageManager.LanguageData["Global.Validation.ProvideAnyPhone"]);

    }

    /// <summary>
    /// Checks wheter or not any phone number has been provided
    /// </summary>
    /// <param name="customer">The customer</param>
    /// <param name="phoneNumber">The phone number</param>
    /// <returns></returns>
    private static bool CheckForAnyPhoneNumber(CustomerDataContract customer, string phoneNumber)
    {
        return (!String.IsNullOrEmpty(customer.MobilePhone) || !String.IsNullOrEmpty(customer.OfficePhone) || !String.IsNullOrEmpty(customer.PrivatePhone));
    }
}

And the Validation Helper:

public class ValidationHelper
{
    public static ValidationResult Validate<T, K>(K entity)
        where T : IValidator<K>, new()
        where K : class
    {
        IValidator<K> validator = new T();
        return validator.Validate(entity);
    }

    public static string GetError(ValidationResult result)
    {
        var errorMessage = "";
        if (result != null)
        {
            var failure = result.Errors.FirstOrDefault();
            if(failure != null)
            {
                errorMessage = failure.ErrorMessage;
            }
        }
        return errorMessage;
    }
}

The ViewModel:

public class CustomerViewModel : BMovingSelectionViewModel, IEventListener, ICustomerViewModel
{

    private CustomerDataContract _customer;

    public CustomerDataContract Customer
    {
        get
        {
            return _customer;
        }

        set
        {
            if (_customer != value)
            {
                _customer = value;
                OnPropertyChanged(() => Customer);
            }
        }
    }
}

And the view:

<TextBlock Text="{MarkupExtensions:LanguageManager Global.Person.PhoneOffice}" Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" />
<TextBox Grid.Row="3" Grid.Column="1" Text="{Validation:ValidationBinding Path=Customer.OfficePhone}" HorizontalContentAlignment="Stretch"  />

<TextBlock Text="{MarkupExtensions:LanguageManager Global.Person.PhonePrivate}" Grid.Row="4" Grid.Column="0" VerticalAlignment="Center" />
<TextBox Grid.Row="4" Grid.Column="1" Text="{Validation:ValidationBinding Path=Customer.PrivatePhone}" />

<TextBlock Text="{MarkupExtensions:LanguageManager Global.Person.PhoneMobile}" Grid.Row="5" Grid.Column="0" VerticalAlignment="Center" />
<TextBox Grid.Row="5" Grid.Column="1" Text="{Validation:ValidationBinding Path=Customer.MobilePhone}" />
Was it helpful?

Solution

To make WPF update the error info it needs to be triggered to do so. That trigger is a PropertyChanged event on the bound property.


The simplest solution would be to involve your view model. If you can do this, you simply need to raise PropertyChanged events for all dependent properties in the setter of each property in the group of dependent properties.
That means that setting PhoneOffice would raise a PropertyChanged for "PhoneOffice", "PhonePrivate" and for "PhoneMobile".


Another solution would be to work with rule sets and put the relevant code into the base class. To make it work, the following steps would need to be performed:

  1. Make the base class listen to its own PropertyChanged event
  2. In the event handler, get the validation rule for this property
  3. Get all other validation rules for the same rule set
  4. Raise PropertyChanged for the properties of the validation rules
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top