代码之家  ›  专栏  ›  技术社区  ›  Phil

当嵌套实现INotifyPropertyChanged的属性时,父对象是否必须传播更改?

  •  18
  • Phil  · 技术社区  · 16 年前

    这个问题将显示我在实现/使用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"));
              }
           }
        }
    

    在这个例子中,我们在Person对象中有一个嵌套的Address对象。两者都实现了INotifyPropertyChanged,因此对其属性的更改将导致向订阅者发送属性更改通知。

    但是,假设使用绑定某人正在订阅Person对象上的更改通知,并且正在“监听”地址属性的更改。如果地址属性本身发生更改(分配了不同的地址对象),则它们将接收通知,但如果嵌套地址对象(城市或街道)包含的数据发生更改,则它们将不会接收通知。

    这就引出了一个问题-绑定基础结构是否需要处理这个问题,或者我是否应该在Person的实现中订阅address对象上的更改通知,然后将它们作为“address”的更改传播?

    如果你说到这一点,谢谢你花时间阅读这个冗长的问题?

    4 回复  |  直到 7 年前
        1
  •  3
  •   tkola    15 年前

    最简单的方法之一是将事件处理程序添加到处理来自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
  •  1
  •   P.K    16 年前

    你回答这个问题时说

    …说用捆绑某人是 订阅上的更改通知 一个人反对,

    某人正在订阅某人,并且无法知道地址是否已更改。 所以你必须自己处理这种情况(这很容易实现)。

        3
  •  0
  •   DanH    13 年前

    如果你想让子对象看起来像是它们的父对象的一部分,你需要自己做泡泡。

    在您的示例中,您将绑定到视图中的“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
  •  0
  •   madsolver    9 年前

    不过,这是个老问题。。。

    我最初的方法是将更改的子属性附加到父属性。这有一个优势,消费父母的事件很容易。只需要订阅家长。

    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

    这样即使是复杂的数据结构也是简单和可预测的,它由订户控制如何订阅和如何响应,它很好地发挥了绑定引擎的功能。

    推荐文章