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

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

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

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

Ответы [ 34 ]

1 голос
/ 04 июня 2017

Я придумал этот базовый класс для реализации наблюдаемого шаблона, в значительной степени делает то, что вам нужно ( "автоматически" реализует set и get). Я потратил на это целый час в качестве прототипа, поэтому он не имеет много модульных тестов, но подтверждает концепцию. Обратите внимание, что он использует Dictionary<string, ObservablePropertyContext> для удаления необходимости в закрытых полях.

  public class ObservableByTracking<T> : IObservable<T>
  {
    private readonly Dictionary<string, ObservablePropertyContext> _expando;
    private bool _isDirty;

    public ObservableByTracking()
    {
      _expando = new Dictionary<string, ObservablePropertyContext>();

      var properties = this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).ToList();
      foreach (var property in properties)
      {
        var valueContext = new ObservablePropertyContext(property.Name, property.PropertyType)
        {
          Value = GetDefault(property.PropertyType)
        };

        _expando[BuildKey(valueContext)] = valueContext;
      }
    }

    protected void SetValue<T>(Expression<Func<T>> expression, T value)
    {
      var keyContext = GetKeyContext(expression);
      var key = BuildKey(keyContext.PropertyName, keyContext.PropertyType);

      if (!_expando.ContainsKey(key))
      {
        throw new Exception($"Object doesn't contain {keyContext.PropertyName} property.");
      }

      var originalValue = (T)_expando[key].Value;
      if (EqualityComparer<T>.Default.Equals(originalValue, value))
      {
        return;
      }

      _expando[key].Value = value;
      _isDirty = true;
    }

    protected T GetValue<T>(Expression<Func<T>> expression)
    {
      var keyContext = GetKeyContext(expression);
      var key = BuildKey(keyContext.PropertyName, keyContext.PropertyType);

      if (!_expando.ContainsKey(key))
      {
        throw new Exception($"Object doesn't contain {keyContext.PropertyName} property.");
      }

      var value = _expando[key].Value;
      return (T)value;
    }

    private KeyContext GetKeyContext<T>(Expression<Func<T>> expression)
    {
      var castedExpression = expression.Body as MemberExpression;
      if (castedExpression == null)
      {
        throw new Exception($"Invalid expression.");
      }

      var parameterName = castedExpression.Member.Name;

      var propertyInfo = castedExpression.Member as PropertyInfo;
      if (propertyInfo == null)
      {
        throw new Exception($"Invalid expression.");
      }

      return new KeyContext {PropertyType = propertyInfo.PropertyType, PropertyName = parameterName};
    }

    private static string BuildKey(ObservablePropertyContext observablePropertyContext)
    {
      return $"{observablePropertyContext.Type.Name}.{observablePropertyContext.Name}";
    }

    private static string BuildKey(string parameterName, Type type)
    {
      return $"{type.Name}.{parameterName}";
    }

    private static object GetDefault(Type type)
    {
      if (type.IsValueType)
      {
        return Activator.CreateInstance(type);
      }
      return null;
    }

    public bool IsDirty()
    {
      return _isDirty;
    }

    public void SetPristine()
    {
      _isDirty = false;
    }

    private class KeyContext
    {
      public string PropertyName { get; set; }
      public Type PropertyType { get; set; }
    }
  }

  public interface IObservable<T>
  {
    bool IsDirty();
    void SetPristine();
  }

Вот использование

