You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WPF DataGrid窗口缩窄编辑时单元格值错乱问题求解

问题描述

WPF DataGrid绑定仅含单张表的DataSet,表内共设置Price、Discount、Total三列,其中仅Discount列允许用户编辑。当DataGrid所有内容都处于可视区域内时,录入Discount数据后的总价计算逻辑运行完全正常。

全可视状态下运行正常

但当缩窄窗口、部分单元格移出可视区域后再录入数据,值会出现混乱:当前单元格编辑提交后,不在可视范围内的列值会被错误修改,放大窗口恢复全表可视后可观察到Total列值完全错位。

窗口缩窄状态下录入数据

放大窗口查看全表的效果如下:

值错乱效果截图

可以看到Total列的计算值完全错位混乱。

复现代码

以下是可直接运行测试的最小复现示例:

XAML

<Window x:Class="DataGrid"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TestDatagrid"
        mc:Ignorable="d"
        Title="DataGrid" Height="450" Width="800" Loaded="Window_Loaded">
    <Grid>
        <DataGrid x:Name="grid" Margin="0,28,0,38" 
            CanUserDeleteRows="True" 
            CanUserSortColumns="True" 
            CanUserAddRows="False" 
            SelectionUnit="CellOrRowHeader"
            CellEditEnding="grid_CellEditEnding"/>
    </Grid>
</Window>

VB.NET

Imports System.Data
Imports System.Globalization

Public Class DataGrid
    Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs)
        Dim ds As New DataSet
        Dim tb As New DataTable("Orders")

        Dim price As New DataColumn
        price.ColumnName = "Price"
        price.DataType = System.Type.GetType("System.Decimal")
        tb.Columns.Add(price)

        Dim discount As New DataColumn
        discount.ColumnName = "Discount"
        discount.DataType = System.Type.GetType("System.Decimal")
        tb.Columns.Add(discount)

        Dim total As New DataColumn
        total.ColumnName = "Total"
        total.DataType = System.Type.GetType("System.Decimal")
        tb.Columns.Add(total)

        For i As Integer = 0 To 20
            Dim dr As DataRow = tb.NewRow()
            dr("Price") = CDec((i + 1) * 10)
            dr("Discount") = 0
            dr("Total") = CDec((i + 1) * 10)
            tb.Rows.Add(dr)
        Next
        ds.Tables.Add(tb)
        grid.ItemsSource = ds.Tables(0).DefaultView

        For Each c In grid.Columns
            If c.Header = "Price" Then c.IsReadOnly = True
            If c.Header = "Total" Then c.IsReadOnly = True
        Next
    End Sub

    Private Sub grid_CellEditEnding(sender As Object, e As DataGridCellEditEndingEventArgs)
        If e.EditAction = DataGridEditAction.Commit Then
            Dim nfi As NumberFormatInfo = New NumberFormatInfo()
            nfi.NumberDecimalSeparator = "."

            Try
                Dim tPrice As TextBlock = TryCast(grid.Columns(0).GetCellContent(e.Row), TextBlock)
                Dim Price As Double = CDbl("0" & Replace(tPrice.Text, ".", ","))

                Dim Discount As Double = CDbl("0" & Replace(TryCast(e.EditingElement, TextBox).Text, ".", ","))
                Dim Total As Double = Price * (1 - (Discount / 100))

                ' 格式化折扣值
                e.EditingElement.SetValue(TextBox.TextProperty, String.Format(nfi, "{0:0.00#}", Discount))

                ' 直接修改控件文本显示总价
                TryCast(grid.Columns(2).GetCellContent(e.Row), TextBlock).Text = String.Format(nfi, "{0:0.00#}", Total)

                ' 更新数据源
                TryCast(grid.CurrentItem, DataRowView).Item(2) = Total
            Catch ex As Exception
            End Try
        End If
    End Sub
End Class
问题成因
  • WPF DataGrid默认开启UI虚拟化,不在可视区域的单元格容器会被回收复用,不会长期驻留可视化树。
  • 复现代码直接通过GetCellContent获取可视化树内的TextBlock控件、手动修改控件属性的写法完全绕过了WPF的数据绑定机制:单元格被复用后,手动修改的控件属性会被带到其他行,最终导致值错位。
  • 代码中通过grid.CurrentItem获取当前编辑行数据源的逻辑不可靠,编辑状态下该属性取值可能和实际触发编辑的行不匹配。
修复方案
  • 禁止直接操作DataGrid可视化单元格的控件属性,所有数据更新直接操作绑定的底层数据源,UI刷新交给WPF绑定机制自动完成。
  • 数值格式化逻辑通过绑定的StringFormat配置实现,不需要手动给TextBlock赋值。
  • 获取当前编辑行的数据源时,直接使用事件参数传入的e.Row.Item,不要依赖全局的CurrentItem属性。
  • 临时验证可在DataGrid上添加属性VirtualizingStackPanel.IsVirtualizing="False"关闭虚拟化,但数据量大时会造成性能问题,不推荐作为长期方案。

修正后的grid_CellEditEnding逻辑参考:

Private Sub grid_CellEditEnding(sender As Object, e As DataGridCellEditEndingEventArgs)
    If e.EditAction = DataGridEditAction.Commit AndAlso e.Column.Header.ToString() = "Discount" Then
        Try
            ' 直接从当前行的数据源拿值,不要从可视化控件取
            Dim rowView = TryCast(e.Row.Item, DataRowView)
            Dim price = CDbl(rowView("Price"))
            Dim discount = CDbl(TryCast(e.EditingElement, TextBox).Text)
            ' 直接更新数据源
            rowView("Discount") = discount
            rowView("Total") = price * (1 - discount / 100)
        Catch ex As Exception
        End Try
    End If
End Sub

如果需要自定义显示格式,在窗口加载时给对应列添加绑定格式即可,不需要手动改控件Text:

' 放在Window_Loaded里设置列属性的位置
CType(grid.Columns(2), DataGridTextColumn).Binding = New Binding("Total") With {
    .StringFormat = "0.00#"
}

内容的提问来源于stack exchange,提问作者Baro

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 09:45:20