Подписаться на INotifyPropertyChanged для вложенных (дочерних) объектов - PullRequest
27 голосов
/ 10 ноября 2010

Я ищу чистое и элегантное решение для обработки события INotifyPropertyChanged вложенных (дочерних) объектов.Пример кода:

public class Person : INotifyPropertyChanged {

  private string _firstName;
  private int _age;
  private Person _bestFriend;

  public string FirstName {
    get { return _firstName; }
    set {
      // Short implementation for simplicity reasons
      _firstName = value;
      RaisePropertyChanged("FirstName");
    }
  }

  public int Age {
    get { return _age; }
    set {
      // Short implementation for simplicity reasons
      _age = value;
      RaisePropertyChanged("Age");
    }
  }

  public Person BestFriend {
    get { return _bestFriend; }
    set {
      // - Unsubscribe from _bestFriend's INotifyPropertyChanged Event
      //   if not null

      _bestFriend = value;
      RaisePropertyChanged("BestFriend");

      // - Subscribe to _bestFriend's INotifyPropertyChanged Event if not null
      // - When _bestFriend's INotifyPropertyChanged Event is fired, i'd like
      //   to have the RaisePropertyChanged("BestFriend") method invoked
      // - Also, I guess some kind of *weak* event handler is required
      //   if a Person instance i beeing destroyed
    }
  }

  // **INotifyPropertyChanged implementation**
  // Implementation of RaisePropertyChanged method

}

Сосредоточиться на свойстве BestFriend и его настройщике значений.Теперь Я знаю, что мог бы сделать это вручную , реализуя все шаги, описанные в комментариях.Но это будет много кода, особенно когда я планирую иметь много дочерних свойств, реализующих INotifyPropertyChanged, как это.Конечно, они не всегда будут одного типа, единственное, что у них общего, - это интерфейс INotifyPropertyChanged.

Причина в том, что в моем реальном сценарии у меня есть сложный «Предмет»(в корзине) объект, который имеет вложенные свойства объекта в нескольких слоях (у Item есть объект «License», который сам может снова иметь дочерние объекты), и мне нужно получать уведомление о любом отдельном изменении «Item», чтобы иметь возможностьпересчитать цену.

Есть ли у вас хорошие советы или хотя бы какая-то реализация, которая поможет мне решить эту проблему?

К сожалению, я не могу / не могу использовать почту-строить шаги вроде PostSharp для достижения моей цели.

Заранее большое спасибо,
- Томас

Ответы [ 6 ]

21 голосов
/ 18 ноября 2010

, так как я не смог найти готовое к использованию решение, я сделал пользовательскую реализацию на основе предложений Питера (и Маркса) (спасибо!).

Используя классы, выбудут уведомлены о любых изменениях в глубоком дереве объектов, это работает для любых INotifyPropertyChanged реализующих типов и INotifyCollectionChanged* реализующих коллекций (очевидно, я использую ObservableCollection для этого).

IНадеюсь, это оказалось довольно чистым и элегантным решением, хотя оно не полностью протестировано и есть возможности для улучшений.Его довольно просто использовать, просто создайте экземпляр ChangeListener, используя его статический метод Create, и передайте INotifyPropertyChanged:

var listener = ChangeListener.Create(myViewModel);
listener.PropertyChanged += 
    new PropertyChangedEventHandler(listener_PropertyChanged);

PropertyChangedEventArgs, обеспечивающий PropertyName, который всегда будетполный «путь» ваших объектов.Например, если вы измените «BestFriend» имя вашего персонажа, PropertyName будет «BestFriend.Name», если BestFriend имеет коллекцию дочерних элементов, а вы изменили его возраст, значение будет «BestFriend.Children []. Возраст "и так далее.Не забудьте Dispose когда ваш объект уничтожен, тогда он (будем надеяться) полностью отписывается от всех слушателей событий.

Он компилируется в .NET (протестировано в 4) и Silverlight (протестировано в 4),Поскольку код разделен на три класса, я разместил код в gist 705450 , где вы можете получить все это: https://gist.github.com/705450 **

*) Одна из причин того, что код работает, заключается в том, что ObservableCollection также реализует INotifyPropertyChanged, иначе он не будет работать должным образом, это известное предостережение

**) Используйте бесплатно, выпущенопод Лицензия MIT

16 голосов
/ 10 ноября 2010

Я думаю, что вы ищете что-то вроде связывания WPF.

