При вложении свойств, которые реализуют INotifyPropertyChanged, родительский объект должен распространять изменения? - PullRequest
18 голосов
/ 26 августа 2009

этот вопрос покажет мое непонимание ожидаемого поведения при реализации / использовании INotifyPropertyChanged:

Вопрос в том, что для привязки к работе, как ожидается, когда у вас есть класс, который сам реализует INotifyPropertyChanged, который имеет вложенные свойства типа INotifyPropertyChanged, вы должны внутренне подписаться на уведомление об изменении этих свойств и затем распространять уведомления? Или ожидается, что инфраструктура привязки будет обладать умом, чтобы сделать это ненужным?

Например (обратите внимание, этот код не является полным - просто предназначен для иллюстрации вопроса):

   public class Address : INotifyPropertyChanged
    {
       string m_street
       string m_city;

       public string Street
       {
          get { return m_street; }
          set
          {
             m_street = value;
             NotifyPropertyChanged(new PropertyChangedEventArgs("Street"));
          }
       }

       public string City
       {
          get { return m_city; }
          set 
          {
             m_city = value;
             NotifyPropertyChanged(new PropertyChangedEventArgs("City"));
          }
       }

    public class Person : INotifyPropertyChanged
    {
       Address m_address;

       public Address
       {
          get { return m_address = value; }
          set
          {
             m_address = value;
             NotifyPropertyChanged(new PropertyChangedEventArgs("Address"));
          }
       }
    }

Итак, в этом примере у нас есть вложенный объект Address в объекте Person. Оба из них реализуют INotifyPropertyChanged, так что изменение их свойств приведет к передаче уведомлений об изменениях свойств подписчикам.

Но, скажем, с помощью привязки кто-то подписывается на уведомление об изменении объекта Person и «прослушивает» изменения свойства Address. Они будут получать уведомления, если само свойство Address изменяется (назначается другой объект Address), но НЕ БУДУТ получать уведомления, если изменяются данные, содержащиеся во вложенном объекте address (город или улица).

Это приводит к вопросу - должна ли справляться с этим инфраструктура привязки, или я должен в рамках моей реализации Person подписываться на уведомления об изменениях на объекте address и затем распространять их как изменения на «Address»?

Если вы дойдете до этого момента, спасибо, что уделили время на чтение этого многословного вопроса?

Ответы [ 4 ]

3 голосов
/ 02 августа 2010

Один из самых простых способов сделать это - добавить обработчик событий в Person, который будет обрабатывать события уведомления от объекта m_address:

public class Person : INotifyPropertyChanged
{
   Address m_address;

   public Address
   {
      get { return m_address = value; }
      set
      {
         m_address = value;
         NotifyPropertyChanged(new PropertyChangedEventArgs("Address"));
         m_address.PropertyChanged += new PropertyChangedEventHandler( AddressPropertyChanged );
      }
   }
   void  AddressPropertyChanged( object sender, PropertyChangedEventArgs e )
   {
       NotifyPropertyChanged(new PropertyChangedEventArgs("Address"))
   }
}
1 голос
/ 26 августа 2009

Вы ответили на этот вопрос, когда сказали

... скажем, с помощью привязки кто-то подписаться на уведомление об изменении объект Person,

То, что кто-то подписывается на Person и не может узнать, изменился ли Address. Таким образом, вам придется справиться с этой ситуацией самостоятельно (что довольно легко реализовать).

0 голосов
/ 29 апреля 2016

Старый вопрос, тем не менее ...

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

public class NotifyChangedBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    readonly Dictionary<string, AttachedNotifyHandler> attachedHandlers = new Dictionary<string, AttachedNotifyHandler>();

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

    protected void AttachPropertyChanged(INotifyPropertyChanged notifyPropertyChanged,
        [CallerMemberName] string propertyName = null)
    {
        if (propertyName == null) throw new ArgumentNullException(nameof(propertyName));
        // ReSharper disable once ExplicitCallerInfoArgument
        DetachCurrentPropertyChanged(propertyName);
        if (notifyPropertyChanged != null)
        {
            attachedHandlers.Add(propertyName, new AttachedNotifyHandler(propertyName, this, notifyPropertyChanged));
        }
    }

    protected void DetachCurrentPropertyChanged([CallerMemberName] string propertyName = null)
    {
        if (propertyName == null) throw new ArgumentNullException(nameof(propertyName));
        AttachedNotifyHandler handler;
        if (attachedHandlers.TryGetValue(propertyName, out handler))
        {
            handler.Dispose();
            attachedHandlers.Remove(propertyName);
        }
    }

    sealed class AttachedNotifyHandler : IDisposable
    {
        readonly string propertyName;
        readonly NotifyChangedBase currentObject;
        readonly INotifyPropertyChanged attachedObject;

        public AttachedNotifyHandler(
            [NotNull] string propertyName,
            [NotNull] NotifyChangedBase currentObject,
            [NotNull] INotifyPropertyChanged attachedObject)
        {
            if (propertyName == null) throw new ArgumentNullException(nameof(propertyName));
            if (currentObject == null) throw new ArgumentNullException(nameof(currentObject));
            if (attachedObject == null) throw new ArgumentNullException(nameof(attachedObject));
            this.propertyName = propertyName;
            this.currentObject = currentObject;
            this.attachedObject = attachedObject;

            attachedObject.PropertyChanged += TrackedObjectOnPropertyChanged;
        }

        public void Dispose()
        {
            attachedObject.PropertyChanged -= TrackedObjectOnPropertyChanged;
        }

        void TrackedObjectOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
        {
            currentObject.OnPropertyChanged(propertyName);
        }
    }
}

Использование просто:

public class Foo : NotifyChangedBase
{
    Bar bar;

    public Bar Bar
    {
        get { return bar; }
        set
        {
            if (Equals(value, bar)) return;
            bar = value;
            AttachPropertyChanged(bar);
            OnPropertyChanged();
        }
    }
}

public class Bar : NotifyChangedBase
{
    string prop;

    public string Prop
    {
        get { return prop; }
        set
        {
            if (value == prop) return;
            prop = value;
            OnPropertyChanged();
        }
    }
}

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

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

https://github.com/buunguyen/notify

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

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

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

В вашем примере вы будете привязываться к Address.Street в своем представлении, поэтому вам нужно отправить сообщение notifypropertychanged, содержащее эту строку.

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

        /// <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; });
    }
...