过滤 CollectionViewSource 产生空结果将抛出异常

filtering CollectionViewSource yielding empty result will throw exception

你知道吗,为什么会抛出

An unhandled exception of type 'System.NullReferenceException' occurred in PresentationFramework.dll

Additional information: Object reference not set to an instance of an object.

当我尝试筛选未产生有效行的 CollectionViewSource 时?

代码如下

xaml:

<ComboBox SelectedItem="{Binding Item}" ItemsSource="{Binding Items}" IsSynchronizedWithCurrentItem="True" />

第一个代码:

public class Model : INotifyPropertyChanged
    {
        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
        public string Item { get; set; }
        public ICollectionView Items { get; set; }
        public Model()
        {
            Items = CollectionViewSource.GetDefaultView(new ObservableCollection<string>(new List<string> { "aaa", "bbb" }));
        }
        public void DoFirst()
        {
            Items.Filter = o => ((string)o).StartsWith("a");
        }
        public void DoSecond()
        {
            Items.Filter = o => false;
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }

DoFirst() 有效。 DoSecond() 没有。异常来自 Items.Filter = o => false; 行。

如果我删除通知 属性 东西,它不会抛出异常,但另一个有趣的错误发生了:

第二个密码:

public class Model
    {
        public string Item { get; set; }
        public ICollectionView Items { get; set; }
        public Model()
        {
            Items = CollectionViewSource.GetDefaultView(new ObservableCollection<string>(new List<string> { "aaa", "bbb" }));
        }
        public void DoFirst()
        {
            Items.Filter = o => ((string)o).StartsWith("a");
        }
        public void DoSecond()
        {
            Items.Filter = o => false;
        }
    }

显示的是空列表。那就对了。但是,当我 DoFirst() 列表显示 'aaa' 正确时,默认情况下它不是 selected。 IsSynchronizedWithCurrentItem 未触发。

如果我尝试保护过滤器免受 NRE 第三种行为的影响。

第三个密码:

public class Model : INotifyPropertyChanged
    {
        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
        public string Item { get; set; }
        public ICollectionView Items { get; set; }
        public Model()
        {
            Items = CollectionViewSource.GetDefaultView(new ObservableCollection<string>(new List<string> { "aaa", "bbb" }));
        }
        public void DoFirst()
        {
            try
            {
                Items.Filter = o => ((string)o).StartsWith("a");
            } catch (NullReferenceException) { }
        }
        public void DoSecond()
        {
            try
            {
                Items.Filter = o => false;
            } catch (NullReferenceException) { }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }

在那种情况下,组合框中的 selectable 项是正确的。在 DoSecond() 之后列表为空,但最后一个 selected 项目仍然是 selected... 在 DoSecond() DoFirst() 也会抛出 NullReferenceException.

如果我们将当前项设置为 null,并对其调用 OnPropertyChanged,则第二个代码的稳定性达到了。 IsSynchronizedWithCurrentItem 的 属性 的 select 从组合框中输入一个有效的 Item 仍然丢失。在下面的代码中,如果我调用 DoFirst()DoThird(),那么 "bbb" 将被 selected。设置Item为null后(之前调用DoSecond()),就不会select "bbb":

第四个密码:

public class Model : INotifyPropertyChanged
{
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }

    public string Item { get; set; }

    public ICollectionView Items { get; set; }
    public Model()
    {
        Items = CollectionViewSource.GetDefaultView(new ObservableCollection<string>(new List<string> { "aaa", "bbb" }));
    }
    public void DoFirst()
    {
        Items.Filter = o => ((string)o).StartsWith("a");
    }

    public void DoSecond()
    {
        Item = null;
        OnPropertyChanged("Item");
        Items.Filter = o => false;
    }

    public void DoThird()
    {
        Items.Filter = o => ((string)o).StartsWith("b");
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

Br, 马顿

出于某种原因,当您将 IsSynchronizedWithCurrentItem 设置为 true 并且 SelectedItem 绑定的源对象实现 INotifyPropertyChanged 时,ICollectionView 不允许您显式地将 CurrentItem 设置为 null(例如,通过调用 MoveCurrentToPosition(-1),否则可以正常工作)。我有一些想法,但我不想推测。

我发现将 CurrentItem 设置为 null 的唯一方法是明确地将 null 赋给 SelectedItem 所在的 属性绑定(在您的情况下为 Item 属性)并使用适当的 属性 名称引发 PropertyChanged 事件。使用调试器,您会注意到此时 CurrentItem 将在内部设置为 null。那么您就可以应用不会产生任何结果的过滤器了。

至于您的另一个顾虑,即在应用一个没有产生结果的过滤器然后另一个产生一些结果的过滤器 IsSynchronizedWithCurrentItem 停止工作之后,这实际上不是真的。同样,如果您使用调试器,您会注意到在应用第二个过滤器后 CurrentItem 属性 保持不变 - 它仍然产生 null,因此 SelectedItem 仍然是与 CurrentItem 同步。恐怕在这种情况下,您需要自己 select 第一个可用项目 - 例如通过为 Item 属性 分配适当的值或调用 Items.MoveCurrentToFirst().

编辑

针对您的评论和更新的相关细节做出回应 - 这正是我在上一段(尤其是最后一句)中所指的内容。您可能会注意到,在应用过滤器时,只要当前值仍然可行,它就不会自动更改。好吧,null(意思是"no current value")总是可行的,所以它永远不会自动改变,所以你必须自己做。

与您问题中的示例相反,这些示例是具有预期结果的任意情况(您知道何时应用空过滤器)我认为您最终会遇到无法分辨的情况预先确定过滤器是否会产生任何结果。最简单的(在我看来)解决方案是编写一个简单但通用的方法来将任何过滤器应用于集合:

public void SetFilter(Predicate<object> filter)
{
    if (Items.CurrentItem != null && !filter(Items.CurrentItem))
    {
        Item = null;
        OnPropertyChanged("Item");
    }
    Items.Filter = filter;
    if (Items.CurrentItem == null && !Items.IsEmpty)
        Items.MoveCurrentToFirst();
}

这会给您带来以下行为:

  • 当您应用保留当前项目的过滤器时,它不会改变
  • 当您应用空过滤器时,null 将被 selected
  • 当您应用非空过滤器且当前项目为 null 时,第一个可用项目将 selected