Элементы поля со списком фильтра WPF на основе элементов ListView

Я создаю приложение WPF, используя шаблон проектирования MVVM, который состоит из ListView и некоторых ComboBox. ComboBoxes используются для фильтрации ListView. Я пытаюсь заполнить поле со списком элементами из связанного столбца ListView. Другими словами, если в моем ListView есть Column1, Column2 и Column3, я хочу, чтобы ComboBox1 отображал все элементы UNIQUE в Column1. После того, как элемент выбран в ComboBox1, я хочу, чтобы элементы в ComboBox2 и ComboBox3 фильтровались на основе выбора ComboBox1, что означает, что ComboBox2 и ComboBox3 могут содержать только действительные выборки. Это было бы чем-то похоже на элемент управления CascadingDropDown при использовании набора инструментов AJAX в ASP.NET, за исключением того, что пользователь может выбрать любой ComboBox случайным образом, а не по порядку.

Моя первая мысль заключалась в том, чтобы привязать ComboBoxes к тому же ListCollectionView, к которому привязан ListView, и установить DisplayMemberPath в соответствующий столбец. Это отлично работает в части совместной фильтрации ListView и ComboBox, но отображает все элементы в ComboBox, а не только уникальные (очевидно). Итак, моей следующей мыслью было использовать ValueConverter, чтобы возвращать только уникальные элементы, но мне это не удалось.

К вашему сведению: я прочитал сообщение Колина Эберхарда о добавлении автофильтра в ListView на CodeProject , но его метод перебирает каждый элемент во всем ListView и добавляет уникальные элементы в коллекцию. Хотя этот метод работает, кажется, что он будет очень медленным для больших списков.

Любые предложения о том, как добиться этого изящно? Спасибо!

Пример кода:

<ListView ItemsSource="{Binding Products}" SelectedItem="{Binding SelectedProduct}">
    <ListView.View>
        <GridView>
            <GridViewColumn Header="Item" Width="100" DisplayMemberBinding="{Binding ProductName}"/>
            <GridViewColumn Header="Type" Width="100" DisplayMemberBinding="{Binding ProductType}"/>
            <GridViewColumn Header="Category" Width="100" DisplayMemberBinding="{Binding Category}"/>
        </GridView>
    </ListView.View>
</ListView>

<StackPanel Grid.Row="1">
    <ComboBox ItemsSource="{Binding Products}" DisplayMemberPath="ProductName"/>
    <ComboBox ItemsSource="{Binding Products}" DisplayMemberPath="ProductType"/>
    <ComboBox ItemsSource="{Binding Products}" DisplayMemberPath="Category"/>
</StackPanel>

person Brent    schedule 22.12.2009    source источник
comment
Можете ли вы объяснить, почему использование ValueConverter для вас не сработало?   -  person Chris Nicol    schedule 22.12.2009
comment
Крис, в моем ValueConverter я пытаюсь вернуть уникальные элементы с помощью оператора LINQ, но мне не удалось понять, как запросить один столбец в ListCollectionView ... Я не уверен, возможно ли это вообще. Даже если это возможно, как ValueConverter узнает об обновлении списка при выборе другого ComboBox? Есть предположения?   -  person Brent    schedule 22.12.2009


Ответы (3)


Проверь это:

<Window x:Class="DistinctListCollectionView.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DistinctListCollectionView"
Title="Window1" Height="300" Width="300">
<Window.Resources>
    <local:PersonCollection x:Key="data">
        <local:Person FirstName="aaa" LastName="xxx" Age="1"/>
        <local:Person FirstName="aaa" LastName="yyy" Age="2"/>
        <local:Person FirstName="aaa" LastName="zzz" Age="1"/>
        <local:Person FirstName="bbb" LastName="xxx" Age="2"/>
        <local:Person FirstName="bbb" LastName="yyy" Age="1"/>
        <local:Person FirstName="bbb" LastName="kkk" Age="2"/>
        <local:Person FirstName="ccc" LastName="xxx" Age="1"/>
        <local:Person FirstName="ccc" LastName="yyy" Age="2"/>
        <local:Person FirstName="ccc" LastName="lll" Age="1"/>
    </local:PersonCollection>
    <local:PersonAutoFilterCollection x:Key="data2" SourceCollection="{StaticResource data}"/>
    <DataTemplate DataType="{x:Type local:Person}">
        <WrapPanel>
            <TextBlock Text="{Binding FirstName}" Margin="5"/>
            <TextBlock Text="{Binding LastName}" Margin="5"/>
            <TextBlock Text="{Binding Age}" Margin="5"/>
        </WrapPanel>
    </DataTemplate>
</Window.Resources>
<DockPanel>
    <WrapPanel DockPanel.Dock="Top">
        <ComboBox DataContext="{Binding Source={StaticResource data2}, Path=Filters[0]}" ItemsSource="{Binding DistinctValues}" SelectedItem="{Binding Value}" Width="120"/>
        <ComboBox DataContext="{Binding Source={StaticResource data2}, Path=Filters[1]}" ItemsSource="{Binding DistinctValues}" SelectedItem="{Binding Value}" Width="120"/>
        <ComboBox DataContext="{Binding Source={StaticResource data2}, Path=Filters[2]}" ItemsSource="{Binding DistinctValues}" SelectedItem="{Binding Value}" Width="120"/>
    </WrapPanel>
    <ListBox ItemsSource="{Binding Source={StaticResource data2}, Path=FilteredCollection}"/>
