ObservableCollection没有注意到其中的项发生变化(即使使用了INotifyPropertyChanged)

201

有人知道为什么这段代码不起作用吗:

public class CollectionViewModel : ViewModelBase {  
    public ObservableCollection<EntityViewModel> ContentList
    {
        get { return _contentList; }
        set 
        { 
            _contentList = value; 
            RaisePropertyChanged("ContentList"); 
            //I want to be notified here when something changes..?
            //debugger doesn't stop here when IsRowChecked is toggled
        }
     }
}

public class EntityViewModel : ViewModelBase
{

    private bool _isRowChecked;

    public bool IsRowChecked
    {
        get { return _isRowChecked; }
        set { _isRowChecked = value; RaisePropertyChanged("IsRowChecked"); }
    }
}

ViewModelBase 包含了 RaisePropertyChanged 等相关内容,除了这个问题之外它对其他一切都有效。


这个回答解决了你的问题吗?强制数据绑定的WPF ListBox更新的更好方法? - Berger
21个回答

2

触发ObservableCollection列表中的OnChange

  1. 获取所选项的索引
  2. 从父级中删除该项
  3. 在父级中相同的索引处添加该项

示例:

int index = NotificationDetails.IndexOf(notificationDetails);
NotificationDetails.Remove(notificationDetails);
NotificationDetails.Insert(index, notificationDetails);

2

我曾使用的标准observablecollection的简单解决方案:

不要直接添加到你的属性或更改它的内部项,而是创建一个临时集合,如下:

ObservableCollection<EntityViewModel> tmpList= new ObservableCollection<EntityViewModel>();

并且添加项目或对tmpList进行更改,

tmpList.Add(new EntityViewModel(){IsRowChecked=false}); //Example
tmpList[0].IsRowChecked= true; //Example
...

然后通过赋值将其传递给您的实际属性。
ContentList=tmpList;

这将更改整个属性,从而引起您需要的INotifyPropertyChanged通知。


如果集合绑定到某个元素(如DataGridListBoxItemsContainer),允许用户通过UI修改元素属性,那该怎么办?您的手动方法将需要更多的代码来处理这种广泛使用的情况。 - AntonK

1
这是我实现的版本。它会检查并抛出错误,如果列表中的对象没有实现INotifyPropertyChanged,则无法忘记该问题。在外部,您可以使用ListItemChanged事件来确定列表或列表项本身是否已更改。
public class SpecialObservableCollection<T> : ObservableCollection<T>
{
    public SpecialObservableCollection()
    {
        this.CollectionChanged += OnCollectionChanged;
    }

    void OnCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        AddOrRemoveListToPropertyChanged(e.NewItems,true); 
        AddOrRemoveListToPropertyChanged(e.OldItems,false); 
    }

    private void AddOrRemoveListToPropertyChanged(IList list, Boolean add)
    {
        if (list == null) { return; }
        foreach (object item in list)
        {
            INotifyPropertyChanged o = item as INotifyPropertyChanged;
            if (o != null)
            {
                if (add)  { o.PropertyChanged += ListItemPropertyChanged; }
                if (!add) { o.PropertyChanged -= ListItemPropertyChanged; }
            }
            else
            {
                throw new Exception("INotifyPropertyChanged is required");
            }
        }
    }

    void ListItemPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        OnListItemChanged(this, e);
    }

    public delegate void ListItemChangedEventHandler(object sender, PropertyChangedEventArgs e);

    public event ListItemChangedEventHandler ListItemChanged;

    private void OnListItemChanged(Object sender, PropertyChangedEventArgs e)
    {
        if (ListItemChanged != null) { this.ListItemChanged(this, e); }
    }


}

1
为了在编译时执行此操作,您可以在“T”上具有“INotifyPropertyChanged”的通用约束。 - nawfal
这个类非常有用。我添加了一个 Find<T> 实现,几乎与列表上的相同。我发现在内存集合中搜索时,使用 Find 可以获得比 FirstOrDefault 更好的性能,因为它通过索引而不是 IEnumerable 搜索集合。 - b.pell

