Marvin Law
Marvin Law

Reputation: 97

How to add items to ObservableCollection?

That might sound like a trivial question, but I couldn't find anything that works online. I'm using PRISM and I'm one step before I walk away and never go back to this framework. Here's why:

I have pretty ObservableCollection that basically works if I assign a list to it and forget about it. But that's not the goal of ObservableCollection, right? It changes.. So, here's the collection:

<DataGrid ItemsSource="{Binding Items, Mode=TwoWay}" AutoGenerateColumns="True" />

    private ObservableCollection<Item> _items = new ObservableCollection<Item>();
    public ObservableCollection<Item> Items
    {
        get { return _items; }
        set { SetProperty(ref _items, value); }
    }

So, here goes:

        Items = InitializeItems(); // Works great!
        Items.Add(new Item() { ItemId = 1 }); // Also works

but then..

        for (int i = 1; i < 10; i++)
        {
            Items.Add(new Item() { ItemId = i });
        }

failed.. sometimes, with exception:

An unhandled exception of type 'System.InvalidOperationException' occurred in PresentationFramework.dll Additional information: An ItemsControl is inconsistent with its items source.

AddRange() ? Forget it..

Everything is done in separate thread:

        Task.Factory.StartNew(() =>
        {
            Items = InitializeItems(); // Works great!
            Items.Add(new Item() { ItemId = 1 }); // Also works
            for (int i = 1; i < 10; i++)
            {
                Items.Add(new Item() { ItemId = i });
            }
        });

I even created extension method:

public static class ObservableCollectionExtensions
{
    public static void AddRange<T>(this ObservableCollection<T> data, List<T> range)
    {
        if (range == null) throw new ArgumentNullException("range");
        foreach (var i in range) data.Add(i);

        // How can I force ObservableCollection to update?!

    }
}

Ehh.. what am I doing wrong? I'm changing ObservableCollection. So, everytime I want to add new items, I have to create new collection from old and new ones and assign to ObservableCollection? Because only assign operator works for me :(

Thanks for any help!

Upvotes: 6

Views: 42303

Answers (2)

MikeT
MikeT

Reputation: 5500

An ItemsControl is inconsistent with its items source

means the the datagrid has detected that the items it is holding don't match those on the source, this happens when you change the source to a new collection with out forcing a refresh on the items control

the easiest way to fix this is to change

private ObservableCollection<Item> _items = new ObservableCollection<Item>();
public ObservableCollection<Item> Items
{
    get { return _items; }
    set { SetProperty(ref _items, value); }
}

to

public ObservableCollection<Item> Items{get;}= new ObservableCollection<Item>();

or if you are not using c#6

private ObservableCollection<Item> _items = new ObservableCollection<Item>();
public ObservableCollection<Item> Items
{
    get { return _items; }
}

this means that you can't change the collection anymore only its content

if you truly require Multithreading then i would add the following code

private Dispatcher dispatcher = Dispatcher.CurrentDispatcher;

this is vital as you need the CurrentDispatcher at the time the class was created not the one currently calling it

then call

dispatcher.Invoke(()=>Items.Add(item));

as this will ensure that only the thread that created the collection changes it

here is a complete working example

public class VM
{
    public VM()
    {
        AddItems = new DelegateCommand(() => Task.Run(()=>
            Parallel.ForEach(
                Enumerable.Range(1,1000),
                (item) => dispatcher.Invoke(() => Items.Add(item))
            ))
        );
    }
    public ObservableCollection<int> Items { get; } = new ObservableCollection<int>();
    private Dispatcher dispatcher = Dispatcher.CurrentDispatcher;

    public DelegateCommand AddItems { get; }
}

with the following xaml

<DockPanel >
    <Button DockPanel.Dock="Top" Content="Add" Command="{Binding AddItems, Mode=OneWay}"  />
    <ListView ItemsSource="{Binding Items}"/>

</DockPanel>

Upvotes: 8

Nikhil Agrawal
Nikhil Agrawal

Reputation: 48558

Couple of problems in your code.

a) When working with ObservableCollection, never initialize it again. Create a single instance and add or remove items from it. So you code becomes.

private ObservableCollection<Item> _items = new ObservableCollection<Item>();
public ObservableCollection<Item> Items
{
    get { return _items; }
}

OR this (if your VS supports)

public ObservableCollection<Item> Items { get; } = new ObservableCollection<Item>();

and for adding items

foreach (var item in InitializeItems()) Items.Add(item);
Items.Add(new Item() { ItemId = 1 });
for (int i = 1; i < 10; i++)
{
    Items.Add(new Item() { ItemId = i });
}

b) You said

Everything is done in separate thread:

Never update UI bound properties from Non-UI Threads. For Data fetching you can use Non-UI Threads, but once data is retrieved, add/update data in property on UI Threads only.

Upvotes: 6

Related Questions