</DockPanel>
</Window>

И модель просмотра:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.ComponentModel;

namespace DistinctListCollectionView
{
    class AutoFilterCollection<T> : INotifyPropertyChanged
    {
        List<AutoFilterColumn<T>> filters = new List<AutoFilterColumn<T>>();
        public List<AutoFilterColumn<T>> Filters { get { return filters; } }

        IEnumerable<T> sourceCollection;
        public IEnumerable<T> SourceCollection
        {
            get { return sourceCollection; }
            set
            {
                if (sourceCollection != value)
                {
                    sourceCollection = value;
                    CalculateFilters();
                }
            }
        }

        void CalculateFilters()
        {
            var propDescriptors = typeof(T).GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
            foreach (var p in propDescriptors)
            {
                Filters.Add(new AutoFilterColumn<T>()
                {
                    Parent = this,
                    Name = p.Name,
                    Value = null
                });
            }
        }

        public IEnumerable GetValuesForFilter(string name)
        {
            IEnumerable<T> result = SourceCollection;
            foreach (var flt in Filters)
            {
                if (flt.Name == name) continue;
                if (flt.Value == null || flt.Value.Equals("All")) continue;
                var pdd = typeof(T).GetProperty(flt.Name);
                {
                    var pd = pdd;
                    var fltt = flt;
                    result = result.Where(x => pd.GetValue(x, null).Equals(fltt.Value));
                }
            }
            var pdx = typeof(T).GetProperty(name);
            return result.Select(x => pdx.GetValue(x, null)).Concat(new List<object>() { "All" }).Distinct();
        }

        public AutoFilterColumn<T> GetFilter(string name)
        {
            return Filters.SingleOrDefault(x => x.Name == name);
        }

        public IEnumerable<T> FilteredCollection
        {
            get
            {
                IEnumerable<T> result = SourceCollection;
                foreach (var flt in Filters)
                {
                    if (flt.Value == null || flt.Value.Equals("All")) continue;
                    var pd = typeof(T).GetProperty(flt.Name);
                    {
                        var pdd = pd;
                        var fltt = flt;
                        result = result.Where(x => pdd.GetValue(x, null).Equals(fltt.Value));
                    }
                }
                return result;
            }
        }

        internal void NotifyAll()
        {
            foreach (var flt in Filters)
                flt.Notify();
            OnPropertyChanged("FilteredCollection");
        }

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string prop)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(prop));
        }

        #endregion
    }

    class AutoFilterColumn<T> : INotifyPropertyChanged
    {
        public AutoFilterCollection<T> Parent { get; set; }
        public string Name { get; set; }
        object theValue = null;
        public object Value
        {
            get { return theValue; }
            set
            {
                if (theValue != value)
                {
                    theValue = value;
                    Parent.NotifyAll();
                }
            }
        }
        public IEnumerable DistinctValues
        {
            get
            {
                var rc = Parent.GetValuesForFilter(Name);
                return rc;
            }
        }

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string prop)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(prop));
        }

        #endregion

        internal void Notify()
        {
            OnPropertyChanged("DistinctValues");
        }
    }
}

Остальные классы:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DistinctListCollectionView
{
    class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
    }
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DistinctListCollectionView
{
    class PersonCollection : List<Person>
    {
    }

    class PersonAutoFilterCollection : AutoFilterCollection<Person>
    {
    }
}
person Aviad P.    schedule 23.12.2009
comment
Авиад, отлично работает! Я скомпилировал ваш исходный код, и он сработал ... Мне нужно попробовать это в моем приложении, но я уверен, что это сработает. У меня только один вопрос. Возможно ли в коде XAML изменить DataContext полей со списком с: DataContext = {Binding Source = {StaticResource data2}, Path = Filters [0]} на что-то вроде: DataContext = {Binding Source = {StaticResource data2}, Path = Filters.FirstName} Я бы предпочел использовать имена, а не числа, если это возможно. Тем не менее, это работает, и я очень благодарен, поэтому отмечу его как ответ. Спасибо за вашу помощь! - person Brent; 23.12.2009
comment
Прежде всего, рад, что вы нашли его полезным, пожалуйста. Использование фактических имен столбцов возможно, если вы реализуете ICustomTypeDescriptor в классе AutoFilterCollection. Я только что сделал это, и он работает, но это утомительно, потому что вам нужно включить стандартные свойства (SourceCollection) в свойства, предоставленные ICustomTypeDescriptor, чтобы избежать ошибок времени разработки (при этом нет ошибок компиляции или ошибок времени выполнения). - person Aviad P.; 24.12.2009

