ListView not updated correctly with ObservableColl

2019-02-21 18:19发布

问题:

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.

回答1:

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


回答2:

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...

After updating Item 4...

Full code sample available here: CODE SAMPLE



回答3:

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.