12

在我的应用程序中,我有大量的表单,其中大多数都有自己绑定的模型!数据验证当然很重要,但没有比为所有模型实现 IDataErrorInfo 然后为所有属性编写代码来验证它们更好的解决方案吗?

我创建了验证助手,它删除了很多实际的验证代码,但我仍然忍不住觉得我错过了一两个技巧!我可以补充一点,这是我在其中使用 MVVM 的第一个应用程序,所以我确信我在这个主题上有很多东西要学习!

编辑:

这是我真的不喜欢的典型模型的代码(让我解释一下):

    string IDataErrorInfo.Error
    {
        get
        {
            return null;
        }
    }

    string IDataErrorInfo.this[string propertyName]
    {
        get
        {
            return GetValidationError(propertyName);
        }
    }

    #endregion

    #region Validation

    string GetValidationError(String propertyName)
    {
        string error = null;

        switch (propertyName)
        {
            case "carer_title":
                error = ValidateCarerTitle();
                break;
            case "carer_forenames":
                error = ValidateCarerForenames();
                break;
            case "carer_surname":
                error = ValidateCarerSurname();
                break;
            case "carer_mobile_phone":
                error = ValidateCarerMobile();
                break;
            case "carer_email":
                error = ValidateCarerEmail();
                break;
            case "partner_title":
                error = ValidatePartnerTitle();
                break;
            case "partner_forenames":
                error = ValidatePartnerForenames();
                break;
            case "partner_surname":
                error = ValidatePartnerSurname();
                break;
            case "partner_mobile_phone":
                error = ValidatePartnerMobile();
                break;
            case "partner_email":
                error = ValidatePartnerEmail();
                break;
        }

        return error;
    }

    private string ValidateCarerTitle()
    {
        if (String.IsNullOrEmpty(carer_title))
        {
            return "Please enter the carer's title";
        }
        else
        {
            if (!ValidationHelpers.isLettersOnly(carer_title))
                return "Only letters are valid";
        }

        return null;
    }

    private string ValidateCarerForenames()
    {
        if (String.IsNullOrEmpty(carer_forenames))
        {
            return "Please enter the carer's forename(s)";
        }
        else
        {
            if (!ValidationHelpers.isLettersSpacesHyphensOnly(carer_forenames))
                return "Only letters, spaces and dashes are valid";
        }

        return null;
    }

    private string ValidateCarerSurname()
    {
        if (String.IsNullOrEmpty(carer_surname))
        {
            return "Please enter the carer's surname";
        }
        else
        {
            if (!ValidationHelpers.isLettersSpacesHyphensOnly(carer_surname))
                return "Only letters, spaces and dashes are valid";
        }

        return null;
    }

    private string ValidateCarerMobile()
    {
        if (String.IsNullOrEmpty(carer_mobile_phone))
        {
            return "Please enter a valid mobile number";
        }
        else
        {
            if (!ValidationHelpers.isNumericWithSpaces(carer_mobile_phone))
                return "Only numbers and spaces are valid";
        }

        return null;
    }

    private string ValidateCarerEmail()
    {
        if (String.IsNullOrWhiteSpace(carer_email))
        {
            return "Please enter a valid email address";
        }
        else
        {
            if (!ValidationHelpers.isEmailAddress(carer_email))
                return "The email address entered is not valid";
        }
        return null;
    }

    private string ValidatePartnerTitle()
    {
        if (String.IsNullOrEmpty(partner_title))
        {
            return "Please enter the partner's title";
        }
        else
        {
            if (!ValidationHelpers.isLettersOnly(partner_title))
                return "Only letters are valid";
        }

        return null;
    }

    private string ValidatePartnerForenames()
    {
        if (String.IsNullOrEmpty(partner_forenames))
        {
            return "Please enter the partner's forename(s)";
        }
        else
        {
            if (!ValidationHelpers.isLettersSpacesHyphensOnly(partner_forenames))
                return "Only letters, spaces and dashes are valid";
        }

        return null;
    }

    private string ValidatePartnerSurname()
    {
        if (String.IsNullOrEmpty(partner_surname))
        {
            return "Please enter the partner's surname";
        }
        else
        {
            if (!ValidationHelpers.isLettersSpacesHyphensOnly(partner_surname))
                return "Only letters, spaces and dashes are valid";
        }

        return null;
    }

    private string ValidatePartnerMobile()
    {
        if (String.IsNullOrEmpty(partner_mobile_phone))
        {
            return "Please enter a valid mobile number";
        }
        else
        {
            if (!ValidationHelpers.isNumericWithSpaces(partner_mobile_phone))
                return "Only numbers and spaces are valid";
        }

        return null;
    }

    private string ValidatePartnerEmail()
    {
        if (String.IsNullOrWhiteSpace(partner_email))
        {
            return "Please enter a valid email address";
        }
        else
        {
            if (!ValidationHelpers.isEmailAddress(partner_email))
                return "The email address entered is not valid";
        }
        return null;
    }

    #endregion