1
这里是上述解决方案的扩展方法...
public static TrulyObservableCollection<T> ToTrulyObservableCollection<T>(this List<T> list)
     where T : INotifyPropertyChanged
{
    var newList = new TrulyObservableCollection<T>();

    if (list != null)
    {
        list.ForEach(o => newList.Add(o));
    }

    return newList;
}  

你可能想要解释答案 - geedubb
1
这是一个描述扩展方法的链接。https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/classes-and-structs/extension-methods - LawMan

1

我尝试了这个解决方案,但只有在集合改变时,像RaisePropertyChange("SourceGroupeGridView")一样对我起作用,这会在每个添加或更改的项上触发。

问题出在:

public void EntityViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
     NotifyCollectionChangedEventArgs args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
    OnCollectionChanged(args);
}

NotifyCollectionChangedAction.Reset 这个操作会完全重新绑定 groupedgrid 中的所有项目,相当于 RaisePropertyChanged。当你使用它时,所有 gridview 的分组都会刷新。

如果你只想在 UI 中刷新新项目的组,则不使用 Reset 操作,你需要模拟 itemproperty 中的添加操作,类似于以下内容:

void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{         
    var index = this.IndexOf((T)sender);

    this.RemoveAt(index);
    this.Insert(index, (T)sender);

    var a = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, sender);
    OnCollectionChanged(a);
}

抱歉我的英语不好,感谢提供基础代码 :) 希望这能帮助到某些人 ^_^
享受吧!!

1

我看到这里的大多数示例都在泛型类型上放置INotifyPropertyChanged约束,这强制模型实现INotifyPropertyChanged。

如果您遵循将INotifyPropertyChanged约束放置在模型上的示例,那么它就相当于在模型中实现了INotifyPropertyChanged,并允许ObservableCollection处理更新属性更改。

但是,如果您不想让模型实现INotifyPropertyChanged,可以尝试使用以下方法。

CustomObservableCollection

 public class CustomObservableCollection<T> : ObservableCollection<T>
 {

      public void Refresh(T item)
      {
          var index = IndexOf(item);

          RemoveAt(index);
          Insert(index, item);

          OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, IndexOf(item)));
      }
 }

模型

public class Person
{
    public string FirstName { get; set; }   
    public string LastName { get; set; }   
    public int Age { get; set; }   
}

视图模型
 public class PersonViewModel
 {       

     public PersonViewModel(){

          People=new CustomObservableCollection<Person>();

     }
    
     private void AddPerson(){

        People.Add(new Person(){
            FirstName="John",
            LastName="Doe",
            Age=20
        });

      }
    
      private void UpdatePerson(){

         var person=People.Where(...).FirstOrDefault();
         person.Age=25;

         People.Refresh(person);
      }

      public CustomObservableCollection<Person> People { get; set; } 
    
    }

1

考虑使用BindingList并调用ResetBindings方法,而不是使用ObservableCollection或TrulyObservableCollection。

例如:

private BindingList<TfsFile> _tfsFiles;

public BindingList<TfsFile> TfsFiles
{
    get { return _tfsFiles; }
    set
    {
        _tfsFiles = value;
        NotifyPropertyChanged();
    }
}

给定一个事件,比如点击,你的代码应该如下所示:

foreach (var file in TfsFiles)
{
    SelectedFile = file;
    file.Name = "Different Text";
    TfsFiles.ResetBindings();
}

我的模型长这样:
namespace Models
{
    public class TfsFile 
    {
        public string ImagePath { get; set; }

        public string FullPath { get; set; }

        public string Name { get; set; }

        public string Text { get; set; }

    }
}

3
这种BindingList方法的信息很好,但是这种方法有一个限制,其他答案克服了这个限制:这种技术依赖于代码中的值被改变,并且可以添加对ResetBindings()的调用。如果列表对象通过其他方式进行更改,例如不可更改的代码或从绑定到第二个控件,则大多数其他答案都可以工作。 - Bob Sammers

