Комплексные расширения / справочная реализация набора тестов

Я считаю, что интерфейс INotifyPropertyChanged является одним из наиболее важных интерфейсов, которые предоставляет платформа .NET Framework. Хотя этот интерфейс задает только одно событие, простота реализации еще не означает правильность. Я видел разные подходы к реализации того интерфейса, и каждый из них имел свои собственные недостатки.

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

Подход 1. Распространенная реализация

internal class Person1 : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
   
    private string name;
   
    public string Name
    {
        get { return name; }
        set
        {
            if (name != value)
            {
                name = value;
                OnPropertyChanged("Name");
            }
        }
    }

    protected virtual void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Характеристики
(+) Легко реализовать и применять.
(+) Событие вызывается только при изменении имени свойства. Это помогает избежать переполнения стека, которое возникает, когда свойства зависят друг от друга.
 
(-) Имя свойства задается как строковая константа в коде. Это не сохраняет рефакторинг, и это способствует возникновению ошибок, поскольку компилятор не может проверить правильность строки имени свойства.
(-) Если свойство изменяется часто, и вопросы производительности имеют большое значение, то может оказаться проблемой то, что каждое изменение свойства создает новый объект EventArgs.
(-) Метод OnPropertyChanged не соответствует рекомендациям по разработке Майкрософт (http://msdn.microsoft.com/ru-ru/library/ms229011.aspx), поскольку этот метод должен иметь аргумент типа PropertyChangedEventArgs вместо типа string. Одну из причин для этого правила можно увидеть в подходе 2.
 
Подход 2. Кэшированный объект PropertyChangedEventArgs

internal class Person2 : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private static readonly PropertyChangedEventArgs NameEventArgs = new PropertyChangedEventArgs("Name");

    private string name;

    public string Name
    {
        get { return name; }
        set
        {
            if (name != value)
            {
                name = value;
                OnPropertyChanged(NameEventArgs);
            }
        }
    }

    protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        if (PropertyChanged != null) { PropertyChanged(this, e); }
    }
}

