tags:

views:

120

answers:

4

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?

A: 

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

Alex Paven
@Alex Paven: I deleted my comment after seeing you had edited your answer. Now we have a broken communications record and I am all sad :( I suggest we both delete our comments and pretend nothing ever happened...
Daren Thomas
+2  A: 

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

Daren Thomas
This seems likely. I thought that the whole point of INotifyPropertyChanged was so that bound objects could tell the things they are bound to that they should refresh. What looks like is happening, then, is that the `ListView` is only refreshing when the reference to the object it is bound to is changed.
Matt Ellen
+4  A: 

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.

Branimir
Hi Branimir. My viewmodel does implement this interface, and the event is raised whenever an object is added to the list.
Matt Ellen
@Matt Ellen, no, not in the code you show (you are binding to the List). Actually, @Branimir has sound advice here. But I'd be surprised if the `List<ObjectMode>` doesn't implement `INotifyCollectionChanged`...
Daren Thomas
It doesn't, as far as I know only ObservableCollection<> does (and several other internal MS collections).
Alex Paven
It seems to me that the `ListView` is checking that I'm telling the truth. It seems like when I call `PropertyChanged`, the `ListView` says "This property has change, you say? Let me check that. Well, the reference is the same, so I disagree. No refresh for you." Well, I want my flipping refresh. I'm glad there is an `ObservableCollection<T>`, but I'd rather that the `ListView` updated when I told it to.
Matt Ellen
@Matt, no, when NotifyPropertyChanged is raised the property's get accessor is called by WPF, you can check that by putting a breakpoint there. However, the sub-properties of the property aren't. Neither the items if the property is a collection. If the reference changes, then indeed it doesn't have a choice and must refresh all the bindings. Otherwise the old bindings remain in effect. All in all, ObservableCollection is by far the better choice, and you only need to instantiate it once.
Alex Paven
+1  A: 

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.

Robert Rossney
Thanks for you answer Robert. I'm not sure you're right about the last bit as I have changed the backing field for the Objects property to `Observable<ObjectModel>` but left the property its self as an `IEnumerable<ObjectModel>` and the `ListView` updates as I expect - when ever I add an item. Also I'm no longer calling `PropertyChanged`.
Matt Ellen
Yeah, I'm pretty sure I was underthinking that; I think binding uses reflection to query the object and find out whether or not it implements `INotifyCollectionChanged`.
Robert Rossney