public class ObservableByTrackingTestClass : ObservableByTracking<ObservableByTrackingTestClass>
  {
    public ObservableByTrackingTestClass()
    {
      StringList = new List<string>();
      StringIList = new List<string>();
      NestedCollection = new List<ObservableByTrackingTestClass>();
    }

    public IEnumerable<string> StringList
    {
      get { return GetValue(() => StringList); }
      set { SetValue(() => StringIList, value); }
    }

    public IList<string> StringIList
    {
      get { return GetValue(() => StringIList); }
      set { SetValue(() => StringIList, value); }
    }

    public int IntProperty
    {
      get { return GetValue(() => IntProperty); }
      set { SetValue(() => IntProperty, value); }
    }

    public ObservableByTrackingTestClass NestedChild
    {
      get { return GetValue(() => NestedChild); }
      set { SetValue(() => NestedChild, value); }
    }

    public IList<ObservableByTrackingTestClass> NestedCollection
    {
      get { return GetValue(() => NestedCollection); }
      set { SetValue(() => NestedCollection, value); }
    }

    public string StringProperty
    {
      get { return GetValue(() => StringProperty); }
      set { SetValue(() => StringProperty, value); }
    }
  }
1 голос
/ 17 февраля 2017

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

public abstract class AbstractObject : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected virtual bool SetValue<TKind>(ref TKind Source, TKind NewValue, params string[] Notify)
    {
        //Set value if the new value is different from the old
        if (!Source.Equals(NewValue))
        {
            Source = NewValue;

            //Notify all applicable properties
            foreach (var i in Notify)
                OnPropertyChanged(i);

            return true;
        }

        return false;
    }

    public AbstractObject()
    {
    }
}

Другими словами, вышеприведенное решение удобно, если вы не возражаете против этого:

public class SomeObject : AbstractObject
{
    public string AnotherProperty
    {
        get
        {
            return someProperty ? "Car" : "Plane";
        }
    }

    bool someProperty = false;
    public bool SomeProperty
    {
        get
        {
            return someProperty;
        }
        set
        {
            SetValue(ref someProperty, value, "SomeProperty", "AnotherProperty");
        }
    }

    public SomeObject() : base()
    {
    }
}

Плюсы

  • Без отражения
  • Уведомляет только если старое значение! = Новое значение
  • Уведомить сразу несколько свойств

Против

  • Нет автоматических свойств (хотя вы можете добавить поддержку для обоих!)
  • Немного многословия
  • Бокс (небольшой удар по производительности?)

Увы, это все же лучше, чем делать это,

set
{
    if (!someProperty.Equals(value))
    {
        someProperty = value;
        OnPropertyChanged("SomeProperty");
        OnPropertyChanged("AnotherProperty");
    }
}

Для каждого отдельного свойства, которое становится кошмаром с дополнительным многословием; - (

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

1 голос
/ 17 февраля 2013

Идея с использованием отражения:

class ViewModelBase : INotifyPropertyChanged {

    public event PropertyChangedEventHandler PropertyChanged;

    bool Notify<T>(MethodBase mb, ref T oldValue, T newValue) {

        // Get Name of Property
        string name = mb.Name.Substring(4);

        // Detect Change
        bool changed = EqualityComparer<T>.Default.Equals(oldValue, newValue);

        // Return if no change
        if (!changed) return false;

        // Update value
        oldValue = newValue;

        // Raise Event
        if (PropertyChanged != null) {
            PropertyChanged(this, new PropertyChangedEventArgs(name));
        }//if

        // Notify caller of change
        return true;

    }//method

    string name;

    public string Name {
        get { return name; }
        set {
            Notify(MethodInfo.GetCurrentMethod(), ref this.name, value);
        }
    }//method

}//class
1 голос
/ 02 августа 2018

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

Проблема в том, что вы не можете ссылаться на свойство. Однако вы можете использовать Action для установки этого свойства.

protected bool TrySetProperty<T>(Action<T> property, T newValue, T oldValue, [CallerMemberName] string propertyName = null)
{
    if (EqualityComparer<T>.Default.Equals(oldValue, newValue))
    {
        return false;
    }

    property(newValue);
    RaisePropertyChanged(propertyName);
    return true;
}

Это можно использовать как следующий фрагмент кода.

public int Prop {
    get => model.Prop;
    set => TrySetProperty(x => model.Prop = x, value, model.Prop);
}

Проверьте это BitBucket repo для полной реализации метода и нескольких различных способов достижения того же результата, включая метод, использующий LINQ, и метод, использующий отражение. Обратите внимание, что эти методы медленнее в производительности.

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

Другие вещи, которые вы можете учитывать при реализации этих видов свойств, - это тот факт, что INotifyPropertyChang * ed * ing оба используют классы аргументов событий.тогда число экземпляров класса аргумента события может быть огромным, вам следует рассмотреть возможность их кэширования, поскольку они являются одной из областей, в которых может произойти взрыв строки..

Блог Джоша Смита

0 голосов
/ 27 октября 2015

Реализация Prism 5:

public abstract class BindableBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual bool SetProperty<T>(ref T storage,
                                          T value,
                                          [CallerMemberName] string propertyName = null)
    {
        if (object.Equals(storage, value)) return false;

        storage = value;
        this.OnPropertyChanged(propertyName);

        return true;
    }

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

    protected void OnPropertyChanged<T>(Expression<Func<T>> propertyExpression)
    {
        var propertyName = PropertySupport.ExtractPropertyName(propertyExpression);
        this.OnPropertyChanged(propertyName);
    }
}

