Предложения по проверке сущности / бизнес-объекта, когда проверка зависит от другой сущности / услуги - PullRequest
2 голосов
/ 04 ноября 2011

Контекст

Для приложения WPF, использующего шаблон MVVM, я проверяю свою сущность (/ бизнес-объект), используя интерфейс IDataErrorInfo для сущности, так что правила проверки в моей сущности автоматически вызываютсяWPF, и ошибки проверки автоматически появляются в представлении.(вдохновленный Джошем Смитом в этой статье: http://joshsmithonwpf.wordpress.com/2008/11/14/using-a-viewmodel-to-provide-meaningful-validation-error-messages/

Это нормально для простых правил проверки, таких как (имя> 10 символов, значение должно быть> 0)

Но чтоделать, когда правило проверки в модели более сложное (например, имя должно быть уникальным / максимальное значение свойства определено в другой сущности). Сначала я подумал об этом, разрешив сущности иметь ссылку на хранилище, но это ненехорошо, потому что я думаю, что должны быть только ссылки из репозитория на сущность, а не иначе (создание циклической ссылки)

«Законно» ли иметь ссылку из рецепта?сущность в ConfigurationRepository. Или у вас есть лучшее предложение? У вас есть предложения, как реализовать проверку сущности / бизнес-объекта, когда проверка зависит от другой сущности / службы, как в примере ниже.

Ниже приведен упрощенный код моей реальной проблемы. В сущности Рецепт я хочу проверить, чтоМаксимальная температура меньше значения, хранящегося в Configuration.MaximumTempera. Как бы вы решили это?

Объект конфигурации (хранит максимально допустимую температуру для рецепта)

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

TheУпрощенный рецепт (Класс, в котором пользователь конфигурирует рецепт с желаемой температурой (температура-точка) и желаемым временем (время-миллисекунды). Температура-точка должна быть

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

Ответы [ 2 ]

2 голосов
/ 04 ноября 2011

Для проверки, основанной на бизнес-правилах, я обычно предоставляю делегат проверки, который может установить мой ViewModel.

Например, 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, и это позволяет это сделать.

Фактическая реализация моего IDataErrorInfo в классе Recipe будет выглядеть следующим образом:

#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
0 голосов
/ 04 ноября 2011

Честно говоря, я обнаружил, что запеченные в методах проверки WPF неполные и / или недостаточно элегантные. Я обнаружил, что использование методов WPF разбросало бы код проверки и логику по всему моему приложению и даже поместило бы некоторые в мой интерфейс. Как и вы, я использовал Custom Business Objects (CBO) для всего, и я действительно хотел сохранить свою проверку в своих объектах, поскольку я использовал их в нескольких проектах (веб-сервис, пользовательский интерфейс, мобильный телефон и т. Д.).

Я взял свой CBO (в данном случае Recipe) и добавил несколько методов проверки в качестве свойств. Например:

    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 (). Кроме того, я могу легко настроить поведение элемента управления в отношении проверки.

Чтобы использовать это, в моей виртуальной машине я бы сначала вызвал .Validate (), а затем решил все проблемы перед сохранением. В моем случае, в частности, я буду хранить сообщения об ошибках в коллекции, а затем запрашивать их (это также позволило мне сохранить несколько сообщений об ошибках вместо первого)

...