I am using a dependency property GroupDescription to group items within a WPF list view according to a property of my list views items source.
My problem is that the grouping is only updated if the GroupDescription value is changed and not once the bound property of an item within the list views source is changed.
In this example below the GroupDescription is set to city which results in a group description of "City: Hamburg". But when an items city property is changed the grouping within the list view is not updated which means that there will be an item with City "Berlin" within the group "City: Hamburg".
The grouping is only updated once the GroupDescription is updated. I tried to find a work around using the PersonPropertyChanged method which changes the GroupDescription to PersonId and immediately back to City. However this work around results in the list view always jumping back to the top in case the scroll position was for example in the middle or at the end of the listview. This can be really annoying when working with a list view with hundres of entries with changing properties.
Is there a way I can "update" the grouping after an items property changed without the list view jumping back to the top?
Thank you in advance for any help! Thomas
GroupingListView.cs
using System.Windows.Controls;
using System.Windows;
using System.Windows.Data;
namespace WpfApplication
{
/// <summary>
/// Enhanced list view based on WPF ListView with dependency properties for GroupDescriptions
/// </summary>
public class GroupingListView : ListView
{
/// <summary>
/// Dependency property for group descriptions
/// </summary>
public string GroupDescription
{
get { return (string)GetValue(GroupDescriptionProperty); }
set { SetValue(GroupDescriptionProperty, value); }
}
/// <summary>
/// Using a DependencyProperty as the backing store for GroupDescription. This enables animation, styling, binding, etc...
/// </summary>
public static readonly DependencyProperty GroupDescriptionProperty =
DependencyProperty.Register("GroupDescription",
typeof(string),
typeof(GroupingListView),
new UIPropertyMetadata(string.Empty, GroupDescriptionChanged));
private static void GroupDescriptionChanged(DependencyObject source, DependencyPropertyChangedEventArgs args)
{
var control = source as GroupingListView;
// Stop if source is not of type DetailedListView
if (control == null) return;
// Stop if myView is not available, myView can not group, groupdescription missing\
// or the argument is empty
var myView = (CollectionView)CollectionViewSource.GetDefaultView(control.ItemsSource);
if (myView == null || !myView.CanGroup || (string) args.NewValue == string.Empty ||
myView.GroupDescriptions == null)
{
return;
}
myView.GroupDescriptions.Clear();
// If a group description already
if(myView.GroupDescriptions.Count > 0)
{
var prop = myView.GroupDescriptions[0] as PropertyGroupDescription;
if(prop != null)
{
if(!prop.PropertyName.Equals((string)args.NewValue))
{
myView.GroupDescriptions.Clear();
}
}
}
// Stop if at this point a group description still exists. This means the newValue is
// equal to the old value and nothing needs to be changed
if (myView.GroupDescriptions.Count != 0) return;
// If this code is reached newValue is different than the current groupDescription value
// therefore the newValue has to be added as PropertyGroupDescription
var groupDescription = new PropertyGroupDescription((string)args.NewValue);
// Clear and add the description only if it's not already existing
myView.GroupDescriptions.Add(groupDescription);
}
}
}
MainWindow.xaml
<Window x:Class="WpfApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:WpfApplication="clr-namespace:WpfApplication"
Title="MainWindow" Height="300" Width="300">
<StackPanel>
<Button Content="Change" Click="btnChangeCity" Height="22"/><Button Content="Change back" Click="btnChangeCityBack" Height="22"/>
<WpfApplication:GroupingListView ItemsSource="{Binding Persons}" Height="200"
GroupDescription="{Binding GroupDescription}" x:Name="GroupingListView">
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<Grid HorizontalAlignment="Stretch">
<Border HorizontalAlignment="Stretch" BorderBrush="Transparent" BorderThickness="1" CornerRadius="3">
<Border HorizontalAlignment="Stretch" BorderBrush="LightGray" BorderThickness="0,0,0,1" CornerRadius="0">
<StackPanel Orientation="Horizontal">
<TextBlock Foreground="LightGray" Text="{Binding GroupDescription, ElementName=GroupingListView}"/>
<TextBlock Foreground="LightGray" Text=" : "/>
<TextBlock Foreground="LightGray" Text="{Binding Name}" HorizontalAlignment="Stretch"/>
</StackPanel>
</Border>
</Border>
</Grid>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>
<ListView.View>
<GridView>
<GridViewColumn Header="PersonId" Width="100">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock TextTrimming="CharacterEllipsis" Text="{Binding PersonId, Mode=Default}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="City" Width="100">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock TextTrimming="CharacterEllipsis" Text="{Binding City, Mode=Default}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</WpfApplication:GroupingListView>
</StackPanel>
</Window>
MainWindow.xaml.cs
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
namespace WpfApplication
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : INotifyPropertyChanged
{
public class Person : INotifyPropertyChanged
{
public Person(string personId, string city)
{
PersonId = personId;
City = city;
}
private string _personId;
private string _city;
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler pc = PropertyChanged;
if (pc != null)
pc(this, new PropertyChangedEventArgs(name));
}
public string PersonId
{
get { return _personId; }
set { _personId = value; OnPropertyChanged("PersonId"); }
}
public string City
{
get { return _city; }
set { _city = value; OnPropertyChanged("City"); }
}
}
public ObservableCollection<Person> Persons { get; set; }
public string GroupDescription
{
get { return _groupDescription; }
set { _groupDescription = value; OnPropertyChanged("GroupDescription"); }
}
private string _groupDescription;
public MainWindow()
{
InitializeComponent();
DataContext = this;
GroupDescription = "City";
Persons = new ObservableCollection<Person>();
Persons.CollectionChanged += PersonsCollectionChanged;
Persons.Add(new Person("1", "Hamburg"));
Persons.Add(new Person("2", "Hamburg"));
Persons.Add(new Person("3", "Hamburg"));
Persons.Add(new Person("4", "Hamburg"));
Persons.Add(new Person("5", "Hamburg"));
Persons.Add(new Person("6", "Hamburg"));
Persons.Add(new Person("7", "Hamburg"));
Persons.Add(new Person("8", "Hamburg"));
Persons.Add(new Person("9", "Berlin"));
Persons.Add(new Person("10", "Hamburg"));
Persons.Add(new Person("11", "Hamburg"));
Persons.Add(new Person("12", "Munich"));
Persons.Add(new Person("13", "Munich"));
OnPropertyChanged("Persons");
}
public void PersonsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach(Person item in e.OldItems)
{
//Removed items
item.PropertyChanged -= PersonPropertyChanged;
}
}
else if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach(Person item in e.NewItems)
{
//Added items
item.PropertyChanged += PersonPropertyChanged;
}
}
}
public void PersonPropertyChanged(object sender, PropertyChangedEventArgs e)
{
//GroupDescription = "PersonId";
//GroupDescription = "City";
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler pc = PropertyChanged;
if (pc != null)
pc(this, new PropertyChangedEventArgs(name));
}
private void btnChangeCity(object sender, System.Windows.RoutedEventArgs e)
{
Persons[0].City = "Berlin";
}
private void btnChangeCityBack(object sender, System.Windows.RoutedEventArgs e)
{
Persons[0].City = "Hamburg";
}
}
}
I realise this is quite late in the day, but if you are using .NET4.5 or higher you could use the Live Grouping feature, which I think would do exactly what you want.
For example, rather than binding the ListView
ItemsSource
directly to Persons
, you would instead bind to a CollectionViewSource
which itself binds to Persons
:
<Window.Resources>
<CollectionViewSource x:Key="PersonsViewSource" Source="{Binding Persons}" IsLiveGroupingRequested="True">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="GroupName" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</Window.Resources>
As shown above you would just add the property IsLiveGroupingRequested="True"
, and add the property name that you want to regroup on.
When the GroupName
property changes (via your use of INotifyPropertyChanged
), the relevant item will move itself into the correct group in the ListView
, without changing anything else.
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