Pavel
Pavel

Reputation: 113

CollectionViewGroup.Items not raising PropertyChanged after having an item added?

I am trying to add grouping with subtotal sum into DataGrid. Read several articles: the solution is to have an ObservableCollection with the data, wrap it into CollectionViewSource which in turn will be ItemsSource for the DataGrid. Subtotal is calculated with a converter, which receives Items of CollectionViewGroup as input and calculates the sum.

All works fine only at the initial population of the ObservableCollection, or when adding an item creates the new group. But if an item is added into any existing group, converter is simply not called for recalculation - apparently CollectionViewGroup.Items is not raising PropertyChanged event? I browsed a bit in CollectionViewGroup source - Items are ReadOnlyObservableCollection<object>, which should trigger PropertyChanged after an item added, shouldn't it?

Then I noticed, that CollectionViewGroup.ItemCount is displayed properly after adding new items, so I tried a trick with a MultiBinding - added a IMultiValueConverter converter which takes both Items and ItemCount as parameters, expecting ItemCount to trigger the recalculation. It worked, but again without full success - somehow the converter gets the correct input only once, when the new group is created. If an item was added to an existing group, ItemCount is correct, but Items are not! Items collection is missing the newly added item! E.g. when ItemCount=2, Items have only 1 "old" item (Items.Count=1). When ItemCount=3, Items have only 2 "old" items (Items.Count=2), etc. So again the converter cannot calculate the correct subtotal, because the input is incomplete...

It looks like the only working solution would be to call Refresh() for the whole CollectionViewSource, but that expands all the groups, cause flickering, breaks MVVM concept, so it is ugly...

So my questions are:

Any advice would be highly appreciated!

The full sample code is on GitHub

Some code excerpts are below - XAML:

            <DataGrid.GroupStyle>
            <GroupStyle>
                <GroupStyle.ContainerStyle>
                    <Style TargetType="{x:Type GroupItem}">
                        <Setter Property="Margin" Value="0,0,0,5"/>
                        <Setter Property="Template">
                            <Setter.Value>
                                <ControlTemplate TargetType="{x:Type GroupItem}">                                       
                                    <Expander IsExpanded="True" BorderThickness="1,1,1,5">
                                        <Expander.Header>
                                            <DockPanel>
                                                <TextBlock FontWeight="Bold" Text="{Binding Path=Name}" Margin="5,0,0,0" Width="100"/>
                                                <TextBlock FontWeight="Bold" Text="{Binding Path=ItemCount}"/>
                                                <TextBlock FontWeight="Bold" Text="Sum 1: " Margin="5,0,0,0"/>
                                                <TextBlock FontWeight="Bold"  >
                                                    <TextBlock.Text>
                                                        <Binding Path="Items" Converter="{StaticResource sumConverter}" ConverterParameter="AmountValue" StringFormat="{}{0:N2}"/>
                                                    </TextBlock.Text>
                                                </TextBlock>
                                                <TextBlock FontWeight="Bold" Text="Sum 2: " Margin="5,0,0,0"/>
                                                <TextBlock FontWeight="Bold"  >
                                                    <TextBlock.Text>
                                                        <MultiBinding Converter="{StaticResource sumMulConverter}" ConverterParameter="AmountValue" StringFormat="{}{0:N2}">
                                                            <Binding Path="Items"/>
                                                            <Binding Path="ItemCount"/>
                                                        </MultiBinding>
                                                    </TextBlock.Text>
                                                </TextBlock>
                                            </DockPanel>
                                        </Expander.Header>
                                        <Expander.Content>
                                            <ItemsPresenter />
                                        </Expander.Content>
                                    </Expander>
                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                    </Style>
                </GroupStyle.ContainerStyle>
            </GroupStyle>
        </DataGrid.GroupStyle>

Converters:

    public class SumConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (value == DependencyProperty.UnsetValue) return DependencyProperty.UnsetValue;
        if (null == parameter) return null;
        string propertyName = (string)parameter;
        if (!(value is ReadOnlyObservableCollection<object>)) return null;
        ReadOnlyObservableCollection<object> collection = (ReadOnlyObservableCollection<object>)value;
        decimal sum = 0;
        foreach (object o in collection)
        {
            sum += (decimal)o.GetType().GetProperty(propertyName).GetValue(o);
        }
        return sum;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

public class SumMulConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (null == parameter) return null;
        if (!(parameter is string)) return null;
        string propertyName = (string)parameter;

        if (values == DependencyProperty.UnsetValue) return DependencyProperty.UnsetValue;
        if (values == null) return null;
        if (values.Length < 2) return null;
        if (!(values[0] is ReadOnlyObservableCollection<object>)) return null;
        ReadOnlyObservableCollection<object> collection = (ReadOnlyObservableCollection<object>)values[0];
        if (!(values[1] is int)) return null;
        Debug.Print($"ItemCount={(int)values[1]}; Collection Count = {collection.Count}");
        decimal sum = 0;
        foreach (object o in collection)
        {
            sum += (decimal)o.GetType().GetProperty(propertyName).GetValue(o);
        }
        return sum; //.ToString("N2", CultureInfo.CurrentCulture);
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Upvotes: 1

