IDataErrorInfo : How to know if all properties are valid?
Asked Answered
G

3

8

I have a WPF application(.Net 3.5) which uses the IDataErrorInfo on the ViewModel to validate input.

It works great, the usercontrol get the correct UI feedback.

The problem is that the user can still change the selected element, or save this element.

So my question is: How can I know that all my properties are valid? Or at least that all my displayed values are valid. The goal is to bind some IsActive on this result

Greybeard answered 28/11, 2012 at 10:47 Comment(7)
If the errors property is not null or empty then there will be an errorTomsk
Where do you want to know if they're all valid? In the View or the ViewModel?Anchylose
I think the answer to this may help you: #105020Officialism
@BobVale Yeah I know, but I want to validate all properties at onceGreybeard
@Anchylose In the view model or in the view, but I need to have one property to bind in the View for the whole data model, not only one fieldGreybeard
@Greybeard How are you implementing this[]?Tomsk
@BobVale Like it's described in this link: codeblitz.wordpress.com/2009/05/08/…Greybeard
T
19

From your comment on your implementation of IDataErrorInfo change your implementation to this style....

#region IDataErrorInfo Members

public string Error
{
    get { return this[null] }
}

public string this[string columnName]
{
    get
    {
        StringBuilder result = new StringBuilder();
        if (string.IsNullOrEmpty(columnName) || columnName == "FirstName")
        {
            if (string.IsNullOrEmpty(FirstName))
                result.Append("Please enter a First Name\n");
        }
        if (string.IsNullOrEmpty(columnName) || columnName == "LastName")
        {
            if (string.IsNullOrEmpty(LastName))
                result.Append("Please enter a Last Name\n");
        }
       if (string.IsNullOrEmpty(columnName) || columnName == "Age")
        {
            if (Age <= 0 || Age >= 99)
                result.Append("Please enter a valid age\n");
        }
        return (result.Length==0) ? null : result.Remove(result.Length-1,1).ToString();
    }
}

#endregion


public bool IsValid {
   get { return string.IsNullOrEmpty(this.Error); }
}

Then in your property changed event

if (e.PropertyName == "Error") {
   OnPropertyChanged(this,new PropertyChangedEventArgs("IsValid"));
}
if (e.PropertyName != "Error" && e.PropertyName != "IsValid") {
   OnPropertyChanged(this,new PropertyChangedEventArgs("Error"));
}
Tomsk answered 28/11, 2012 at 14:43 Comment(10)
Oh, this is why the Error is for? I didn't understood that before and in all example I found, this wasn't implementedGreybeard
@Greybeard yes, Error is supposed to be an summary of what is wrong with the object itself, usually done as a summary like in this answer.Tomsk
I modified my code to use your solution(almost, I've still a IsValid property which contains a boolean, and I update it in my event change element.Greybeard
This works great! Thanks! A minor detail: It's ((IDataErrorInfo)this)[null] not just this[null]. Same for ((IDataErrorInfo)this).Error in the "IsValid" check. I also rewrote your 1-line return statement in the this[] function to 8 lines to make it clearer. :PAmoritta
@SimonF I think you only need the cast if you are implementing the interface explicitly, ie string IDataErrorInfo.Error {Tomsk
@Bob Vale: You are correct. I changed the "this[]" method declaration to "public string this[string ..." rather than "string IDataErrorInfo.this[ string ..." and it works fine without the cast.Amoritta
@BobVale Great answer. Question though: shouldn't IsValid be return string.IsNullOrEmpty(this.Error); instead of return !string.IsNullOrEmpty(this.Error);?Kiosk
@Kiosk yes typo, will correct, it was a cut an paste and I usually have the property as IsInvalidTomsk
The code that calls OnPropertyChanged() doesn't seem to work for me, but I can't tell what you are doing differently. Where are you putting that block of code?Prochoras
@Prochoras You need to make sure your class implements INotifyPropertyChanged and then OnPropertyChanged will then raise the PropertyChangedEvent - void OnPropertyChanged(string name) { var handler=this.PropertyChanged; if (handler!=null) {handler(this,new PropertyChangedEventArgs(name));}}Tomsk
G
1

For now, I added this method on my model.

    public Boolean IsModelValid()
    {
        Boolean isValid = true;
        PropertyInfo[] properties = GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);

        foreach (PropertyInfo p in properties)
        {
            if (!p.CanWrite || !p.CanRead)
            {
                continue;
            }
            if (this[p.Name] != null)
            {
                isValid = false;
            }
        }
        return isValid;
    }

I bound the object itself on the PropertyChanged event,

    public MyClassName()
    {
        PropertyChanged += CheckModelValidity;
        CheckModelValidity(null, null);
    }

when it change, I call this method, and if the result is different than my actual public member, I update it:

    private void CheckModelValidity(object sender, PropertyChangedEventArgs e)
    {
        bool isModelValid = IsModelValid();
        if(isModelValid!= IsValid)
        {
            IsValid = isModelValid;
        }
    }

And then I can just bind the IsValid property.

I don't know if there is a better solution?

Greybeard answered 28/11, 2012 at 12:24 Comment(3)
If you are going to go down this route, initialize the list of valid property names in the static constructor storing it list in a static field. Then you only have the reflection occur onceTomsk
@BobVale Yeah, I'm not proud of that too, but I prefer this to the risk of somebody forgetting to do one check. Nice, I will initialize this in a static var.Greybeard
This doesn't reevaluate the Error. You may want to do this instead: Validator.TryValidateObject(this, new ValidationContext(this, null), null); which returns whether the overall validation (including Errors) was successful.Dejong
I
0

Thank you Bob Vale for the idea! I noticed that I have several models and the code is very repetitive. I created this base class:

using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text;

namespace Core.Models
{
    public abstract class ValidatableObject : IDataErrorInfo
    {
        public virtual bool IsValid() => this[null] == null;

        [NotMapped]
        public string? Error => this[null];

        [NotMapped]
        protected abstract (string, Func<string?>)[] Checks { get; }

        public virtual string? this[string? name] => validate(name, Checks);

        private static string? validate(string? name, params (string, Func<string?>)[] checks)
        {
            StringBuilder results = new();

            foreach ((string val, Func<string?> check) in checks)
            {
                if (String.IsNullOrEmpty(name) || name == val)
                {
                    string? result = check();
                    if (result != null)
                        results.AppendLine(result);
                }
            }

            return results.Length == 0
                ? null
                : results.ToString(0, results.Length - Environment.NewLine.Length);
        }
    }
}

And here's usage:

    private class Validatable : ValidatableObject
    {
        public string Email { get; set; }
        public string Comment { get; set; }

        protected override (string, Func<string?>)[] Checks => new (string, Func<string?>)[]
        {
            (nameof(Email), () => Validate.Email(Email)),
            (nameof(Comment), () => Validate.LengthOfOptionalString(Comment)),
        };
    }
Icily answered 23/7, 2021 at 18:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.