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

时间:2009-08-26 17:46:29

标签: .net inotifypropertychanged

这个问题将表明我在实现/使用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对象的更改通知,并且正在“监听”对Address属性的更改。如果Address属性本身发生更改(分配了不同的Address对象),它们将收到通知,但如果嵌套地址对象(城市或街道)包含的数据发生更改,则不会收到通知。

这导致了一个问题 - 期望处理此问题的绑定基础结构,还是我应该在Person的实现中订阅地址对象上的更改通知,然后将它们作为“地址”的更改传播?

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

4 个答案:

答案 0 :(得分:3)

最简单的方法之一是向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 :(得分:1)

当你说

时,你回答了这个问题
  

...说使用绑定某人是   订阅更改通知   一个Person对象,

有人订阅了Person并且无法知道地址是否已更改。 因此,您必须自己处理这种情况(这很容易实现)。

答案 2 :(得分:0)

如果您希望子对象直接看到他们是父母的一部分,您需要自己冒泡。

对于您的示例,您将在视图中绑定到“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; });
    }

答案 3 :(得分:0)

一个老问题,不过......

我原来的方法是将子属性更改为父级。这具有优势,消耗父母的事件很容易。只需要订阅父母。

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

这样即使是复杂的数据结构也很简单且可预测,它在订阅者控制下如何订阅和如何反应,它与绑定引擎的功能相得益彰。