使用 switch 语句来识别正确的属性,然后必须为每个属性编写唯一的验证函数的想法感觉太多了(不是在工作方面,而是在所需的代码量方面)。也许这是一个优雅的解决方案,但感觉不像一个!

注意:我将按照答案之一中的建议将我的验证助手转换为扩展(感谢 Sheridan)

解决方案:

所以,按照我接受的答案,这是我最初实现它的基本原理(显然我会改进部分 - 但我只是想先让它开始,因为我没有使用 lambda 表达式或反射的经验在实施此之前)。

Validtion Dictionary 类(显示主要功能):

    private Dictionary<string, _propertyValidators> _validators;
    private delegate string _propertyValidators(Type valueType, object propertyValue);


    public ValidationDictionary()
    {
        _validators = new Dictionary<string, _propertyValidators>();
    }

    public void Add<T>(Expression<Func<string>> property, params Func<T, string>[] args)
    {
        // Acquire the name of the property (which will be used as the key)
        string propertyName = ((MemberExpression)(property.Body)).Member.Name;

        _propertyValidators propertyValidators = (valueType, propertyValue) =>
        {
            string error = null;
            T value = (T)propertyValue;

            for (int i = 0; i < args.Count() && error == null; i++)
            {
                error = args[i].Invoke(value);
            }

            return error;
        };

        _validators.Add(propertyName, propertyValidators);
    }

    public Delegate GetValidator(string Key)
    {
        _propertyValidators propertyValidator = null;
        _validators.TryGetValue(Key, out propertyValidator);
        return propertyValidator;
    }

模型实现:

public FosterCarerModel()
    {
        _validationDictionary = new ValidationDictionary();
        _validationDictionary.Add<string>( () => carer_title, IsRequired);
    }

    public string IsRequired(string value)
    { 
        string error = null;

        if(!String.IsNullOrEmpty(value))
        {
            error = "Validation Dictionary Is Working";
        }

        return error;
    }

IDataErrorInfo 实现(它是模型实现的一部分):

string IDataErrorInfo.this[string propertyName]
    {
        get
        {
            Delegate temp = _validationDictionary.GetValidator(propertyName);

            if (temp != null)
            {
                string propertyValue = (string)this.GetType().GetProperty(propertyName).GetValue(this, null);
                return (string)temp.DynamicInvoke(typeof(string), propertyValue);
            }                

            return null;
        }
    }

忽略我草率的命名约定和地方编码,我很高兴能得到这个工作!当然要特别感谢 nmclean,还要感谢为这个问题做出贡献的每个人,所有的回复都非常有帮助,但经过一番考虑,我决定采用这种方法!

4

4 回答 4

6

