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 INotifyDataErrorInfo
works 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