11

Ok, esta pregunta está relacionada con Windows Phone 7/Silverlight (herramientas actualizadas de WP7, septiembre de 2010), filtrando específicamente un subyacente ObservableCollection<T>.¿Cómo actualizar automáticamente el filtro y/o el orden de clasificación en CollectionViewSource, cuando cambia la propiedad de un elemento individual?

En la limpieza con la aplicación de control de pivote de la plantilla WP7, me he encontrado con un problema por el cual cambiar un elemento subyacente en un ObservableCollection<T>, no da como resultado la actualización del ListBox en pantalla. Básicamente, la aplicación de muestra tiene dos pivotes, el primero directamente ligado al ObservableCollection<T> subyacente, y el segundo a un CollectionViewSource (es decir, que representa una vista filtrada en el ObservableCollection<T> subyacente).

Los elementos subyacentes que se están agregando a la ObservableCollection<T> implementar INotifyPropertyChanged, así:

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)); 
     } 
    } 
} 

Luego, en la clase principal, una colección de datos se inventó (lista reducida por razones de brevedad, también tenga en cuenta que a diferencia de otros artículos, tres de los LoadData() entradas han 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)); 
    } 
    } 
} 

en el archivo MainPage.xaml, el primer pivote tiene su ItemSource basada directamente en la lista ObservableCollection<T>. Dentro del segundo Pivot, el ListBox en pantalla tiene su propiedad ItemSource establecida en CollectionViewSource, cuya fuente subyacente se basa en el ObservableCollection<T> poblado en LoadData() arriba.

<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> 

Tenga en cuenta que en los MainPage.xaml.cs, el atributo Filter en el CollectionViewSource en la sección Resources arriba se asigna un controlador de filtro, que tamiza a través de esos elementos que tienen IsSelected establecido en verdadero:

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; 
    } 
} 

También tenga en cuenta que inmediatamente después de la carga Por encima de los datos, obtengo el CollectionViewSource y configuro su fuente de datos como la lista ObservableCollection<T>, para que haya datos base sobre los que pueda realizarse el filtrado.

Cuando se carga la aplicación, se muestran los datos como se esperaba, con esos elementos en el ObservableCollection<T>IsSelected cierto, se está visualizando en el segundo pivote que tienen:

alt text alt text

Se dará cuenta de que yo Descomentó los iconos de la barra de aplicaciones, el primero de los cuales alterna la propiedad IsSelected del último elemento en el ObservableCollection<T> al hacer clic (consulte la última función en MainPage.xaml.cs).

Aquí está el quid de mi pregunta - al hacer clic en el icono de la barra de aplicación, que se puede ver cuando el último elemento de la lista tiene su IsSelected propiedad se establece en true, howoever el segundo pivote no muestra este elemento cambiado . Puedo ver que el controlador NotifyPropertyChanged() se está disparando en el elemento, sin embargo, la colección no está recogiendo este hecho, y por lo tanto, el cuadro de lista en el Pivote 2 no cambia para reflejar el hecho de que debe haber un nuevo elemento agregado a la colección .

Estoy bastante seguro de que me estoy perdiendo algo bastante fundamental/básico aquí, pero en su defecto, ¿alguien sabe la mejor manera de obtener la colección y los elementos subyacentes para jugar felizmente juntos?

Supongo que este problema también se aplica tanto a la clasificación como al filtrado ((en el sentido de que si CollectionViewSource se basa en la clasificación, cuando cambia la propiedad de un elemento que se utiliza en la clasificación, el orden de la colección debería reflejar esto también)

+0

Estoy teniendo el mismo problema.Esperamos que la vista se actualice de forma dinámica en función de los cambios en la colección subyacente, pero no es así. Así que este es realmente un problema de que el software no hace lo que esperamos de forma natural y/o que la documentación de MSDN no está completa. – JustinM

Respuesta

3

No solo lo odias cuando eso sucede, no han pasado 5 minutos desde que publiqué la pregunta, y he descubierto cuál es el problema, y ​​era algo bastante básico. En el objeto CollectionViewSource, hay una propiedad View, que tiene una función Refresh(). Llamar a esta función después de una propiedad en un elemento subyacente contenido en los cambios ObservableCollection<T>, parece haberlo hecho.

Básicamente, todo lo que tenía que hacer era cambiar el objeto CollectionViewSource en una variable miembro, y luego guardarlo cuando LoadData() se llama:

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; 
     } 
    } 
} 

Entonces, llama a Refresh() en la vista, después de cualquiera de los artículos en los cambios subyacentes ObservableCollection<T>. Así que en MainPage.xaml.cs, justo después de cambiar el último punto, añadir la llamada para refrescar:

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(); 
} 

... y ListBox del segundo pivote se actualiza al instante. ¡Una línea tan corta de código, todo un mundo de diferencia!

En el tiempo que tardé en escribir esa pregunta, hay un centenar de cosas que podría haber hecho :-(Ah, bueno, mejor tarde que nunca, supongo que pensé en publicar la respuesta aquí, aunque solo sea para guardar Alguien más se arrancó el pelo como lo hice.

+1

El 'ObservableCollection' implementa automáticamente' INotifyPropertyChanged' donde no lo hace 'CollectionViewSource'. Esto significa que tiene que decirle explícitamente al 'CollectionViewSource' que necesita actualizarse. Esto es lo que está haciendo al llamar a 'Refresh();'. –

4

Tuve que manejar este problema y aunque la solución 'Refresh()' funciona bien, es bastante larga de ejecutar porque actualiza toda la lista solo para una propiedad de elemento cambiada evento. No muy bueno. Y en un escenario de datos en tiempo real que ingresa a la colección cada 1 segundos, te dejo imaginar el resultado en la experiencia del usuario si usas este enfoque :)

Encontré una solución cuya base es : cuando un dding un elemento a la colección envuelto en una vista de colección, el elemento se evalúa mediante el predicado de filtro y, en función de este resultado, se muestra o no en la vista.

Así que, en lugar de llamar a refresh(), aparecí simulando una inserción del objeto que tenía su propiedad actualizada. Al simular la inserción del objeto, el predicado de filtro lo evaluará automáticamente sin necesidad de actualizar toda la lista con una actualización.

Este es el código con el fin de hacer eso:

La colección observables derivados:

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); 
    } 
} 
} 

Y está el código para utilizar cuando receiveing ​​evento de cambio de propiedad del elemento donde origen sustituto es un 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); 
      } 

     } 
    } 

Espero que esto ayude!

+0

Gracias Bruno, me gusta tu solución (obtuviste un voto favorable). Parece que debería haber una mejor manera de "activar" el predicado del filtro sin tener que simular un inserto ... Veremos esto de nuevo cuando tengo la oportunidad ... –

+0

Thx;) Pero buena suerte para encontrar otra solución, he estado atrapado en eso durante mucho tiempo, tratando de encontrar una mejor solución para mi componente de búsqueda inteligente. Hasta ahora, no se ha encontrado una mejor solución ... – Bruno

Cuestiones relacionadas