【问题标题】:MVVM - Does validation really have to be so cumbersome?MVVM - 验证真的必须如此繁琐吗?
【发布时间】:2013-09-18 10:00:04
【问题描述】:

在我的应用程序中,我有大量的表单,其中大多数都有自己绑定的模型!数据验证当然很重要,但没有比为所有模型实现 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,还要感谢为这个问题做出贡献的每个人,所有的回复都非常有帮助,但经过一番考虑,我决定采用这种方法!

【问题讨论】:

  • 对于共享对象,能否不将验证属性移到接口中,省去一些重复?
  • 另一种选择是通过异常进行验证,但当然这有其他限制。你到底觉得什么很麻烦?我认为 IDataErrorInfo 在这方面非常优雅。当然,有了一些帮助类,我想到了一些带有验证策略的自定义属性。
  • 我想展示如何实现IDataErrorInfo 以使用一种类型的“错误/验证工厂/助手”类会很好。
  • 数据注释不能省去一些工作吗? blog.paulbetts.org/index.php/2010/04/27/…
  • @mattytommo - 这样的共享对象并不多,我确实考虑过你的方法,但我觉得这对这种情况没有太大帮助,如果有什么让事情变得更“混乱” - 感谢您的回复!

标签: c# wpf validation mvvm


【解决方案1】:

我使用extension 方法来减少我必须编写的验证文本的数量。如果您不熟悉它们,请查看 MSDN 的 Extension Methods (C# Programming Guide) 页面以了解有关 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"));
}

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

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);
}

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

【讨论】:

  • 我喜欢这种方法。打得很好。
  • 这与我正在做的非常相似,但是我还没有将它们实现为扩展 - 我现在肯定会这样做!我将使用一些我非常讨厌并希望减少的代码来更新我的问题。谢谢
  • @Gusdor,这是一个简单的extension 方法,具有处理依赖于数字将单数单词变为复数的重载。这并不意味着所有的歌唱,所有的舞蹈功能......它只是意味着我的应用程序将永远显示“1项”或“6项”之类的东西,这比我能说的要多许多其他应用程序。它对我来说总是很有效,但如果你不喜欢这个想法,就不要实施它。另外,Pluralise 方法与这个答案无关,所以我真的不明白你为什么又要无所事事。
  • @Gusdor 如果不考虑本地化或有关字符串标签的边缘情况,则暗示该代码是错误的,这是过度设计并违反了 YAGNI。应用程序中可能存在特定要求,表明目前可以。提出缺乏本地化的观点很好,但强调它只会让你看起来很暴躁。
  • @Gusdor - 您认为公司需要翻译这一事实清楚地表明您正在考虑本地化问题。没关系,对你来说。并非世界上所有公司都关心支持多种语言(我个人从未为关心的公司工作过)。同样,您将您的需求投射到这个问题中。如果为需要本地化的公司工作的初级程序员看到这个答案,他们很快就会发现如何修改它。并非每个 SO 答案都需要考虑所有可能的要求。
【解决方案2】:

我个人喜欢 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

【讨论】:

    【解决方案3】:

    你是对的。 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 中的一些功能结合使用时,它会变得更加强大,从而完全避免验证。

    有几个使用CallerMemberNameAttributesimply and cleanly implement INotifyPropertyChanged in a base class 的示例。如果您设置了属性的名称,并使用反射(如果您担心性能,则在第一次调用后缓存它)来获取属性上的任何数据注释,您可以执行所有验证检查并将结果全部存储在基类中。这会将派生类的属性简化为如下所示:

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

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

    【讨论】:

      【解决方案4】:

      我的看起来像这样:

      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 是通过简单地在字典中查找属性名称并调用委托来获取错误消息来实现的。

      【讨论】:

      • 感谢您的意见,我对您的回答非常感兴趣,但是我有几个问题。 1)你为什么将密钥作为 lambda 表达式而不是作为字符串传递,即“carer_title”。是否可以在运行时解析该属性?如果是这样,为什么?其次,您如何将错误消息作为 lambda 表达式传递给您的验证器(我知道您说过它们可以是动态的,但我无法理解正在发生的事情)?不要生我的气,我从来没有创造性地使用过 lambda 表达式(就像你的解决方案一样),我真的很喜欢你这样做的方式,但我正在努力实现它。谢谢
      • 只是为了扩大我的困惑,通常 Validation.Required 会将属性文本作为值,好吧,如果它以错误消息作为参数怎么办(同样,为什么一个 lambda 表达式而不是一个字符串)?!注意:我不是在质疑您或您的解决方案,而是在质疑它是如何工作的......任何额外的代码将不胜感激!再次感谢您的宝贵时间
      • 更新 - 所以我很好理解为什么你将 carer_title 实现为 lambda 表达式而不是字符串 - 这样,如果你重命名属性,你也不必担心重命名所有字符串等等(对吧?)
      • @Sam 1) 是的,它确保字符串始终与实际属性“同步”。 IDE 可以在我键入时建议属性,而不是手动将它们键入为字符串,这也很好。
      • @Sam 2) 假设您想在错误文本中包含属性的当前值(甚至是不同的属性),因此您使用String.Format("the value {0} is not valid", this.carer_title)。如果您直接将其传入,它将始终显示创建字典时的值。通过使用委托(每次调用),您将始终看到 current 值。这也可以根据当前语言查找字符串,因此您的应用程序可以支持动态切换语言。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-12-12
      • 2013-03-05
      • 2020-01-09
      • 2017-05-06
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多