如何向ObservableCollection添加项?

6

这可能听起来像是一个琐碎的问题,但我在网上找不到任何有效的解决方案。我正在使用PRISM框架,我已经快要放弃了它。原因如下:

我有一个相当好用的ObservableCollection,只要给它分配一个列表并将其遗忘即可正常工作。但这不是ObservableCollection的目标,对吧?它应该具有变化的能力...所以,这就是集合:

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

所以,开始了:
        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 });
        }

失败.. 有时会出现异常:

未处理的类型为 'System.InvalidOperationException' 的异常在 PresentationFramework.dll 中发生,附加信息:ItemsControl 与其项源不一致。

AddRange()?忘了吧..

所有操作都在单独的线程中完成:

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

我甚至创建了扩展方法:
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?!

    }
}

嗯...我做错了什么?我在更改ObservableCollection。所以,每次我想添加新项时,我都必须从旧的和新的中创建一个新集合,并将其分配给ObservableCollection吗?因为只有赋值运算符对我有效 :(

谢谢任何帮助!


有可能 ItemId 的重复值为 1 会导致与 ObservableCollection 无关的问题吗? - Cory Nelson
1
当在视图中绑定ObservableCollection属性时,它应该始终仅在UI线程中更新。否则会出现STA线程错误。 - Nikhil Agrawal
在WPF中,PropertyChanged事件会自动调度到UI线程,但是对于INotifyCollectionChanged并非如此。如果您希望修改ObservableCollection,则必须确保您的修改是在UI线程上完成的,否则将会出现异常。 - Brandon Kramer
当使用ObserveableCollection时,您应该尽量避免执行Items =操作,因为频繁替换整个集合会使集合的作用失去意义。如果您永远不会向现有集合添加或删除项目,并且总是替换它,则最好只使用List<T>并将其填充并分配给Items,以便它被捕获。 - Scott Chamberlain
ItemsControl与其项源不一致。这意味着数据网格已检测到它所持有的项与源上的项不匹配,当您将源更改为新集合时会发生这种情况。 - MikeT
2个回答

8

ItemsControl与其项源不一致

意味着数据表格检测到它所持有的项与源中的项不匹配,这通常是因为您更改了源到一个新集合,而没有强制刷新项控件导致的。

最简单的解决方法是更改

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

或者如果您没有使用C#6

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

这意味着您不能再更改集合本身,只能更改其内容。

如果您真正需要多线程,那么我建议添加以下代码:

private Dispatcher dispatcher = Dispatcher.CurrentDispatcher;

这一点非常重要,因为您需要在创建类时获得CurrentDispatcher而不是当前调用它的CurrentDispatcher

然后调用

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

这样可以确保仅有创建集合的线程会更改它

下面是一个完整的工作示例

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

以下是XAML代码:

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

</DockPanel>

1
我正在寻找 dispatcher.Invoke(()=>Items.Add(item));。谢谢。抱歉回复晚了。 - Marvin Law

4

您的代码存在几个问题。

a) 在使用 ObservableCollection 时,不要重新初始化它。创建一个单一实例并向其中添加或删除项目。所以您的代码应该变成这样。

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

或者这样(如果您的VS支持)

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

添加项目的方法

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) 你说:

所有操作都在单独的线程中完成:

永远不要从非UI线程更新UI绑定属性。对于数据获取,您可以使用非UI线程,但是一旦获取了数据,请仅在UI线程上添加/更新属性中的数据。


A) 是正确的。B) 不太对。INPC绑定会自动封送到UI线程,但不幸的是,INCC绑定不会。所以,您必须使用Dispatcher来更新OC。 - user1228
@Will 如果你使用的是.NET 4.5或更高版本,你也可以自动进行封送。请参考另一个问题的这个答案来设置它。 - Scott Chamberlain
@ScottChamberlain 不,这一直是你应该使用的方式——使用调度程序。在4.5中没有什么新的东西。 - user1228
1
@Will 我没有链接到正确的内容,我链接了问题而不是答案。我指的是这个答案,以及它对BindingOperations.EnableCollectionSynchronization(的解释。这是.NET 4.5中的新功能,让你不需要使用调度程序。 - Scott Chamberlain
@ScottChamberlain 很好!谢谢你的信息。 - user1228

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接