首页 > 解决方案 > 添加项目后,CollectionViewGroup.Items 没有提高 PropertyChanged?

问题描述

我正在尝试将带有小计的分组添加到 DataGrid 中。看了几篇文章:解决办法是有一个ObservableCollection带有数据的,把它包装进去CollectionViewSource,然后又会ItemsSource为DataGrid。使用转换器计算小计,该转换器接收Items作为CollectionViewGroup输入并计算总和。

仅在 的初始人口ObservableCollection或添加项目创建新组时,所有工作正常。但是,如果将一个项目添加到任何现有组中,则根本不会调用转换器进行重新计算 - 显然CollectionViewGroup.Items不是引发PropertyChanged事件?我在CollectionViewGroup 源代码中浏览了一下- Itemsare ReadOnlyObservableCollection<object>,应该PropertyChanged在添加项目后触发,不是吗?

然后我注意到,CollectionViewGroup.ItemCount添加新项目后显示正确,所以我尝试了一个技巧MultiBinding- 添加了一个IMultiValueConverter转换器,它同时接受ItemsItemCount作为参数,期望ItemCount触发重新计算。它起作用了,但又没有完全成功——不知何故,当创建新组时,转换器只能获得一次正确的输入。如果将项目添加到现有组中,ItemCount则正确,但Items不正确!Items收藏缺少新添加的项目!例如,当ItemCount=2 时,Items只有 1 个“旧”项目(Items.Count=1)。当ItemCount=3 时,Items只有 2 个“旧”项目(Items.Count=2),等等。所以转换器再次无法计算正确的小计,因为输入不完整......

看起来唯一可行的解​​决方案是调用Refresh()整体CollectionViewSource,但这会扩展所有组,导致闪烁,破坏 MVVM 概念,所以它很难看......

所以我的问题是:

任何建议将不胜感激!

完整的示例代码在GitHub 上

一些代码摘录如下 - 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>

转换器

    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();
    }
}

标签: c#wpfdata-bindingdatagrid

解决方案


如果您想总结视图中的值,一个简单的解决方案是创建一个附加行为。

Enumerable.Cast<T>还可以使用 LINQ:您可以使用or将集合从对象转换为显式类型,而不是使用反射Enumerable.OfType<T>。要根据项目的属性计算集合的总和,请使用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>
  <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>

推荐阅读