MVVM - Действительно ли валидация должна быть такой громоздкой?

В моем приложении у меня множество форм, большинство из которых имеют собственные модели, к которым они привязаны! Конечно, валидация данных важна, но разве нет лучшего решения, кроме реализации 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 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 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 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 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 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 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)

РЕШЕНИЕ:

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

Класс словаря Validtion (показывающий основные функции):

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

Игнорируйте мои соглашения о присвоении имен slapdash и в кодировании мест, я просто так рад, что получил эту работу! Особая благодарность nmclean, конечно, но и благодаря всем, кто внес свой вклад в этот вопрос, все ответы были чрезвычайно полезны, но после некоторого рассмотрения я решил пойти с таким подходом!

Ответ 1

Моя выглядит примерно так:

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

ValidationDictionary - словарь словаря string → delegate. Он перегружает Add, чтобы принять лямбда-выражение, которое преобразуется в строку имени свойства для ключа, и массив делегатов params, которые объединены в один делегат для значения. Делегаты принимают некоторую информацию, такую ​​как тип и значение свойства, и возвращают сообщение об ошибке или null.

В этом случае Required и LettersOnly - это функции более высокого порядка, которые генерируют делегаты, которые возвращают заданные строки, когда они недействительны. Строки сами передаются в качестве делегатов, поэтому они могут быть динамическими.

IDataErrorInfo реализуется путем простого поиска имени свойства в словаре и вызова делегата для получения сообщения об ошибке.

Ответ 2

Я использую методы extension, чтобы уменьшить объем текста проверки, который я должен написать. Если вы не знакомы с ними, ознакомьтесь с Руководство по программированию расширения (С#) в MSDN, чтобы узнать о методах 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, который просто добавляет "s" в конец входного параметра, если входное значение не равно 1. Другим методом может быть:

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

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

Ответ 3

Мне лично нравится подход 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/ - хотя документы в основном основаны на веб-MVC. Для Wpf также есть несколько сообщений в блогах, например http://blogsprajeesh.blogspot.co.uk/2009/11/fluent-validation-wpf-implementation.html

Ответ 4

Ты прав. Оператор переключения слишком много. Гораздо проще изолировать логику 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(); } }

Это радикально упрощает весь конвейер проверки только за небольшую работу.