简体   繁体   中英

Why does an IEnumerable<T> require a call to ToList to update the listview?

I'm sure there is good explanation for this. I'm guessing it has something to do with me having a cold and missing something obvious...

I have a simple window:

<Window x:Class="WpfIdeas.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:w="clr-namespace:WpfIdeas"
    Title="Window1" Height="300" Width="315">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="20"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>
        <Button Grid.Row="0" x:Name="btnAddObject" Click="btnAddObject_Click">Add Object</Button>
        <ListView Grid.Row="1"  ItemsSource="{Binding Objects}">
        </ListView>
    </Grid>
</Window>

the code behind the window is:

using System.Windows;

namespace WpfIdeas
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
            DataContext = new ObjectVM();
        }

        private void btnAddObject_Click(object sender, RoutedEventArgs e)
        {
            (DataContext as ObjectVM).AddObject();
        }
    }
}

And its DataContext is set to be the following class:

class ObjectVM : INotifyPropertyChanged
{
    private readonly List<ObjectModel> objects = new List<ObjectModel>();

    //public IEnumerable<ObjectModel> Objects { get { return objects } } //doesn't work
    public IEnumerable<ObjectModel> Objects { get { return objects.ToList() } } //works

    private Random r = new Random();

    public void AddObject()
    {
        ObjectModel o = new ObjectModel(r);
        objects.Add(o);
        if(PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs("Objects"));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

The ObjectModel class is in fact a struct that generates a 14 character string when it is instantiated. It's ToString() method just outputs this string.

As the code is above, when I click the "Add Object" button then a new string appears in the ListView .

However if I remove the ToList() call in the Objects property, nothing is ever displayed in the ListView . It just remains blank.

Why is this?

Using Collection Objects as a Binding Source :

You can enumerate over any collection that implements the IEnumerable interface. However, to set up dynamic bindings so that insertions or deletions in the collection update the UI automatically, the collection must implement the INotifyCollectionChanged interface. This interface exposes an event that must be raised whenever the underlying collection changes.

objects.ToList() will create a new list on each Button-Click. This is probably the cue for the list to refresh itself?

I'm guessing here... But when you NotifyPropertyChanged, then the framework might check to see if the property really did change (it didn't in the return objects case - it is still the same list).

If you raise the PropertyChanged event on a property, the binding checks to see if the value of the property has changed, and refreshes the target if it has. Since Objects is a reference type, its value only changes if you assign it to a new instance - which is what using ToList() or ToArray() does.

In other words, when your code raises PropertyChanged , you're not asserting that the list's contents have changed, you're asserting that the property contains a new list. Binding checks the property on the source against the property on the target and disagrees.

This is why you should be using an ObservableCollection<T> , or some other collection that implements INotifyCollectionChanged . IF you bind to a property that implements INotifyCollectionChanged , binding will listen to both PropertyChanged events (raised if you create a new collection and change the value of the property) and CollectionChanged (raised when items are added to or removed from the collection).

Also, note that it's not enough to change your underlying collection to an ObservableCollection<T> . You have to change the type of the property you're exposing. Binding won't try to listen to events on an IEnumerable<T> property, because those events aren't exposed by that interface.

That's most commonly because the IEnumerable is the result of a Linq query and the actual type is something entirely different than a simple List<> or Collection<>. What happens is that it (Linq) builds a logical representation of the 'query' but does not run it right away, instead running it when values are requested and yielding each value. That's one of the basic concepts of Linq. Different flavors of Linq may choose to implement it differently under the covers, but the concept is the same.

Actually nevermind, I should read the code more carefully when I answer; I don't think there's any way that happens with your code because you're just instantiating the List. However, ListBox never exposes items directly, instead wrapping them in an ICollectionView. The ICollectionView may have something to do with this, opting to lazily load the items if the type is seen as IEnumerable. Not sure though. It may also depend on the internal structure of ObjectModel ... although probably not, since that wouldn't be affected by the call to ToList() .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM