GroupStyle sum is not updated when multiple items
I have successfully applied the trick explained here . But I still have one problem.
Quick summary: I am showing users in a ListView. Users are regrouped by country, and the GroupStyle DataTemplate displays the sum of all users associated with the Group. Totally using a converter. But user interface users can change the "Total" value for users through a modal window.
If there is only one item in the group, both the user displayed and the amount are displayed. But when there are multiple items in the group, only User Total is updated (via binding), but the converter that should make the sum (TotalSumConverter) is not even called!
Do you have any idea where this might come from? Do I have to use some kind of trigger to make sure the converter is called when there are changes in the elements?
The problem is that the value converter that calculates the sum for all the items in the group does not start when the item changes because there is no notification of the changed items. One solution is to bind to something else that you can control how it makes notifications and notify the group header as needed.
Below is a working example. You can change the counter for the user in the text box and the totals are recalculated.
XAML:
<Window x:Class="UserTotalTest.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:userTotalTest="clr-namespace:UserTotalTest"
Title="Window1" Height="300" Width="300"
Name="this">
<Window.Resources>
<userTotalTest:SumConverter x:Key="SumConverter" />
<CollectionViewSource Source="{Binding Path=Users}" x:Key="cvs">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Country"/>
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="10" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ListView
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
ItemsSource="{Binding Source={StaticResource cvs}}">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn Header="Name" DisplayMemberBinding="{Binding Path=Name}" />
<GridViewColumn Header="Count" DisplayMemberBinding="{Binding Path=Count}" />
</GridView.Columns>
</GridView>
</ListView.View>
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<StackPanel Margin="10">
<TextBlock Text="{Binding Path=Name}" FontWeight="Bold" />
<ItemsPresenter />
<TextBlock FontWeight="Bold">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource SumConverter}">
<MultiBinding.Bindings>
<Binding Path="DataContext.Users" ElementName="this" />
<Binding Path="Name" />
</MultiBinding.Bindings>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
<ComboBox Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2"
ItemsSource="{Binding Path=Users}"
DisplayMemberPath="Name"
SelectedItem="{Binding Path=SelectedUser}" />
<TextBlock Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2" Text="{Binding Path=SelectedUser.Country}" />
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding Path=SelectedUser.Count}" />
</Grid>
</Window>
Code behind:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Data;
namespace UserTotalTest
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = new UsersVM();
}
}
public class UsersVM : INotifyPropertyChanged
{
public UsersVM()
{
Users = new List<User>();
Countries = new string[] { "Sweden", "Norway", "Denmark" };
Random random = new Random();
for (int i = 0; i < 25; i++)
{
Users.Add(new User(string.Format("User{0}", i), Countries[random.Next(3)], random.Next(1000)));
}
foreach (User user in Users)
{
user.PropertyChanged += OnUserPropertyChanged;
}
SelectedUser = Users.First();
}
private void OnUserPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Count")
{
PropertyChanged(this, new PropertyChangedEventArgs("Users"));
}
}
public List<User> Users { get; private set; }
private User _selectedUser;
public User SelectedUser
{
get { return _selectedUser; }
set
{
_selectedUser = value; if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("SelectedUser"));
}
}
}
public string[] Countries { get; private set; }
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
public class User : INotifyPropertyChanged
{
public User(string name, string country, double total)
{
Name = name;
Country = country;
Count = total;
}
public string Name { get; private set; }
private string _country;
public string Country
{
get { return _country; }
set
{
_country = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Country"));
}
}
}
private double _count;
public double Count
{
get { return _count; }
set
{
_count = value; if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Count"));
}
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
public class SumConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
IEnumerable<User> users = values[0] as IEnumerable<User>;
string country = values[1] as string;
double sum = users.Cast<User>().Where(u =>u.Country == country).Sum(u => u.Count);
return "Count: " + sum;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
a source to share
The trick you are using is to bind the group footer to a ListView.Items item, which will not automatically update your view, like DependencyObject for example. Instead, run the update after each update to Total as follows:
CollectionViewSource viewSource = FindResource("ViewSource") as CollectionViewSource;
viewSource.View.Refresh();
a source to share
The problem with refreshing the view is that it completely refreshes it and I have extensions for my grouping that will expand to their original state even if the user has closed them. So yes, this is a possible workaround, but it is not completely satisfying.
Also explains your explanation of DependencyObject, but then why does it work when I only have one item in my group?