views:

101

answers:

2

I am using the Required attribute on via the WCF RIA Services Metadata class, with the Entity Framework underneath that.

I create a new entity, and let the view bind to the view model. User looks at it for awhile, clicks around, and then attempts to save it.

The scenario here is that the user did not tab or click into a fields with a required field attribute.

How do I ensure all the required fields have data prior to submitting the data?

Winforms has this same problem, and I use to cycle all the fields in the form to ensure all the validators have passed.

Do I have to re-write that base class for a page - AGAIN?

Is there not a way to ensure all attribute validations are fired before sending it back to the server? Do I have to use reflection and pick up all the fields with the Required attribute?

I'm using the Entity Framework, and I've researched INotifyDataErrorInfo - but that is used after a trip to the DB (as I understand it).

I know I'm not the first person that has hit this - yet in my research I can't find a good example of this scenario.

A: 

INotifyDataErrorInfo doesn't have anything to do with trips to the DB (or calls over a service boundary, which is what I assume you meant). INotifyDataErrorInfo is an interface that you view model can implement in order to report to your view that your view model has validation errors. Hooking up the validation of your view model to use that interface is still up to you, unless that's something WCF RIA Services gives you for free, which I'm not positive about.

I use the [Required] attribute in my view models just to give a hint to the UI that my fields are required. I also implement INotifyDataErrorInfo in my view model and make sure to call my validation method whenever any properties in the view model change. I also manually call my validation method when the user executes a save command.

In my case I'm using the Fluent Validation library to implement my validation logic. I also built a new base class for any view models that need validation logic.

public class ValidatingViewModelBase<T> : ViewModelBase, IValidatingViewModel, INotifyDataErrorInfo
{
    private readonly IValidator<T> _validator;
    private readonly Dictionary<string, List<ValidationInfo>> _errors;

    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    public ValidatingViewModelBase() : this(null, null)
    {
    }

    public ValidatingViewModelBase(IValidator<T> validator) : this(validator, null)
    {
    }

    public ValidatingViewModelBase(IValidator<T> validator, IMessenger messenger) : base(messenger)
    {
        _validator = validator;
        _errors = new Dictionary<string, List<ValidationInfo>>();
    }

    public IEnumerable GetErrors(string propertyName)
    {
        if (string.IsNullOrEmpty(propertyName))
            return _errors.Values;

        CreateValidationErrorInfoListForProperty(propertyName);
        return _errors[propertyName];
    }

    public bool HasErrors
    {
        get { return _errors.Count > 0; }
    }

    protected virtual void AddValidationErrorForProperty(string propertyName, ValidationInfo validationInfo)
    {
        CreateValidationErrorInfoListForProperty(propertyName);

        if (!_errors[propertyName].Contains(validationInfo))
        {
            _errors[propertyName].Add(validationInfo);
            RaiseErrorsChanged(propertyName);
        }
    }

    protected virtual void ClearValidationErrorsForProperty(string propertyName)
    {
        CreateValidationErrorInfoListForProperty(propertyName);

        if (_errors[propertyName].Count > 0)
        {
            _errors[propertyName].Clear();
            RaiseErrorsChanged(propertyName);
        }
    }

    protected virtual void ClearAllValidationErrors()
    {
        foreach (var propertyName in _errors.Keys)
            ClearValidationErrorsForProperty(propertyName);
        _errors.Clear();
    }

    private void CreateValidationErrorInfoListForProperty(string propertyName)
    {
        if (!_errors.ContainsKey(propertyName))
            _errors[propertyName] = new List<ValidationInfo>();
    }

    protected void RaiseErrorsChanged(string propertyName)
    {
        var handler = ErrorsChanged;
        if (handler != null)
        {
            handler.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
        }
    }

    protected override void RaisePropertyChanged(string propertyName)
    {
        Validate();
        base.RaisePropertyChanged(propertyName);
    }

    public bool Validate()
    {
        if (_validator == null)
            return true;

        if (this is ILoadAndSaveData && !((ILoadAndSaveData)this).HasLoadedData)
            return true;

        ClearAllValidationErrors();

        var results = _validator.Validate(this);

        if (!results.IsValid)
        {
            foreach (var failure in results.Errors)
            {
                AddValidationErrorForProperty(failure.PropertyName, 
                    new ValidationInfo(failure.ErrorMessage, ValidationType.Error));
            }
        }

        return results.IsValid;
    }

    public void SendValidationMessage()
    {
        var message = _errors.Values.SelectMany(propertyValidations => propertyValidations)
            .Aggregate("Please correct validation errors before saving.\r\n",
                (current, validationInfo) => current + ("\r\n· " + validationInfo.Message));

        MessengerInstance.Send(new ErrorMessage(new ErrorInfo { Message = message, Type = "Validation Error" }));
    }

    public bool ValidateAndSendValidationMessage()
    {
        var isValid = Validate();
        if (!isValid)
        {
            SendValidationMessage();
        }
        return isValid;
    }
}

public interface IValidatingViewModel
{
    bool Validate();
    void SendValidationMessage();
    bool ValidateAndSendValidationMessage();
}

public enum ValidationType { Error, Warning }

public class ValidationInfo
{
    public string Message { get; set; }
    public ValidationType Type { get; set; }

    public ValidationInfo(string message, ValidationType validationType)
    {
        Message = message;
        Type = validationType;
    }

    public override string ToString()
    {
        var result = Message;

        if (Type == ValidationType.Warning)
            result = "Warning: " + result;

        return result;
    }
}

So my view models inherit from this new base class.

public class ExampleViewModel : ValidatingViewModelBase<IExampleViewModel>, IExampleViewModel
{
    public ExampleViewModel(IValidator<IExampleViewModel> validator,
        IMessenger messenger)
        : base(validator, messenger)

    {
        SaveCommand = new RelayCommand(SaveCommandExecute);
    }

    public RelayCommand SaveCommand { get; private set; }

    private void SaveCommandExecute()
    {
        if (!ValidateAndSendValidationMessage())
            return;

        // save stuff here
    }
}

And I make sure to create a validator class for each view model.

public class ExampleValidator : AbstractValidator<IExampleViewModel>
{
    public TripInformationValidator()
    {
        // validation logic here
    }
}

Hope this helps!

Matt Casto
Matt - thanks very much for your extensive post, and the work you have done with Validations. I have not seen the Fluent Validation library-but I took a quick review after your post. In WCF RIA Services (as well as other offerings from MS), the use of attributes to create validation rules via the Component Library is currently all the rage. I see your design (as well as the Fluent library to be a departure from that. Would you say that to be true? I'm willing to look at all possibilities, but I've gone down the road of the attributes. Perhaps you have an example that I can download? Tks. R
codputer
I've never liked attributes for validations. IMO, attributes are useful for meta-data but validations are business logic. However, like I mentioned in the answer, I do use attributes on my VM properties to give a hint to the view on which values are required.
Matt Casto
+1  A: 

Have you looked at Validator.ValidateObject in System.ComponentModel.DataAnnotations? I'm pretty sure it does exactly what your asking.

Kyle McClellan
YES! You are correct - but the documentation, and examples leveraging this class sadly lacking. I would love to know more, and how to best integrate it with WCF RIA Services. I'm very surprised that WCF RIA does not do an entity level check of all required fields before it sends the changeset off to the server. It would be fantastic to have an entity-level Validate Method that would enforce all property attributes on the entity. I'm kind of amazed that MS would have missed this validation level! Am I missnig somethign? Start here: http://jeffhandley.com/archive/2009/10/16/validator.aspx
codputer