Immediate validation

It can be interesting to see how bugs are spotted and corrected in Lc. 

Problem description

If you go to the Accounts tab, the Auto refresh is turned on and you start to edit the Template of balance filename then after each keystroke the change there is a refresh and you loose the focus, so you have to click into the cell again.



Auto refresh makes sure that if you change a cell, then the change is saved into the local db at once and the whole table is recalculated. For example, if you change the Amount at the Transaction tab then the balance will be recalculated. 

But what does it means "at once". It can mean PropertyChanged (when you press a key) or LostFocus (when you finish editing and leave the cell). 

PropertyChanged was used because we want a validation on Template of balance filename. If we use a character which is invalid in a filename, e.g. '*' then we want an error message at once. This immediate validation works fine if Auto refresh is turned off, but if it is on, then we got the described problem.

Workaround

A workaround can be to change the PropertyChanged to LostFocus. Now you can edit the cell, and when you finish only then there will refresh and validation. Of course, now the validation is not immediate, an invalid character is only detected when you leave the cell.

 <DataGridTextColumn Width="300"  
   Binding="{Binding BalanceFilenameTemplate, TargetNullValue='', ValidatesOnDataErrors=True, UpdateSourceTrigger=LostFocus}"
   ElementStyle="{StaticResource TextAlignmentRightElementStyle}"  
   Header="{x:Static r:Resource.BalanceFilenameTemplate}" />

Above you can see part of a view in WPF. We are in a DataGrid and a DataGridTextColumn element is shown. Here we define that the header of the column comes from a resource which is set according to active language, the width of the column is 300 and the text should be aligned to the right. The Binding property defines that this column contains the BalanceFilenameTemplate,  which can be found in the DisplayAccount model file. If the bound value is null then the empty string should be displayed, there is validation for this column and the data source should be updated if the cell looses the focus. The validation happens in the model file when the source is updated. That is why the validation is not immediate now.

Solution 1

A real solution should provide immediate validation, and for that, we need to react both to the the PropertyChanged and LostFocus event. Let us introduce a TempBalanceFilenameTemplate property in the DisplayAccount model file. If this property is changed then the PropertyChanged event should not fire because it would trigger the refresh process.

[DoNotNotify]
public string TempBalanceFilenameTemplate { get; set; }

I use the Fody.PropertyChanged nuget package to fire the PropertyChanged event automatically. The [DoNotNotify] attribute excludes the property from this. 

TempBalanceFilenameTemplate = BalanceFilenameTemplate;

TempBalanceFilenameTemplate should be initialized with BalanceFilenameTemplate in the constructor of DisplayAccount.

<DataGridTextColumn Width="300"
    Binding="{Binding TempBalanceFilenameTemplate, TargetNullValue='', ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}"
    ElementStyle="{StaticResource TextAlignmentRightElementStyle}"
    Header="{x:Static r:Resource.BalanceFilenameTemplate}">
    <DataGridTextColumn.EditingElementStyle>
        <Style TargetType="{x:Type TextBox}">
	    <EventSetter Event="LostFocus" Handler="TextBox_LostFocus" />
        </Style>
    </DataGridTextColumn.EditingElementStyle>
</DataGridTextColumn>

Now the TempBalanceFilenameTemplate is used in the binding and the value UpdateSourceTrigger is PropertyChanged again. An event trigger is created for LostFocus in EditingElementStyle.

private void TextBox_LostFocus(object sender, RoutedEventArgs e) { if (sender is TextBox textBox && textBox.DataContext is DisplayAccount displayAccount) displayAccount.BalanceFilenameTemplate = textBox.Text; }

The event handler checks the sender and its context and if they are correct then copies the value of the textBox to BalanceFilenameTemplate.

Solution 2

Using event handlers and code behind (the file where the event handler is placed, e.g., AccountsView.xaml.cs) is not nice. Fortunately you can create a behavior. 

public class TextBoxLostFocusBehavior : Behavior<TextBox>
{
    public static readonly DependencyProperty LostFocusCommandProperty =
DependencyProperty.Register("LostFocusCommand", typeof(ICommand),
typeof(TextBoxLostFocusBehavior), new PropertyMetadata(null));

    public ICommand LostFocusCommand
    {
        get { return (ICommand)GetValue(LostFocusCommandProperty); }
	set { SetValue(LostFocusCommandProperty, value); }
    }

    protected override void OnAttached() => AssociatedObject.LostFocus += AssociatedObject_LostFocus;

    protected override void OnDetaching() => AssociatedObject.LostFocus -= AssociatedObject_LostFocus;

    private void AssociatedObject_LostFocus(object sender, RoutedEventArgs e)
    {
        if (LostFocusCommand is not null && LostFocusCommand.CanExecute(null))
            LostFocusCommand.Execute(null);
    }
}

This behavior defines the LostFocusCommand property. Whenever the TextBox loose the focus the command is executed. 

public RelayCommand MyCommand { get; set; }

Create a command in the DisplayAccount model file.

MyCommand = new RelayCommand(TextBox_LostFocus);			

private void TextBox_LostFocus(object sender)
    => BalanceFilenameTemplate = TempBalanceFilenameTemplate;

Set its value in the constructor.

<DataTemplate x:Key="BalanceFilenameValidationTemplate">
    <TextBox Text="{Binding TempBalanceFilenameTemplate, TargetNullValue='', ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" Margin="0" BorderThickness="0">
        <i:Interaction.Behaviors>
	    <hhb:TextBoxLostFocusBehavior LostFocusCommand="{Binding SetBalanceFilenameTemplateCommand}"/>
        </i:Interaction.Behaviors>
    </TextBox>
</DataTemplate>

Create DataTemplate which contains TextBox with TextBoxLostFocusBehavior.

<DataGridTemplateColumn Width="300"
    CellEditingTemplate="{StaticResource BalanceFilenameValidationTemplate}"
    CellTemplate="{StaticResource BalanceFilenameValidationTemplate}"
    Header="{x:Static r:Resource.BalanceFilenameTemplate}" />

Use DataGridTemplateColumn instead of DataGridTextColumn. and use the created DataTemplate.

To reach this solution I had to explore some dead ends as well.
Previous Post Next Post

Contact Form