【问题标题】:Re-evaluate LINQ query when ObservableCollection changesObservableCollection 更改时重新评估 LINQ 查询
【发布时间】:2012-05-01 13:04:46
【问题描述】:

我有一个常见问题,我想(希望)找到一个更好的解决方案来继续前进。我有一个包含数据主列表的 ObservableCollection。在我的客户端代码中,我需要将数据“转换”为新表单以显示给用户。我使用如下 LINQ 语句:

var newList = (from item in observable
               select new { FirstInitial = item.Name[0] });

我知道这很简陋,但足以证明问题所在。 (注意投影,这不是一个简单的过滤器或分组语句。)然后我通过数据绑定在我的 UI 中显示 newList。

问题是,当原始集合发生变化时,UI 不会。到目前为止,我应用的解决方案是将事件处理程序附加到原始集合的 CollectionChanged 事件,该事件重新评估查询并更新绑定属性。

虽然这可以正常工作,但每次我遇到这种情况时都会重复很多代码。有没有办法让 LINQ 查询返回一个 ObservableCollection,当源 Observable 发生更改时“自动”更新?

换句话说,我想实现一些“固定”功能,以便在遇到这种情况时可以简单地重复使用它。

更新

感谢 Scroog1 帮助我看到我的原始帖子与 UI 过于耦合,无法满足我的真正要求。以下面的例子来更好地描述问题:

public class SomeClass
{
    private ObservableCollection<Employee> _allEmployees;
    private ObservableCollection<Employee> _currentEmployees;

    public ObservableCollection<Employee> CurrentEmployees
    {
        get
        {
            if (_currentEmployees == null)
                _currentEmployees = _allEmployees.Where(e => !e.IsTerminated);

            return _currentEmployees;
        }
    }
}

public class SomeViewModel
{
    private ICollectionView _view;

    public ICollectionView CurrentView
    {
        if (_view == null)
        {
            var cvs = new CollectionViewSource()
            {
                Source = someClass.CurrentEmployees
            }

            cvs.Add(new SortDescription("Name", ListSortDirection.Ascending));

            _view = cvs.View;
        }

        return _view;
    }
}

如您所见,查询所在的代码并不是直接绑定到 UI 的。我使用这个例子来证明我是从更一般的用例而不是严格的 UI 数据绑定场景中询问的。

