Kjara
Kjara

Reputation: 2912

using INotifyPropertyChanged instead of ObservableCollection

I have a ListView bound to a collection, and I want the ListView to automatically update when an item is added to the collection. I managed to get it working using an ObservableCollection, but I'd rather to use INotifyPropertyChanged instead. Maybe you can give me a hint what I am doing wrong?

First, here is the (relevant part of) XAML:

<StackPanel DataContext="{Binding Family}"> <!-- DataContext is of type Family -->
    <TextBlock Text="{Binding LastName}"/>
    <ListView ItemsSource="{Binding Members}">
        <ListView.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding FirstName}"/>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</StackPanel>

Here are the relevant classes:

public class Family : INotifyPropertyChanged
{
    public string LastName { get; private set; }

    private readonly IList<Member> _members;
    public IEnumerable<Member> Members { get => _members; }

    public Family(string lastName, IEnumerable<Member> members)
    {
        LastName = lastName;
        _members = members.ToList();
    }

    public void AddMember(string name)
    {
        var member = new Member { FirstName = name };
        _members.Add(member);
        OnPropertyChanged(nameof(Members));
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string name = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}

public class Member
{
    public string FirstName { get; set; }
}

If I use this code and call AddMember somewhere, it will not update the ListView GUI. I don't see why not, because AddMember calls OnPropertyChanged(nameof(Members)), and Members is what the ListView is bound to. So it should get notified about the change.

So what am I doing wrong?

If I change IList<Member> _members into ObservableCollection<Member> _members and _members = members.ToList() into _members = new ObservableCollection<Member>(members) accordingly, it works as expected.

Upvotes: 0

Views: 239

Answers (2)

thatguy
thatguy

Reputation: 22119

After adding an item to the _members collection, the reference returned by Members is still the same. The Equals method of collections will usually compare references, not items. Consequently, the binding will not detect a change and does not reevaluate the property.

If you want to get this to work, you could do one of the following:

  • Assign null temporarily, raise property changed, reassign the collection and raise property changed again, so the binding detects a changed reference (thanks to @Ash).

    public void AddMember(string name)
    {
       var member = new Member { FirstName = name };
       _members.Add(member);
    
       var members = _members;
       _members = null;
       OnPropertyChanged(nameof(Members));
    
       _members = members;
       OnPropertyChanged(nameof(Members));
    }
    
  • Naive approach, recreate the collection when you add a member, e.g:

    public void AddMember(string name)
    {
       var member = new Member { FirstName = name };
       _members = _members.ToList();
       _members.Add(member);
       OnPropertyChanged(nameof(Members));
    }
    

    This is costly due to lots of unnecessary allocations, don't do it.

As you can see, both approaches have their downsides, either firing additional property changed notifications or unnecessary allocations which will additionally cause the ListView to remove and recreate all of its items each time. This is why there is an ObservableCollection<T> type that implements INotifyCollectionChanged, which allows notifying added and removed items specifically, as well as other operations.

Upvotes: 1

Harald Coppoolse
Harald Coppoolse

Reputation: 30512

First of all, it is good that you decided that a Family is not an ObservableCollection. After all, you can do a lot of things with ObservableCollections that can't be done in Families. For instance: what would Replace(Member) mean in the context of a family?

The problem is, that you forgot to implement INotifyCollectionChanged. With this interface you can notify others that you added an element (and moved, and deleted, etc.)

public class Family : INotifyPropertyChanged, INotifyCollectionChanged
{
    ...

Because you also have to notify if elements are moved / deleted / etc. This will cost some development effort if your family can do more than just Add.

Therefore it might be a good idea to change your private readonly IList<Member> _members; into an ObservableCollection<Member>, and implement the interface via this ObservableCollection.

class Family : INotifyPropertyChanged, INotifyCollectionChanged
{
    public string LastName { get; private set; }

private readonly ObservableCollection<Member> members;

public IEnumerable<Member> Members => this.members;

public event NotifyCollectionChangedEventHandler CollectionChanged
{
    add => this.members.NotifyCollectionChangedEventHandler.CollectinChanged += value;
    remove => this.members.NotifyCollectionChangedEventHandler.CollectinChanged -= value;
}

Now your Add / Remove / Replace / Move / etc methods will be one-liners; the appropriate event will be raised.

public void Add(Member member)
{
    this.members.Add(member);
}

public void Remove(Member member)
{
    this.members.Remove(member);
}

Not sure if you need methods to Move and Replace family Members, but even if you need to, they will also be one-liner calls to the corresponding ObservableCollection method

You have completely hidden that you are using an ObservableCollection<Member>, so if in future you need to completely get rid of the ObservableCollection, your users won't have to change, as long as you promise to implement the interfaces.

Upvotes: 2

Related Questions