If you have databound a collection, then modify the collection and the ListView will update. This is only true for collection that implement
INotifyCollectionChanged
.
INotifyCollectionChanged
is used by the data binding system to communicate changes between the subscriber (UI) and the subject (data) - this is called the
Observer pattern[
^].
For example, the
ObservableCollection<>
class implements
INotifyCollectionChanged
, so the ListViews will change automatically (through data binding) however the
List<>
does not use
INotifyCollectionChanged
and the ListViews won't see the change.
The alternative, as the two ListViews are working with the same data, bind each ListView to a separate
CollectionViewSource
. Add a grouping to the data items if one does not already exist - this is to identify which list the item belongs to. Now set the filter of each
CollectionViewSource
to the gouping property and the ListViews will show their respective groups. Now, to move data items from one ListView to the other, simply change the grouping property on the data item and the ListViews will automatically update and the item appears to move from one ListView to the other. Lastly, for this to work, your data item needs to implement
INotifyPropertyChanged
and the collection of data items needs to implement
INotifyCollectionChanged
.
** Update: I decided to put together a quick example of two working versions:
- Moving objects from one collection to another
- Using CollectionViewSource filtering and changing a property on the selected item
First we need a model:
public class PersonModel : INotifyPropertyChanged
{
private string name;
public string Name
{
get { return name; }
set
{
name= value;
RaisePropertyChanged();
}
}
private string group = "A";
public string Group
{
get { return group; }
set {
group = value;
RaisePropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Now we can set up the collections,
CollectionViewSource
s and the test data:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
Models1A = new ObservableCollection<PersonModel>(Models.Where(x => GroupA(x)));
Models1B = new ObservableCollection<PersonModel>(Models.Where(x => GroupB(x)));
const string filterProperty = nameof(PersonModel.Group);
CsvA.Source = Models;
SetFiltering(CsvA, GroupA, filterProperty);
CsvB.Source = Models;
SetFiltering(CsvB, GroupB, filterProperty);
}
private void SetFiltering(CollectionViewSource Csv, Predicate<PersonModel> filter, string FilterProperty)
{
Csv.IsLiveFilteringRequested = true;
Csv.LiveFilteringProperties.Add(FilterProperty);
Csv.View.Filter = x => filter((PersonModel)x);
}
public ObservableCollection<PersonModel> Models1A { get; }
public ObservableCollection<PersonModel> Models1B { get; }
private Predicate<PersonModel> GroupA { get; } = new Predicate<PersonModel>(x => x.Group == "A");
private Predicate<PersonModel> GroupB { get; } = new Predicate<PersonModel>(x => x.Group == "B");
public ObservableCollection<PersonModel> Models { get; }
= new ObservableCollection<PersonModel>
{
new PersonModel { Name = "Person 1", Group = "B" },
new PersonModel { Name = "Person 2" },
new PersonModel { Name = "Person 3" },
new PersonModel { Name = "Person 4" }
};
public CollectionViewSource CsvA { get; } = new CollectionViewSource();
public CollectionViewSource CsvB { get; } = new CollectionViewSource();
private void OnManualMove(object sender, RoutedEventArgs e)
{
var model = (PersonModel)((Button)sender).DataContext;
if (Models1A.Contains(model))
{
Models1A.Remove(model);
Models1B.Add(model);
}
else
{
Models1B.Remove(model);
Models1A.Add(model);
}
}
private void OnFilteredMove(object sender, RoutedEventArgs e)
{
var model = (PersonModel)((Button)sender).DataContext;
model.Group = model.Group == "A" ? "B" : "A";
}
}
Lastly, the Window (UI):
<Window
x:Class="SwitchLists.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Code Project Q&A | Move item between Lists"
Height="350" Width="525" WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.Resources>
<Style TargetType="{x:Type ListBox}">
<Setter Property="Margin" Value="10"/>
</Style>
<DataTemplate x:Key="ManualItemTempate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}"/>
<Button Content="MOVE"
Padding="2" Margin="2" Grid.Column="1"
Click="OnManualMove"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="FilterItemTempate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}"/>
<Button Content="MOVE"
Padding="2" Margin="2" Grid.Column="1"
Click="OnFilteredMove"/>
</Grid>
</DataTemplate>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
</Grid.Resources>
<TextBlock Text="Between Lists"/>
<ListBox Grid.Row="1"
ItemsSource="{Binding Models1A}"
ItemTemplate="{StaticResource ManualItemTempate}">
</ListBox>
<ListBox Grid.Row="1" Grid.Column="1"
ItemsSource="{Binding Models1B}"
ItemTemplate="{StaticResource ManualItemTempate}">
</ListBox>
<TextBlock Text="CollectionViewSource" Grid.Row="2"/>
<ListBox Grid.Row="3"
ItemsSource="{Binding CsvA.View}"
ItemTemplate="{StaticResource FilterItemTempate}">
</ListBox>
<ListBox Grid.Row="3" Grid.Column="1"
ItemsSource="{Binding CsvB.View}"
ItemTemplate="{StaticResource FilterItemTempate}">
</ListBox>
</Grid>
</Window>
Now, when you click the "Move" button, the item will switch Lists:
- The top two lists are moving from one collection to the other
- The bottom two are Filtered views to the same collection with only the item Group property changing
UPDATE Below I've separated the C# code and Xaml to help clarify the two different solutions.
Yes, you are right, there is shared code. They share the main collection (Models) and they share the Predicate filter to seperate the data into two different groupings.
However there are two different solutions:
1. Manually (physically) moving (OnManualMove) an item (PersonModel) from one collection (Models1a) to the other collection (Models1b).
The C# code:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
Models1A = new ObservableCollection<PersonModel>(Models.Where(x => GroupA(x)));
Models1B = new ObservableCollection<PersonModel>(Models.Where(x => GroupB(x)));
}
public ObservableCollection<PersonModel> Models1A { get; }
public ObservableCollection<PersonModel> Models1B { get; }
private Predicate<PersonModel> GroupA { get; } = new Predicate<PersonModel>(x => x.Group == "A");
private Predicate<PersonModel> GroupB { get; } = new Predicate<PersonModel>(x => x.Group == "B");
public ObservableCollection<PersonModel> Models { get; }
= new ObservableCollection<PersonModel>
{
new PersonModel { Name = "Person 1", Group = "B" },
new PersonModel { Name = "Person 2" },
new PersonModel { Name = "Person 3" },
new PersonModel { Name = "Person 4" }
};
private void OnManualMove(object sender, RoutedEventArgs e)
{
var model = (PersonModel)((Button)sender).DataContext;
if (Models1A.Contains(model))
{
Models1A.Remove(model);
Models1B.Add(model);
}
else
{
Models1B.Remove(model);
Models1A.Add(model);
}
}
}
The Xaml used is as follows:
<Grid.Resources>
<DataTemplate x:Key="ManualItemTempate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}"/>
<Button Content="MOVE"
Padding="2" Margin="2" Grid.Column="1"
Click="OnManualMove"/>
</Grid>
</DataTemplate>
</Grid.Resources>
<ListBox Grid.Row="1"
ItemsSource="{Binding Models1A}"
ItemTemplate="{StaticResource ManualItemTempate}"/>
<ListBox Grid.Row="1" Grid.Column="1"
ItemsSource="{Binding Models1B}"
ItemTemplate="{StaticResource ManualItemTempate}"/>
2. Changing (OnFilteredMove) an item (PersonModel) grouping (PersonModel.Group) and the CollectionViewSource Filters automatically changes the collections from one collection (CsvA) to the other collection (CsvB).
The C# code:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
const string filterProperty = nameof(PersonModel.Group);
CsvA.Source = Models;
SetFiltering(CsvA, GroupA, filterProperty);
CsvB.Source = Models;
SetFiltering(CsvB, GroupB, filterProperty);
}
private void SetFiltering(CollectionViewSource Csv, Predicate<PersonModel> filter, string FilterProperty)
{
Csv.IsLiveFilteringRequested = true;
Csv.LiveFilteringProperties.Add(FilterProperty);
Csv.View.Filter = x => filter((PersonModel)x);
}
private Predicate<PersonModel> GroupA { get; } = new Predicate<PersonModel>(x => x.Group == "A");
private Predicate<PersonModel> GroupB { get; } = new Predicate<PersonModel>(x => x.Group == "B");
public ObservableCollection<PersonModel> Models { get; }
= new ObservableCollection<PersonModel>
{
new PersonModel { Name = "Person 1", Group = "B" },
new PersonModel { Name = "Person 2" },
new PersonModel { Name = "Person 3" },
new PersonModel { Name = "Person 4" }
};
public CollectionViewSource CsvA { get; } = new CollectionViewSource();
public CollectionViewSource CsvB { get; } = new CollectionViewSource();
private void OnFilteredMove(object sender, RoutedEventArgs e)
{
var model = (PersonModel)((Button)sender).DataContext;
model.Group = model.Group == "A" ? "B" : "A";
}
}
The Xaml used is as follows:
<Grid.Resources>
<DataTemplate x:Key="FilterItemTempate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}"/>
<Button Content="MOVE"
Padding="2" Margin="2" Grid.Column="1"
Click="OnFilteredMove"/>
</Grid>
</DataTemplate>
</Grid.Resources>
<ListBox Grid.Row="3"
ItemsSource="{Binding CsvA.View}"
ItemTemplate="{StaticResource FilterItemTempate}"/>
</ListBox>
<ListBox Grid.Row="3" Grid.Column="1"
ItemsSource="{Binding CsvB.View}"
ItemTemplate="{StaticResource FilterItemTempate}"/>
The Predicate Filter is just a static lambda method that is used to select data and needs to be called to be used. You don't need to use it for your project. You also don't need to have a single collection for your items however the data is common to both lists and acts as a central repository for your data that can be used elsewhere in your app - this could be replaced by your storage like a database.
I hope that this clarifies.