Реализация INotifyPropertyChanged - существует ли лучший способ? - PullRequest
604 голосов
/ 22 августа 2009

Microsoft должна была реализовать что-то быстрое для INotifyPropertyChanged, как в автоматических свойствах, просто укажите {get; set; notify;} Я думаю, что в этом есть смысл. Или есть какие-то осложнения для этого?

Можем ли мы сами реализовать что-то вроде «уведомить» в наших свойствах. Есть ли изящное решение для реализации INotifyPropertyChanged в вашем классе или единственный способ сделать это - вызвать событие PropertyChanged в каждом свойстве.

Если нет, то можем ли мы написать что-то для автоматической генерации кода, чтобы вызвать событие PropertyChanged?

Ответы [ 34 ]

581 голосов
/ 22 августа 2009

Без использования что-то вроде postsharp, минимальная версия, которую я использую, использует что-то вроде:

public class Data : INotifyPropertyChanged
{
    // boiler-plate
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
    protected bool SetField<T>(ref T field, T value, string propertyName)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }

    // props
    private string name;
    public string Name
    {
        get { return name; }
        set { SetField(ref name, value, "Name"); }
    }
}

Каждое свойство тогда выглядит примерно так:

    private string name;
    public string Name
    {
        get { return name; }
        set { SetField(ref name, value, "Name"); }
    }

, который не огромен; его также можно использовать как базовый класс, если хотите. Возвращаемое bool из SetField сообщает вам, был ли он неактивным, если вы хотите применить другую логику.


или даже проще с C # 5:

protected bool SetField<T>(ref T field, T value,
    [CallerMemberName] string propertyName = null)
{...}

который можно назвать так:

set { SetField(ref name, value); }

, с помощью которого компилятор автоматически добавит "Name".


C # 6.0 облегчает реализацию:

protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

... и теперь с C # 7:

protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
   => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));


private string name;
public string Name
{
    get => name;
    set => SetField(ref name, value);
}
192 голосов
/ 15 мая 2012

Начиная с .Net 4.5, наконец-то появился простой способ сделать это.

.Net 4.5 представляет новые атрибуты информации о вызывающем абоненте.

private void OnPropertyChanged<T>([CallerMemberName]string caller = null) {
     // make sure only to call this if the value actually changes

     var handler = PropertyChanged;
     if (handler != null) {
        handler(this, new PropertyChangedEventArgs(caller));
     }
}

Вероятно, хорошей идеей будет добавить в функцию также компаратор.

EqualityComparer<T>.Default.Equals

Дополнительные примеры здесь и здесь

