Как автоматически обновить фильтр и / или порядок сортировки на CollectionViewSource при изменении свойства отдельного элемента?

Итак, этот вопрос относится к Windows Phone 7 / Silverlight (обновленные инструменты WP7, сентябрь 2010 г.), в частности, к фильтрации базовойObservableCollection<T>.

Взбираясь с приложением управления сводными шаблонами WP7, я столкнулся с проблемой изменения основного элемента вObservableCollection<T>, не приводит к обновлению ListBox на экране. По сути, образец приложения имеет два центра, первый из которых напрямую связан сObservableCollection<T>а вторая привязана кCollectionViewSource (т. е. представление отфильтрованногоObservableCollection<T>).

Основные элементы, которые добавляются кObservableCollection<T> воплощать в жизньINotifyPropertyChanged, вот так:

public class ItemViewModel : INotifyPropertyChanged
{       
    public string LineOne
    {
        get { return _lineOne; }
        set
        {
            if (value != _lineOne)
            {
                _lineOne = value;
                NotifyPropertyChanged("LineOne");
            }
        }
    } private string _lineOne;

    public string LineTwo
    {
        get { return _lineTwo; }
        set
        {
            if (value != _lineTwo)
            {
                _lineTwo = value;
                NotifyPropertyChanged("LineTwo");
            }
        }
    } private string _lineTwo;

    public bool IsSelected
    {
        get { return _isSelected; }
        set
        {
            if (value != _isSelected)
            {
                _isSelected = value;
                NotifyPropertyChanged("IsSelected");
            }
        }
    } private bool _isSelected = false;

