Подтвердить что ты не робот

Подписаться на INotifyPropertyChanged для вложенных (дочерних) объектов

Я ищу чистое и элегантное решение для обработки события 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 INotifyPropertyChanged Event
      //   if not null

      _bestFriend = value;
      RaisePropertyChanged("BestFriend");

      // - Subscribe to _bestFriend INotifyPropertyChanged Event if not null
      // - When _bestFriend 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 и его BestFriend значений. Теперь я знаю, что могу сделать это вручную, выполнив все шаги, описанные в комментариях. Но это будет много кода, особенно когда я планирую иметь множество дочерних свойств, реализующих INotifyPropertyChanged как это. Конечно, они не будут всегда одного типа, единственное, что у них есть, это интерфейс INotifyPropertyChanged.

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

Ответ 2

Я думаю, что то, что вы ищете, похоже на привязку WPF.

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

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

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

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

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

Подробнее о привязке WPF см. http://msdn.microsoft.com/en-us/library/ms750413.aspx.

Ответ 3

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

Я нашел другое решение. Он назывался шаблоном проектирования Propagator. Вы можете найти больше в Интернете (например, в CodeProject: Propagator in С# - альтернатива шаблону проектирования наблюдателя).

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

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

A class diagram of the re-usable Propagator classes

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

Ответ 4

Я написал легкий помощник, чтобы сделать это. Вы просто вызываете 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; });
    }

Ответ 5

Я искал Интернет в течение одного дня, и я нашел еще одно приятное решение от Sacha Barber:

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

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

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

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

Ответ 6

Откажитесь от моего решения в 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;
    }
}