【问题标题】:Suggestions for Entity/Business object validation where the validation is dependent on other Entity/Service实体/业务对象验证的建议,其中验证依赖于其他实体/服务
【发布时间】:2011-12-22 03:42:12
【问题描述】:

上下文

对于使用 MVVM 模式的 WPF 应用程序,我使用实体上的 IDataErrorInfo 接口验证我的实体(/业务对象),以便 WPF 自动调用我的实体中的验证规则,并且验证错误自动出现在视图中。 (在本文中受到 Josh Smith 的启发:http://joshsmithonwpf.wordpress.com/2008/11/14/using-a-viewmodel-to-provide-meaningful-validation-error-messages/

这适用于简单的验证规则,例如(名称 > 10 个字符,值必须 > 0)

但是当模型中的验证规则更复杂时该怎么办(例如名称必须是唯一的/属性的最大值在另一个实体中定义)。我首先想到通过让实体引用存储库来解决这个问题,但这感觉不好,因为我认为应该只有从存储库到实体的引用而不是其他方式(创建循环引用)

从 Recipe 实体引用 ConfigurationRepository 是否“合法”。或者您有更好的建议吗? 您对如何实现实体/业务对象验证有什么建议,其中验证依赖于其他实体/服务,如下例所示。

在我的现实世界问题的简化代码下方。 在配方实体中,我想验证最高温度是否小于存储在 Configuration.MaximumTemperature 中的值。 你会如何解决这个问题?

配置实体(存储配方的最高允许温度)

public class Configuration: INotifyPropertyChanged, IDataErrorInfo
{
    private int _MaxTemperatureSetpoint;
    public int MaxTemperatureSetpoint
    {
        get { return _MaxTemperatureSetpoint; }
        set
        {
            if (value != _MaxTemperatureSetpoint)
            {
                _Setpoint = value;
                RaisePropertyChanged("MaxTemperatureSetpoint");
            }
        }
    }

简化配方(用户配置具有所需温度 (TemperatureSetpoint) 和所需时间 (TimeMilliSeconds) 的配方的类。TemperatureSetpoint 必须为

public class Recipe: INotifyPropertyChanged, IDataErrorInfo
{
    private int _TemperatureSetpoint;
    public int TemperatureSetpoint
    {
        get { return _TemperatureSetpoint; }
        set
        {
            if (value != _TemperatureSetpoint)
            {
                _Setpoint = value;
                RaisePropertyChanged("Setpoint");
            }
        }
    }

    private int _TimeMilliSeconds;
    public int TimeMilliSeconds
    {
        get { return _TimeMilliSeconds; }
        set
        {
            if (value != _TimeMilliSeconds)
            {
                _TimeMilliSeconds= value;
                RaisePropertyChanged("TimeMilliSeconds");
            }
        }
    }

   #region IDataErrorInfo Members
    public string Error
    {
        get { throw new NotImplementedException(); }
    }

    public string this[string propertyName]
    {
        get 
        { 
             switch(propertyName)
             {
                 case "TimeMilliSeconds":
                     //TimeMilliSeconds must be < 30 seconds
                     if (TimeMilliSeconds < 30000)
                     { return "TimeMilliSeconds must be > 0 milliseconds";}
                 case "TemperatureSetpoint":

                    //MaxTemperatureSetpoint < maxTemperature stored in the ConfigurationRepository

                    int maxTemperatureSetpoint = ConfigurationRepository.GetConfiguration().MaxTemperatureSetpoint;
                     if (TemperatureSetpoint> maxTemperatureSetpoint )
                     { return "TemperatureSetpoint must be < " + maxTemperatureSetpoint.ToString();}
       }
    }

    #endregion
}

配方存储库

public interface IRecipeRepository
{
    /// <summary>
    /// Returns the Recipe with the specified key(s) or <code>null</code> when not found
    /// </summary>
    /// <param name="recipeId"></param>
    /// <returns></returns>
    TemperatureRecipe Get(int recipeId);

    .. Create + Update + Delete methods
}

配置库

public interface IConfigurationRepository
{
      void Configuration GetConfiguration();
}

【问题讨论】:

    标签: wpf silverlight validation mvvm entity


    【解决方案1】:

    对于基于业务规则的验证,我通常会公开一个我的 ViewModel 可以设置的验证委托。

    例如,Recipe 的 ViewModel 可能包含如下代码:

    public GetRecipe(id)
    {
        CurrentRecipe = DAL.GetRecipe(id);
        CurrentRecipe.AddValidationErrorDelegate(ValidateRecipe);
    }
    
    private string ValidateRecipe(string propertyName)
    {
        if (propertyName == "TemperatureSetpoint")
        {
            var maxTemp = Configuration.MaxTemperatureSetpoint;
            if (CurrentRecipe.TemperatureSetpoint >= maxTemp )
            {
                return string.Format("Temperature cannot be greater than {0}", maxTemp);
            }
        }
        return null;
    }
    

    这个想法是您的Model 应该只包含原始数据,因此它应该只验证原始数据。这可以包括验证最大长度、必填字段和允许的字符等内容。包含业务规则的业务逻辑应在ViewModel 中进行验证,这样就可以实现。

    IDataErrorInfoRecipe 类上的实际实现如下所示:

    #region IDataErrorInfo & Validation Members
    
    /// <summary>
    /// List of Property Names that should be validated
    /// </summary>
    protected List<string> ValidatedProperties = new List<string>();
    
    #region Validation Delegate
    
    public delegate string ValidationErrorDelegate(string propertyName);
    
    private List<ValidationErrorDelegate> _validationDelegates = new List<ValidationErrorDelegate>();
    
    public void AddValidationErrorDelegate(ValidationErrorDelegate func)
    {
        _validationDelegates.Add(func);
    }
    
    #endregion // Validation Delegate
    
    #region IDataErrorInfo for binding errors
    
    string IDataErrorInfo.Error { get { return null; } }
    
    string IDataErrorInfo.this[string propertyName]
    {
        get { return this.GetValidationError(propertyName); }
    }
    
    public string GetValidationError(string propertyName)
    {
        // If user specified properties to validate, check to see if this one exists in the list
        if (ValidatedProperties.IndexOf(propertyName) < 0)
        {
            //Debug.Fail("Unexpected property being validated on " + this.GetType().ToString() + ": " + propertyName);
            return null;
        }
    
        string s = null;
    
        // If user specified a Validation method to use, Validate property
        if (_validationDelegates.Count > 0)
        {
            foreach (ValidationErrorDelegate func in _validationDelegates)
            {
                s = func(propertyName);
                if (s != null)
                {
                    return s;
                }
            }
        }
    
        return s;
    }
    
    #endregion // IDataErrorInfo for binding errors
    
    #region IsValid Property
    
    public bool IsValid
    {
        get
        {
            return (GetValidationError() == null);
        }
    }
    
    public string GetValidationError()
    {
        string error = null;
    
        if (ValidatedProperties != null)
        {
            foreach (string s in ValidatedProperties)
            {
                error = GetValidationError(s);
                if (error != null)
                {
                    return error;
                }
            }
        }
    
        return error;
    }
    
    #endregion // IsValid Property
    
    #endregion // IDataErrorInfo & Validation Members
    

    【讨论】:

    • 感谢您的回答!我喜欢将业务规则注入模型的想法,因为这消除了模型对存储库的依赖。我看到的唯一缺点是模型验证现在分散在代码中(在模型中用于简单属性,在 ViewModel 中用于依赖于其他服务的验证)而不是在一个地方(模型)
    • @ArjendenHartog 您还可以在创建对象的任何层中添加验证。例如,您可以有一个 CustomerRepository 查询数据库、创建模型、添加验证,然后将对象返回给调用它的人。
    【解决方案2】:

    说实话,我发现 WPF 中的验证方法不够完整和/或优雅。我发现使用 WPF 方法会在我的应用程序中分散验证代码和逻辑,甚至会在我的 UI 中放置一些。和你一样,我对所有事情都使用了自定义业务对象 (CBO),我真的很想在我的对象中保留我的验证,因为我在多个项目(Web 服务、UI、移动设备等)中使用它们。

    我所做的是使用我的 CBO(在本例中为配方),并添加一些验证方法作为属性。例如:

        public Func<string> NameValidation
        {
            get
            {
                return () =>
                {
                    string result = null;
                    if (String.IsNullOrEmpty(Name)) result = "Name cannot be blank";
                    else if (Name.Length > 100) result = "Name cannot be longer than 100 characters";
                    return result;
                };
            }
        }
    

    之后,我用自定义属性装饰了它:

    [AttributeUsage(AttributeTargets.Property)]
        public class CustomValidationMethod : Attribute
        {
        }
    

    然后我为对象级验证创建了一个 Validate() 方法:

        public override void Validate()
        {
            var a = GetType().GetProperties().Where(w => w.GetCustomAttributes(typeof(CustomValidationMethod), true).Length > 0);
            foreach (var a2 in a)
            {
                var result = a2.GetValue(this, null) as Func<string>;
                if (result != null)
                {
                    var message = result();
                    if (message != null)
                        //There was an error, do something
                    else if (message == null && Errors.ContainsKey(a2.Name))
                        //There was no error
                }
            }
        }
    

    然后我创建了支持我的验证的自定义控件。在这种情况下,它是一个 ComboBox,我从标准 ComboBox 派生并添加了以下代码:

        public Func<string> ValidationMethod
        {
            get { return (Func<string>) GetValue(ValidationMethodProperty); }
            set { SetValue(ValidationMethodProperty, value); }
        }
    
        protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            base.OnPropertyChanged(e);
            if (ValidationMethod != null && !String.IsNullOrEmpty(ValidationMethod()))
                SetControlAsInvalid();
            else
                SetControlAsValid();
        }
    

    一旦设置完成,我可以在验证方法中添加字段验证(这些验证方法存储在我的 CBO 中,而不是分散在我的代码中),我可以在我的 Validate() 方法中添加对象级验证。同样,我可以轻松自定义控件在验证方面的行为方式。

    要使用它,在我的 VM 中,我会先调用 .Validate(),然后在保存之前处理任何问题。具体来说,我会将错误消息存储在一个集合中,然后查询它们(这也允许我存储多个错误消息而不是第一个)

    【讨论】:

    • 您好 CamronBute,感谢您的回答。我喜欢重写 OnPropertyChanged 以强制验证自动完成的想法。我找不到我的主要问题的答案:“您是否有建议如何实现实体/业务对象验证,其中验证依赖于其他实体/服务,如下例所示。”
    • 您可以在几个不同的地方添加验证。您可以通过创建方法签入 ViewModel,或将其直接放入 .Validate() 方法中。我个人没有方法或没有这样做,但我会将它添加到 .Validate() 方法中,因为您知道每次都会调用 .Validate()。
    猜你喜欢
    • 1970-01-01
    • 2011-04-30
    • 1970-01-01
    • 2011-07-22
    • 2011-10-28
    • 2011-01-22
    • 2012-03-20
    • 2012-03-30
    • 2011-08-20
    相关资源
    最近更新 更多