Как работает INotifyPropertyChanged, так это то, что RaisePropertyChanged("BestFriend"); должен только быть предопределеннымкогда свойство BestFriend изменяется.Не тогда, когда что-либо в самом объекте меняется.

Как вы бы это реализовали - это двухэтапный INotifyPropertyChanged обработчик событий.Ваш слушатель будет регистрироваться на измененном событии Person.Когда BestFriend устанавливается / изменяется, вы регистрируетесь на измененном событии BestFriend Person.Затем вы начинаете прослушивать измененные события этого объекта.

Именно так привязка WPF реализует это.Прослушивание изменений вложенных объектов осуществляется через эту систему.

Причина, по которой это не сработает, когда вы реализуете его в Person, заключается в том, что уровни могут стать очень глубокими и измененное событие BestFriend больше ничего не значит («что изменилось?»).Эта проблема становится больше, когда у вас круговые отношения, где, например, лучший друг вашего матери - мать вашего лучшего друга.Затем, когда одно из свойств изменяется, вы получаете переполнение стека.

Итак, как вы можете решить эту проблему - создать класс, с помощью которого вы можете создавать слушатели.Например, вы должны собрать слушателя на BestFriend.FirstName.Затем этот класс помещает обработчик события в измененное событие Person и прослушивает изменения BestFriend.Затем, когда это изменяется, он помещает слушателя в BestFriend и слушает изменения FirstName.Затем, когда это меняется, оно отправляет событие, и вы можете прослушать это.Это в основном то, как работает привязка WPF.

См. http://msdn.microsoft.com/en-us/library/ms750413.aspx для получения дополнительной информации о привязке WPF.

3 голосов
/ 26 января 2012

Интересное решение Томас.

Я нашел другое решение.Это называется шаблон дизайна пропагатора.Вы можете найти больше в Интернете (например, в CodeProject: Распространитель в C # - Альтернатива шаблону проектирования Observer ).

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

Диаграмма классов повторно используемых классов Пропагатора:

A class diagram of the re-usable Propagator classes

Подробнее о CodeProject .

0 голосов
/ 27 мая 2014

Ознакомьтесь с моим решением на CodeProject: http://www.codeproject.com/Articles/775831/INotifyPropertyChanged-propagator Он делает именно то, что вам нужно - помогает распространять (элегантным образом) зависимые свойства, когда меняются соответствующие зависимости в этой или любой модели вложенного представления:

public decimal ExchTotalPrice
{
    get
    {
        RaiseMeWhen(this, has => has.Changed(_ => _.TotalPrice));
        RaiseMeWhen(ExchangeRate, has => has.Changed(_ => _.Rate));
        return TotalPrice * ExchangeRate.Rate;
    }
}
0 голосов
/ 09 мая 2014

Я искал в Интернете один день и нашел другое хорошее решение от Саши Барбера:

http://www.codeproject.com/Articles/166530/A-Chained-Property-Observer

Он создал слабые ссылки в Обозревателе цепочек свойств.Изучите статью, если вы хотите увидеть другой отличный способ реализовать эту функцию.

И я также хочу упомянуть хорошую реализацию с Reactive Extensions @ http://www.rowanbeach.com/rowan-beach-blog/a-system-reactive-property-change-observer/

Это решение работает только дляодин уровень наблюдателя, а не полная цепочка наблюдателей.

0 голосов
/ 21 августа 2012

Я написал простой помощник для этого. Вы просто вызываете BubblePropertyChanged (x => x.BestFriend) в модели родительского представления. нотабене есть предположение, что у вас есть родительский метод NotifyPropertyChagned, но вы можете его адаптировать.

        /// <summary>
    /// Bubbles up property changed events from a child viewmodel that implements {INotifyPropertyChanged} to the parent keeping
    /// the naming hierarchy in place.
    /// This is useful for nested view models. 
    /// </summary>
    /// <param name="property">Child property that is a viewmodel implementing INotifyPropertyChanged.</param>
    /// <returns></returns>
    public IDisposable BubblePropertyChanged(Expression<Func<INotifyPropertyChanged>> property)
    {
        // This step is relatively expensive but only called once during setup.
        MemberExpression body = (MemberExpression)property.Body;
        var prefix = body.Member.Name + ".";

        INotifyPropertyChanged child = property.Compile().Invoke();

        PropertyChangedEventHandler handler = (sender, e) =>
        {
            this.NotifyPropertyChanged(prefix + e.PropertyName);
        };

        child.PropertyChanged += handler;

        return Disposable.Create(() => { child.PropertyChanged -= handler; });
    }
...