Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ListView Grouping does not update when bound property is changed

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";
        }

    }
}
like image 388
Thomas Huber Avatar asked Jun 04 '12 08:06

Thomas Huber


1 Answers

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.

like image 123
Greg Avatar answered Oct 01 '22 16:10

Greg