问题
I have a common issue that I'd like to (hopefully) find a better solution for moving forward. I have an ObservableCollection containing a master list of data. In my client code I need to 'transform' the data into a new form for display to the user. I use a LINQ statement like:
var newList = (from item in observable
select new { FirstInitial = item.Name[0] });
I know it's pretty rudimentary but it is enough to demonstrate the problem. (Notice the projection, this is not a simple filter or grouping statement.) I then display newList in my UI via data-binding.
Problem is, when the original collection changes, the UI doesn't. The solution I've applied thus far has been to attach an event handler to the original collection's CollectionChanged event which re-evaluates the query and updates the bound property.
While this works fine, it's a lot of repeated code every time I run across this scenario. Is there a way that I can have the LINQ query return an ObservableCollection that "automatically" updates when the source Observable is changed?
In other words, I'd like to implement some 'canned' functionality so I can simply reuse it whenever I have this scenario.
UPDATE
Thanks to Scroog1 for helping me see my original post was too coupled to the UI for what I was really asking. Take the following example as a better description of the problem:
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;
}
}
As you can see, the code where the query exists is not what is directly bound to the UI. I use this example to demonstrate that I am asking from a more general use-case than strictly a UI data-binding scenario.
回答1:
I would do something like this (assuming an ObservableCollection called observable and class implementing INotifyPropertyChanged with RaisePropertyChanged method):
public IEnumerable<string> NewList
{
return from item in observable
select item.Name;
}
observable.CollectionChanged += delegate { RaisePropertyChanged("NewList"); };
Then the when observable is changed, the UI will be told that NewList has changed to and re-evaluate the query.
For multiple dependent items you can do:
observable.CollectionChanged += delegate
{
RaisePropertyChanged("NewList",
"OtherProperty",
"YetAnotherProperty",
"Etc");
};
Update
The above works fine in general for properties, as you will get the latest value every time you access it and INPC can be used to tell things to re-read it.
For the slightly more interesting case of collections, I would implement a custom class that implements INotifyCollectionChanged and IEnumerable and wraps the LINQ. E.g.,
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() { ... }
...
}
Then you can do:
var newList = new CustomObservableCollection(from item in observable
select item.Name);
observable.CollectionChanged += delegate { newList.RaiseCollectionChanged(); };
Update 2
You could even pass the dependency to 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() { ... }
...
}
回答2:
Why not just bind directly to observable
, and select the properties you want.
e.g.:
public IEnumerable<Item> Items { get { return this.observable;} }
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
回答3:
As I referenced in the comments to Scroog1's answer, I was able to take his answer and modify it slightly to use in my application. I ended up with a wrapper class that accepts the original collection, a predicate and a selector function as constructor arguments, as shown here:
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();
}
}
My original example becomes:
var newList = new ObservableWrapper<Person, Char>(observable,
item => { return true; },
item => { return item.Name[0]; });
The downside is that the projection is unnamed. But, in more complex scenarios, I simply define TElement as a class and return that from the selector function.
来源:https://stackoverflow.com/questions/10398014/re-evaluate-linq-query-when-observablecollection-changes