【问题讨论】:

    标签: .net linq linq-to-objects observablecollection


    【解决方案1】:

    我会做这样的事情(假设 ObservableCollection 称为 observable 并且类使用 RaisePropertyChanged 方法实现 INotifyPropertyChanged):

    public IEnumerable<string> NewList
    {
        return from item in observable
               select item.Name;
    }
    
    observable.CollectionChanged += delegate { RaisePropertyChanged("NewList"); };
    

    然后当 observable 改变时,UI 会被告知 NewList 已经改变并重新评估查询。

    对于多个依赖项,您可以这样做:

    observable.CollectionChanged += delegate
        {
            RaisePropertyChanged("NewList",
                                 "OtherProperty",
                                 "YetAnotherProperty",
                                 "Etc");
        };
    

    更新

    以上对于属性一般都适用,因为您每次访问它时都会获得最新的值,并且 INPC 可用于告诉事情重新读取它。

    对于稍微有趣的集合案例,我将实现一个自定义类,该类实现 INotifyCollectionChanged 和 IEnumerable 并包装 LINQ。例如,

    public class CustomObservableCollection<T> : INotifyCollectionChanged,
                                                 INotifyPropertyChanged,
                                                 IEnumerable<T>
    {
        private readonly IEnumerable<T> _collection;
        public CustomObservableCollection(IEnumerable<T> collection)
        {
            _collection = collection;
        }
        public IEnumerator<T> GetEnumerator()
        {
            _collection.GetEnumerator();
        }
        public void RaiseCollectionChanged() { ... }
        ...
    }
    

    那么你可以这样做:

    var newList = new CustomObservableCollection(from item in observable
                                                 select item.Name);
    observable.CollectionChanged += delegate { newList.RaiseCollectionChanged(); };
    

    更新 2

    您甚至可以将依赖项传递给 CustomObservableCollection:

    public class CustomObservableCollection<T> : INotifyCollectionChanged,
                                                 INotifyPropertyChanged,
                                                 IEnumerable<T>
    {
        private readonly IEnumerable<T> _collection;
        public CustomObservableCollection(IEnumerable<T> collection,
                     params ObservableCollection[] dependencies)
        {
            _collection = collection;
            foreach (var dep in dependencies)
                dep.CollectionChanged += RaiseCollectionChanged();
        }
        public IEnumerator<T> GetEnumerator()
        {
            _collection.GetEnumerator();
        }
        public void RaiseCollectionChanged() { ... }
        ...
    }
    

    【讨论】:

    • 是的,这在结果集合直接绑定到 UI 时有效,这只是一个用例。我正在寻找一个更通用的解决方案,它也涵盖其他用途。查看我的更新。
    • WRT 您的更新,我看不到 CustomObservableCollection 在原始集合(可观察的)更改时如何接收通知。如果您有更多想法,请详细说明,因为您所展示的只是使用 ObservableCollection 已经可用的内容。
    • 增加的是手动触发更新的能力,因此将其放在最后一行的委托中。此外,ObservableCollection 会制作列表的副本,而这会保留原始 LINQ。
    • 当然,您可以将 observable 传递给 CustomObservableCollection 并在内部设置事件处理。
    • 请记住,当原始 ObservableCollection 发生变化时,必须重新评估 LINQ 语句以确定新的结果集。不是冒泡改变事件那么简单。
    【解决方案2】:

    为什么不直接绑定到observable,然后选择你想要的属性。

    例如:

    public IEnumerable<Item> Items { get { return this.observable;} }
    
    <ItemsControl ItemsSource="{Binding Items}">
      <ItemsControl.ItemTemplate>
        <DataTemplate>
          <TextBlock Text="{Binding Name}" />
        </DataTemplate>
      </ItemsControl.ItemTemplate>
    </ItemsControl>
    

    【讨论】:

    • 我认为 OP 是针对任何依赖于 observable 的一般情况,而不仅仅是 observable 的属性。
    • 正如我对@Scroog1 所说,结果集合直接绑定到 UI 的情况只是一个用例,我已经更新了原始帖子,因为这不是我问题的症结所在.我正在寻找更通用的解决方案。
    • 至于您的具体回复,@Scroog1 是正确的,该解决方案也仅限于简单的投影案例,这也不是我真正想要的。
    【解决方案3】:

    正如我在 cmets 中对 Scroog1 的回答所引用的那样,我能够接受他的回答并稍加修改以在我的应用程序中使用。我最终得到了一个包装类,它接受原始集合、谓词和选择器函数作为构造函数参数,如下所示:

    public class ObservableWrapper<TSource, TElement> : IEnumerable<TElement>,
                                                        INotifyCollectionChanged
    {
        private Collection<TElement> _items;
        private Func<TSource, Boolean> _predicate;
        private Func<TSource, TElement> _selector;
        private ObservableCollection<TSource> _source;
    
        public ObservableWrapper(ObservableCollection<TSource> source,
                                 Func<TSource, Boolean> predicate,
                                 Func<TSource, TElement> selector)
        {
            _predicate = predicate;
            _selector = selector;
            _source = source;
    
            _source.CollectionChanged += SourceCollectionChanged;
        }
    
        public IEnumerator<TElement> GetEnumerator()
        {
            EnsureItems();
    
            return _items.GetEnumerator();
        }
    
        private void EnsureItems()
        {
            if (_items == null)
            {
                _items = new Collection<TElement>();
    
                RefreshItems();
            }
        }
    
        private void NotifyCollectionChanged(NotifyCollectionChangedAction action)
        {
            var handlers = CollectionChanged;
    
            if (handlers != null)
            {
                var args = new NotifyCollectionChangedEventArgs(action);
    
                handlers(this, args);
            }
        }
    
        private void RefreshItems()
        {
            _items.Clear();
    
            foreach (var element in _source)
            {
                if (_predicate(element))
                {
                    var item = _selector(element);
    
                    _items.Add(item);
                }
            }
    
            NotifyCollectionChanged(NotifyCollectionChangedAction.Reset);
        }
    
        private void SourceCollectionChanged(Object sender, NotifyCollectionChangedEventArgs e)
        {
            RefreshItems();
        }
    
        public event NotifyCollectionChangedEventHandler CollectionChanged;
    
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
    

    我原来的例子变成了:

    var newList = new ObservableWrapper<Person, Char>(observable,
                                                      item => { return true; },
                                                      item => { return item.Name[0]; });
    

    缺点是投影未命名。但是,在更复杂的场景中,我只是将 TElement 定义为一个类,然后从选择器函数中返回它。

    【讨论】:

      猜你喜欢
      • 2011-05-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-11-23
      • 2016-11-11
      • 1970-01-01
      • 2023-03-31
      相关资源
      最近更新 更多