Reusable ItemsControl (e.g. ListViews) + MVVM - wpf

I'm building a MVVM application with WPF that uses a number of relatively complex list views. I've adopted the pattern that the collection that the list view binds to is a collection of View-Model objects, rather than a list of the underlying model objects - I've done this by databinding to an entirely separate column that is populated with code that looks a bit like this
var itemsSource = messages.Select(i => new MessageViewModel(i));
In this case the list view is displaying a list of Message objects to the user. This works OK, however is fairly clunky - expecially when dealing with collection change events.
Now I want to re-use this ListView elsewhere in my application to display a different list of messages to the user in a consistent way - the options that I can see are
Create a list view that derives from ListView and data binds to a collection of type MessageViewModel
Create a control that databinds to a collection of Message objects that either contains or derives from a list view data bound to some internally constructed collection of MessageViewModel
The first option requires that everyone who uses the control run the clunky code that builds and maintains the MessageViewModel collection, the second option encapsulates the maintenance of this view model collection however means that I need to re-implement any member of ListView which exposes the underlying items in the collection so that they can be converted back to the original Message type.
I have a number of similar list views that have similar re-usability problems.
Is there a better approach to dealing with WPF ItemsControl based views that allows for these views to be re-used in an MVVM application?

It looks to me that there are two things you want to reuse:
Exposing a collection of MessageViewModel, so you can bind this collection to the itemsSource of the ListView.
(Optionally), you have a style (or content presenter, or data template) on your specific list view, which you want to reuse. This part may also include code behind, triggers, etc.
You should not mixed the two.
#2 can be achieved with a style which you'll apply to the list view, or a data template. Personally, I like to define a dedicated class as a collection of MessageViewModel, and in your data template set the TargetType to be that class.
#1 is a class that implements Collection, INotifyCollecitonChanged, and INotifyPropertyChanged. Best way (and easiest) would be to merely a wrap it around ObservableCollection. In construction, do the Select method. Then have method for the book keeping.
Below some sample (working!) code. Note there is no code behind for the view. I put the two lists twice in the grid. The usage of ContentControl and DataTemplate is my style - there are dozens of other ways to do it.
======= Model.cs ====
using System;
namespace SO
{
class Message
{
public string from { get; set; }
public string to { get; set; }
public string subject { get; set; }
public DateTime received { get; set; }
}
}
======= ViewModel.cs ====
using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace SO
{
class MessageVM : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private Message m_model;
public MessageVM( Message model ) {
m_model = model;
}
private void raize( string prop ) {
PropertyChanged( this, new PropertyChangedEventArgs("prop") );
}
public string from {
get { return m_model.from; }
set { m_model.from = value; raize("from"); }
}
public string to {
get { return m_model.to; }
set { m_model.subject = value; raize("to") ); }
}
public string subject {
get { return m_model.subject; }
set { m_model.subject = value; raize("subject") ); }
}
public DateTime received {
get { return m_model.received; }
set { m_model.received = value; raize("recieved") ); }
}
}
class FolderVM : ObservableCollection<MessageVM>
{
public FolderVM( IEnumerable<Message> models )
:base( models.Select( msg => new MessageVM(msg) ) )
{
}
}
class SampleData
{
//static public FolderVM folder { get; set; }
static public FolderVM folder;
static SampleData( )
{
// create a sample model
List<Message> model = new List<Message>();
model.Add( new Message { from = "Bill", to = "Steve", subject = "Resusable Items Control", received = DateTime.Now.AddDays(-4) } );
model.Add( new Message { from = "Steve", to = "Bill", subject = "Resusable Items Control", received = DateTime.Now.AddDays( -3 ) } );
model.Add( new Message { from = "Bill", to = "Jeff", subject = "stack", received = DateTime.Now.AddDays( -2 ) } );
// initialize the view model
folder = new FolderVM( model );
}
}
}
======= MainWindow.xaml ====
<Window x:Class="Paf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:SO"
Title="MainWindow" Height="350" Width="525"
>
<Window.Resources>
<DataTemplate DataType="{x:Type src:FolderVM}">
<ListView ItemsSource="{Binding}">
<ListView.View>
<GridView>
<GridViewColumn Header="from" Width="80" DisplayMemberBinding="{Binding Path=from}" />
<GridViewColumn Header="to" Width="80" DisplayMemberBinding="{Binding Path=to}" />
<GridViewColumn Header="subject" Width="200" DisplayMemberBinding="{Binding Path=subject}" />
<GridViewColumn Header="received" Width="160" DisplayMemberBinding="{Binding Path=received}" />
</GridView>
</ListView.View>
</ListView>
</DataTemplate>
</Window.Resources>
<StackPanel>
<ContentControl Content="{Binding Source={x:Static src:SampleData.folder}}" />
<ContentControl Content="{Binding Source={x:Static src:SampleData.folder}}" />
</StackPanel>
</Window>