public static class PropertySupport
{
    public static string ExtractPropertyName<T>(Expression<Func<T>> propertyExpression)
    {
        if (propertyExpression == null)
        {
            throw new ArgumentNullException("propertyExpression");
        }

        var memberExpression = propertyExpression.Body as MemberExpression;
        if (memberExpression == null)
        {
            throw new ArgumentException("The expression is not a member access expression.", "propertyExpression");
        }

        var property = memberExpression.Member as PropertyInfo;
        if (property == null)
        {
            throw new ArgumentException("The member access expression does not access a property.", "propertyExpression");
        }

        var getMethod = property.GetMethod;
        if (getMethod.IsStatic)
        {
            throw new ArgumentException("The referenced property is a static property.", "propertyExpression");
        }

        return memberExpression.Member.Name;
    }
}
0 голосов
/ 24 сентября 2015

Я использую следующий метод расширения (используя C # 6.0), чтобы сделать реализацию INPC максимально простой:

public static bool ChangeProperty<T>(this PropertyChangedEventHandler propertyChanged, ref T field, T value, object sender,
    IEqualityComparer<T> comparer = null, [CallerMemberName] string propertyName = null)
{
    if (comparer == null)
        comparer = EqualityComparer<T>.Default;

    if (comparer.Equals(field, value))
    {
        return false;
    }
    else
    {
        field = value;
        propertyChanged?.Invoke(sender, new PropertyChangedEventArgs(propertyName));
        return true;
    }
}

Реализация INPC сводится к (вы можете реализовать это каждый раз или создать базовый класс):

public class INPCBaseClass: INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected bool changeProperty<T>(ref T field, T value,
        IEqualityComparer<T> comparer = null, [CallerMemberName] string propertyName = null)
    {
        return PropertyChanged.ChangeProperty(ref field, value, this, comparer, propertyName);
    }
}

Тогда напишите ваши свойства так:

private string testProperty;
public string TestProperty
{
    get { return testProperty; }
    set { changeProperty(ref testProperty, value); }
}

ПРИМЕЧАНИЕ. Вы можете опустить объявление [CallerMemberName] в методе расширения, если хотите, но я хотел сохранить его гибким.

Если у вас есть свойства без вспомогательного поля, вы можете перегрузить changeProperty:

protected bool changeProperty<T>(T property, Action<T> set, T value,
    IEqualityComparer<T> comparer = null, [CallerMemberName] string propertyName = null)
{
    bool ret = changeProperty(ref property, value, comparer, propertyName);
    if (ret)
        set(property);
    return ret;
}

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

