2009-12-22 4 views
2

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

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

FYI: Я прочитал сообщение Колина Эберхардта о добавлении AutoFilter в 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> 
+0

Можете ли вы объяснить, почему с помощью ValueConverter не работает для вас ? –

+0

Chris, В моем ValueConverter я пытаюсь вернуть уникальные элементы с помощью оператора LINQ, но мне не удалось выяснить, как запросить один столбец в ListCollectionView ... Я не уверен, что это даже возможно. Даже если это возможно, как ValueConverter знает, что «обновит» список, когда будет выбран выбор в другом ComboBox? Есть предположения? – Brent

ответ

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> 
    { 
    } 
} 
+0

Aviad, Это отлично работает! Я скомпилировал ваш исходный код, и он сработал ... Мне нужно попробовать его в своем приложении, но я уверен, что он сработает. У меня только один вопрос.Возможно, в коде XAML можно изменить DataContext из списков со списком: DataContext = "{Binding Source = {StaticResource data2}, Path = Filters [0]}", : что-то вроде DataContext = "{Binding Source = {StaticResource data2}, Path = Filters.FirstName} " Я предпочел бы использовать имена, а не числа, если это возможно. Тем не менее, он работает, и я очень благодарен, поэтому я буду отмечать его как ответ. Спасибо за вашу помощь! – Brent

+0

Прежде всего, рад, что вы сочтете это полезным, пожалуйста. Использование фактических имен столбцов возможно, если вы реализуете ICustomTypeDescriptor в классе AutoFilterCollection. Я просто сделал это, и это работает, но это утомительно, потому что вы должны включать стандартные свойства (SourceCollection) в свойствах ICustomTypeDescriptor, чтобы избежать ошибок времени разработки (например, ошибок компиляции или ошибок времени выполнения). –

0

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

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

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

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

+0

Спасибо, Джоэл, хотя ваш ответ будет работать, кажется, что для выполнения этой работы во всех направлениях потребуется много кода. Например, в Setter for SelectedItemType1 мне нужно будет заполнить список для всех других ComboBoxes и принять во внимание, что у этих comboboxes уже есть выделенный элемент, то есть мне нужно заполнить список на основе двух или более выбранных элементов , Чем больше comboboxes я добавляю, тем хуже это получится. – Brent

+0

Мне нравится идея использования одного главного списка с несколькими столбцами и привязки comboboxes к одному из столбцов. Каждый раз, когда выполняется выбор, другие выпадающие списки фильтруются на основе выбора автоматически. Однако недостатком является то, что в раскрывающемся списке отображаются все элементы, а не уникальные. – Brent

+0

Я думаю, что если вы хотите, чтобы они были отфильтрованы, вам придется подвергать разные свойства каждому фильтру. Они могут быть в методе Get и будут вытаскиваться при срабатывании события PropertyChanged. Вам все равно потребуется какое-то двухстороннее связывание, чтобы указать, как фильтр должен фильтроваться. Я полагаю, что другая идея заключается в том, чтобы попытаться изменить ItemsTemplate для каждого списка, чтобы установить элемент Видимость. Это не поможет при сортировке, но может быть другим способом фильтрации. Еще одна идея. –

0

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

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

... и т.п.

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

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

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

+0

Спасибо Anderson, но если я создаю ICollectionView или ListCollectionView и фильтрую список, как я могу обновить свойство ProductNameFilters, если я всегда выбираю отдельные записи из ObservableCollection? Всякий раз, когда я фильтрую список, он все равно возвращает все записи, используя ваш метод. – Brent

+0

Извините ... не следует. Можете ли вы обновить свой вопрос? –

+0

Anderson, ListView привязан к ObservableCollection. Затем я создаю ICollectionView для выполнения таких операций, как фильтрация, сортировка и группировка в ListView. Используя ваш метод, если я создаю свойство для возврата отдельных значений, этот список IEnumerable никогда не фильтруется при фильтрации ICollectionView. Имеет ли это смысл? Другими словами, вызов myICollectionView.Filter = delgate (object obj) {...}; а затем повышение измененного уведомления не фильтрует свойство IEnumerable ProductNameFilters. – Brent

Смежные вопросы