我正在尝试将小计总和的分组添加到 DataGrid 中。阅读几篇文章:解决方案是将数据ObservableCollection
,将其包装到CollectionViewSource
然后ItemsSource
DataGrid。小计是使用转换器计算的,转换器接收CollectionViewGroup
Items
作为输入并计算总和。
只有在ObservableCollection
的初始填充或添加项目创建新组时,所有工作正常。但是,如果将项目添加到任何现有组中,则根本不需要转换器进行重新计算 - 显然CollectionViewGroup.Items
不会引发PropertyChanged
事件? 我在 CollectionViewGroup 源代码中浏览了一下 -Items
ReadOnlyObservableCollection<object>
,这应该在添加项目后触发PropertyChanged
,不是吗?
然后我注意到,添加新项目后CollectionViewGroup.ItemCount
可以正确显示,所以我尝试了一个MultiBinding
技巧 - 添加一个IMultiValueConverter
转换器,它将Items
和ItemCount
作为参数,期望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>