Welcome toVigges Developer Community-Open, Learning,Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.0k views
in Technique[技术] by (71.8m points)

wpf - Validate row when underlying data changes

Imagine a DataGrid with its ItemsSource set to an ObservableCollection. This collection provides a view model for each row in the DataGrid. The view model in turn provides the data that is displayed in one row and a command that may change this data. Additionally, I added a rule to the RowValidationRules property of DataGrid. This validation rule works fine in case I enter invalid data.

However, if I change the invalid data to valid data via the command the view model provides, the row validation rule only gets triggered again if the current row in DataGrid loses focus. Hence, the displayed data may be actually valid, but the DataGrid still displays a red exclamation mark showing it has invalid data. This remains the case until the current row loses focus or I enter valid data again.

How do I force a second validation of the current row? I already set ValidatesOnTargetUpdated="True" but this didn't solve the problem. I also have implemented the INotifyPropertyChanged interface but this also didn't fix the problem.

Solution

As user mm8 pointed out, INotifyDataErrorInfo is the approach to go. I removed the row validation rule and exposed a property named HasErros in my view model that proxies the HasErrors property of my model that in turn implements INotifyDataErrorInfo. Next I added a custom RowValidationErrorTemplate

<DataGrid.RowValidationErrorTemplate>
    <ControlTemplate>
        <Grid>
            <Ellipse Width="12" Height="12" Fill="Red"/>
            <Label Content="!" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
                   Foreground="White" FontSize="11"/>
        </Grid>
    </ControlTemplate>
</DataGrid.RowValidationErrorTemplate>

and created the following custom style for DataGridRowHeader

<Style x:Key="MyDataGridRowHeaderStyle" TargetType="{x:Type DataGridRowHeader}">
    <!-- ... -->
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type DataGridRowHeader}">
                <Border>
                    <Grid>
                        <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                          VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                          SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                        <Control SnapsToDevicePixels="True"
                                 Template="{Binding ValidationErrorTemplate, RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}}}"
                                 Visibility="{Binding Path=HasErrors, UpdateSourceTrigger=PropertyChanged, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"/>
                    </Grid>
                </Border>
                <!-- ... -->
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Note the binding of Visibility. The HasErrors property is the proxy property I mentioned above.

And finally, use that style in the DataGrid as follows

<DataGrid RowHeaderStyle="{StaticResource MyDataGridRowHeaderStyle}"
...

An implementation of BoolToVisibilityConverter can be found here.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

You can handle CellEditEnding, find the Rows and call UpdateSources of the BindingGroup:

    private void dataGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
    {
        DataGrid dg = sender as DataGrid;
        foreach (var r in dg.Items)
        {
            DataGridRow row = dg.ItemContainerGenerator.ContainerFromItem(r) as DataGridRow;
            if (row != null)
                row.BindingGroup.UpdateSources();
        }
    }

Also, note to set UpdateSourceTrigger=PropertyChanged for your bindings too.

Edit

Note that you can also use EventTrigger:

 <DataGrid x:Name="dataGrid1"   ItemsSource="{Binding Models}" DataContext="{Binding}"> 
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="CellEditEnding" >
            <i:InvokeCommandAction Command="{Binding PCommand}" 
                                   CommandParameter="{Binding  RelativeSource={RelativeSource AncestorType=DataGrid, Mode=FindAncestor}}">
            </i:InvokeCommandAction>
        </i:EventTrigger>
    </i:Interaction.Triggers> 
</DataGrid>

in which PCommand is in the ViewModel and:

    private void DoPCommand(object parameter)
    {
        DataGrid dg = parameter as DataGrid;
        if (dg != null)
            foreach (var r in dg.Items)
            {
                DataGridRow row = dg.ItemContainerGenerator.ContainerFromItem(r) as DataGridRow;
                if (row != null)
                    row.BindingGroup.UpdateSources();
            }
    }

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to Vigges Developer Community for programmer and developer-Open, Learning and Share
...