为什么 DataGrid 在编辑时用旧的无效值替换绑定 属性?

Why is the DataGrid replacing a bound property with an old, invalid value when edited?

我有一个简单的 DataGrid 绑定到视图模型的 ObservableCollection。一列是 DataGridTemplateColumn,其 CellTemplate 是一个 TextBlock,而 CellEditingTemplate 是一个 TextBox(我知道我可以使用 DataGridTextColumn,但我想明确说明要使用哪些控件)。 TextBox 配置为验证错误。

<DataGrid
    ItemsSource="{Binding People}"
    AutoGenerateColumns="False"
    >
    <DataGrid.Columns>
        <DataGridTemplateColumn Header="First Name">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding FirstName}" />
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
            <DataGridTemplateColumn.CellEditingTemplate>
                <DataTemplate>
                    <TextBox Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged,
                                                       ValidatesOnDataErrors=True}" />
                </DataTemplate>
            </DataGridTemplateColumn.CellEditingTemplate>
        </DataGridTemplateColumn>
    </DataGrid.Columns>
</DataGrid>

每个项目的视图模型 (PersonViewModel) 只定义一个 FirstName 属性,并实现 IDataErrorInfo 以进行验证。我正在使用 MVVM Light Toolkit 进行 属性 通知。

public class PersonViewModel : ViewModelBase, IDataErrorInfo
{
    private string firstName;
    public string FirstName
    {
        get => firstName;
        set => Set(nameof(FirstName), ref firstName, value);
    }

    public string this[string columnName]
    {
        get
        {
            if (columnName == nameof(FirstName))
            {
                if (string.IsNullOrEmpty(FirstName))
                    return "First name cannot be empty";
            }

            return null;
        }
    }

    public string Error => null;
}

此外,我还有一个按钮,单击该按钮会将每个人的名字设置为“你好”。

public ICommand HelloCommand =>
    new RelayCommand(() =>
    {
        foreach (var person in People)
            person.FirstName = "Hello";
    });

问题是:当我输入一个无效的 FirstName(即,我将其设置为空字符串),然后单击 Hello 按钮时,它正确地将 FirstName 替换为“Hello”。但是如果我再次尝试编辑它,FirstName 会立即被空字符串替换。

作为测试,我将字符串“a”设为错误。执行与上述相同的步骤,将 TextBox 中的“Hello”字符串替换为“a”。就好像 TextBox 不知道 FirstName 已更改为“Hello”,即使 TextBlock 正确显示它并且它们都绑定到相同的 属性.

有谁知道发生了什么或解决这个问题的方法吗?我预期的行为是 TextBox 包含绑定 属性 更改为的值(无论该 TextBox 是否存在验证错误)。

注意:我正在使用 .NET 4.0,因为我必须这样做。

显然,这种行为的原因是在 HelloCommand 执行时,发起验证错误的 TextBox 还不存在。因此,验证错误的清除无法按常规方式进行。

很难说为什么在新建TextBox和恢复绑定的时候没有从ViewModel中取新值。更有趣的是这个错误值的来源。有人可能会想,如果我们从 ViewModel 中获取 FirstName 属性 的新值,并在 DataContext_Changed 事件处理程序中将其设置为 TextBox.Text 属性 TextBox,那么它应该解决问题,因为此时 TextBoxViewModel 都有一个新的(有效的)值,而且没有空间可以取错一个从。但神奇的是它仍然来自某个地方:)

幸运的是,有一个技巧可以帮助解决这个问题。这个想法是在执行 HelloCommand 之前取消所有挂起的绑定操作。为什么它应该起作用并不是很明显。毕竟此时导致错误的 BindingExpression 不存在。尽管如此,它仍然有效。

DataGrid起名字:

<DataGrid x:Name="myGrid" ItemsSource="{Binding People}"  AutoGenerateColumns="False">

向按钮添加点击处理程序:

<Button Content="Hello" Command="{Binding HelloCommand}" Click="Hello_Click"/>

使用该代码

    private void Hello_Click(object sender, RoutedEventArgs e)
    {
        foreach (var bg in BindingOperations.GetSourceUpdatingBindingGroups(myGrid))
            bg.CancelEdit();
    }

UPD

由于GetSourceUpdatingBindingGroups在.NET 4.0中不可用,您可以尝试这样:

private void Hello_Click(object sender, RoutedEventArgs e)
{
    for (int i = 0; i < myGrid.Items.Count; i++)
    {
        DataGridRow row = (DataGridRow)myGrid.ItemContainerGenerator.ContainerFromIndex(i);
        if (row != null && Validation.GetHasError(row))
        {
            row.BindingGroup?.CancelEdit();
        }
    }
}

它不是那么优雅,但确实是一样的。