收藏视图组.项目未引发属性添加项目后已更改?



我正在尝试将小计总和的分组添加到 DataGrid 中。阅读几篇文章:解决方案是将数据ObservableCollection,将其包装到CollectionViewSource然后ItemsSourceDataGrid。小计是使用转换器计算的,转换器接收CollectionViewGroupItems作为输入并计算总和。

只有在ObservableCollection的初始填充或添加项目创建新组时,所有工作正常。但是,如果将项目添加到任何现有组中,则根本不需要转换器进行重新计算 - 显然CollectionViewGroup.Items不会引发PropertyChanged事件? 我在 CollectionViewGroup 源代码中浏览了一下 -ItemsReadOnlyObservableCollection<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),依此类推。所以转换器再次无法计算正确的小计,因为输入不完整......

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

所以我的问题是:

  • 是否还有任何变化可以使CollectionViewGroup.Items适当地提高PropertyChanged

  • 这不是
  • CollectionViewGroup中的一个错误,多功能转换器收到Items.Count=ItemCount- 1?

任何建议将不胜感激!

完整的示例代码在 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();
}
}

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

还可以使用 LINQ:您可以使用Enumerable.Cast<T>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>

最新更新