11

Ok, quindi questa domanda è relativa a Windows Phone 7/Silverlight (strumenti WP7 aggiornati, settembre 2010), in particolare il filtro di uno ObservableCollection<T> sottostante.Come aggiornare automaticamente il filtro e/o l'ordinamento su CollectionViewSource, quando la proprietà di un singolo oggetto cambia?

In chat con l'applicazione di controllo Pivot modello WP7, ho riscontrato un problema in cui la modifica di un elemento sottostante in un ObservableCollection<T> non comporta l'aggiornamento del ListBox su schermo. Fondamentalmente, l'app di esempio ha due perni, il primo direttamente legato allo ObservableCollection<T> sottostante e il secondo associato a uno CollectionViewSource (vale a dire, rappresenta una vista filtrata sul sottostante ObservableCollection<T>).

Gli elementi sottostanti che vengono aggiunti al ObservableCollection<T> attuare INotifyPropertyChanged, in questo modo:

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

Poi, nella classe principale, una raccolta di dati viene inventata (lista ridotta per brevità, si noti anche che a differenza di altri elementi, tre dei LoadData() le voci sono 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)); 
    } 
    } 
} 

Nel file MainPage.xaml, il primo pivot ha il suo ItemSource basato direttamente sulla lista ObservableCollection<T>. All'interno del secondo Pivot, il ListBox visualizzato sullo schermo ha la proprietà ItemSource impostata su CollectionViewSource, la cui origine sottostante è basata sullo ObservableCollection<T> popolato in LoadData() precedente.

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

noti che nelle MainPage.xaml.cs, l'attributo Filter sul CollectionViewSource nella sezione Resources sopra è assegnato un gestore filtro, che setaccia attraverso quegli elementi che hanno IsSelected impostato su 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; 
    } 
} 

Si noti inoltre che immediatamente dopo il caricamento g sui dati, ottengo il CollectionViewSource e imposta la sua origine dati come lista ObservableCollection<T>, in modo che ci siano dati di base su cui può avvenire il filtraggio.

Quando carichi applicativi, i dati vengono visualizzati come previsto, con quegli elementi della ObservableCollection<T> che hanno IsSelected vero, essere visualizzato nella seconda Pivot:

alt text alt text

Avrete notato che ho Sono state eliminate le icone della barra delle applicazioni, la prima delle quali attiva la proprietà IsSelected dell'ultimo elemento nello ObservableCollection<T> quando si fa clic (vedere l'ultima funzione in MainPage.xaml.cs).

Qui è il nocciolo della mia domanda - quando clicco l'icona della barra del caso, posso vedere quando l'ultimo elemento della lista ha la proprietà IsSelected impostata su true, howoever il secondo pivot non visualizza questa voce modificata . Posso vedere che il gestore NotifyPropertyChanged() è stato attivato sull'elemento, tuttavia la raccolta non sta rilevando questo fatto e quindi la casella di riepilogo in Pivot 2 non cambia per riflettere il fatto che dovrebbe essere aggiunto un nuovo elemento alla raccolta .

Sono quasi certo che mi manca qualcosa di fondamentale/fondamentale qui, ma in caso contrario, qualcuno sa qual è il modo migliore per ottenere la raccolta e gli elementi sottostanti per giocare felicemente insieme?

Suppongo che questo problema si applichi anche all'ordinamento e al filtraggio ((nel senso che se un CollectionViewSource è basato sull'ordinamento, allora quando cambia una proprietà di un oggetto che viene utilizzato nell'ordinamento, l'ordinamento del la raccolta dovrebbe riflettere anche questo))

+0

Ho lo stesso problema.Ci aspettiamo che la vista si aggiorni dinamicamente in base alle modifiche alla raccolta sottostante, ma non lo fa. Quindi questo è davvero un problema del software che non fa ciò che ci aspettiamo naturalmente e/o che la documentazione MSDN non sia completa. – JustinM

risposta

3

Non si odia solo quando ciò accade, non sono passati 5 minuti da quando ho postato la domanda, e ho capito qual è il problema - e lo era qualcosa abbastanza di base. Sull'oggetto CollectionViewSource, c'è una proprietà View, che ha una funzione Refresh(). Chiamare questa funzione dopo una proprietà su un oggetto sottostante contenuto nelle modifiche ObservableCollection<T>, sembra averlo fatto.

In sostanza, tutto quello che dovevo fare era cambiare l'oggetto CollectionViewSource in una variabile membro, e quindi salvarlo quando LoadData() si chiama:

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

Poi, chiamare Refresh() sulla vista, dopo una delle voci nelle modifiche ObservableCollection<T> sottostanti. Quindi, in MainPage.xaml.cs, poco dopo aver cambiato l'ultima voce, aggiungere la chiamata per aggiornare:

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

... e ListBox del secondo pivot viene aggiornato immediatamente. Una tale linea di codice, un intero mondo di differenza!

Nel tempo impiegato per scrivere la domanda, ci sono cento cose che avrei potuto fare :-(Ah, beh, meglio tardi che mai immagino - pensavo di pubblicare la risposta qui, se non altro per salvare qualcun altro che strappa i capelli come ho fatto io

+1

'ObservableCollection' implementa automaticamente' INotifyPropertyChanged' dove invece 'CollectionViewSource' no. Ciò significa che devi dire esplicitamente a 'CollectionViewSource' che è necessario aggiornarsi. Questo è ciò che stai facendo chiamando 'Refresh();'. –

4

Ho dovuto gestire questo problema e sebbene la soluzione 'Refresh()' funzioni bene, è piuttosto lunga da eseguire perché aggiorna l'intero elenco solo per una proprietà di un oggetto modificata evento.Non molto buono.In uno scenario di dati in tempo reale che entrano nella raccolta ogni 1 secondi, ti permetto di immaginare il risultato nell'esperienza utente se usi questo approccio :)

Ho trovato una soluzione a base : Quando un dding un oggetto alla raccolta racchiuso in una collectionview, quindi l'elemento viene valutato dal predicato del filtro e, in base a questo risultato, visualizzato o meno nella vista.

Quindi, invece di chiamare refresh(), ho trovato la simulazione di un inserto dell'oggetto che ha aggiornato la sua proprietà. Simulando l'inserimento dell'oggetto, verrà valutato automaticamente dal predicato del filtro senza necessità di aggiornare l'intero elenco con un aggiornamento.

Ecco il codice per farlo:

La collezione osservabile derivato:

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

E c'è il codice da utilizzare quando receiveing ​​proprietà dell'elemento evento modificato dove fonte sostitutiva è 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); 
      } 

     } 
    } 

Spero che questo aiuti!

+0

Grazie Bruno, mi piace la tua soluzione (hai ricevuto un voto da me). Sembra che ci dovrebbe essere un modo migliore per "attivare" il predicato del filtro senza dover simulare un inserto. di nuovo quando ho una possibilità .. –

+0

Thx;) Ma buona fortuna a trovare un'altra soluzione, sono stato bloccato su questo per molto tempo, cercando di capire una soluzione migliore per il mio componente di ricerca intelligente. Finora, non è stata trovata una soluzione migliore ... – Bruno