I currently have an ObservableCollection named MyList
that is binded to the itemsource of on listview. It also adds and removes items from MyList.
What I want is to add items in each listview based on some criteria. More specific, If the property Status
is "Yes" the item should go to the first listview MyListview
, if it is "No" to go to the second listview MySecondListview
and If Status==""
and the property Date
which is a DateTime property points today, then it goes to the third listview.
My MainPage code is:
public sealed partial class MainPage : Page
{
private ObservableCollection<MyClass> MyList = new ObservableCollection<MyClass>();
public MainPage()
{
this.InitializeComponent();
DataContext = MyList;
}
private void Add_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
MyList.Add(new MyClass("Yes", new DateTime(2015, 5, 4)));
}
private void Delete_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
MyList.Remove((MyClass)MyListview.SelectedItem);
}
}
My XAML for Main Page is:
<Page
x:Class="App17.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:App17"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Viewbox>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" Height="768" Width="1366">
<Button x:Name="Delete" Content="Delete" HorizontalAlignment="Left" Height="116" Margin="376,89,0,0" VerticalAlignment="Top" Width="224" Click="Delete_Click"/>
<Button x:Name="Add" Content="Add" HorizontalAlignment="Left" Height="116" Margin="111,89,0,0" VerticalAlignment="Top" Width="214" Click="Add_Click"/>
<ListView x:Name="MyListview" HorizontalAlignment="Left" Height="497" Margin="71,261,0,0" VerticalAlignment="Top" Width="349" ItemsSource="{Binding}"/>
<ListView x:Name="MySecondListview" HorizontalAlignment="Left" Height="497" Margin="468,261,0,0" VerticalAlignment="Top" Width="317"/>
<ListView x:Name="MyThirdListview" HorizontalAlignment="Left" Height="497" Margin="893,261,0,0" VerticalAlignment="Top" Width="317"/>
</Grid>
</Viewbox>
</Page>
My Class code is:
class MyClass
{
public string Status { get; set; }
public DateTime Date { get; set; }
public MyClass(string status, DateTime date)
{
Status = status;
Date = date;
}
}
Ugh. So, I didn't realize this but it turns out that Winrt (i.e. Windows Store apps) does not support filtering in ICollectionView
. It seems like every time I turn around, I find another XAML/WPF feature that was inexplicably omitted from Winrt, and this is one of them.
So, while this would have been trivial to accomplish in WPF (just create a CollectionViewSource
object, subscribe to the Filter
event, and bind to the object's View
), this turns out to be a bit more of a hassle in Winrt.
There are already some related articles, including one on Stack Overflow that provide alternatives. For example:
That second one is a very elaborate implementation to support filtering and other operations. It's overkill for your purposes, but you may find in the future you would like to use features it includes.
The first example, while more appropriate in scale to your question, approached the problem in somewhat different way than I would. In particular, it implements a single ObservableCollection<T>
subclass that in turn has a single view. This prevents it from being useful for the "one collection, many views" scenario.
So I wrote an ObservableCollection<T>
that is in the same vein, but which itself is the view, and references an independent instance of ObservableCollection<T>
. You can use as many instances of this class, which I called FilteredObservableCollection<T>
, as you need while still maintaining a single source collection. Each view will update itself as needed, according to the filter, when the source collection changes.
<caveat index="0">
In this approach (the original inspiration for my class, as well as my own), the bound view is itself an ObservableCollection<T>
. And in particular, this is not a read-only collection. It's very important that any consumers of these "view" objects not try to directly modify the view object, even though they can. They should only ever display the object.
The second link above is better in this respect, as it's implementing an actual ICollectionView
interface, which addresses the mutability question. Frankly, it would be better from a code maintenance point of view to do it that way, to make it clearer what object is the real list, and what objects are just the views.
But fact is, this way is a lot simpler and easier to implement. Just be careful using it, and you won't get hurt. :)
</caveat>
<caveat index="1">
The other very important limitation to understand is that this implementation will not update the viewed collection on changes to the filter logic (i.e. in case you pass a Predicate<T>
that itself depends on some mutable state somewhere), or on changes to the displayed data (i.e. if a property being checked by the filter is modified in one or more displayed data items).
These limitations could be addressed in a variety of ways, but to do so would significantly increase the complexity of this answer. I would like to try to keep things simple. It's my hope that merely pointing these limitations out is sufficient, to ensure that if you do run into a situation where you need a more reactive view implementation, you're aware of the limitation and know you'll have to extend this solution to support your need.
</caveat>
That class winds up looking like this:
public class FilteredObservableCollection<T> : ObservableCollection<T>
{
private Predicate<T> _filter;
public FilteredObservableCollection(ObservableCollection<T> source, Predicate<T> filter)
: base(source.Where(item => filter(item)))
{
source.CollectionChanged += source_CollectionChanged;
_filter = filter;
}
private void _Fill(ObservableCollection<T> source)
{
Clear();
foreach (T item in source)
{
if (_filter(item))
{
Add(item);
}
}
}
private int this[T item]
{
get
{
int foundIndex = -1;
for (int index = 0; index < Count; index++)
{
if (this[index].Equals(item))
{
foundIndex = index;
break;
}
}
return foundIndex;
}
}
private void source_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
ObservableCollection<T> source = (ObservableCollection<T>)sender;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (T item in e.NewItems)
{
if (_filter(item))
{
Add(item);
}
}
break;
case NotifyCollectionChangedAction.Move:
// Without a lot more work maintaining the view state, it would be just as hard
// to figure out where the moved item should go, as it would be to just regenerate
// the whole view. So just do the latter.
_Fill(source);
break;
case NotifyCollectionChangedAction.Remove:
foreach (T item in e.OldItems)
{
// Don't bother looking for the item if it was filtered out
if (_filter(item))
{
Remove(item);
}
}
break;
case NotifyCollectionChangedAction.Replace:
for (int index = 0; index < e.OldItems.Count; index++)
{
T item = (T)e.OldItems[index];
if (_filter(item))
{
int foundIndex = this[item];
if (foundIndex == -1)
{
// i.e. should never happen
throw new Exception("internal state failure. object not present, even though it should be.");
}
T newItem = (T)e.NewItems[index];
if (_filter(newItem))
{
this[foundIndex] = newItem;
}
else
{
RemoveAt(foundIndex);
}
}
else
{
// The item being replaced wasn't in the filtered
// set of data. Rather than do the work to figure out
// where the new item should go, just repopulate the
// whole list. (Same reasoning as for Move event).
_Fill(source);
}
}
break;
case NotifyCollectionChangedAction.Reset:
_Fill(source);
break;
}
}
}
So, with that helper object implemented, setting up your UI is simple. Just create a new instance of the above class for each view of your data you want, and bind to that instance.
For this example, I reworked your original UI fairly extensively, to clean things up a bit and make it easier to observe what's going on. I added buttons to create items that should appear in each view, and there are now four views: the whole list (where you can select and delete items), and then one each for the three filtering options.
With this approach, the DataContext
winds up being this
instead of the one list, because we want access to the individual views. The members also need to be changed to public properties, so that the XAML binding engine can find them. This example assumes that they will be initialized once and never changed; if you want the bindings to be updatable, you'll want to make these dependency properties or implement INotifyPropertyChanged
in the MainPage
class.
I also added a DataTemplate
for your class, so that the presentation of each item in each list was useful (i.e. something other than the type name).
With all that, the XAML winds up looking like this:
<Page
x:Class="TestSO30038588ICollectionView.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:TestSO30038588ICollectionView"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Page.Resources>
<DataTemplate x:Key="myClassTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Status}"/>
<TextBlock Text="{Binding Date}" Margin="20, 0, 0, 0"/>
</StackPanel>
</DataTemplate>
</Page.Resources>
<Viewbox>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" Height="768" Width="1366">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button x:Name="Delete" Content="Delete"
HorizontalAlignment="Left" VerticalAlignment="Top"
Height="116" Width="224"
Grid.Row="0" Grid.Column="0"
Click="Delete_Click"/>
<Button x:Name="Toggle" Content="Toggle Yes/No"
HorizontalAlignment="Left" VerticalAlignment="Top"
Height="116" Width="224"
Grid.Row="1" Grid.Column="0"
Click="Toggle_Click"/>
<Button x:Name="AddYes" Content="Add Yes"
HorizontalAlignment="Left" VerticalAlignment="Top"
Height="116" Width="214"
Grid.Row="0" Grid.Column="1"
Click="AddYes_Click"/>
<Button x:Name="AddNo" Content="Add No"
HorizontalAlignment="Left" VerticalAlignment="Top"
Height="116" Width="214"
Grid.Row="0" Grid.Column="2"
Click="AddNo_Click"/>
<Button x:Name="AddEmpty" Content="Add Empty"
HorizontalAlignment="Left" VerticalAlignment="Top"
Height="116" Width="214"
Grid.Row="0" Grid.Column="3"
Click="AddEmpty_Click"/>
<ListView x:Name="AllElementsList"
ItemTemplate="{StaticResource myClassTemplate}"
HorizontalAlignment="Left" VerticalAlignment="Top"
Grid.Row="2" Grid.Column="0"
ItemsSource="{Binding MyList}"/>
<ListView x:Name="MyListview"
ItemTemplate="{StaticResource myClassTemplate}"
HorizontalAlignment="Left" VerticalAlignment="Top"
Grid.Row="1" Grid.Column="1" Grid.RowSpan="2"
ItemsSource="{Binding yesList}"/>
<ListView x:Name="MySecondListview"
ItemTemplate="{StaticResource myClassTemplate}"
HorizontalAlignment="Left" VerticalAlignment="Top"
Grid.Row="1" Grid.Column="2" Grid.RowSpan="2"
ItemsSource="{Binding noList}"/>
<ListView x:Name="MyThirdListview"
ItemTemplate="{StaticResource myClassTemplate}"
HorizontalAlignment="Left" VerticalAlignment="Top"
Grid.Row="1" Grid.Column="3" Grid.RowSpan="2"
ItemsSource="{Binding emptyList}"/>
</Grid>
</Viewbox>
</Page>
And the MainPage
code looks like this:
public sealed partial class MainPage : Page
{
public ObservableCollection<MyClass> MyList { get; set; }
public FilteredObservableCollection<MyClass> yesList { get; set; }
public FilteredObservableCollection<MyClass> noList { get; set; }
public FilteredObservableCollection<MyClass> emptyList { get; set; }
public MainPage()
{
this.InitializeComponent();
MyList = new ObservableCollection<MyClass>();
yesList = new FilteredObservableCollection<MyClass>(MyList, item => item.Status == "Yes");
noList = new FilteredObservableCollection<MyClass>(MyList, item => item.Status == "No");
emptyList = new FilteredObservableCollection<MyClass>(MyList, item => item.Status == "" && item.Date.Date == DateTime.Now.Date);
DataContext = this;
}
private void AddYes_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
MyList.Add(new MyClass("Yes", DateTime.Now));
}
private void AddNo_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
MyList.Add(new MyClass("No", DateTime.Now));
}
private void AddEmpty_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
MyList.Add(new MyClass("", DateTime.Now));
}
private void Delete_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
MyList.Remove((MyClass)AllElementsList.SelectedItem);
}
private void Toggle_Click(object sender, RoutedEventArgs e)
{
MyClass oldItem = (MyClass)AllElementsList.SelectedItem,
newItem = new MyClass(oldItem.Status == "Yes" ? "No" : (oldItem.Status == "No" ? "Yes" : ""), oldItem.Date);
MyList[AllElementsList.SelectedIndex] = newItem;
}
}
With the above, changes are only ever made directly to the MyList
object. The state of this list is shown in the first ListView
object on the page (i.e. the left-most one). The three different buttons add items that are configured to be filtered differently. The items are added to the main list, but then the three filtered views update themselves automatically to reflect those changes, and you will see that in the other ListView
objects on the page (each one is below the button that adds an item that is displayed in that ListView
).
Hope that helps!
EDIT:
Some additional points that were in comments and which I think could improve the usefulness of this answer:
ListView
(i.e. the SelectedItem
in a given ListView
) can be done easily, as long as you still just delete from the actual MyList
collection; the object references in each list are your original objects, so even though they may be selected in one particular view, you can always delete that object from MyList
MyClass
objects but, without additional work in this example, the filtering won't change. I.e. if MyClass correctly implements binding features (either dependency properties or INotifyPropertyChanged), changes to an item's properties will be shown on-screen, but won't affect filtering. The above example requires an object to be replaced in MyList
with a different instance having the new values for it to be refiltered.