0

曾经遇到过同样的问题,我想发表一下我的解决方案,使用从ObservableCollection派生的类。它并没有太多不同于上面的实现,但它确实使用了PropertyChangedEventManager,具有两个优点:1. 它使用弱事件,因此消除了未取消挂钩事件和任何导致内存泄漏的问题。2. 它允许指定更改时将触发CollectionChangedEvent的特定属性。

此外,对于像@Martin Harris这样对ObservableCollection行为感到困惑的人,我建议阅读这篇优秀的文章

/// <summary>
/// Implements an ObservableCollection that raises a CollectionChanged (Reset) event if an item in the collection raises PropertyChanged
/// The property name or names mey be optionally specified.
/// Note, could have performance issues if used on a large collection.
/// </summary>
/// <typeparam name="T"></typeparam>
public sealed class ObservableCollectionResetOnItemChange<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
    public IEnumerable<string> PropertyNames { get; private set; }

    public ObservableCollectionResetOnItemChange(IEnumerable<string> propertyNames = null)
    {
        PropertyNames = propertyNames?? new List<string>();
        CollectionChanged += OnCollectionChanged;
    }

    public ObservableCollectionResetOnItemChange(string propertyName = null) :
        this(propertyName is null ? null : new List<string>() { propertyName } )
    {
    }

    public ObservableCollectionResetOnItemChange(IEnumerable<T> items, IEnumerable<string> propertyNames = null) :
        this(propertyNames)
    {
        foreach (T item in items)
        {
            {
                Add(item);
            }
        }
    }

    public ObservableCollectionResetOnItemChange(IEnumerable<T> items, string propertyName = null) :
        this(items, propertyName is null ? null : new List<string>() { propertyName })
    {
    }

    private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.NewItems != null)
        {
            foreach (T item in e.NewItems)
            {
                if (PropertyNames.Any())
                {
                    foreach (string name in PropertyNames)
                    {
                        PropertyChangedEventManager.AddHandler(item, ItemPropertyChanged, name);
                    }
                }
                else
                {
                    PropertyChangedEventManager.AddHandler(item, ItemPropertyChanged, string.Empty);
                }
            }
        }
    }

    private void ItemPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        NotifyCollectionChangedEventArgs args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
        OnCollectionChanged(args);
    }
}

0
对我来说,这个技巧很有帮助——使用removeAt和insert来替换项目,在此过程中,“Change”事件会正确触发。
private ObservableCollection<CollectionItem> collection = new ObservableCollection<CollectionItem>();

public void Update(CollectionItem newItem, CollectionItem old ) {
            int j = collection.IndexOf(old);
            collection.RemoveAt(j); 
            collection.Insert(j, newComplexCondition);
        }

0

您还可以使用此扩展方法轻松地为相关集合中的项目属性更改注册处理程序。该方法会自动添加到所有实现INotifyCollectionChanged并持有实现INotifyPropertyChanged的项目的集合中:

public static class ObservableCollectionEx
{
    public static void SetOnCollectionItemPropertyChanged<T>(this T _this, PropertyChangedEventHandler handler)
        where T : INotifyCollectionChanged, ICollection<INotifyPropertyChanged> 
    {
        _this.CollectionChanged += (sender,e)=> {
            if (e.NewItems != null)
            {
                foreach (Object item in e.NewItems)
                {
                    ((INotifyPropertyChanged)item).PropertyChanged += handler;
                }
            }
            if (e.OldItems != null)
            {
                foreach (Object item in e.OldItems)
                {
                    ((INotifyPropertyChanged)item).PropertyChanged -= handler;
                }
            }
        };
    }
}

如何使用:

public class Test
{
    public static void MyExtensionTest()
    {
        ObservableCollection<INotifyPropertyChanged> c = new ObservableCollection<INotifyPropertyChanged>();
        c.SetOnCollectionItemPropertyChanged((item, e) =>
        {
             //whatever you want to do on item change
        });
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接