Related

How to programmatically add multiple strings to one line in a multiple column ListView

So I'm new to WPF as I'm more familiar with WinForms, But for the sake of drawing performance and good looking UI I switched to WPF, I have no experience in XAML but I'm working my things out.
I have a ListView which works as a playlist for my Media Player App. Adding Multiple Data in one line of multiple columns wasn't a problem in WinForms, I just had to add a ListViewItem and fill it's SubItems , but in WPF it's a problem, the ListViewItem doesn't have SubItems property nor the ListView, I tried multiple questions from Stack Overflow and other website which didn't help me , and it was all about DisplayMemberBinding but I still can't / don't know how to reference it in my code.
XAML for ListView:
<ListView x:Name="Playlist_Main" Margin="0" ItemsSource="{Binding SourceCollection}">
<ListView.View>
<GridView>
<GridViewColumn Header="#" DisplayMemberBinding="{Binding Num}"/>
<GridViewColumn Header="Title" DisplayMemberBinding="{Binding Title}"/>
<GridViewColumn Header="Artist" DisplayMemberBinding="{Binding Artist}"/>
<GridViewColumn Header="Album" DisplayMemberBinding="{Binding Album}"/>
<GridViewColumn Header="Year" DisplayMemberBinding="{Binding Year}"/>
<GridViewColumn Header="Track Num" DisplayMemberBinding="{Binding Track}"/>
</GridView>
</ListView.View>
</ListView>
Main Code
Playlist_Main.Items.Add(New ListViewItem({Playlist_Main.Items.Count + 1, Info(0), Info(1), Info(2), Info(3), Info(4)}))
The bindings in your ListView / GridView require an item type that exposes properties for Num, Title, and so on. You have to expose a collection of that item type and assign or bind it to the ItemsSource property.
Create a model for an item in your playlist. The following example implements the INotifyPropertyChanged interface that enables bindings to update through the PropertyChanged event if a property value changes. If your properties are read-only or you do not need to update values at runtime, you do not have to implement it.
public class PlaylistItem : INotifyPropertyChanged
{
private int _num;
private string _title;
private string _artist;
private string _album;
private int _year;
private int _track;
public PlaylistItem(int num, string title, string artist, string album, int year, int track)
{
Num = num;
Title = title;
Artist = artist;
Album = album;
Year = year;
Track = track;
}
public int Num
{
get => _num;
set
{
if (_num == value)
return;
_num = value;
OnPropertyChanged();
}
}
public string Title
{
get => _title;
set
{
if (_title == value)
return;
_title = value;
OnPropertyChanged();
}
}
public string Artist
{
get => _artist;
set
{
if (_artist == value)
return;
_artist = value;
OnPropertyChanged();
}
}
public string Album
{
get => _album;
set
{
if (_album == value)
return;
_album = value;
OnPropertyChanged();
}
}
public int Year
{
get => _year;
set
{
if (_year == value)
return;
_year = value;
OnPropertyChanged();
}
}
public int Track
{
get => _track;
set
{
if (_track == value)
return;
_track = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
In a code-behind approach, you could create a collection of PlaylistItems. Use an ObservableCollection<T> that implements the INotifyCollectionChanged interface, if you want to reflect changes to the collection in the user interface, e.g. adding or removing items. The ObservableCollection<T> does that automatically through the CollectionChanged event. If your list is not modified at runtime, you can use any other collection.
var playlistItems = new ObservableCollection<PlaylistItem>();
playlistItems.Add(new PlaylistItem(1, "Enter Sandman", "Metallica", "Metallica", 1991, 1));
// ...add other playlist items.
You could assign this collection directly to the ListView e.g. in the constructor.
public MainWindow()
{
var playlistItems = // ...create the items collection or load them from somewhere.
Playlist_Main.ItemsSource = playlistItems;
}
A different approach is creating a public property in your code-behind. I assume it is the MainWindow.
public partial class MainWindow
{
public MainWindow()
{
PlaylistItems = // ...create the items collection or load them from somewhere.
}
public ObservableCollection<PlaylistItem> PlaylistItems { get; }
}
You would bind this collection in XAML using a RelativeSource binding to the window.
<ListView ItemsSource="{Binding Tracks, RelativeSource={RelativeSource AncestorType={x:Type local:MainWindow}}}">
As you can see, there are multiple approaches, even more. The best, in my opinion, would be to use the MVVM pattern. For that, you would create a view model for your main window that contains the collection.
public class MainViewModel : INotifyPropertyChanged
{
public MainViewModel()
{
PlaylistItems = // ...create the items collection or load them from somewhere.
}
public ObservableCollection<PlaylistItem> PlaylistItems { get; }
// ...other properties and methods.
}
Next you would set an instance of this view model as DataContext of your window.
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
Then you can bind to the collection like this. The data context (MainViewModel) is inherited automatically.
<ListView ItemsSource="{Binding PlaylistItems}">
This pattern helps you to separate the user interface from the data and business logic. As you can see, there are no references from the view model to the view, only properties that expose data that can be bound.
Further resources for learning:
Data binding overview in WPF
How to: Create and Bind to an ObservableCollection

Populate ListView using custom DataTemplate and dynamic data

I have a ListView that I would like to populate in XAML. I'm using a custom DataTemplate to make each ListViewItem added contain a Label and a TextBlock.
The problem is I need to dynamically populate the text of the TextBlock of each ListViewItem with data from a settings property, and I don't know how to create this binding.
Right now I am populating the ListView with an XmlDataProvider, but I can't (or at least can't figure out how to) bind values to the xml data. (I'm not stuck using this method of data population, it's just what I was originally doing when I ran into this problem.)
Basically I need something as follows:
The user enters some data into a text box. That data is saved to user settings. When that happens, the corresponding TextBlock of the ListViewItem in the ListView is updated with the user setting data.
Normally I would bind a TextBlock's text to a user setting as follows:
Text="{Binding Source={x:Static properties:Settings.Default},Path=User_Data_1}"
But how do I do this when the text of the TextBlock is defined in the DataTemplate?
My DataTemplate and XmlDataProvider:
<DataTemplate x:Key="listViewTemplate">
<StackPanel Orientation="Vertical">
<Label x:Name="lblName" Content="{Binding XPath=name}"/>
<TextBlock x:Name="tbValue" Text="{Binding XPath=value}"/>
</StackPanel>
</DataTemplate>
<XmlDataProvider x:Key="PagesData" XPath="Pages">
<x:XData>
<Pages xmlns="">
<page id="page01">
<name>Text file:</name>
<value></value>
<source>Pages/Page_CreateFiles1.xaml</source>
</page>
<page id="page02">
<name>Xml file:</name>
<value></value>
<source>Pages/Page_CreateFiles2.xaml</source>
</page>
<page id="page03">
<name>Memory object database:</name>
<value></value>
<source>Pages/Page_CreateFiles3.xaml</source>
</page>
<page id="page04">
<name>Output database:</name>
<value></value>
<source>Pages/Page_CreateDB.xaml</source>
</page>
</Pages>
</x:XData>
</XmlDataProvider>
My ListView
<ListView x:Name="lvNavigation"
ItemTemplate="{DynamicResource listViewTemplate}"
ItemsSource="{Binding Source={StaticResource PagesData}, XPath=page}"/>
Create a view model with a collection of items
public class Item
{
public string Name { get; set; }
public string Value { get; set; }
}
public class ViewModel
{
public ObservableCollection<Item> Items { get; } = new ObservableCollection<Item>();
}
and set the MainWindow's DataContext to an instance of the view model class
public MainWindow()
{
InitializeComponent();
var vm = new ViewModel();
DataContext = vm;
vm.Items.Add(new Item { Name = "Name 1", Value = "Value 1" });
vm.Items.Add(new Item { Name = "Name 2", Value = "Value 2" });
}
Bind to it like this:
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Value}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Clemmens answer is right, but I just wanted to put it out there that I basically did his approach with a slight modification. I used events to trigger the data change in the listview.
I think it is my own fault though, because I didn't explain my problem well enough. First of all, I wanted to do everything from xaml and I don't think that was possible. Second, I failed to mention that I was using pages in a frame, where the data was coming from the pages and the listview was in my main window that contained the frame. So that's why I ended up using events to communicate between the page and the main window.
So in my main window I've defined my observable collection:
ObservableCollection<NavItem> NavItems = new ObservableCollection<NavItem>();
public MainWindow()
{
InitializeComponent();
NavItems.Add(new NavItem { Name = "Text file:", Value = "", Source = "Pages/Page_CreateFiles.xaml" });
NavItems.Add(new NavItem { Name = "Xml file:", Value = "", Source = "Pages/Page_CreateFiles.xaml" });
NavItems.Add(new NavItem { Name = "Memory object db:", Value = "", Source = "Pages/Page_CreateFiles.xaml" });
NavItems.Add(new NavItem { Name = "Output database:", Value = "", Source = "Pages/Page_CreateDB.xaml" });
lvNavigation.ItemsSource = NavItems;
...
}
"NavItem" is a class that is subscribed to INotifyPropertyChanged. Posting that code will just be a lot, so check out how to do that here: INotifyPropertyChanged
Then in each page I set up an event that I call with the data to send:
public static event EventHandler<NavUpdateMessage> UpdateMessage;
private void OnUpdateMessage(int id, string message)
{
NavUpdateMessage navUpdateMessage = new NavUpdateMessage();
navUpdateMessage.Id = id;
navUpdateMessage.Message = message;
var e = UpdateMessage;
if (e != null)
e(this, navUpdateMessage);
}
With the main window subscribed to that event:
public MainWindow()
{
...
Pages.Page_CreateFiles.UpdateMessage += Pages_UpdateMessage;
Pages.Page_CreateDB.UpdateMessage += Pages_UpdateMessage;
}
private void Pages_UpdateMessage(object sender, NavUpdateMessage e)
{
Dispatcher.BeginInvoke(new Action(() =>
{
NavItems[e.Id].Value = e.Message;
}));
}
I'm sure there's a better, more simple approach to this, but this is what I could figure out. And even though I'm sure no one will see this because this question definitely did not get any traction, please feel free to suggest a better solution so at least I can learn.

How to Bind a Listview to an observallable collection in a WCF service?

I'm Hosting a WCF service in a WPF application. The WCF service has an Observablecollection which I'm trying to bind to the listview in WPF but i keep getting nullreference exception since the list is empty at the time of intialisation. It only gets elements added when the client connects to the Service. My XAML is below
<ListView HorizontalAlignment="Left" Height="341" Margin="42,35,0,0" VerticalAlignment="Top" Width="621" x:Name="listView">
<ListView.View>
<GridView>
<GridViewColumn Header=" Time" Width="100" DisplayMemberBinding="{Binding Time}"/>
<GridViewColumn Header="Blade Name" Width="200" DisplayMemberBinding="{Binding Name}"/>
</GridView>
</ListView.View>
</ListView>
The Simplified version of the code behind file is below
using System.ServiceModel.Description;
using SomeNameSpace.WCFService;
namespace SomeNameSpace.WPFHost
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
ServiceHost host;
MyClass SingleTonService;
MyClass srvc;
public MainWindow()
{
try
{
SingleTonService = new MyClass();
host = new ServiceHost(SingleTonService, baseAddresses);
host.Open();
var obj = host.SingletonInstance;
srvc = (SomeNameSpace.WCFService.MyClass)obj;
//WPF Section
*** Getting null reference exception here since the list is emtpy ***
listView.ItemsSource = srvc.__clientList;
//Refresh();
}
catch(NullReferenceException e)
{
}
catch (Exception e)
{
host.Abort();
}
}
The simplified WCFService class is as below
using System.Collections.ObjectModel;
namespace SomeNameSpace.WCFService
{
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single,
ConcurrencyMode = ConcurrencyMode.Multiple,
UseSynchronizationContext = false)]
public class MyClass : IMyClass
{
public ObservableCollection<Client> __clientList = new ObservableCollection<Client>();
public ObservableCollection<Client> clientList
{ get { return __clientList; } }
public ICallback Callback
{
get
{
return OperationContext.Current.GetCallbackChannel<ICallback>();
}
}
public bool Connect(Client client)
{
if (!clients.ContainsValue(CurrentCallback) && !SearchByName(client.Name))
{
__clientList.Add(client);
}
return true;
}
return false;
}
}
Client Class
[DataContract]
public class Client
{
private string _name;
private DateTime _time;
[DataMember]
public string Name
{
get { return _name; }
set { _name = value; }
}
[DataMember]
public DateTime Time
{
get { return _time; }
set { _time = value; }
}
}
Also, If i bind the observablecollection to the listview after the connect method is called, everything works since the list isn't empty but i can't do this at run time since i tested this using buttonclick event and setting listview.itemsource
in the event after the connect method is called. There will not be any buttons in the final code.
The disconnect is that an observable collection only sends out events when the collection changes due to adds or removals and not when itself is created.
What you really want to do is bind the listbox to an ordinary list (or keep it an observable collection) on a class (probably a view model) which adheres to INotifyPropertyChanged and that will notify the XAML that there is new data to show. The binding is best done in XAML.
How
So what happens is that when data comes in from the wire, you load up a local (on the stack) list until all the items are gathered. Then when filled assign the local list reference to the List on your view model. At that point the event of Property changed will fire and the data will be shown to the user.
I provide an example of MVVM which can be used in the above situation. Xaml: ViewModel Main Page Instantiation and Loading Strategy for Easier Binding
i replaced the line below
listView.ItemsSource = srvc.__clientList;
with
srvc.__clientList.CollectionChanged += OnCollectionChanged;
and in the event handler i used the code below to manually update the list
this.Dispatcher.Invoke((Action)(() =>
{
listView.Items.Clear();
//update listview here
}));