Views: 311

Answers (2)

Chiller
Chiller

Reputation: 1

Another solution that worked for me, but may not work for all situations, was to add another binding to the multibinding in the OPs code to grab the full collection for the datagrid. In my case, the path datagrid.itemssource.sourcecollection returned the full ObservableCollection for the datagrid, which was already updated with the new item.

Then in the MultiValueConverter, I used the first item in the ReadOnlyObservableCollection for the group to obtain the common value for the group, did a LINQ query on the full ObservableCollection for the datagrid to obtain the items for the group, and calculated the sum from there.

Upvotes: 0

BionicCode
BionicCode

Reputation: 28968

If you want to sum up the values in the view, a simple solution would be to create an Attached Behavior.

Also make use of LINQ: instead of using reflection you can cast the collection from object to the explicit type using Enumerable.Cast<T> or Enumerable.OfType<T>. To compute a sum of a collection based on an item's property, use Enumerable.Sum:

GroupItemSumBehavior.cs

public class GroupItemSumBehavior : DependencyObject
{
  #region IsEnabled attached property

  public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.RegisterAttached(
    "IsEnabled", typeof(bool), typeof(GroupItemSumBehavior), new PropertyMetadata(default(bool), OnIsEnabledChanged));

  public static void SetIsEnabled(DependencyObject attachingElement, bool value) => attachingElement.SetValue(GroupItemSumBehavior.IsEnabledProperty, value);

  public static bool GetIsEnabled(DependencyObject attachingElement) => (bool) attachingElement.GetValue(GroupItemSumBehavior.IsEnabledProperty);

  #endregion

  #region Sum attached property

  public static readonly DependencyProperty SumProperty = DependencyProperty.RegisterAttached(
    "Sum", typeof(decimal), typeof(GroupItemSumBehavior), new PropertyMetadata(default(decimal)));

  public static void SetSum(DependencyObject attachingElement, decimal value) => attachingElement.SetValue(GroupItemSumBehavior.SumProperty, value);

  public static decimal GetSum(DependencyObject attachingElement) => (decimal) attachingElement.GetValue(GroupItemSumBehavior.SumProperty);

  #endregion

  private static Dictionary<IEnumerable, GroupItem> CollectionToGroupItemMap { get; set; }

  static GroupItemSumBehavior() => GroupItemSumBehavior.CollectionToGroupItemMap =
    new Dictionary<IEnumerable, GroupItem>();

  private static void OnIsEnabledChanged(DependencyObject attachingElement, DependencyPropertyChangedEventArgs e)
  {
    if (!(attachingElement is GroupItem groupItem))
    {
      return;
    }

    var collectionViewGroup = groupItem.DataContext as CollectionViewGroup;
    bool isEnabled = (bool) e.NewValue;

    if (isEnabled)
    {
      CollectionToGroupItemMap.Add(collectionViewGroup.Items, groupItem);
      (collectionViewGroup.Items as INotifyCollectionChanged).CollectionChanged += CalculateSumOnCollectionChanged;
      CalculateSum(collectionViewGroup.Items);
    }
    else
    {
      CollectionToGroupItemMap.Remove(collectionViewGroup.Items);
      (collectionViewGroup.Items as INotifyCollectionChanged).CollectionChanged -= CalculateSumOnCollectionChanged;
    }
  }

  private static void CalculateSum(IEnumerable collection)
  {
    if (GroupItemSumBehavior.CollectionToGroupItemMap.TryGetValue(collection, out GroupItem groupItem))
    {
      decimal sum = collection
        .OfType<LineItem>()
        .Sum(lineItem => lineItem.AmountValue);

      SetSum(groupItem, sum);
    }
  }

  private static void CalculateSumOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 
    => CalculateSum(sender as IEnumerable);
}

DataGrid GroupStyle

<DataGrid.GroupStyle>
  <GroupStyle>
    <GroupStyle.ContainerStyle>
      <Style TargetType="{x:Type GroupItem}">

        <Setter Property="GroupItemSumBehavior.IsEnabled" Value="True" />

        <Setter Property="Template">
          <Setter.Value>
            <ControlTemplate TargetType="{x:Type GroupItem}">
              <Expander IsExpanded="True" BorderThickness="1,1,1,5">
                <Expander.Header>
                  <DockPanel>
                    <TextBlock FontWeight="Bold" Text="{Binding Path=Name}" Margin="5,0,0,0" Width="100" />
                    <TextBlock FontWeight="Bold" Text="{Binding Path=ItemCount}" />
                    <TextBlock FontWeight="Bold" Text="Sum: " Margin="5,0,0,0" />

                    <TextBlock FontWeight="Bold"
                               Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(GroupItemSumBehavior.Sum)}" />
                  </DockPanel>
                </Expander.Header>
                <Expander.Content>
                  <ItemsPresenter />
                </Expander.Content>
              </Expander>
            </ControlTemplate>
          </Setter.Value>
        </Setter>
      </Style>
    </GroupStyle.ContainerStyle>
  </GroupStyle>
</DataGrid.GroupStyle>

Upvotes: 1

Related Questions