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.
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>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With