    public event PropertyChangedEventHandler PropertyChanged;
    private void NotifyPropertyChanged(String propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Затем в главном классе создается сбор данных (для краткости сокращен список, также обратите внимание, что в отличие от других элементов три записи LoadData () имеют IsSelected == true):

 public class MainViewModel : INotifyPropertyChanged
 {
  public MainViewModel()
  {
   this.Items = new ObservableCollection<ItemViewModel>();
  }

  public ObservableCollection<ItemViewModel> Items { get; private set; }

  public bool IsDataLoaded
  {
   get;
   private set;
  }

   public void LoadData()
  {
   this.Items.Add(new ItemViewModel() { LineOne = "runtime one", IsSelected = true, LineTwo = "Maecenas praesent accumsan bibendum" });
   this.Items.Add(new ItemViewModel() { LineOne = "runtime two", LineTwo = "Dictumst eleifend facilisi faucibus" });
   this.Items.Add(new ItemViewModel() { LineOne = "runtime three", IsSelected = true, LineTwo = "Habitant inceptos interdum lobortis" });
   this.Items.Add(new ItemViewModel() { LineOne = "runtime four", LineTwo = "Nascetur pharetra placerat pulvinar" });
   this.Items.Add(new ItemViewModel() { LineOne = "runtime five", IsSelected = true, LineTwo = "Maecenas praesent accumsan bibendum" });
   this.Items.Add(new ItemViewModel() { LineOne = "runtime six", LineTwo = "Dictumst eleifend facilisi faucibus" });
   this.IsDataLoaded = true;
  }

  public event PropertyChangedEventHandler PropertyChanged;
  public void NotifyPropertyChanged(String propertyName)
  {
   if (null != PropertyChanged)
   {
    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
   }
  }
 }

В файле MainPage.xaml первый сводныйItemSource основанный непосредственно наObservableCollection<T> список. Во втором Pivot на экране ListBox естьItemSource Свойство установлено вCollectionViewSource, чей основной источник основан наObservableCollection<T> заселены вLoadData() выше.

<phone:PhoneApplicationPage.Resources>
    <CollectionViewSource x:Key="IsSelectedCollectionView" Filter="CollectionViewSource_SelectedListFilter">
    </CollectionViewSource>
</phone:PhoneApplicationPage.Resources>

<!--LayoutRoot is the root grid where all page content is placed-->
<Grid x:Name="LayoutRoot" Background="Transparent">
    <!--Pivot Control-->
    <controls:Pivot Title="MY APPLICATION">
        <!--Pivot item one-->
        <controls:PivotItem Header="first">
            <!--Double line list with text wrapping-->
            <ListBox x:Name="FirstListBox" Margin="0,0,-12,0" ItemsSource="{Binding Items}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                      <StackPanel Margin="0,0,0,17" Width="432">
                          <TextBlock Text="{Binding LineOne}" TextWrapping="Wrap" Style="{StaticResource PhoneTextExtraLargeStyle}"/>
                          <TextBlock Text="{Binding LineTwo}" TextWrapping="Wrap" Margin="12,-6,12,0" Style="{StaticResource PhoneTextSubtleStyle}"/>
                      </StackPanel>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
        </controls:PivotItem>

        <!--Pivot item two-->
        <controls:PivotItem Header="second"> 
            <!--Triple line list no text wrapping-->
            <ListBox x:Name="SecondListBox" Margin="0,0,-12,0" ItemsSource="{Binding  Source={StaticResource IsSelectedCollectionView}}">
                    <ListBox.ItemTemplate>
                        <DataTemplate>
                            <StackPanel Margin="0,0,0,17">
                                <TextBlock Text="{Binding LineOne}" TextWrapping="NoWrap" Margin="12,0,0,0" Style="{StaticResource PhoneTextExtraLargeStyle}"/>
                                <TextBlock Text="{Binding LineThree}" TextWrapping="NoWrap" Margin="12,-6,0,0" Style="{StaticResource PhoneTextSubtleStyle}"/>
                            </StackPanel>
                        </DataTemplate>
                    </ListBox.ItemTemplate>
                </ListBox>
        </controls:PivotItem>
    </controls:Pivot>
</Grid>

<!--Sample code showing usage of ApplicationBar-->
<phone:PhoneApplicationPage.ApplicationBar>
    <shell:ApplicationBar IsVisible="True" IsMenuEnabled="True">
        <shell:ApplicationBarIconButton IconUri="/Images/appbar_button1.png" Text="Button 1" Click="ApplicationBarIconButton_Click"/>
        <shell:ApplicationBarIconButton IconUri="/Images/appbar_button2.png" Text="Button 2"/>
        <shell:ApplicationBar.MenuItems>
            <shell:ApplicationBarMenuItem Text="MenuItem 1"/>
            <shell:ApplicationBarMenuItem Text="MenuItem 2"/>
        </shell:ApplicationBar.MenuItems>
    </shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>

Обратите внимание, что в MainPage.xaml.cs,Filter атрибут наCollectionViewSource вResources В разделе выше назначен обработчик фильтра, который просматривает те элементы, которые имеютIsSelected установить в true:

public partial class MainPage : PhoneApplicationPage
{
    public MainPage()
    {
        InitializeComponent();
        DataContext = App.ViewModel;
        this.Loaded += new RoutedEventHandler(MainPage_Loaded);
    }

    private void MainPage_Loaded(object sender, RoutedEventArgs e)
    {
        if (!App.ViewModel.IsDataLoaded)
        {
            App.ViewModel.LoadData();
            CollectionViewSource isSelectedListView = this.Resources["IsSelectedCollectionView"] as CollectionViewSource;
            if (isSelectedListView != null)
            {
                isSelectedListView .Source = App.ViewModel.Items;
            }
        }
    }

    private void CollectionViewSource_SelectedListFilter(object sender, System.Windows.Data.FilterEventArgs e)
    {
        e.Accepted = ((ItemViewModel)e.Item).IsSelected;
    }

    private void ApplicationBarIconButton_Click(object sender, EventArgs e)
    {
        ItemViewModel item = App.ViewModel.Items[App.ViewModel.Items.Count - 1];
        item.IsSelected = !item.IsSelected;
    }
}

Также обратите внимание, что сразу после загрузки данных я получаюCollectionViewSource и установите его источник данных в качествеObservableCollection<T> список, для того, чтобы были базовые данные, по которым может выполняться фильтрация.

При загрузке приложения данные отображаются, как и ожидалось, с этими элементами вObservableCollection<T> который имеетIsSelected true, отображается во втором Pivot:

Вы заметите, что я раскомментировал значки панели приложений, первый из которых включаетIsSelected свойство последнего элемента вObservableCollection<T> при нажатии (см. последнюю функцию в MainPage.xaml.cs).

Вот суть моего вопроса - когда я нажимаю соответствующий значок панели, я вижу, когда последний элемент в спискеIsSelected свойство установлено в true, однако второй сводный элемент не отображает этот измененный элемент. Я вижу, чтоNotifyPropertyChanged() обработчик запускается для элемента, однако коллекция не улавливает этот факт, и, следовательно, список в сводной таблице 2 не изменяется, чтобы отразить тот факт, что в коллекцию должен быть добавлен новый элемент.

Я почти уверен, что мне здесь не хватает чего-то совершенно фундаментального / базового, но если это не удастся, кто-нибудь знает лучший способ получить коллекцию и ее основные элементы для счастливой совместной игры?

Я полагаю, что эта проблема также относится к сортировке, а также фильтрации ((в том смысле, что еслиCollectionViewSource основан на сортировке, тогда, когда изменяется свойство элемента, используемого в сортировке, порядок сортировки коллекции также должен это отражать))

 JustinM19 мар. 2011 г., 19:07
Я с той же проблемой. Мы ожидаем, что представление будет динамически обновляться на основе изменений в базовой коллекции, но это не так. Так что это действительно проблема программного обеспечения, которое не выполняет то, что мы, естественно, ожидаем, и / или документация MSDN не является полной.

Ответы на вопрос(2)

когда это происходит, не прошло и 5 минут, как я опубликовал вопрос, и я выяснил, в чем проблема - и этобыло что-то довольно простое. НаCollectionViewSource объект, естьView свойство, которое имеетRefresh() функция. Вызов этой функции после свойства базового элемента, содержащегося вObservableCollection<T> изменения, кажется, сделали это.

По сути, все, что мне нужно было сделать, это изменитьCollectionViewSource объект в переменную-член, а затем сохранить его, когдаLoadData() называется:

private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
    if (!App.ViewModel.IsDataLoaded)
    {
        App.ViewModel.LoadData();
        m_isSelectedListView = this.Resources["IsSelectedCollectionView"] as CollectionViewSource;
        if (m_isSelectedListView != null)
        {
            m_isSelectedListView.Source = App.ViewModel.Items;
        }
    }
}

Затем позвонитеRefresh() на вид, после любого из пунктов в базовомObservableCollection<T> изменения. Поэтому в MainPage.xaml.cs сразу после изменения последнего элемента добавьте вызов для обновления:

private void ApplicationBarIconButton_Click(object sender, EventArgs e)
{
    ItemViewModel item = App.ViewModel.Items[App.ViewModel.Items.Count - 1];
    item.IsSelected = !item.IsSelected;
    m_isSelectedListView.View.Refresh();
}

... и ListBox второго Pivot обновляется мгновенно. Такая короткая строка кода, целый мир различий!

За то время, которое мне потребовалось, чтобы написать этот вопрос, я мог бы сделать сотню вещей :-( Ну, лучше поздно, чем никогда, я думаю - подумал опубликовать ответ здесь, хотя бы для того, чтобы спасти кого-то другого. их волосы как у меня.

 Matt Lacey22 сент. 2010 г., 19:04
ObservableCollection автоматически реализуетINotifyPropertyChanged где какCollectionViewSource не делает. Это означает, что вы должны явно сказатьCollectionViewSource что он должен обновить себя. Это то, что вы делаете, звоняRefresh();.

и хотя решение Refresh () работает хорошо, оно выполняется довольно долго, потому что оно обновляет весь список только для одного события, измененного свойством элемента. Не очень хорошо. И в сценарии ввода данных в режиме реального времени каждые 1 секунду, я позволю вам представить результат в пользовательском опыте, если вы используете этот подход :)

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

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

Вот код для того, чтобы сделать это:

Полученная наблюдаемая коллекция:

namespace dotnetexplorer.blog.com
{
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;

/// <summary>
/// Derived class used to be able to manage filter application when a collection item property changed
///   whithout having to do a refresh
/// </summary>
internal sealed class CustomObservableCollection : ObservableCollection<object>
{
    /// <summary>
    ///   Initializes a new instance of the <see cref = "CustomObservableCollection " /> class.
    /// </summary>
    public CustomObservableCollection ()
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="CustomObservableCollection "/> class.
    /// </summary>
    /// <param name="source">
    /// The source.
    /// </param>
    public CustomObservableCollection (IEnumerable<object> source)
        : base(source)
    {
    }

