Только начал использовать MVVM, и возник вопрос. Как сделать коллекцию с фильтром и сортировкой в духе MVVM? Раньше я делал так: ObjectDataProvider асинхронно вытаскивал список, дергая указанный метод. Потом CollectionViewSource указывал сортировки и метод фильтрации, который находился в code-behind. Далее этот CollectionViewSource использовался в качестве источника данных.
В стиле MVVM, как я понимаю, code-behind должен быть по возможности пуст, а вся логика находиться в ViewModel. Поэтому на событие фильтра я подписываюсь во ViewModel:
var collectionViewSource = CollectionViewSource.GetDefaultView(Collection);
collectionViewSource.Filter += FilterItems;
Но при этом не получается заиспользовать CollectionViewSource в XAML, потому что CollectionViewSource в XAML и то, что вернет CollectionViewSource.GetDefaultView будут совершенно разные экземпляры. А следовательно я не могу задавать из XAML сортировки, группировки и пр. При этом каким-либо образом передавать конкретный CollectionViewSource в ViewModel из View тоже кажется некошерным.
... << RSDN@Home 1.2.0 alpha 4 rev. 1096>>
Здравствуйте, SiAVoL, Вы писали:
SAV>Только начал использовать MVVM, и возник вопрос. Как сделать коллекцию с фильтром и сортировкой в духе MVVM?
Cоздавайте объект типа ListCollectionView прямо во ViewModel и используйте его в биндингах. Соответственно, всю сортировку (лучше использовать свойство ListCollectionView.CustomSort, что увеличит скорость сортировки) и фильтрацию можно будет сделать во ViewModel. Вроде подводных камней в таком решении нет.
В качестве коллекции-источника для ListCollectionView можно взять обычную ObservableCollection. Данные для коллекции-источника по возможности формируйте в отдельном потоке (если не хочется получить чёрные окна вместо интерфейса), но учтите, что заполнять ObservableCollection придётся в UI-потоке, потому что CollectionView, которая будет дёргаться при изменениях в коллекции-источнике, может работать только в UI-потоке (так уж спроектированы все наследники ICollectionView, наследуются от DispatcherObject). Поэтому, лучше всего, коллекцию-источник заполнять через Dispatcher.BeginInvoke (ViewModel рекомендую наследовать от DispatcherObject) по одному элементу за раз, желательно с приоритетом DispatcherPriority.Background.
То же самое, в коде:
public class MyViewModel : DispatcherObject, INotifyPropertyChanged
{
private readonly IEnumerable<Data> myData;
private readonly ObservableCollection<DataViewModel> myCollection;
private ListCollectionView myCollectionView;
public MyViewModel(IEnumerable<Data> data)
{
myData = data;
myCollection = new ObservableCollection<DataViewModel>();
}
public ListCollectionView MyCollection
{
get { return myCollectionView ?? CreateMyCollectionView(); }
}
private ListCollectionView CreateMyCollectionView()
{
myCollectionView = new ListCollectionView(myCollection) {
CustomSort = new MyComparer(),
Filter = MyFilter
};
FetchData(myData, dataObject => myCollection.Add(new DataViewModel(dataObject)));
return myCollectionView;
}
protected void FetchData<T>(IEnumerable<T> data, Action<T> fetch)
{
State = ViewModelState.Fetching;
var dataEnumerator = data.GetEnumerator();
Dispatcher.BeginInvoke(
new Action<IEnumerator<T>, Action<T>>
(
FetchDataImpl
),
DispatcherPriority.Background,
dataEnumerator, fetch
);
}
private void FetchDataImpl<T>(IEnumerator<T> sourceEnumerator, Action<T> fetch)
{
if (!sourceEnumerator.MoveNext()) {
State = ViewModelState.Active;
return;
}
fetch(sourceEnumerator.Current);
Dispatcher.BeginInvoke(
new Action<IEnumerator<T>, Action<T>>
(
FetchDataImpl
),
DispatcherPriority.Background,
sourceEnumerator, fetch
);
}
}
Сегодня наткнулся на
интересный вариант решения проблемы с обновлением CollectionView/ObservableCollection из другого потока, здесь коллекция-источник посылает уведомления о своём изменении в подходящем контексте синхронизации, что позволяет заполнять/изменять её в отдельном потоке. Этот класс надо иcпользовать вместо ObservableCollection/FetchData и можно забивать его данными в отдельном потоке:
/// <summary>
/// An ObservableCollection<T> enhanced with capability of free threading.
/// </summary>
[Serializable]
public class BindableCollection<T> : ObservableCollection<T>
{
/// <summary>
/// Initializes a new instance of the<see cref="BindingCollection<T>">BindingCollection</see>.
/// </summary>
public BindableCollection() : base() { }
/// <summary>
/// Initializes a new instance of the<see cref="BindingCollection<T>">BindingCollection</see>
/// class that contains elements copied from the specified List<T>.
/// </summary>
/// <param name="list">The list from which the elements are copied.</param>
/// <exception cref="System.ArgumentNullException">The list parameter cannot be null.</exception>
public BindableCollection(List<T> list) : base(list) { }
/// <summary>
/// Initializes a new instance of the<see cref="BindingCollection<T>">BindingCollection</see>
/// class that contains elements copied from the specified IEnumerable<T>.
/// </summary>
/// <param name="list">The list from which the elements are copied.</param>
/// <exception cref="System.ArgumentNullException">The list parameter cannot be null.</exception>
public BindableCollection(IEnumerable<T> list)
{
if (list == null)
throw new ArgumentNullException("list");
foreach (var item in list) {
Items.Add(item);
}
}
/// <summary>
/// Occurs when an item is added, removed, changed, moved, or the entire list is refreshed.
/// </summary>
public override event NotifyCollectionChangedEventHandler CollectionChanged;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
var collectionChanged = CollectionChanged;
if (collectionChanged != null) {
using (var blockReentrancy = BlockReentrancy()) {
foreach (var @delegate in collectionChanged.GetInvocationList()) {
var dispatcherInvoker = @delegate.Target as DispatcherObject;
var syncInvoker = @delegate.Target as ISynchronizeInvoke;
if (dispatcherInvoker != null) {
// We are running inside DispatcherSynchronizationContext,
// so we should invoke the event handler in the correct dispatcher.
dispatcherInvoker.Dispatcher.Invoke(@delegate, DispatcherPriority.Background, this, e);
}
else if (syncInvoker != null) {
// We are running inside WindowsFormsSynchronizationContext,
// so we should invoke the event handler in the correct context.
syncInvoker.Invoke(@delegate, new object[] { this, e });
}
else {
// We are running in free threaded context, so just directly invoke the event handler.
var handler = (NotifyCollectionChangedEventHandler)@delegate;
handler(this, e);
}
}
}
}
}
}
Everything is an object.