WPF ListView:更改ItemsSource不会改变ListView

23

我正在使用 ListView 控件来显示一些数据行。有一个后台任务接收列表内容的外部更新。新接收到的数据可能包含数量较少、较多或相同数量的项,并且项本身可能已更改。

ListView.ItemsSource 绑定到 ObservableCollection (_itemList),以便对 _itemList 的更改也应在 ListView 中可见。

_itemList = new ObservableCollection<PmemCombItem>();
_itemList.CollectionChanged += new NotifyCollectionChangedEventHandler(OnCollectionChanged);
L_PmemCombList.ItemsSource = _itemList;
为了避免刷新整个 ListView,我会简单比较新检索到的列表和当前的 _itemList,更改不同的项并在必要时添加/删除项。集合“newList”包含新创建的对象,因此替换_itemList中的项目将正确发送“刷新”通知(我可以使用ObservableCollection的事件处理程序OnCollectionChanged记录它)。
Action action = () =>
{
    for (int i = 0; i < newList.Count; i++)
    {
        // item exists in old list -> replace if changed
        if (i < _itemList.Count)
        {
            if (!_itemList[i].SameDataAs(newList[i]))
                _itemList[i] = newList[i];
        }
        // new list contains more items -> add items
        else
            _itemList.Add(newList[i]);
     }
     // new list contains less items -> remove items
     for (int i = _itemList.Count - 1; i >= newList.Count; i--)
         _itemList.RemoveAt(i);
 };
 Dispatcher.BeginInvoke(DispatcherPriority.Background, action);

我的问题是,如果在此循环中更改了许多项,ListView 不会刷新,屏幕上的数据仍然保持原样...我不明白这一点。

即使是像这样更简单的版本(交换所有元素)

List<PmemCombItem> newList = new List<PmemCombItem>();
foreach (PmemViewItem comb in combList)
    newList.Add(new PmemCombItem(comb));

if (_itemList.Count == newList.Count)
    for (int i = 0; i < newList.Count; i++)
        _itemList[i] = newList[i];
else
{
    _itemList.Clear();
    foreach (PmemCombItem item in newList)
        _itemList.Add(item);
}

无法正常工作

有什么线索吗?

更新

如果我在更新所有元素后手动调用以下代码,则一切正常运行。

OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));

当然,这会导致用户界面更新所有内容,我仍然希望避免这种情况。


1
你尝试过同步的 Dispatcher.Invoke 而不是异步的 Dispatcher.BeginInvoke 吗? - Clemens
我使用Dispatcher.Invoke得到了相同的结果。 - Danny1075
该列表大约有100个项目,更改其中4个项目可以正常工作,但更改10个项目没有任何视觉效果。我尝试了几种优先级,但到目前为止行为没有改变。 - Danny1075
为了确保您的更新代码没有问题,如果您只清除整个“_itemList”,然后从“newList”添加所有项目,它会如何表现? - Clemens
我在上面发布了一个新的、更简单的版本,但如果有多个项目被替换,它仍然只能有时候工作。 - Danny1075
显示剩余5条评论
7个回答

43
在更改后,您可以使用以下方法刷新Listview,这样更容易。
listView.Items.Refresh();

14
如何使用MVVM实现这个? - Matthis Kohli
如果您已经用新对象替换了ItemsSource,那么似乎这并不起作用。在这种情况下,我们需要像Xopher建议的那样首先将ItemsSource设置为null,或者更好的是像Anatolii建议的那样。 - Etherman
@MatthisKohli,我能想到的唯一方法是让视图模型公开一个事件,视图可以订阅该事件,然后调用上述代码。也许不是理想的解决方案... - The Muffin Man
这不正是使用ObservableCollection和WPF的确切原因吗?避免使用硬编码的Refresh调用。由于这是一个旧问题,在VS 2022中仍然存在该问题。我只是使用了Refresh方法,因为它更易读。根据下面的答案,添加特殊的事件处理程序和逻辑,当对象本来应该自动执行时,似乎存在风险会被弃用或在未来修复此问题。显式地再次调用Refresh似乎虽然不理想,但在保持尽可能简单的同时更好地保护了未来。 - Daniel Brown

32

这就是我必须做的才能让它工作。

MyListView.ItemsSource = null;
MyListView.ItemsSource = MyDataSource;

6
我知道这是一个老问题,但我刚刚遇到了这个问题。我不想使用null赋值技巧或刷新仅更新的字段。
因此,在查看MSDN后,我找到了这篇文章:https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.inotifypropertychanged?redirectedfrom=MSDN&view=netframework-4.7.2 简而言之,您只需要让项目实现此接口,它就会自动检测到该对象可以被观察。
public class MyItem : INotifyPropertyChanged
{
    private string status;

    public string Status
    {
        get => status;
        set
        {
            OnPropertyChanged(nameof(Status));
            status = value;
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

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

所以,每当有人更改“状态”时,事件将被调用。在你的情况下,listview将自动添加一个处理程序到“PropertyChanged”事件上。
这并没有真正解决你的问题(添加/删除)。 但为此,我建议你看一下BindingList https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.bindinglist-1?view=netframework-4.7.2 使用相同的模式,你的listview将被正确更新,而不需要使用任何技巧。

5
每次可观察到的集合发生变化时,不应该重置ListViewItemsSource。只需设置适当的绑定即可解决问题。在xaml中实现:
<ListView ItemsSource='{Binding ItemsCollection}'
 ...
</ListView>

在代码后端(建议使用MVVM),需要一个属性来保存_itemList

public ObservableCollection<PmemCombItem> ItemsCollection
{
   get 
   {
      if (_itemList == null)
      {
          _itemList = new ObservableCollection<PmemCombItem>();
      }
      return _itemList;
   }
}
更新: 有一个类似的帖子,很可能可以回答你的问题:如何通过工作线程更新ObservableCollection?

我只在初始化期间设置ItemsSource属性一次,只有当列表发生更改时才执行第二段代码。 - Danny1075
1
@Danny1075 是的,但这不是它应该被使用的方式。Set != bind. - Anatolii Gabuza
尽管这并没有回答问题(在这里使用绑定与否并不重要),但为什么要在ItemsSource属性上使用双向绑定? - Clemens
@Clemens 确实。这不是必需的。 - Anatolii Gabuza
1
抱歉,即使您进行了编辑,这也不能回答问题。将ItemsSource属性绑定到ObservableCollection或直接设置它并不重要。在这两种情况下,ObservableCollection都会通知对集合所做的更改。绑定再也不起作用了。 - Clemens
1
另外需要补充的是:当只有少量项目被更改时,代码可以正常工作并且ListView会显示新的项目。但如果在循环中有许多项目被更改,ListView将完全不会发生变化。 - Danny1075

0

我找到了一种方法来做这件事。虽然不是很好,但它能够工作。

YourList.ItemsSource = null;
// Update the List containing your elements (lets call it x)
YourList.ItemsSource = x;

这应该可以刷新你的 ListView (它在我的 UAP 上运行 :) )


3
Xopher提供的答案的副本 - Mark Hosang

0
请查看这个答案: 使用Prism库将ListView项目传递给命令 列表视图项目需要通知关于更改(在setter中完成)。
public ObservableCollection<Model.Step> Steps 
{
    get { return _steps; }
    set { SetProperty(ref _steps, value); }
} 

需要在XAML中设置UpdateSourceTrigger

<Image Source="{Binding ImageData, UpdateSourceTrigger=PropertyChanged}" />

0

对Xopher的回答的另一种选择。

MyListView.ItemsSource = MyDataSource.ToList();

这会刷新Listview,因为它是另一个列表。


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