Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
322 views
in Technique[技术] by (71.8m points)

c# - Force INotifyDataErrorInfo validation

I have implemented INotifyDataErrorInfo exactly as described in the following link:

http://blog.micic.ch/net/easy-mvvm-example-with-inotifypropertychanged-and-inotifydataerrorinfo

I have a TextBox which is bound to a string property in my model.

XAML

<TextBox Text="{Binding FullName,
                        ValidatesOnNotifyDataErrors=True,
                        NotifyOnValidationError=True,
                        UpdateSourceTrigger=PropertyChanged}" />

Model

private string _fullName;
public string FullName
{
    get { return _fullName; }
    set
    {
        // Set raises OnPropertyChanged
        Set(ref _fullName, value);

        if (string.IsNullOrWhiteSpace(_fullName))
            AddError(nameof(FullName), "Name required");
        else
            RemoveError(nameof(FullName));                
    }
}

INotifyDataError Code

private Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>();

public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

// get errors by property
public IEnumerable GetErrors(string propertyName)
{
    if (_errors.ContainsKey(propertyName))
        return _errors[propertyName];
    return null;
}

public bool HasErrors => _errors.Count > 0;

// object is valid
public bool IsValid => !HasErrors;

public void AddError(string propertyName, string error)
{
    // Add error to list
    _errors[propertyName] = new List<string>() { error };
    NotifyErrorsChanged(propertyName);
}

public void RemoveError(string propertyName)
{
    // remove error
    if (_errors.ContainsKey(propertyName))
        _errors.Remove(propertyName);
    NotifyErrorsChanged(propertyName);
}

public void NotifyErrorsChanged(string propertyName)
{
    // Notify
    if (ErrorsChanged != null)
       ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}

Now all this works fine, but it only validates as soon as I type something in my TextBox. I would like some way to validate on demand, without even touching the textbox, say on a button click.

I have tried raising PropertyChanged for all my properties as described in this question, but it does not detect the errors. I somehow need my property setter to be called so the errors can be detected. I'm looking for a MVVM solution.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

The INotifyDataErrorInfo implementation you use is somewhat flawed IMHO. It relies on errors kept in a state (a list) attached to the object. Problem with stored state is, sometimes, in a moving world, you don't have the chance to update it when you want. Here is another MVVM implementation that doesn't rely on a stored state, but computes error state on the fly.

Things are handled a bit differently as you need to put validation code in a central GetErrors method (you could create per-property validation methods called from this central method), not in the property setters.

public class ModelBase : INotifyPropertyChanged, INotifyDataErrorInfo
{
    public event PropertyChangedEventHandler PropertyChanged;
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    public bool HasErrors
    {
        get
        {
            return GetErrors(null).OfType<object>().Any();
        }
    }

    public virtual void ForceValidation()
    {
        OnPropertyChanged(null);
    }

    public virtual IEnumerable GetErrors([CallerMemberName] string propertyName = null)
    {
        return Enumerable.Empty<object>();
    }

    protected void OnErrorsChanged([CallerMemberName] string propertyName = null)
    {
        OnErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
    }

    protected virtual void OnErrorsChanged(object sender, DataErrorsChangedEventArgs e)
    {
        var handler = ErrorsChanged;
        if (handler != null)
        {
            handler(sender, e);
        }
    }

    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        OnPropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    protected virtual void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(sender, e);
        }
    }
}

And here are two sample classes that demonstrate how to use it:

public class Customer : ModelBase
{
    private string _name;

    public string Name
    {
        get
        {
            return _name;
        }
        set
        {
            if (_name != value)
            {
                _name = value;
                OnPropertyChanged();
            }
        }
    }

    public override IEnumerable GetErrors([CallerMemberName] string propertyName = null)
    {
        if (string.IsNullOrEmpty(propertyName) || propertyName == nameof(Name))
        {
            if (string.IsNullOrWhiteSpace(_name))
                yield return "Name cannot be empty.";
        }
    }
}

public class CustomerWithAge : Customer
{
    private int _age;
    public int Age
    {
        get
        {
            return _age;
        }
        set
        {
            if (_age != value)
            {
                _age = value;
                OnPropertyChanged();
            }
        }
    }

    public override IEnumerable GetErrors([CallerMemberName] string propertyName = null)
    {
        foreach (var obj in base.GetErrors(propertyName))
        {
            yield return obj;
        }

        if (string.IsNullOrEmpty(propertyName) || propertyName == nameof(Age))
        {
            if (_age <= 0)
                yield return "Age is invalid.";
        }
    }
}

It works like a charm with a simple XAML like this:

<TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Text="{Binding Age, UpdateSourceTrigger=PropertyChanged}" />

(UpdateSourceTrigger is optional, if you don't use it it will only work when focus is lost).

With this MVVM base class, you shouldn't have to force any validation. But should you need it, I have added a ForceValidation sample method in ModelBase that should work (I have tested it with for example a member value like _name that would have been changed without passing through the public setter).


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...