我使用extension方法来减少我必须编写的验证文本的数量。如果您不熟悉它们,请查看 MSDN 上的扩展方法(C# 编程指南)页面以了解extension方法。我有几十个可以验证每一种情况。举个例子:

if (propertyName == "Title" && !Title.ValidateMaximumLength(255)) error = 
    propertyName.GetMaximumLengthError(255);

Validation.cs课堂上:

public static bool ValidateMaximumLength(this string input, int characterCount)
{
    return input.IsNullOrEmpty() ? true : input.Length <= characterCount;
}

public static string GetMaximumLengthError(this string input, int characterCount, 
    bool isInputAdjusted)
{
    if (isInputAdjusted) return input.GetMaximumLengthError(characterCount);
    string error = "The {0} field requires a value with a maximum of {1} in it.";
    return string.Format(error, input, characterCount.Pluralize("character"));
}

请注意,如果输入值不等于 1 ,Pluralize则另一种方法只是在输入参数的末尾添加一个“s”。另一种方法可能是:extension

public static bool ValidateValueBetween(this int input, int minimumValue, int 
    maximumValue)
{
    return input >= minimumValue && input <= maximumValue;
}

public static string GetValueBetweenError(this string input, int minimumValue, int 
    maximumValue)
{
    string error = "The {0} field value must be between {1} and {2}.";
    return string.Format(error, input.ToSpacedString().ToLower(), minimumValue, 
        maximumValue);
}

当然,实现您需要的所有方法需要一段时间,但之后您将节省大量时间,并且您将获得所有错误消息一致的额外好处。

于 2013-09-18T10:21:11.203 回答
5

我个人喜欢 FluentValidation 方法。

这会将您的开关表替换为基于表达式的规则,例如:

            RuleFor(x => x.Username)
                .Length(3, 8)
                .WithMessage("Must be between 3-8 characters.");

            RuleFor(x => x.Password)
                .Matches(@"^\w*(?=\w*\d)(?=\w*[a-z])(?=\w*[A-Z])\w*$")
                .WithMessage("Must contain lower, upper and numeric chars.");

            RuleFor(x => x.Email)
                .EmailAddress()
                .WithMessage("A valid email address is required.");

            RuleFor(x => x.DateOfBirth)
                .Must(BeAValidDateOfBirth)
                .WithMessage("Must be within 100 years of today.");

来自http://stevenhollidge.blogspot.co.uk/2012/04/silverlight-5-validation.html

有关此http://fluentvalidation.codeplex.com/的更多信息- 尽管那里的文档主要基于 web-MVC。对于 Wpf,还有一些博客文章,例如http://blogsprajeesh.blogspot.co.uk/2009/11/fluent-validation-wpf-implementation.html

于 2013-09-18T14:18:14.377 回答
4

你是对的。switch 语句太多了。将 IDEI(和 INotifyDataErrorInfo)逻辑隔离到基类中要容易得多。

实现此目的的一个简单方法是公开一个方法来设置属性的错误,并清除属性的错误。这很容易实现,尽管您必须对每个属性的验证进行编码。

public string SomeProperty { get { return _someProperty; }
    set
    {
        _someProperty = value;
        if(string.IsNullOrWhiteSpace(value))
            SetError("SomeProperty", "You must enter a value or something kthx");
        else
            ClearError("SomeProperty");
    } 

在基类中,您保留一个只保存这些错误值的字典

protected void SetError(string propertyName, string error)
{
    _errors[propertyName] = error;
{

并按需交付,例如,

string IDataErrorInfo.Error
{
    get
    {
        return string.Join(Environment.NewLine, _errors.Values);
    }
}

当您将这种模式与数据注释、一点反射和 4.5 中的一些功能结合起来时,这种模式会变得更加强大,从而完全避免验证。

有几个使用CallerMemberNameAttribute在基类中简单而干净地实现 INotifyPropertyChanged 的​​示例。如果您设置了属性的名称,并使用反射(如果您担心性能,则在第一次调用后将其缓存)来获取属性上的任何数据注释,您可以执行所有验证检查并将结果全部存储在基类中。这会将您的派生类的属性简化为如下所示:

[NotNullOrWhiteSpace, NotADirtyWord, NotViagraSpam]
public string SomeProperty{ 
    get {return _lol;} 
    set{ _lol = value; PropertyChanged(); } }

这从根本上简化了整个验证流程,只需少量工作。

于 2013-09-18T14:24:03.070 回答
3

我的看起来像这样:

new ValidationDictionary() {
    {() => carer_title,
        ValidationHelpers.Required(() => "Please enter the carer's title"),
        ValidationHelpers.LettersOnly(() => "Only letters are valid")}
}

ValidationDictionary 是一个字符串字典 -> 委托。它重载Add以接受转换为键的属性名称字符串的 lambda 表达式,以及params合并为值的一个委托的委托数组。委托接受一些信息,如属性类型和值,并返回错误消息或null.

在这种情况下,RequiredLettersOnly是高阶函数,它们生成在无效时返回给定字符串的委托。字符串本身作为委托传递,因此它们可以是动态的。

IDataErrorInfo 是通过简单地在字典中查找属性名称并调用委托来获取错误消息来实现的。

于 2013-09-18T14:51:44.693 回答