Search code examples
wpfwpfdatagrid

DataGrid Group Header not Updating on new Record


I am having issues with the DataGrid Grouping Header updating. When I add or delete a record the total does not update unless i resort it and sometimes even editing the amount does not update the total. I added INotifyPropertyChanged, IEditableObject and IsLiveGroupingRequested per the suggestions of some of the forum posts that i read with no luck. I put a breakpoint on the converter function and i noticed that it is not firing when i add/remove/edit a record it is not firing until i sort the table. I am not sure what I am doing wrong here.

xaml

<Window x:Class="WpfApp2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp2"
        mc:Ignorable="d"
        xmlns:s="clr-namespace:System.ComponentModel;assembly=WindowsBase"
        Title="MainWindow" Height="350" Width="525">

    <Window.Resources>
        <local:Converter1 x:Key="c1"/>
        <CollectionViewSource x:Key="cvs1" Source="{Binding Path=Lines, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
            <CollectionViewSource.GroupDescriptions>
                <PropertyGroupDescription PropertyName="Type"/>
            </CollectionViewSource.GroupDescriptions>
        </CollectionViewSource>
    </Window.Resources>

    <DataGrid Name="_dataGrid1" ItemsSource="{Binding Source={StaticResource cvs1}}">
        <DataGrid.GroupStyle>
            <GroupStyle>
                <GroupStyle.ContainerStyle>
                    <Style TargetType="GroupItem">
                        <Setter Property="Template">
                            <Setter.Value>
                                <ControlTemplate TargetType="GroupItem">
                                    <StackPanel>
                                        <Grid>
                                            <Grid.ColumnDefinitions>
                                                <ColumnDefinition Width="*"/>
                                                <ColumnDefinition Width="Auto"/>
                                            </Grid.ColumnDefinitions>
                                            <Label Grid.Column="0" Content="{Binding Path=Name}" Padding="0"/>
                                            <Label Grid.Column="1" Content="{Binding Converter={StaticResource c1}}" Padding="0"/>
                                        </Grid>
                                        <ItemsPresenter/>
                                    </StackPanel>
                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                    </Style>
                </GroupStyle.ContainerStyle>
            </GroupStyle>
        </DataGrid.GroupStyle>
    </DataGrid>

</Window>

codebehind

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Data;

namespace WpfApp2
{

    public partial class MainWindow : Window
    {

        public MainWindow()
        {

            InitializeComponent();


            Transaction transaction = new Transaction();
            transaction.Lines.Add(new TransactionLine() { Account = "ACCOUNT1", Type = "ACCOUNT", Amount = -41.86 });
            transaction.Lines.Add(new TransactionLine() { Account = "BUDGET1", Type = "BUDGET", Amount = 41.86 });
            transaction.Lines.Add(new TransactionLine() { Account = "CATEGORY1", Type = "CATEGORY", Amount = 41.86 });
            this.DataContext = transaction;

        }

    }

    public class Transaction
    {
        public Transaction()
        {
            this.Lines = new ObservableCollection<TransactionLine>();
        }
        public DateTime Date { get; set; }
        public string Payee { get; set; }
        public ObservableCollection<TransactionLine> Lines { get; set; }
    }
    public class TransactionLine : INotifyPropertyChanged, IEditableObject
    {

        void IEditableObject.BeginEdit()
        {

        }
        void IEditableObject.CancelEdit()
        {

        }
        void IEditableObject.EndEdit()
        {

        }

        public TransactionLine() { }

        private string _account;
        private string _type;
        private double _amount;

        public string Account
        {
            get
            {
                return this._account;
            }
            set
            {
                this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Account"));
                this._account = value;
            }
        }
        public string Type
        {
            get
            {
                return this._type;
            }
            set
            {
                this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Type"));
                this._type = value;
            }
        }
        public double Amount
        {
            get
            {
                return this._amount;
            }
            set
            {
                this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Amount"));
                this._amount = value;
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

    }

    public class Converter1 : IValueConverter
    {

        object IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return Math.Round(((CollectionViewGroup)value).Items.Sum(l => ((TransactionLine)l).Amount), 2);
        }
        object IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }

    }

}

Solution

  • The problem here is that the converter is only invoked when the databound property is set. And since you are binding to the CollectionViewGroup itself, the Convert method won't be called when you add a new item to the ObservableCollection<TransactionLine>.

    Instead of just binding to the CollectionViewGroup itself, you could use a MultiBinding and an IMultiValueConverter implementation to also bind to the Count property of the source collection:

    public class Converter1 : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            return Math.Round(((CollectionViewGroup)values[0]).Items.Sum(l => ((TransactionLine)l).Amount), 2);
        }
    
        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
    

    XAML:

    <ControlTemplate TargetType="GroupItem">
        <StackPanel>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="Auto"/>
                </Grid.ColumnDefinitions>
                <Label Grid.Column="0" Content="{Binding Path=Name}" Padding="0"/>
                <Label Grid.Column="1" Padding="0">
                    <Label.Content>
                        <MultiBinding Converter="{StaticResource c1}">
                            <Binding Path="."  />
                            <Binding Path="Items.Count" />
                        </MultiBinding>
                    </Label.Content>
                </Label>
            </Grid>
            <ItemsPresenter/>
        </StackPanel>
    </ControlTemplate>