[WPF+MVVM] коллекция с фильтром по-фэншуй
От: SiAVoL Россия  
Дата: 27.02.09 07:49
Оценка:
Только начал использовать 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>>
Re: [WPF+MVVM] коллекция с фильтром по-фэншуй
От: Vladek Россия Github
Дата: 28.02.09 10:22
Оценка: 19 (4)
Здравствуйте, 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&lt;T&gt; enhanced with capability of free threading.
/// </summary>
[Serializable]
public class BindableCollection<T> : ObservableCollection<T>
{
    /// <summary>
    /// Initializes a new instance of the<see cref="BindingCollection&lt;T&gt;">BindingCollection</see>.
    /// </summary>
    public BindableCollection() : base() { }

    /// <summary>
    /// Initializes a new instance of the<see cref="BindingCollection&lt;T&gt;">BindingCollection</see>
    /// class that contains elements copied from the specified List&lt;T&gt;.
    /// </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&lt;T&gt;">BindingCollection</see>
    /// class that contains elements copied from the specified IEnumerable&lt;T&gt;.
    /// </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.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.