PAYATHON 2026

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。

备注:内容仅供参考。