Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trigger validation on click of Search button

I'm building an MVVM Light WPF app using Visual Studio 2015 Update 1. I have the following two search fields: cmbSearchColumn and txtSearchValue. Neither can be blank when the user clicks the Search button. Note that I've got ValidationRules set for both fields.

Here's the relevant XAML:

<TextBlock Grid.Row="1"
           Grid.Column="0"
           Style="{StaticResource FieldLabel}">
    Search Column
</TextBlock>
<StackPanel Grid.Row="1"
            Grid.Column="1"
            Style="{StaticResource ValidationStackPanel}">
    <ComboBox x:Name="cmbSearchColumn"
              DisplayMemberPath="MemberName"
              IsEditable="True"
              ItemsSource="{Binding SearchColumns}"
              SelectedValuePath="MemberValue"
              Style="{StaticResource ComboBoxStyle}">
        <ComboBox.SelectedItem>
            <Binding Mode="TwoWay"
                     Path="SelectedColumn}"
                     UpdateSourceTrigger="Explicit">
                <Binding.ValidationRules>
                    <helpers:NotEmptyStringValidationRule 
  Message="Search Column cannot be blank." ValidatesOnTargetUpdated="True" />
                </Binding.ValidationRules>
            </Binding>
        </ComboBox.SelectedItem>
    </ComboBox>
    <TextBlock Style="{StaticResource FieldLabelError}" 
  Text="{Binding (Validation.Errors)[0].ErrorContent, ElementName=cmbSearchColumn}" />
</StackPanel>
<TextBlock Grid.Row="2"
           Grid.Column="0"
           Padding="0 0 9 9"
           Style="{StaticResource FieldLabel}">
    Search Value
</TextBlock>
<StackPanel Grid.Row="1"
            Grid.Column="1"
            Style="{StaticResource ValidationStackPanel}">
    <TextBox x:Name="txtSearchValue" Style="{StaticResource FieldTextBox}">
        <TextBox.Text>
            <Binding Mode="TwoWay"
                     Path="SearchValue"
                     UpdateSourceTrigger="Explicit">
                <Binding.ValidationRules>
                    <helpers:NotEmptyStringValidationRule 
  Message="Search Value cannot be blank." ValidatesOnTargetUpdated="True" />
                </Binding.ValidationRules>
            </Binding>
        </TextBox.Text>
    </TextBox>
    <TextBlock Style="{StaticResource FieldLabelError}" 
  Text="{Binding (Validation.Errors)[0].ErrorContent, ElementName=txtSearchValue}" />
</StackPanel>   
<Button Grid.Row="4"
    Grid.Column="1"
    Command="{Binding SearchEmployeesRelayCommand}"
    Content="Search"
    Style="{StaticResource FieldButton}" />

When the app loads, it immediately displays the error next to the fields, saying that they cannot be blank. However, I need to trigger the validation on them only when the user clicks the Search button.

How do I do this? Thanks.

like image 945
Alex Avatar asked Mar 21 '16 15:03

Alex


1 Answers

You can use INotifyDataErrorInfo

Note that INotifyDataErrorInfoworks with custom rules added to the binding. The custom rule and the code for RelayCommand are not included in this answer.

Sample implementation:

using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;

public class PropertyErrors : INotifyDataErrorInfo
{
    private static readonly IReadOnlyList<object> EmptyErrors = new object[0];
    private readonly Action<DataErrorsChangedEventArgs> ownerOnErrorsChanged;
    private readonly Type type;
    private readonly ConcurrentDictionary<string, List<object>> propertyErrors = new ConcurrentDictionary<string, List<object>>();

    public PropertyErrors(INotifyDataErrorInfo owner, Action<DataErrorsChangedEventArgs> ownerOnErrorsChanged)
    {
        this.ownerOnErrorsChanged = ownerOnErrorsChanged;
        this.type = owner.GetType();
    }

    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    public bool HasErrors => this.propertyErrors.Count > 0;

    public IEnumerable GetErrors(string propertyName)
    {
        Debug.Assert(this.type.GetProperty(propertyName) != null, $"The type {this.type.Name} does not have a property named {propertyName}");
        List<object> errors;
        return this.propertyErrors.TryGetValue(propertyName, out errors)
            ? errors
            : EmptyErrors;
    }

