С# validation: IDataErrorInfo без жестко закодированных строк имени свойства?

Какая наилучшая практика реализации IDataErrorInfo? Есть ли способ реализовать его без жестко закодированных строк для имени свойства?

Ответ 1

Базовый класс для общих процедур проверки

Вы можете использовать DataAnnotations, если вы выполните futzing в реализации IDataErrorInfo. Например, вот модель базового представления, которую я часто использую (из Windows Forms, но вы можете экстраполировать):

public class ViewModelBase : IDataErrorInfo, INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public SynchronizationContext Context
    {
        get;
        set;
    }

    public bool HasErrors
    {
        get
        {
            return !string.IsNullOrWhiteSpace(this.Error);
        }
    }

    public string Error
    {
        get 
        {
            var type = this.GetType();
            var modelClassProperties = TypeDescriptor
                .GetProperties(type)
                .Cast();

            return
                (from modelProp in modelClassProperties
                    let error = this[modelProp.Name]
                    where !string.IsNullOrWhiteSpace(error)
                    select error)
                    .Aggregate(new StringBuilder(), (acc, next) => acc.Append(" ").Append(next))
                    .ToString();
        }
    }

    public virtual string this[string columnName]
    {
        get
        {
            var type = this.GetType();
            var modelClassProperties = TypeDescriptor
                .GetProperties(type)
                .Cast();

            var errorText =
                (from modelProp in modelClassProperties
                    where modelProp.Name == columnName
                    from attribute in modelProp.Attributes.OfType()
                    from displayNameAttribute in modelProp.Attributes.OfType()
                    where !attribute.IsValid(modelProp.GetValue(this))
                    select attribute.FormatErrorMessage(displayNameAttribute == null ? modelProp.Name : displayNameAttribute.DisplayName))
                    .FirstOrDefault();

            return errorText;
        }
    }

    protected void NotifyPropertyChanged(string propertyName)
    {
        if (string.IsNullOrWhiteSpace(propertyName))
        {
            throw new ArgumentNullException("propertyName");
        }

        if (!this.GetType().GetProperties().Any(x => x.Name == propertyName))
        {
            throw new ArgumentException(
                "The property name does not exist in this type.",
                "propertyName");
        }

        var handler = this.PropertyChanged;
        if (handler != null)
        {
            if (this.Context != null)
            {
                this.Context.Post(obj => handler(this, new PropertyChangedEventArgs(propertyName)), null);
            }
            else
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

Пример использования:

public class LogOnViewModel : ViewModelBase
{
    [DisplayName("User Name")]
    [Required]
    [MailAddress] // This is a custom DataAnnotation I wrote
    public string UserName
    {
        get
        {
                return this.userName;
        }
        set
        {
                this.userName = value;
                this.NotifyPropertyChanged("UserName");
        }
    }

    [DisplayName("Password")]
    [Required]
    public string Password
    {
        get; // etc
        set; // etc
    }
}

Воспользовавшись IDataErrorInfo для одноразовых процедур проверки

Честно говоря, я в конечном итоге использую как аннотации, так и переключатель. Я использую аннотации для простых проверок, и если у меня есть более сложные (например, "только проверять это свойство, если это другое свойство установлено" ), я прибегну к переключателю в переопределении индекса this[]. Этот шаблон часто выглядит так (просто составленный пример, это не имеет смысла:

public override string this[string columnName]
{
    get
    {
        // Let the base implementation run the DataAnnotations validators
        var error = base[columnName];

        // If no error reported, run my custom one-off validations for this
        // view model here
        if (string.IsNullOrWhiteSpace(error))
        {
            switch (columnName)
            {
                case "Password":
                    if (this.Password == "password")
                    {
                        error = "See an administrator before you can log in.";
                    }

                    break;
            }
        }

        return error;
    }

Некоторые мнения

Что касается указания имен свойств как строк: вы можете притворяться с lambdas, но мой честный совет - просто пережить это. Вы можете заметить, что в моем ViewModelBase мой маленький помощник NotifyPropertyChanged делает некоторую магию отражения, чтобы удостовериться, что у меня не было пальца имени свойства - это помогает мне быстро обнаруживать ошибку привязки данных, а не бегать за 20 минут, выясняя, что я пропустил.

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

Надеюсь, что это поможет. Удачи!

Ответ 3

Для этой ситуации (и INotifyPropertyChanged) я склонен идти с частным статическим классом, объявляющим все имена свойств как константы:

public class Customer : INotifyPropertyChanging, INotifyPropertyChanged, IDataErrorInfo, etc
{
  private static class Properties
  {
    public const string Email = "Email";
    public const string FirstName = "FirstName";
  }  


}

Там все еще немного повторения, но это работало отлично для меня в нескольких проектах.

Что касается организации проверки... Вы можете рассмотреть отдельный класс CustomerValidator, который будет поставляться во время выполнения. Затем вы можете менять различные реализации для разных контекстов. Так, например, новые клиенты могут быть проверены по-другому с существующими без беспорядка условностей.