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
相关产品推荐
相关产品推荐