public string MyTestProperty
{
    get { return base.TestProperty; }
    set { changeProperty(base.TestProperty, (x) => { base.TestProperty = x; }, value); }
}
0 голосов
/ 02 мая 2014

Я решил таким образом (это немного трудоемко, но, безусловно, быстрее во время выполнения).

В VB (извините, но я думаю, что это не сложно перевести на C #), я делаю эту замену с помощью RE:

(?<Attr><(.*ComponentModel\.)Bindable\(True\)>)( |\r\n)*(?<Def>(Public|Private|Friend|Protected) .*Property )(?<Name>[^ ]*) As (?<Type>.*?)[ |\r\n](?![ |\r\n]*Get)

с:

Private _${Name} As ${Type}\r\n${Attr}\r\n${Def}${Name} As ${Type}\r\nGet\r\nReturn _${Name}\r\nEnd Get\r\nSet (Value As ${Type})\r\nIf _${Name} <> Value Then \r\n_${Name} = Value\r\nRaiseEvent PropertyChanged(Me, New ComponentModel.PropertyChangedEventArgs("${Name}"))\r\nEnd If\r\nEnd Set\r\nEnd Property\r\n

Это преобразование всего кода, как это:

<Bindable(True)>
Protected Friend Property StartDate As DateTime?

В

Private _StartDate As DateTime?
<Bindable(True)>
Protected Friend Property StartDate As DateTime?
    Get
        Return _StartDate
    End Get
    Set(Value As DateTime?)
        If _StartDate <> Value Then
            _StartDate = Value
            RaiseEvent PropertyChange(Me, New ComponentModel.PropertyChangedEventArgs("StartDate"))
        End If
    End Set
End Property

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

Private _(?<Name>.*) As (?<Type>.*)[\r\n ]*(?<Attr><(.*ComponentModel\.)Bindable\(True\)>)[\r\n ]*(?<Def>(Public|Private|Friend|Protected) .*Property )\k<Name> As \k<Type>[\r\n ]*Get[\r\n ]*Return _\k<Name>[\r\n ]*End Get[\r\n ]*Set\(Value As \k<Type>\)[\r\n ]*If _\k<Name> <> Value Then[\r\n ]*_\k<Name> = Value[\r\n ]*RaiseEvent PropertyChanged\(Me, New (.*ComponentModel\.)PropertyChangedEventArgs\("\k<Name>"\)\)[\r\n ]*End If[\r\n ]*End Set[\r\n ]*End Property

С

${Attr} ${Def} ${Name} As ${Type}

Я выкидываю вместо кода IL метода set, но я не могу написать много скомпилированного кода в IL ... Если я напишу день, я скажу вам!

0 голосов
/ 12 марта 2013

Другая идея ...

 public class ViewModelBase : INotifyPropertyChanged
{
    private Dictionary<string, object> _propertyStore = new Dictionary<string, object>();
    protected virtual void SetValue<T>(T value, [CallerMemberName] string propertyName="") {
        _propertyStore[propertyName] = value;
        OnPropertyChanged(propertyName);
    }
    protected virtual T GetValue<T>([CallerMemberName] string propertyName = "")
    {
        object ret;
        if (_propertyStore.TryGetValue(propertyName, out ret))
        {
            return (T)ret;
        }
        else
        {
            return default(T);
        }
    }

    //Usage
    //public string SomeProperty {
    //    get { return GetValue<string>();  }
    //    set { SetValue(value); }
    //}

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
    {
        var temp = PropertyChanged;
        if (temp != null)
            temp.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
0 голосов
/ 22 августа 2009

Поговорим о массовом переобработке. Это значительно сложнее, чем , просто сделать все правильно и практически ничего не даст. Если ваша IDE поддерживает фрагменты кода (Visual Studio / MonoDevelop делают), то вы можете сделать это невероятно простым. Все, что вам действительно нужно будет ввести, - это тип свойства и имя свойства. Три дополнительные строки кода будут сгенерированы автоматически.

...