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

Когда свойства вложенности, которые реализуют INotifyPropertyChanged, должны быть изменены родительский объект propogate?

этот вопрос покажет мое отсутствие понимания ожидаемого поведения при реализации/использовании 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, и "прослушивает" изменения свойства "Адрес". Они получат уведомления, если само свойство "Адреса" будет изменено (другой объект адреса назначен), но НЕ будет получать уведомления, если данные, содержащиеся в вложенном адресном объекте (городе или улице), будут изменены.

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

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

Комментарий очень оценен!

Фил

4b9b3361

Ответ 1

Один из простейших способов сделать это - добавить обработчик событий к 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"))
   }
}

Ответ 2

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

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

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

Ответ 3

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

В вашем примере вы бы привязывались к "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; });
    }

Ответ 4

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

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

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();
        }
    }
}

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

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

https://github.com/buunguyen/notify

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