    /// <summary>
    /// Custom Raise collection changed
    /// </summary>
    /// <param name="e">
    /// The notification action
    /// </param>
    public void RaiseCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        OnCollectionChanged(e);
    }
}
}

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

        private void ItemPropertyChanged(object sender, PropertyChangedEventArgs e)
    {

                // To avoid doing a refresh on a property change which would end in a very hawful user experience
                // we simulate a replace to the collection because the filter is automatically applied in this case
                int index = _substituteSource.IndexOf(sender);

                var argsReplace = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace,
                                                                       new List<object> { sender },
                                                                       new List<object> { sender }, index);
                _substituteSource.RaiseCollectionChanged(argsReplace);
            }

        }
    }

Надеюсь, это поможет!

 Patrick Simpe-Asante13 мая 2011 г., 16:09
Спасибо, Бруно, мне нравится ваше решение (вы получили положительный отзыв от меня). Такое ощущение, что должен быть лучший способ «инициировать» предикат фильтра, не имитируя вставку. Рассмотрим это снова когда я получу шанс ..
 Bruno13 мая 2011 г., 17:42
Спасибо;) Но удачи в поиске другого решения, я застрял на этом в течение длительного времени, пытаясь найти лучшее решение для моего интеллектуального компонента поиска. Пока что лучшего решения не найдено ...

Ваш ответ на вопрос