Link ListView Items to Individually Different Lists

I've defined a ListView where each item is displayed with a read-only (i.e. selection-only, no TextBox) ComboBox (as per the ItemTemplate).
There is a given list of possible items that can be chosen from each of these combo boxes. The caveat, however, is that no item may be selected in two combo boxes at a time. In order to ensure this, once an item has been selected in one of the combo boxes, it must be removed from all other combo boxes (obviously except for the one where it is selected), and once it gets deselected, it must be added to all other combo boxes again.
Oh, and one more thing: The order of the visible items must not change compared to the complete list.
My question is: How can I achieve this behavior?
I have tried the three possible solutions that came to my mind:
I have written a new helper control class that takes the complete list of existing items and a list of excluded (used) items via bindings to the outside world, as well as a property for a selected item. I could include that control class in the ItemTemplate; the combo box within that template would then bind its ItemsSource property to an ItemsProvider property of the helper class that was supposed to tie together the list of existing items, the list of excluded items and the selected item and return a single enumeration of items for that particular combo box.
However, I somehow got lost in all the update notifications about list changes; I fear I'd have to individually react to all combinations of NotifyCollectionChangedAction of the two input lists, and with the prospect of having a dozen update methods, I thought this cannot be the right way.
I changed the list of existing items to a list that stores a boolean along with each item, so I could flag each item as hidden or not. This would relieve me of the necessity to have a list of excluded items while maintaining the order of items, thereby reducing the aforementioned complexity of combined change notifications.
Unfortunately, as the list itself doesn't change with that solution, I don't know how to have the dependency property infrastructure notify my ItemsSource property in my helper class.
I don't have to use the WPF with bindings way; I can do code-behind here, too. So, I tried iterating over all ListViewItems and retrieving the combo box for each of them to manually refresh the item lists. However, I couldn't find a good time to access the ListViewItems after their item template had been loaded. There seems to be no event for that situation, and ListView.ItemContainerGenerator is read-only, so even if ItemContainerGenerator were not a sealed class, I couldn't assign my own specialized ItemContainerGenerator that would create custom list view items where I could override OnApplyTemplate.
I would probably bind all ComboBoxes to different CollectionViews over the source collection which filter out the selected items of the other ComboBoxes. You also need to Refresh the views if the selections of the combo-boxes change.
If you bind the Lists to different Lists in the ViewModel, and bind the selected item to trigger a method that changes those Lists, then you could get your result. Similar to the below.
Xaml of MainWindow.xaml:
<Window x:Class="ComboBox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" >
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180" />
<ColumnDefinition Width="180" />
<ColumnDefinition Width="180" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="26" />
</Grid.RowDefinitions>
<ComboBox Name="cboOne" Grid.Column="0" Grid.Row="0" ItemsSource="{Binding CboOneList, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectedValue="{Binding CboOneValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></ComboBox>
<ComboBox Name="cboTwo" Grid.Column="1" Grid.Row="0" ItemsSource="{Binding CboTwoList, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectedValue="{Binding CboTwoValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></ComboBox>
<ComboBox Name="cboThree" Grid.Column="2" Grid.Row="0" ItemsSource="{Binding CboThreeList, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectedValue="{Binding CboThreeValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></ComboBox>
</Grid>
</Window>
Code-behind for MainWindow.xaml:
using System.Windows;
using System.Windows.Controls;
namespace ComboBox {
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
this.DataContext = new ComboBoxViewModel();
}
private void cboOne_SelectionChanged(object sender, SelectionChangedEventArgs e) {
}
}
}
ViewModel:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
namespace ComboBox {
class ComboBoxViewModel : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
List<string> master = new List<string>() { "A", "B", "C", "D", "E", "F" };
#region C'tor
public ComboBoxViewModel() {
RetrieveLists();
}
#endregion
#region Methods
protected void OnPropertyChanged(String propertyName) {
PropertyChangedEventHandler handler = this.PropertyChanged;
if(null != handler) {
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public void RetrieveLists() {
List<string> tempOne = (from a in master
where !a.Equals(CboTwoValue) && !a.Equals(CboThreeValue)
select a).ToList();
CboOneList = tempOne;
List<string> tempTwo = (from a in master
where !a.Equals(CboOneValue) && !a.Equals(CboThreeValue)
select a).ToList();
CboTwoList = tempTwo;
List<string> tempThree = (from a in master
where !a.Equals(CboTwoValue) && !a.Equals(CboOneValue)
select a).ToList();
CboThreeList = tempThree;
}
#endregion
#region Properties
private string cboOneValue = string.Empty;
public string CboOneValue {
get {
return cboOneValue;
}
set {
if(!value.Equals(cboOneValue)) {
cboOneValue = value;
RetrieveLists();
OnPropertyChanged("CboOneValue");
}
}
}
private string cboTwoValue = string.Empty;
public string CboTwoValue {
get {
return cboTwoValue;
}
set {
if(!value.Equals(cboTwoValue)) {
cboTwoValue = value;
RetrieveLists();
OnPropertyChanged("CboTwoValue");
}
}
}
private string cboThreeValue = string.Empty;
public string CboThreeValue {
get {
return cboThreeValue;
}
set {
if(!value.Equals(cboThreeValue)) {
cboThreeValue = value;
RetrieveLists();
OnPropertyChanged("CboThreeValue");
}
}
}
private List<string> cboOneList = new List<string>();
public List<string> CboOneList {
get {
return cboOneList;
}
set {
cboOneList = value;
OnPropertyChanged("CboOneList");
}
}
private List<string> cboTwoList = new List<string>();
public List<string> CboTwoList {
get {
return cboTwoList;
}
set {
cboTwoList = value;
OnPropertyChanged("CboTwoList");
}
}
private List<string> cboThreeList = new List<string>();
public List<string> CboThreeList {
get {
return cboThreeList;
}
set {
cboThreeList = value;
OnPropertyChanged("CboThreeList");
}
}
#endregion
}
}

Trigger Filter on CollectionViewSource

I am working on a WPF desktop application using the MVVM pattern.
I am trying to filter some items out of a ListView based on the text typed in a TextBox. I want the ListView items to be filtered as I change the text.
I want to know how to trigger the filter when the filter text changes.
The ListView binds to a CollectionViewSource, which binds to the ObservableCollection on my ViewModel. The TextBox for the filter text binds to a string on the ViewModel, with UpdateSourceTrigger=PropertyChanged, as it should be.
<CollectionViewSource x:Key="ProjectsCollection"
Source="{Binding Path=AllProjects}"
Filter="CollectionViewSource_Filter" />
<TextBox Text="{Binding Path=FilterText, UpdateSourceTrigger=PropertyChanged}" />
<ListView DataContext="{StaticResource ProjectsCollection}"
ItemsSource="{Binding}" />
The Filter="CollectionViewSource_Filter" links to an event handler in the code behind, which simply calls a filter method on the ViewModel.
Filtering is done when the value of FilterText changes - the setter for the FilterText property calls a FilterList method that iterates over the ObservableCollection in my ViewModel and sets a boolean FilteredOut property on each item ViewModel.
I know the FilteredOut property is updated when the filter text changes, but the List does not refresh. The CollectionViewSource filter event is only fired when I reload the UserControl by switching away from it and back again.
I've tried calling OnPropertyChanged("AllProjects") after updating the filter info, but it did not solve my problem.
("AllProjects" is the ObservableCollection property on my ViewModel to which the CollectionViewSource binds.)
How can I get the CollectionViewSource to refilter itself when the value of the FilterText TextBox changes?
Many thanks
Don't create a CollectionViewSource in your view. Instead, create a property of type ICollectionView in your view model and bind ListView.ItemsSource to it.
Once you've done this, you can put logic in the FilterText property's setter that calls Refresh() on the ICollectionView whenever the user changes it.
You'll find that this also simplifies the problem of sorting: you can build the sorting logic into the view model and then expose commands that the view can use.
EDIT
Here's a pretty straightforward demo of dynamic sorting and filtering of a collection view using MVVM. This demo doesn't implement FilterText, but once you understand how it all works, you shouldn't have any difficulty implementing a FilterText property and a predicate that uses that property instead of the hard-coded filter that it's using now.
(Note also that the view model classes here don't implement property-change notification. That's just to keep the code simple: as nothing in this demo actually changes property values, it doesn't need property-change notification.)
First a class for your items:
public class ItemViewModel
{
public string Name { get; set; }
public int Age { get; set; }
}
Now, a view model for the application. There are three things going on here: first, it creates and populates its own ICollectionView; second, it exposes an ApplicationCommand (see below) that the view will use to execute sorting and filtering commands, and finally, it implements an Execute method that sorts or filters the view:
public class ApplicationViewModel
{
public ApplicationViewModel()
{
Items.Add(new ItemViewModel { Name = "John", Age = 18} );
Items.Add(new ItemViewModel { Name = "Mary", Age = 30} );
Items.Add(new ItemViewModel { Name = "Richard", Age = 28 } );
Items.Add(new ItemViewModel { Name = "Elizabeth", Age = 45 });
Items.Add(new ItemViewModel { Name = "Patrick", Age = 6 });
Items.Add(new ItemViewModel { Name = "Philip", Age = 11 });
ItemsView = CollectionViewSource.GetDefaultView(Items);
}
public ApplicationCommand ApplicationCommand
{
get { return new ApplicationCommand(this); }
}
private ObservableCollection<ItemViewModel> Items =
new ObservableCollection<ItemViewModel>();
public ICollectionView ItemsView { get; set; }
public void ExecuteCommand(string command)
{
ListCollectionView list = (ListCollectionView) ItemsView;
switch (command)
{
case "SortByName":
list.CustomSort = new ItemSorter("Name") ;
return;
case "SortByAge":
list.CustomSort = new ItemSorter("Age");
return;
case "ApplyFilter":
list.Filter = new Predicate<object>(x =>
((ItemViewModel)x).Age > 21);
return;
case "RemoveFilter":
list.Filter = null;
return;
default:
return;
}
}
}
Sorting kind of sucks; you need to implement an IComparer:
public class ItemSorter : IComparer
{
private string PropertyName { get; set; }
public ItemSorter(string propertyName)
{
PropertyName = propertyName;
}
public int Compare(object x, object y)
{
ItemViewModel ix = (ItemViewModel) x;
ItemViewModel iy = (ItemViewModel) y;
switch(PropertyName)
{
case "Name":
return string.Compare(ix.Name, iy.Name);
case "Age":
if (ix.Age > iy.Age) return 1;
if (iy.Age > ix.Age) return -1;
return 0;
default:
throw new InvalidOperationException("Cannot sort by " +
PropertyName);
}
}
}
To trigger the Execute method in the view model, this uses an ApplicationCommand class, which is a simple implementation of ICommand that routes the CommandParameter on buttons in the view to the view model's Execute method. I implemented it this way because I didn't want to create a bunch of RelayCommand properties in the application view model, and I wanted to keep all the sorting/filtering in one method so that it was easy to see how it's done.
public class ApplicationCommand : ICommand
{
private ApplicationViewModel _ApplicationViewModel;
public ApplicationCommand(ApplicationViewModel avm)
{
_ApplicationViewModel = avm;
}
public void Execute(object parameter)
{
_ApplicationViewModel.ExecuteCommand(parameter.ToString());
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
}
Finally, here's the MainWindow for the application:
<Window x:Class="CollectionViewDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:CollectionViewDemo="clr-namespace:CollectionViewDemo"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<CollectionViewDemo:ApplicationViewModel />
</Window.DataContext>
<DockPanel>
<ListView ItemsSource="{Binding ItemsView}">
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding Name}"
Header="Name" />
<GridViewColumn DisplayMemberBinding="{Binding Age}"
Header="Age"/>
</GridView>
</ListView.View>
</ListView>
<StackPanel DockPanel.Dock="Right">
<Button Command="{Binding ApplicationCommand}"
CommandParameter="SortByName">Sort by name</Button>
<Button Command="{Binding ApplicationCommand}"
CommandParameter="SortByAge">Sort by age</Button>
<Button Command="{Binding ApplicationCommand}"
CommandParameter="ApplyFilter">Apply filter</Button>
<Button Command="{Binding ApplicationCommand}"
CommandParameter="RemoveFilter">Remove filter</Button>
</StackPanel>
</DockPanel>
</Window>
Nowadays, you often don't need to explicitly trigger refreshes. CollectionViewSource implements ICollectionViewLiveShaping which updates automatically if IsLiveFilteringRequested is true, based upon the fields in its LiveFilteringProperties collection.
An example in XAML:
<CollectionViewSource
Source="{Binding Items}"
Filter="FilterPredicateFunction"
IsLiveFilteringRequested="True">
<CollectionViewSource.LiveFilteringProperties>
<system:String>FilteredProperty1</system:String>
<system:String>FilteredProperty2</system:String>
</CollectionViewSource.LiveFilteringProperties>
</CollectionViewSource>
CollectionViewSource.View.Refresh();
CollectionViewSource.Filter is reevaluated in this way!
Perhaps you've simplified your View in your question, but as written, you don't really need a CollectionViewSource - you can bind to a filtered list directly in your ViewModel (mItemsToFilter is the collection that is being filtered, probably "AllProjects" in your example):
public ReadOnlyObservableCollection<ItemsToFilter> AllFilteredItems
{
get
{
if (String.IsNullOrEmpty(mFilterText))
return new ReadOnlyObservableCollection<ItemsToFilter>(mItemsToFilter);
var filtered = mItemsToFilter.Where(item => item.Text.Contains(mFilterText));
return new ReadOnlyObservableCollection<ItemsToFilter>(
new ObservableCollection<ItemsToFilter>(filtered));
}
}
public string FilterText
{
get { return mFilterText; }
set
{
mFilterText = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("FilterText"));
PropertyChanged(this, new PropertyChangedEventArgs("AllFilteredItems"));
}
}
}
Your View would then simply be:
<TextBox Text="{Binding Path=FilterText,UpdateSourceTrigger=PropertyChanged}" />
<ListView ItemsSource="{Binding AllFilteredItems}" />
Some quick notes:
This eliminates the event in the code behind
It also eliminates the "FilterOut" property, which is an artificial, GUI-only property and thus really breaks MVVM. Unless you plan to serialize this, I wouldn't want it in my ViewModel, and certainly not in my Model.
In my example, I use a "Filter In" rather than a "Filter Out". It seems more logical to me (in most cases) that the filter I am applying are things I do want to see. If you really want to filter things out, just negate the Contains clause (i.e. item => ! Item.Text.Contains(...)).
You may have a more centralized way of doing your Sets in your ViewModel. The important thing to remember is that when you change the FilterText, you also need to notify your AllFilteredItems collection. I did it inline here, but you could also handle the PropertyChanged event and call PropertyChanged when the e.PropertyName is FilterText.
Please let me know if you need any clarifications.
If I understood well what you are asking:
In the set part of your FilterText property just call Refresh() to your CollectionView.
I just discovered a much more elegant solution to this issue. Instead of creating a ICollectionView in your ViewModel (as the accepted answer suggests) and setting your binding to
ItemsSource={Binding Path=YourCollectionViewSourceProperty}
The better way is to create a CollectionViewSource property in your ViewModel. Then bind your ItemsSource as follows
ItemsSource={Binding Path=YourCollectionViewSourceProperty.View}
Notice the addition of .View This way the ItemsSource binding is still notified whenever there is a change to the CollectionViewSource and you never have to manually call Refresh() on the ICollectionView
Note: I can't determine why this is the case. If you bind directly to a CollectionViewSource property the binding fails. However, if you define a CollectionViewSource in your Resources element of a XAML file and you bind directly to the resource key, the binding works fine. The only thing I can guess is that when you do it completely in XAML it knows you really want to bind to the CollectionViewSource.View value and binds it for you acourdingly behind the scenes (how helpful! :/) .

Resources