2013-09-18 3 views
12

В моем приложении у меня множество форм, большинство из которых имеют собственные модели, к которым они привязаны! Конечно, валидация данных важна, но разве нет лучшего решения, кроме реализации IDataErrorInfo для всех ваших моделей, а затем написания кода для всех свойств для их проверки?MVVM - Действительно ли валидация должна быть такой громоздкой?

Я создал помощников по валидации, которые удаляют много фактического кода проверки, но все же я не могу не чувствовать, что мне не хватает трюка или двух! Могу ли я добавить, что это первое приложение, которое я использовал MVVM внутри, поэтому я уверен, что мне нужно многому научиться на эту тему!

EDIT:

Это код от типичной модели, что я действительно не люблю (позвольте мне объяснить):

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 

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

Примечание: Я буду преобразовывать мои помощники проверки в расширениях, как рекомендовано в одном из ответов (спасибо 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; 
     } 
    } 

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

+0

Для общих объектов вы не можете переместить атрибуты проверки в интерфейс, чтобы сохранить некоторые повторения? – mattytommo

+0

Другой вариант заключается в проверке исключения, но, конечно, это имеет другие ограничения. Что именно вы находите громоздким? Я думаю, что IDataErrorInfo в этом вопросе тихий. Разумеется, с некоторыми вспомогательными классами мне приходят некоторые пользовательские атрибуты со стратегиями проверки. – dowhilefor

+0

Я предполагаю, что показать, как может быть реализован 'IDataErrorInfo' для работы с типом класса« ошибка/валидация фабрики/помощника ». – MoonKnight

ответ

3

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

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

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

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

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

+0

Спасибо за ваш вклад, я очень заинтригован вашим ответом, однако у меня есть несколько вопросов. 1) почему вы передаете ключ как выражение лямбда, а не как строку, то есть «carer_title». Так вы можете разрешить свойство во время выполнения? Если да, то почему? Во-вторых, как вы передаете сообщение об ошибке своим валидаторам в виде лямбда-выражений (я знаю, что вы сказали, что они могут быть динамичными, но я не могу понять, что происходит)? Не сердитесь на меня, я никогда не использовал лямбда-выражения творчески (как ваше решение), и мне очень нравится ваш способ сделать это, но я изо всех сил пытаюсь его реализовать. Спасибо – Sam

+0

Просто, чтобы развернуть мое замешательство, обычно Validation.Required принял бы текст свойств как значение правильно, ну, как бы он, если он принимает сообщение об ошибке в качестве параметра (и снова, почему в качестве выражения лямбда и не как строка)?! Примечание. Я не спрашиваю вас или ваше решение, но как все это работает ... любой дополнительный код будет очень благодарен! Еще раз спасибо за ваше время – Sam

+0

Обновление - так что я в порядке с тем, почему вы реализуете carer_title как выражение лямбда, противоположное как строка, так что если вы переименуете свойства, вам не придется беспокоиться о переименовании всех строк и т. Д. (правильно?) – Sam

6

Использование методов extension для уменьшения количества текста проверки, который я должен написать. Если вы не знакомы с ними, просмотрите страницу Extension Methods (C# Programming Guide) в 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); 
} 

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

+1

Мне нравится этот подход. Отлично сработано. – MoonKnight

+0

Это очень похоже на то, что я делаю, однако я не реализовал их как расширения - я определенно буду делать это сейчас! Я собираюсь обновить свой вопрос некоторым кодом, который я действительно ненавижу и хотел бы уменьшить. Спасибо – Sam

+0

Это расширение pluralize ужасающее. Как вы справляетесь с локализацией? Является ли это гендерным, напряженным? – Gusdor

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/ - хотя документы существуют в основном на основе веб-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 - simply and cleanly implement INotifyPropertyChanged in a base class. Если у вас есть имя установленного свойства и используйте отражение (кэш после первого вызова, если вы беспокоитесь о перфомансе), чтобы получить какие-либо аннотации данных к свойству, вы можете выполнить все проверки проверки и сохранить результаты в базовом классе. Это было бы упростить полученные свойства класса на что-то вроде следующего:

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

Который радикально упрощает весь трубопровод валидации лишь небольшое количество работ.

Смежные вопросы