23

I am using a ListView control to display some lines of data. There is a background task which receives external updates to the content of the list. The newly received data may contain less, more or the same number of items and also the items itself may have changed.

The ListView.ItemsSource is bound to an OberservableCollection (_itemList) so that changes to _itemList should be visible also in the ListView.

_itemList = new ObservableCollection<PmemCombItem>();
_itemList.CollectionChanged += new NotifyCollectionChangedEventHandler(OnCollectionChanged);
L_PmemCombList.ItemsSource = _itemList;

In order to avoid refreshing the complete ListView I do a simple comparison of the newly retrieved list with the current _itemList, change items which are not the same and add/remove items if necessary. The collection "newList" contains newly created objects, so replacing an item in _itemList is correctly sending a "Refresh" notification (which I can log by using the event handler OnCollectionChanged of the ObservableCollection`)

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

My problem is that if many items are changed in this loop, the ListView is NOT refreshing and the data on screen stay as they are...and this I don't understand.

Even a simpler version like this (exchanging ALL elements)

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

is not working properly

Any clue on this?

UPDATE

If I call the following code manually after updating all elements, everything works fine

OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));

But of course this causes the UI to update everything which I still want to avoid.

Clemens
  • 123,504
  • 12
  • 155
  • 268
Danny1075
  • 231
  • 1
  • 2
  • 4
  • 1
    Did you try a synchronous `Dispatcher.Invoke` instead of the asynchronous `Dispatcher.BeginInvoke`? – Clemens Jan 08 '14 at 13:04
  • I get the same results using Dispatcher.Invoke – Danny1075 Jan 08 '14 at 13:10
  • About how many items are we talking here? And did you try different dispatcher priorities? – Clemens Jan 08 '14 at 13:31
  • The list has about 100 items and changing e.g. 4 of them works fine, changing 10 has no visual effect. I tried several priorities, up to now no change of behaviour – Danny1075 Jan 08 '14 at 13:38
  • And just to make sure there is nothing wrong in your update code, how does it behave if you simply clear the entire `_itemList` and then add all items from `newList`? – Clemens Jan 08 '14 at 13:41
  • This is working perfectly, but of course this is rather slow and I would like to avoid that – Danny1075 Jan 08 '14 at 13:44
  • And you are positively sure that changed items are at the proper index positions in the new list, and also that your SameDataAs method is ok? – Clemens Jan 08 '14 at 13:49
  • I posted a new, simpler version above which still does only work sometimes if several items get replaced – Danny1075 Jan 08 '14 at 14:03
  • And the new items in `newList` are really references to *new instances* of class `PmemCombItem`. Or is there any chance they are referencing the same objects? – Clemens Jan 08 '14 at 14:08
  • I added the code for creating newList above... – Danny1075 Jan 08 '14 at 14:11

7 Answers7

43

After a change, you can use the following to refresh the Listview, it's more easy

listView.Items.Refresh();
matthewpepper
  • 439
  • 4
  • 3
  • 14
    And how to do that using MVVM? – Matthis Kohli Aug 10 '16 at 14:03
  • That does not seem to work if you have actually replaced the ItemsSource with a new object. In that case we need to set ItemsSource to null first as Xopher suggests, or rather as Anatolii suggests maybe is better. – Etherman Apr 10 '19 at 08:57
  • @MatthisKohli only way I can think of is for View Model to expose an event that the view can subscribe to which just invokes the code above. Maybe not ideal... – The Muffin Man Apr 21 '20 at 21:13
  • Isn't this the exact reason TO use an ObservableCollection and WPF? To avoid having to use hardcoded Refresh calls. Since this is an old issue, noting this still has the problem in VS 2022. I just used the Refresh method since it is readable. Noting an answer below, adding in a special event handler and logic when that is literally what the object is supposed to do automatically seems to be a risk to deprecation or a fix to this issue in the future. Explicitly calling Refresh again seems to, while not ideal, futureproof it much better while remaining as simple as possible currently. – Daniel Brown Feb 25 '23 at 20:20
32

This is what I had to do to get it to work.

MyListView.ItemsSource = null;
MyListView.ItemsSource = MyDataSource;
NG_
  • 6,895
  • 7
  • 45
  • 67
Xopher
  • 496
  • 4
  • 5
6

I know that's an old question, but I just stumbled upon this issue. I didn't really want to use the null assignation trick or the refresh for just a field that was updated.

So, after looking at MSDN, I found this article: https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.inotifypropertychanged?redirectedfrom=MSDN&view=netframework-4.7.2

To summarize, you just need the item to implement this interface and it will automatically detect that this object can be observed.

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

So, the event will be called everytime someone changes the Status. And, in your case, the listview will add a handler automatically on the PropertyChanged event.

This doesn't really handle the issue in your case (add/remove). But for that, I would suggest that you have a look at BindingList<T> https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.bindinglist-1?view=netframework-4.7.2

Using the same pattern, your listview will be updated properly without using any tricks.

Sauleil
  • 2,573
  • 1
  • 24
  • 27
5

You should not reset ItemsSource of ListView each time observable collection changed. Just set proper binding that will do your trick. In xaml:

<ListView ItemsSource='{Binding ItemsCollection}'
 ...
</ListView>

And in code-behind (suggest to use MVVM) property that will be responsible for holding _itemList:

public ObservableCollection<PmemCombItem> ItemsCollection
{
   get 
   {
      if (_itemList == null)
      {
          _itemList = new ObservableCollection<PmemCombItem>();
      }
      return _itemList;
   }
}


UPDATE: There is similar post which most probably will Answer your question: How do I update an ObservableCollection via a worker thread?
Community
  • 1
  • 1
Anatolii Gabuza
  • 6,184
  • 2
  • 36
  • 54
  • I am setting the ItemsSource property only once during initialization, only the second piece of code is executed when the list gets changed – Danny1075 Jan 08 '14 at 13:17
  • 1
    @Danny1075 Yes, but it's not the way it should be used. Set != bind. – Anatolii Gabuza Jan 08 '14 at 13:18
  • Despite that this does not answer the question (and using a binding or not doesn't matter here), why have a two-way binding on an ItemsSource property? – Clemens Jan 08 '14 at 13:20
  • @Clemens True. It is not needed. – Anatolii Gabuza Jan 08 '14 at 13:22
  • 1
    Sorry, but even after your edit this does not answer the question. It really doesn't matter if you bind the ItemsSource property to an ObservableCollection or if you set it directly. In both cases changes to the collection are notified by the ObservableCollection. The binding never comes into play again. – Clemens Jan 08 '14 at 13:27
  • 1
    To add another thing: When only a few items are changed, the code works fine and the ListView shows the new items. If in the loop many items get changed, the ListView is not changing at all – Danny1075 Jan 08 '14 at 13:30
0

I found a way to do it. It is not really that great but it works.

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

this should refresh your ListView (it works for my UAP :) )

martijnn2008
  • 3,552
  • 5
  • 30
  • 40
0

An alternative on Xopher's answer.

MyListView.ItemsSource = MyDataSource.ToList();

This refreshes the Listview because it's a other list.

Under_Koen
  • 881
  • 8
  • 20
0

Please check this answer: Passing ListView Items to Commands using Prism Library

List view Items needs to notify about changes (done is setter)

public ObservableCollection<Model.Step> Steps 
{
    get { return _steps; }
    set { SetProperty(ref _steps, value); }
} 

and UpdateSourceTrigger need to be set in xaml

<Image Source="{Binding ImageData, UpdateSourceTrigger=PropertyChanged}" />
Grigory Zhadko
  • 1,484
  • 1
  • 19
  • 33