8

I'm currently using an observable collection to store my data objects for a ListView. Adding new objects to the collection works just fine, and the listView updates properly. However when I try to change one of the properties of an object in the collection the listView will not update properly. For example, I have an observable collection DataCollection. I try

_DataCollections.ElementAt(count).Status = "Active";

I perform this change before a long operation due to a button press. The listView will not reflect the change. So I addmyListView.Items.Refresh();. This works, however the listView does not get refreshed till button_click method is complete, which is no good by then. For example:

   button1_Click(...)
    {
      _DataCollections.ElementAt(count).Status = "Active";
      myListView.Items.Refresh();
      ExecuteLongOperation();
      _DataCollections.ElementAt(count).Status = "Finished";
      myListView.Items.Refresh();
    }

The status will never goto "Active", it will go straight to "Finished" after the method completes. I also tried using a dispatcher like this:

button1_Click(...)
    {
      this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Background,
            (NoArgDelegate)delegate { _DataCollection.ElementAt(count).Status =  "Active"; myListView.Items.Refresh(); });

      ExecuteLongOperation();
     this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Background,
            (NoArgDelegate)delegate { _DataCollection.ElementAt(count).Status =  "Finished"; myListView.Items.Refresh(); });

    }

However, that does not seem to work correctly either. Any tips or ideas would be appreciated.

user793491
  • 193
  • 2
  • 7

3 Answers3

5

You have to use proper data binding techniques, and then this will work automagically.

Required...

  1. Implement INotifyPropertyChanged on your class inside the ObservableCollection (and make sure you're triggering the event when you are setting properties on that class)
  2. On your ListView's ItemTemplate, be sure you're using Binding to the properties

If you do those two things, there's no need for a "Refresh" call or anything else. Setting a property that triggers INotifyPropertyChanged will cause the Binding of the ItemTemplate to update.

Implementing INotifyPropertyChanged on your class inside the ObservableCollection... (look up the BindableBase class if you don't know about it already)

public class ToDoItem : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private string _name;
    public string Name
    {
        get { return _name; }
        set { SetProperty(ref _name, value); }
    }

    private DateTime _date;
    public DateTime Date
    {
        get { return _date; }
        set { SetProperty(ref _date, value); }
    }

    protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName]string propertyName = null)
    {
        if (object.Equals(storage, value)) return false;

        storage = value;
        this.OnPropertyChanged(propertyName);
        return true;
    }

    protected void OnPropertyChanged(string propertyName)
    {
        var eventHandler = this.PropertyChanged;
        if (eventHandler != null)
        {
            eventHandler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Your ListView

<ListView
    x:Name="listView">

    <ListView.ItemTemplate>
        <DataTemplate>

            <StackPanel>

                <TextBlock
                    Text="{Binding Name}"/>

                <TextBlock
                    Text="{Binding Date}"/>

            </StackPanel>

        </DataTemplate>
    </ListView.ItemTemplate>

</ListView>

Your ObservableCollection...

private ObservableCollection<ToDoItem> _toDoItems = new ObservableCollection<ToDoItem>();

// Assign the collection to the ListView
listView.ItemsSource = _toDoItems;

Adding things to the collection works...

_toDoItems.Add(new ToDoItem()
{
    Name = "Item " + (_toDoItems.Count + 1),
    Date = DateTime.Now
});

And updating, what you were asking for, works...

ToDoItem item = _toDoItems[randomIndex];

item.Name = "Updated " + item.Name;
item.Date = DateTime.Now;

No calls to "Refresh" or anything else needed. The item itself updates, without the list changing.

Before updating Item 4...

Before updating Item 4

After updating Item 4...

After updating Item 4

Full code sample available here: CODE SAMPLE

Andrew Leader
  • 975
  • 6
  • 13
2

You have run into the classic problem with ObservableCollection. it only notifies when an item is added or removed. it does NOT notify when a property of an item in the collection changes. if you want to be notified of such changes you are going to have to make your own custom collection and add/remove the property changed events on the individual objects manually. sorry, dude.

Muad'Dib
  • 28,542
  • 5
  • 55
  • 68
2

To solve this I created a class called VeryObservableCollection. For each object you add, it hooks the object's NotifyPropertyChanged event to a handler that triggers a CollectionChanged event. For each object removed, it removes the handler. Very simple and will give you exactly what you want. Partial code:

public class VeryObservableCollection<T> : ObservableCollection<T>

/// <summary>
/// Override for setting item
/// </summary>
/// <param name="index">Index</param>
/// <param name="item">Item</param>
protected override void SetItem(int index, T item)
{
    try
    {
        INotifyPropertyChanged propOld = Items[index] as INotifyPropertyChanged;
        if (propOld != null)
            propOld.PropertyChanged -= new PropertyChangedEventHandler(Affecting_PropertyChanged);
    }
    catch (Exception ex)
    {
        Exception ex2 = ex.InnerException;
    }
    INotifyPropertyChanged propNew = item as INotifyPropertyChanged;
    if (propNew != null)
        propNew.PropertyChanged += new PropertyChangedEventHandler(Affecting_PropertyChanged);

    base.SetItem(index, item);
}
Ed Bayiates
  • 11,060
  • 4
  • 43
  • 62
  • i need long time to find the correct implementation of Affecting_PropertyChanged method.. you need only one line: `MyBase.OnCollectionChanged(New NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))` and i recommend to overwrite InsertItem and RemoveItem of ObservableCollection, because SetItem didn't work for me. – Felix C Jan 06 '12 at 13:02
  • @Felix, yes, there was more to it. The class implementation is over 800 lines. I did say I had posted partial code, mainly to give the general idea. – Ed Bayiates Jan 09 '12 at 18:30
  • I wrote this for other people who find this post and don't know how to implement this code lines. It was not a correction, just an extension to get this working. :) but why your class has a length of 800 lines? I would really enjoy to see your full class implementation. – Felix C Jan 11 '12 at 07:20
  • I added a number of extensions. The basic implementation just to set the event handler is under 200 lines and overrides SetItem, ClearItems, RemoveItem, InsertItem, and the constructor along with a few minor things. – Ed Bayiates Jan 25 '12 at 17:05
  • Apologies for the downvote, but this is not a complete solution as it includes 1 method from the implementation and approximate number of lines of the final solution to the problem + helpful clues in comments from @Felix-C. – too Feb 03 '15 at 09:27