Элементы комбинированного списка фильтров WPF на основе элементов ListView - PullRequest
2 голосов
/ 22 декабря 2009

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

Моей первой мыслью было связать ComboBox с тем же 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>

Ответы [ 3 ]

3 голосов
/ 23 декабря 2009

Проверьте это:

<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>
    {
    }
}
0 голосов
/ 23 декабря 2009

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

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

... и т.д.

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

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

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

0 голосов
/ 22 декабря 2009

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...