Ok, so this question is related to Windows Phone 7/Silverlight (updated WP7 Tools, Sept 2010), specifically filtering an underlying ObservableCollection<T>
.
In mucking about with the WP7 template Pivot control application, I've run into an issue whereby changing an underlying item in an ObservableCollection<T>
, does not result in the on-screen ListBox being updated. Basically, the sample app has two pivots, the first directly bound to the underlying ObservableCollection<T>
, and the second bound to a CollectionViewSource
(i.e., representing a filtered view on the underlying ObservableCollection<T>
).
The underlying items that are being added to the ObservableCollection<T>
implement INotifyPropertyChanged
, like so:
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));
}
}
}
Then, in the main class, a data collection is concocted (list reduced for brevity, also note that unlike other items, three of the LoadData() entries have 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));
}
}
}
In the MainPage.xaml file, the first Pivot has its ItemSource
based directly on the ObservableCollection<T>
list. Within the second Pivot, the on-screen ListBox has its ItemSource
Property set to a CollectionViewSource
, whose underlying source is based on the ObservableCollection<T>
populated in LoadData()
above.
<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>
Note that in the MainPage.xaml.cs, the Filter
attribute on the CollectionViewSource
in the Resources
section above is assigned a filter handler, which sifts through those items that have IsSelected
set to 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;
}
}
Also note that immediately after loading up the data, I obtain the CollectionViewSource
and set its data source as the ObservableCollection<T>
list, in order that there is base data upon which the filtering can take place.
When application loads, the data is displayed as expected, with those items in the ObservableCollection<T>
which have IsSelected
true, being displayed in the second Pivot:
You'll notice that I've uncommented the Application Bar Icons, the first of which toggles the IsSelected
property of the last item in the ObservableCollection<T>
when clicked (see the last function in MainPage.xaml.cs).
Here is the crux of my question - when I click the applicable bar icon, I can see when the last item in the list has its IsSelected
property set to true, howoever the second Pivot does not display this changed item. I can see that the NotifyPropertyChanged()
handler is being fired on the item, however the collection is not picking up this fact, and hence the list box in Pivot 2 does not change to reflect the fact that there should be a new item added to the collection.
I'm pretty certain that I'm missing something quite fundamental/basic here, but failing that, does anyone know the best way to get the collection and it's underlying items to play happily together?
I suppose this problem also applies to sorting as well as filtering ((in the sense that if a CollectionViewSource
is based on sorting, then when a property of an item that is used in the sort changes, the sort order of the collection should reflect this as well))
I had to handle this problem and although the 'Refresh()' solution works well, it is quite long to execute because its refreshes the whole list just for one item property changed event. Not very good. And in a scenario of real time data entering the collection every 1 seconds, I let you imagine the result in user experience if you use this approach :)
I came up with a solution which base is : when adding an item to collection wrapped in a collectionview, then the item is evaluated by the filter predicate and, based on this result, displayed or not in the view.
So instead of calling refresh(), I came up simulating an insert of the object that got its property updated. By simulating the insert of the object, it is going to be automatically evaluated by the filter predicate without need to refresh the whole list with a refresh.
Here is the code in order to do that :
The derived observable collection :
And there is the code to use when receiveing item property changed event where substitute source is a CustomObservableCollection :
Hope this will help !
Don't you just hate it when that happens, not 5 minutes gone since I posted the question, and I've figured out what the problem is - and it was something quite basic. On the
CollectionViewSource
object, there is aView
property, which has aRefresh()
function. Calling this function after a property on an underlying item contained in theObservableCollection<T>
changes, seems to have done it.Basically, all I had to do was change the
CollectionViewSource
object into a member variable, and then save it whenLoadData()
is called:Then, call
Refresh()
on the view, after any of the items in the underlyingObservableCollection<T>
changes. So in MainPage.xaml.cs, just after changing the last item, add the call to refresh:... and the second Pivot's ListBox is updated instantly. Such a short line of code, a whole world of difference!
In the time it took me to write up that question, there are a hundred things I could've done :-( Ah well, better late than never I guess - thought to post the answer here, if only to save someone else tearing out their hair like I did.