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 :
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);
}
}
}
And there is the code to use when receiveing item property changed event where substitute source is a 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);
}
}
}
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 a View
property, which has a Refresh()
function. Calling this function after a property on an underlying item contained in the ObservableCollection<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 when LoadData()
is called:
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;
}
}
}
Then, call Refresh()
on the view, after any of the items in the underlying ObservableCollection<T>
changes. So in MainPage.xaml.cs, just after changing the last item, add the call to refresh:
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();
}
... 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With