WPF DataGrid 如何在复选框变化后立即更新分组?
结论
这里涉及两个更新时机:复选框绑定何时把值写回 Task.Complete,以及 ICollectionView 何时根据新值重新分组。设置 UpdateSourceTrigger=PropertyChanged 可以让绑定及时写回,但分组视图未必会自动重新计算。建议先启用实时分组;如果当前视图不支持,再在属性变化后刷新视图。
问题中的 Task 已实现 INotifyPropertyChanged,可以通知绑定属性发生变化,但普通的 ICollectionView 未必会因分组属性变化而自动移动项目。焦点移开后才看到更新,通常是因为 DataGrid 在提交当前单元格编辑时更新了视图。
设置复选框绑定和实时分组
如果使用自动生成的列,可以先设置复选框列绑定的更新时机:
<DataGrid x:Name="dataGrid1"
ItemsSource="{Binding Source={StaticResource cvsTasks}}"
CanUserAddRows="False"
AutoGenerateColumns="True">
<DataGrid.ItemContainerStyle>
<Style TargetType="DataGridRow">
<Setter Property="IsEditing" Value="False" />
</Style>
</DataGrid.ItemContainerStyle>
</DataGrid>
不过,自动生成的 DataGridCheckBoxColumn 不便于在 XAML 中直接指定其 Binding。如果需要明确设置绑定,可以关闭自动生成,并显式定义复选框列:
<DataGrid x:Name="dataGrid1"
ItemsSource="{Binding Source={StaticResource cvsTasks}}"
CanUserAddRows="False"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="项目" Binding="{Binding ProjectName}" />
<DataGridTextColumn Header="任务" Binding="{Binding TaskName}" />
<DataGridTextColumn Header="截止日期" Binding="{Binding DueDate, StringFormat=d}" />
<DataGridCheckBoxColumn Header="完成"
Binding="{Binding Complete, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataGrid.Columns>
</DataGrid>
视图创建后,检查它是否支持实时分组,并将 Complete 加入实时分组属性。将这段代码放在 InitializeComponent() 之后:
var view = CollectionViewSource.GetDefaultView(dataGrid1.ItemsSource);
if (view is ICollectionViewLiveShaping liveView &&
liveView.CanChangeLiveGrouping)
{
if (!liveView.LiveGroupingProperties.Contains(nameof(Task.Complete)))
{
liveView.LiveGroupingProperties.Add(nameof(Task.Complete));
}
liveView.IsLiveGrouping = true;
}
这种方式要求底层视图支持实时分组。如果 CanChangeLiveGrouping 为 false,当前视图就无法通过这种方式启用实时分组。
不支持实时分组时刷新视图
可以监听任务集合中的 PropertyChanged,并在 Complete 变化后刷新视图。为避免在 DataGrid 提交编辑时立即刷新,可以把刷新操作排入 UI 队列:
using System.ComponentModel;
using System.Windows.Threading;
private void Tasks_CollectionChanged(
object sender,
System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (Task task in e.NewItems)
{
task.PropertyChanged += Task_PropertyChanged;
}
}
if (e.OldItems != null)
{
foreach (Task task in e.OldItems)
{
task.PropertyChanged -= Task_PropertyChanged;
}
}
}
private void Task_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Task.Complete))
{
Dispatcher.BeginInvoke(
new Action(() =>
CollectionViewSource.GetDefaultView(dataGrid1.ItemsSource)?.Refresh()),
DispatcherPriority.DataBind);
}
}
订阅事件时,应覆盖任务加入集合的过程。如果集合在订阅前已有数据,也要为现有项订阅事件。任务移除时记得取消订阅,避免对象被不必要地保留。
自定义分组内容
GroupStyle 用于定义分组标题和容器,ItemsPresenter 则负责呈现组内的 DataGrid 行。保留 ItemsPresenter,再通过 GroupStyle.HeaderTemplate 或 GroupStyle.ContainerStyle 自定义分组外观。例如,可以继续使用现有的 Expander 容器模板。
要自定义每条任务记录的显示方式,可以在 DataGrid 中添加 DataGridTemplateColumn。如果改用普通列表控件呈现分组,应将 ItemsControl 绑定到视图,并通过 GroupStyle.Panel、GroupStyle.ContainerStyle 和条目模板配置分组及记录的呈现方式。若在任务条目模板中再放一个绑定到 Items 的 ItemsControl,就会把任务对象当成分组对象处理。普通 Task 没有这里需要的组 Items,因此这种嵌套结构不适合用来替换 ItemsPresenter。
备注:内容仅供参考。