Если вы используете MVVM, то все ваши связанные объекты данных находятся в вашем классе ViewModel, а ваш класс ViewModel реализует INotifyPropertyChanged, верно?

Если да, то вы можете поддерживать переменные состояния для SelectedItemType1, SelectedItemType2 и т. Д., Которые привязаны к свойству зависимости SelectedItem вашего ComboBox (ов). В Setter для SelectedItemType1 заполните свойство List (которое привязано к ItemsSource для ComboBoxType2) и активируйте NotifyPropertyChanged для свойства List. Повторите это для Type3, и вы должны быть в хорошей форме.

Что касается проблемы «обновления» или того, как View узнает, что что-то изменилось, все сводится к режиму привязки и срабатыванию события NotifyPropertyChanged в нужные моменты.

Вы можете сделать это с помощью ValueConverter, и мне нравятся ValueConverters, но я думаю, что в этом случае более элегантно управлять вашей ViewModel, чтобы привязка просто происходила.

person Joel Cochran    schedule 22.12.2009
comment
Спасибо, Джоэл, хотя ваш ответ сработает, похоже, что потребуется много кода, чтобы заставить эту работу работать во всех направлениях. Например, в Setter для SelectedItemType1 мне пришлось бы заполнить список для всех других ComboBox и принять во внимание, что в этих comboboxes может уже быть выбранный элемент, то есть мне нужно заполнить список на основе двух или более выбранных элементов. . Чем больше я добавлю комбинированных списков, тем хуже будет. - person Brent; 22.12.2009
comment
Мне нравится идея использования одного главного списка с несколькими столбцами и привязки комбинированных списков к одному из столбцов. Каждый раз, когда делается выбор, другие поля со списком автоматически фильтруются на основе выбора. Однако недостатком является то, что в раскрывающемся списке отображаются все элементы, а не уникальные. - person Brent; 22.12.2009
comment
Я думаю, что если вы хотите, чтобы они были отфильтрованы, вам придется предоставлять разные свойства для каждого фильтра. Они могут быть в методе Get и будут извлечены при возникновении события PropertyChanged. Вам все равно понадобится какая-то двусторонняя привязка, чтобы указать, как список должен фильтровать. Я полагаю, что еще одна идея - попробовать изменить ItemsTemplate для каждого списка, чтобы установить видимость элемента. Это не поможет с сортировкой, но может быть другим способом фильтрации. Еще одна идея. - person Joel Cochran; 23.12.2009

Почему бы просто не создать другое свойство, содержащее только отдельные значения из списка, с помощью запроса linq или чего-то подобного?

public IEnumerable<string> ProductNameFilters
{
     get { return Products.Select(product => product.ProductName).Distinct(); }
}

...и т.д.

Вам нужно будет создавать уведомления об изменении свойств для каждого из списков фильтров, когда изменяется свойство вашего продукта, но это не имеет большого значения.

Вам действительно следует рассматривать вашу ViewModel как большой ValueConverter для вашего представления. Единственный раз, когда я использую ValueConverter в MVVM, - это когда мне нужно изменить данные из типа данных, не зависящего от вида, на тот, который является зависящим от вида. Пример: для значений больше 10 текст должен быть красным, а для значений меньше 10 текст должен быть синим ... Синий и красный являются типами, зависящими от представления, и не должны возвращаться из ViewModel. Это действительно единственный случай, когда этой логики не должно быть во ViewModel.

Я сомневаюсь в обоснованности комментария «очень медленно для больших списков» ... в общем, «большой» для людей и «большой» для компьютера - это две очень разные вещи. Если вы относитесь к сфере «больших» как для компьютеров, так и для людей, я бы также усомнился в отображении такого количества данных на экране. Дело в том, что он, вероятно, недостаточно велик, чтобы вы могли заметить стоимость этих запросов.

person Anderson Imes    schedule 22.12.2009
comment
Спасибо, Андерсон, но если я создам ICollectionView или ListCollectionView и отфильтрую список, как я могу обновить свойство ProductNameFilters, если я всегда выбираю отдельные записи из ObservableCollection? Всякий раз, когда я фильтрую список, он по-прежнему возвращает все записи с использованием вашего метода. - person Brent; 23.12.2009
comment
Андерсон, ListView привязан к ObservableCollection. Затем я создаю ICollectionView для таких вещей, как фильтрация, сортировка и группировка в ListView. Используя ваш метод, если я создаю свойство для возврата отдельных значений, этот список IEnumerable никогда не фильтруется, когда я фильтрую ICollectionView. Имеет ли это смысл? Другими словами, вызов myICollectionView.Filter = delgate (object obj) {...}; а затем создание уведомления об изменении свойства не фильтрует свойство IEnumerable ‹string› ProductNameFilters. - person Brent; 23.12.2009
comment
Это потому, что вам нужно изменить выражение linq, чтобы оно соответствовало набору результатов фильтра, а не исходной наблюдаемой коллекции. - person Anderson Imes; 24.12.2009
comment
Это то, чем я хотел бы заниматься, но не знаю как. ICollectionView не имеет метода Select. Можете ли вы мне помочь? - person Brent; 24.12.2009