    public void Add(string propertyName, object error)
    {
        Debug.Assert(this.type.GetProperty(propertyName) != null, $"The type {this.type.Name} does not have a property named {propertyName}");
        this.propertyErrors.AddOrUpdate(
            propertyName,
            _ => new List<object> { error },
            (_, errors) => UpdateErrors(error, errors));

        this.OnErrorsChanged(new DataErrorsChangedEventArgs(propertyName));
    }

    public void Remove(string propertyName, Predicate<object> filter)
    {
        Debug.Assert(this.type.GetProperty(propertyName) != null, $"The type {this.type.Name} does not have a property named {propertyName}");
        List<object> errors;
        if (this.propertyErrors.TryGetValue(propertyName, out errors))
        {
            errors.RemoveAll(filter);
            if (errors.Count == 0)
            {
                this.Clear(propertyName);
            }
        }
    }

    public void Clear(string propertyName)
    {
        Debug.Assert(this.type.GetProperty(propertyName) != null, $"The type {this.type.Name} does not have a property named {propertyName}");
        List<object> temp;
        if (this.propertyErrors.TryRemove(propertyName, out temp))
        {
            this.OnErrorsChanged(new DataErrorsChangedEventArgs(propertyName));
        }
    }

    protected virtual void OnErrorsChanged(DataErrorsChangedEventArgs e)
    {
        this.ErrorsChanged?.Invoke(this, e);
        this.ownerOnErrorsChanged(e);
    }

    private static List<object> UpdateErrors(object error, List<object> errors)
    {
        if (!errors.Contains(error))
        {
            errors.Add(error);
        }

        return errors;
    }
}
using System;
using System.Collections;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Input;

public class ViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
    private readonly PropertyErrors errors;
    private string searchText;

    public ViewModel()
    {
        this.SearchCommand = new RelayCommand(this.Search);
        this.errors = new PropertyErrors(this, this.OnErrorsChanged);
    }

    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    public event PropertyChangedEventHandler PropertyChanged;

    public string SearchText
    {
        get { return this.searchText; }
        set
        {
            if (value == this.searchText)
            {
                return;
            }

            this.searchText = value;
            this.errors.Clear(nameof(this.SearchText));
            this.OnPropertyChanged();
        }
    }

    public bool HasErrors => this.errors.HasErrors;

    public ICommand SearchCommand { get; }

    public IEnumerable GetErrors(string propertyName) => this.errors.GetErrors(propertyName);

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private void Search()
    {
        if (string.IsNullOrEmpty(this.searchText))
        {
            this.errors.Add(nameof(this.SearchText), "Search text cannot be empty");
            return;
        }

        MessageBox.Show("searching");
    }

    protected virtual void OnErrorsChanged(DataErrorsChangedEventArgs e)
    {
        this.ErrorsChanged?.Invoke(this, e);
    }
}
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>
    <TextBlock Grid.Row="0"
               Grid.Column="0"
               Text="Search text" />
    <TextBox x:Name="SearchTextBox"
             Grid.Row="0"
             Grid.Column="1">
        <TextBox.Text>
            <Binding Path="SearchText"
                     UpdateSourceTrigger="PropertyChanged">
                <Binding.ValidationRules>
                    <local:MustStartWithValidationRule StartsWith="a" />
                </Binding.ValidationRules>
            </Binding>
        </TextBox.Text>
    </TextBox>
    <ItemsControl Grid.Row="0"
                  Grid.Column="2"
                  Margin="6,0,0,0"
                  ItemsSource="{Binding Path=(Validation.Errors),
                                        ElementName=SearchTextBox}">
        <ItemsControl.ItemTemplate>
            <DataTemplate DataType="{x:Type ValidationError}">
                <TextBlock Foreground="Red"
                           Text="{Binding ErrorContent}" />
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>

    <Button Grid.Row="1"
            Grid.Column="1"
            Command="{Binding SearchCommand}"
            Content="Search" />
</Grid>
like image 194
Johan Larsson Avatar answered Nov 14 '22 10:11

Johan Larsson