Также см. Информация о вызывающем абоненте (C # и Visual Basic)

160 голосов
/ 22 августа 2009

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

private string name;
public string Name
{
    get { return name; }
    set { SetField(ref name, value, () => Name); }
}

Просто добавьте следующие методы к коду Марка, это поможет:

protected virtual void OnPropertyChanged<T>(Expression<Func<T>> selectorExpression)
{
    if (selectorExpression == null)
        throw new ArgumentNullException("selectorExpression");
    MemberExpression body = selectorExpression.Body as MemberExpression;
    if (body == null)
        throw new ArgumentException("The body must be a member expression");
    OnPropertyChanged(body.Member.Name);
}

protected bool SetField<T>(ref T field, T value, Expression<Func<T>> selectorExpression)
{
    if (EqualityComparer<T>.Default.Equals(field, value)) return false;
    field = value;
    OnPropertyChanged(selectorExpression);
    return true;
}

Кстати, это было вдохновлено этим сообщением в блоге обновленный URL

113 голосов
/ 01 августа 2013

Существует также Fody , который имеет надстройку PropertyChanged , которая позволяет написать следующее:

[ImplementPropertyChanged]
public class Person 
{        
    public string GivenNames { get; set; }
    public string FamilyName { get; set; }
}

... и во время компиляции вставляет уведомления об изменении свойства.

64 голосов
/ 30 октября 2013

Я думаю, что люди должны уделять немного больше внимания производительности, это действительно влияет на пользовательский интерфейс, когда нужно связать много объектов (например, сетка с 10 000+ строками) или если значение объекта часто меняется (в реальном времени приложение для мониторинга).

Я взял различные реализации, найденные здесь и в других местах, и провел сравнение, проверил его Сравнение производительности реализаций INotifyPropertyChanged .


Вот взгляд на результат Implemenation vs Runtime

35 голосов
/ 14 августа 2013

Я представляю класс Bindable в своем блоге на http://timoch.com/blog/2013/08/annoyed-with-inotifypropertychange/ Bindable использует словарь в качестве пакета свойств. Достаточно просто добавить необходимые перегрузки для подкласса, чтобы управлять собственным полем поддержки, используя параметры ref.

  • Нет волшебной строки
  • Без отражения
  • Может быть улучшено для подавления поиска в словаре по умолчанию

код:

public class Bindable : INotifyPropertyChanged {
    private Dictionary<string, object> _properties = new Dictionary<string, object>();

    /// <summary>
    /// Gets the value of a property
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="name"></param>
    /// <returns></returns>
    protected T Get<T>([CallerMemberName] string name = null) {
        Debug.Assert(name != null, "name != null");
        object value = null;
        if (_properties.TryGetValue(name, out value))
            return value == null ? default(T) : (T)value;
        return default(T);
    }

    /// <summary>
    /// Sets the value of a property
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="value"></param>
    /// <param name="name"></param>
    /// <remarks>Use this overload when implicitly naming the property</remarks>
    protected void Set<T>(T value, [CallerMemberName] string name = null) {
        Debug.Assert(name != null, "name != null");
        if (Equals(value, Get<T>(name)))
            return;
        _properties[name] = value;
        OnPropertyChanged(name);
    }

    public event PropertyChangedEventHandler PropertyChanged;

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

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

public class Contact : Bindable {
    public string FirstName {
        get { return Get<string>(); }
        set { Set(value); }
    }
}
15 голосов
/ 22 августа 2009

У меня еще не было возможности попробовать это сам, но в следующий раз я настраиваю проект с большим требованием для INotifyPropertyChanged. Я намереваюсь написать атрибут Postsharp , который будет введите код во время компиляции. Что-то вроде:

[NotifiesChange]
public string FirstName { get; set; }

станет:

private string _firstName;

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

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

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


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

11 голосов
/ 11 августа 2013

Да, лучший способ, безусловно, существует. Вот оно:

Пошаговое мной пошаговое руководство, основанное на этой полезной статье .

  • Создать новый проект
  • Установить основной пакет замка в проект

Install-Package Castle.Core

  • Установить только легкие библиотеки mvvm

Install-Package MvvmLightLibs

  • Добавить два класса в проект:

NotifierInterceptor

public class NotifierInterceptor : IInterceptor
    {
        private PropertyChangedEventHandler handler;
        public static Dictionary<String, PropertyChangedEventArgs> _cache =
          new Dictionary<string, PropertyChangedEventArgs>();

        public void Intercept(IInvocation invocation)
        {
            switch (invocation.Method.Name)
            {
                case "add_PropertyChanged":
                    handler = (PropertyChangedEventHandler)
                              Delegate.Combine(handler, (Delegate)invocation.Arguments[0]);
                    invocation.ReturnValue = handler;
                    break;
                case "remove_PropertyChanged":
                    handler = (PropertyChangedEventHandler)
                              Delegate.Remove(handler, (Delegate)invocation.Arguments[0]);
                    invocation.ReturnValue = handler;
                    break;
                default:
                    if (invocation.Method.Name.StartsWith("set_"))
                    {
                        invocation.Proceed();
                        if (handler != null)
                        {
                            var arg = retrievePropertyChangedArg(invocation.Method.Name);
                            handler(invocation.Proxy, arg);
                        }
                    }
                    else invocation.Proceed();
                    break;
            }
        }

        private static PropertyChangedEventArgs retrievePropertyChangedArg(String methodName)
        {
            PropertyChangedEventArgs arg = null;
            _cache.TryGetValue(methodName, out arg);
            if (arg == null)
            {
                arg = new PropertyChangedEventArgs(methodName.Substring(4));
                _cache.Add(methodName, arg);
            }
            return arg;
        }
    }

ProxyCreator

public class ProxyCreator
{
    public static T MakeINotifyPropertyChanged<T>() where T : class, new()
    {
        var proxyGen = new ProxyGenerator();
        var proxy = proxyGen.CreateClassProxy(
          typeof(T),
          new[] { typeof(INotifyPropertyChanged) },
          ProxyGenerationOptions.Default,
          new NotifierInterceptor()
          );
        return proxy as T;
    }
}
  • Создайте модель вида, например:

-

 public class MainViewModel
    {
        public virtual string MainTextBox { get; set; }

        public RelayCommand TestActionCommand
        {
            get { return new RelayCommand(TestAction); }
        }

        public void TestAction()
        {
            Trace.WriteLine(MainTextBox);
        }
    }
  • Поместите привязки в xaml:

    <TextBox Text="{Binding MainTextBox}" ></TextBox>
    <Button Command="{Binding TestActionCommand}" >Test</Button>
    
  • Поместите строку кода в файл с выделенным кодом MainWindow.xaml.cs следующим образом:

DataContext = ProxyCreator.MakeINotifyPropertyChanged<MainViewModel>();

  • Наслаждайтесь.

enter image description here

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

7 голосов
/ 25 августа 2009

Очень похожий на AOP подход заключается в том, чтобы внедрить содержимое INotifyPropertyChanged в уже созданный объект на лету. Вы можете сделать это с чем-то вроде Castle DynamicProxy. Вот статья, которая объясняет технику:

Добавление INotifyPropertyChanged к существующему объекту

5 голосов
/ 04 ноября 2012

Смотрите здесь: http://dotnet -forum.de / blogs / thearchitect / archive / 2012/11/01 / die-optimale-Implementierung-des-inotifypropertychanged-interfaces.aspx

Он написан на немецком языке, но вы можете скачать ViewModelBase.cs. Все комментарии в CS-файле написаны на английском языке.

С помощью этого ViewModelBase-Class можно реализовать привязываемые свойства, подобные хорошо известным свойствам зависимости:

public string SomeProperty
{
    get { return GetValue( () => SomeProperty ); }
    set { SetValue( () => SomeProperty, value ); }
}
...