Характеристики
(+) Событие вызывается только при изменении имени свойства. Это помогает избежать переполнения стека, которое возникает, когда свойства зависят друг от друга.
(+) PropertyChangedEventArgs создаются один раз для класса, а не для каждого изменения свойства. Это улучшает производительность.
(+) Метод OnPropertyChanged соответствует рекомендациям по разработке Майкрософт (http://msdn.microsoft.com/ru-ru/library/ms229011.aspx). Это позволяет нам кэшировать PropertyChangedEventArgs для повторного использования при каждом изменении свойства. Более того, подкласс сможет передавать производный тип PropertyChangedEventArgs посредством этого метода. Например, этот производный класс может включать старое и новое значение операции изменения свойства. 
 
(-) Имя свойства задается как строковая константа в коде. Это не сохраняет рефакторинг, и это способствует возникновению ошибок, поскольку компилятор не может проверить правильность строки имени свойства.
(-) Каждому свойству необходимо статическое поле для PropertyChangedEventArgs.
 
Подход 3. Проверка PropertyName во время выполнения

internal class Person3 : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private string name;

    public string Name
    {
        get { return name; }
        set
        {
            if (name != value)
            {
                name = value;
                OnPropertyChanged("Name");
            }
        }
    }

    protected virtual void OnPropertyChanged(string propertyName)
    {
        CheckPropertyName(propertyName);
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    [Conditional("DEBUG")]
    private void CheckPropertyName(string propertyName)
    {
        PropertyDescriptor propertyDescriptor = TypeDescriptor.GetProperties(this)[propertyName];
        if (propertyDescriptor == null)
        {
            throw new InvalidOperationException(string.Format(null,
                "The property with the propertyName '{0}' doesn't exist.", propertyName));
        }
    }
}

Характеристики
(+)
Событие вызывается только при изменении имени свойства. Это помогает избежать переполнения стека, которое возникает, когда свойства зависят друг от друга.
(+) Метод OnPropertyChanged выполняет проверки допустимости переданного имени свойства. Это очень помогает найти неправильные имена свойств, которые часто возникают в результате опечаток или рефакторинга кода. Однако эта проверка потребляет большой объем производительности и поэтому выполняется только в режиме отладки.
 
(-) Если свойство изменяется часто, и вопросы производительности имеют большое значение, то может оказаться проблемой то, что каждое изменение свойства создает новый объект EventArgs.
(-) Метод OnPropertyChanged не соответствует рекомендациям по разработке Майкрософт (http://msdn.microsoft.com/ru-ru/library/ms229011.aspx), поскольку этот метод должен иметь аргумент типа PropertyChangedEventArgs вместо типа string. Одну из причин для этого правила можно увидеть в подходе 2.
 
Подход 4. Извлечение имени свойства с помощью лямбда-выражения

internal class Person4 : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private static readonly string NamePropertyName = TypeManager.GetProperty<Person4>(x => x.Name).Name;
   
    private string name;

    public string Name
    {
        get { return name; }
        set
        {
            if (name != value)
            {
                name = value;
                OnPropertyChanged(NamePropertyName);
            }
        }
    }

    protected virtual void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Примечание. Реализацию метода TypeManager.GetProperty можно увидеть в загрузке кода.
 
Характеристики
(+) Событие вызывается только при изменении имени свойства. Это помогает избежать переполнения стека, которое возникает, когда свойства зависят друг от друга.
(+) Метод TypeManager.GetProperty извлекает имя свойства с помощью лямбда-выражения. Лямбда-выражение типобезопасно. 
 
(-) Для извлечения свойства лямбда-выражение использует отражение внутренним образом. Это увеличивает рабочее множество (потребление памяти) приложения и замедляется, когда доступ к классу осуществляется впервые.
(-) Использование статического метода TypeManager.GetProperty не прямолинейно.
(-) Если свойство изменяется часто, и вопросы производительности имеют большое значение, то может оказаться проблемой то, что каждое изменение свойства создает новый объект EventArgs.
(-) Метод OnPropertyChanged не соответствует рекомендациям по разработке Майкрософт (http://msdn.microsoft.com/ru-ru/library/ms229011.aspx), поскольку этот метод должен иметь аргумент типа PropertyChangedEventArgs вместо типа string. Одну из причин для этого правила можно увидеть в подходе 2.
 
Подход 5. Отказ от использования класса StackTrace

// DO NOT USE THIS!
protected void OnPropertyChanged()
{
    StackTrace s = new StackTrace(1, false);
    string propertyName = s.GetFrame(0).GetMethod().Name.Substring(4);
    if (PropertyChanged != null)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

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

  • Реализация StackTrace полагается на символы отладки. По умолчанию отладочные построения включают символы отладки, в то время как рабочие построения — нет. Следовательно, приложение работает в режиме отладки, но больше не работает в рабочем режиме.
  • Класс StackTrace не предназначен для частых вызовов. Если его использовать для реализации измененного свойства, то производительность может быть низкой.

Существует дополнительный подход, который я использую, и который является комбинацией 2 и 4 подходов — кэширование объекта PropertyChangedEventArgs в статической переменной.  Используется вспомогательная функция, которая применяет синтаксис выражений для возврата созданного объекта EventArgs.  Затем кэшированный объект EventArgs используется для уведомлений об изменении.

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

class Person6 : INotifyPropertyChanged

{

    static readonly PropertyChangedEventArg _nameChangedArgs = ObservableHelper.CreateArgs<Person6>(x => x.Name); 

    string _name;

 

    public string Name

    {

        get { return _name; }

        set

        {

            if (_name != value)

            {

                _name = value;

                OnPropertyChanged(_nameChangedArgs);

            }

        }

    }

 

    protected void OnPropertyChanged(PropertyChangedEventArgs propertyChangeArgs)

    {

        var changeEvent = PropertyChanged;

 

        if (changeEvent != null)

        {

            changeEvent(this, propertyChangeArgs);

        }

    }

 

   public event PropertyChangedEventHandler PropertyChanged;

 

}

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

С уважением,

Фил

ИСПРАВЛЕНИЕ. Между прочим, в базовом классе, который у меня был для реализации INotifyPropertyChanged, я *не* предоставлял метод "OnPropertyChanged", который принимает строковый аргумент. Причина в том, что я активно хотел отговорить пользователей от передачи в жестко закодированных строках и подтолкнуть их к использованию моего предоставленного вспомогательного метода и синтаксиса выражений, чтобы их код был безопасен в плане рефакторинга и запутанности.  В конечном счете ничто не может помешать им ввести объект EventArgs, если они этого хотят, но я объясняю в XML-комментариях метода OnPropertyChanged, что им следует использовать вспомогательный метод CreateArgs и синтаксис выражений.

Имеется дополнительный метод, предоставляемый в базовом классе с именем "OnAllPropertiesChanged()", который передает событие изменения свойства с помощью кэшированного объекта EventArgs измененного свойства, созданного с помощью string.empty. Это поддерживает соглашение для INotifyPropertyChanged, когда значение NULL или пустая строка может использоваться в аргументах изменения свойств, чтобы указать, что получатель должен предполагать изменение всех свойств (и если привязка повторно запрашивает у этих свойств их текущие значения).

Далее приводится типобезопасная реализация с использованием деревьев выражений.

public void NotifyOfPropertyChange<TProperty>(Expression<Func<TProperty>> property)
{
    var lambda = (LambdaExpression)property;
    MemberExpression memberExpression;
    if (lambda.Body is UnaryExpression)
    {
        var unaryExpression = (UnaryExpression)lambda.Body;
        memberExpression = (MemberExpression)unaryExpression.Operand;
    }
    else memberExpression = (MemberExpression)lambda.Body;
    NotifyOfPropertyChange(memberExpression.Member.Name);
 }

 

 

Она может использоваться следующим образом.


public string Name
{
    get { return _name; }
    set
    {
        if (_name != value)
        {
            _name = value;
           NotifyOfPropertyChange(() => Name);
        }
    }
}