I Have looked through some posts regarding Validation Rules but havn't come across the problem I am experiencing.
I am using a Validation Rule for a Textbox in a WPF Window. I have two checks, one for empty Textbox, and one for invalid characters using a RegEx match.
My problem is such:
In my Textbox:
- Type A - Works, Displays nothings
- Hit Backspace for empty string - Works, displays validation error message "Please enter a value in all fields"
- Type ! - Does not work - It should display "Invalid characters were found", but still displays "Please enter a value in all fields."
- Backspace to empty string- Technically works because it still displays first error "Please enter a value in all fields."
- Type A - Works, no error message
- Type ! - Fine, displays message "Invalid characters were found.
Same happens the other way round.
Open Window
- Type ! - Fine, displays "Invalid characters were found.
- Backspace to empty string - Still displays "Invalid characters were found" instead of "Please enter a value in all fields.
My code as follows:
ProviderNew.xaml:
<Label Name="lblProviderName" Content="Provider Name: "
Grid.Row="1" Grid.Column="0"/>
<TextBox Name="txtBoxProviderName" Margin="2"
MaxLength="20" CharacterCasing="Upper"
Grid.Row="1" Grid.Column="1" LostFocus="TxtBoxProviderNameLostFocus" TextChanged="TxtBoxProviderNameTextChanged">
<TextBox.Text>
<Binding ElementName="This" Path="ProviderName" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<Domain:ProviderValidation />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<TextBlock x:Name="tbNameValidation" Foreground="Red" FontWeight="Bold" Margin="10,0,0,0"
Text="{Binding ElementName=txtBoxProviderName, Path=(Validation.Errors), Converter={StaticResource eToMConverter}}"
Grid.Row="1" Grid.Column="2" />
Privder code behind - ProviderNew.xaml.cs
public static readonly DependencyProperty NewProviderNameProperty = DependencyProperty.Register("ProviderName",
typeof (string), typeof(ProviderNew), new UIPropertyMetadata(""));
public string ProviderName
{
get { return (string) GetValue(NewProviderNameProperty); }
set { SetValue(NewProviderNameProperty, value); }
}
Value Converter Class
public class ErrorsToMessageConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var sb = new StringBuilder();
var errors = value as ReadOnlyCollection<ValidationError>;
if (errors != null && errors.Count > 0)
{
foreach (var e in errors.Where(e => e.ErrorContent != null))
{
sb.AppendLine(e.ErrorContent.ToString());
}
}
return sb.ToString();
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Provider Class
private string _providerName;
public string ProviderName
{
get { return _providerName; }
set
{
_providerName = value;
RaisePropertyChanged("ProviderName");
}
}
private void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Validation Rule class
public class ProviderValidation : ValidationRule
{
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
var str = value as string;
if (str != null && !Regex.IsMatch(str, @"^[a-zA-Z0-9]*$"))
{
return new ValidationResult(false, "Invalid characters were found.");
}
if (String.IsNullOrEmpty(str))
{
return new ValidationResult(false, "Please enter a value in all fields.");
}
return new ValidationResult(true, null);
}
}
What I have tried is setting both LostFocus and TextChanged events to force update using:
var expression = txtBoxProviderName.GetBindingExpression(TextBox.TextProperty);
if (expression != null)
expression.UpdateSource();
Setting breakpoints on the Validate method shows that the correct matches are done and the correct ValidationResult is returned, but it does not update the text correctly.
Am I doing something incredibly silly?
Any suggestions will be appreciated.
Edit 1.
Yeah, I have that working, using MultiDataTrigger and Binding to the textboxes.
What doesn't work is when I first show the Window, the button is Enabled, which I don't want it to be because this could allow the user to click save with empty textboxes.
The Validation Rule doesn't work straight from the beginning when the Window opens.
I set the focus on the textbox and if it loses focus or incorrect data is enter, then The validation rule kicks in and disables the button.
Setting the button to be disabled by default, makes it disabled on opening, but then it isn't enabling when there are no Validation errors.
I can make it work by forcing a check of the Validation Rule on say the Load event, using
var expression = txtBoxProviderName.GetBindingExpression(TextBox.TextProperty);
if (expression != null)
expression.UpdateSource();
But then when the Window first opens, it immediately shows the Validation Error message "Please enter a value in all fields", which I don't really like the look of.
Any way round this, or can I not have the best of both worlds.
Here is the Button code
<Button x:Name="btnSave" Height="25" Width="100" Content="Save" HorizontalAlignment="Right" Margin="0,0,120,0"
Grid.Row="2" Click="BtnSaveClick" >
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="IsEnabled" Value="False" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding ElementName=txtBoxProviderName, Path=(Validation.HasError)}" Value="False" />
<Condition Binding="{Binding ElementName=txtBoxHelpDesk, Path=(Validation.HasError)}" Value="False" />
<Condition Binding="{Binding ElementName=txtBoxRechargeInstructions, Path=(Validation.HasError)}" Value="False" />
</MultiDataTrigger.Conditions>
<Setter Property="IsEnabled" Value="True" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
Thanks,
Neill
Edit 2
A quick question. I couldn't find the RelayCommand namespace. Searching other code around I found an MVVM example from microsoft that implements a RelayCommand : ICommand class.
Is this correct?
The code is:
public class RelayCommand : ICommand
{
#region Fields
readonly Action<object> _execute;
readonly Predicate<object> _canExecute;
#endregion // Fields
#region Constructors
/// <summary>
/// Creates a new command that can always execute.
/// </summary>
/// <param name="execute">The execution logic.</param>
public RelayCommand(Action<object> execute)
: this(execute, null)
{
}
/// <summary>
/// Creates a new command.
/// </summary>
/// <param name="execute">The execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute;
}
#endregion // Constructors
#region ICommand Members
[DebuggerStepThrough]
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter)
{
_execute(parameter);
}
#endregion // ICommand Members
}
I implemented the following in my ProviderNew.xaml.cs:
private ICommand saveCommand;
public ICommand SaveCommand
{
get
{
if (saveCommand == null)
{
saveCommand = new RelayCommand(p => DoSave(), p => CanSave() );
}
return saveCommand;
}
}
private bool CanSave()
{
if (!string.IsNullOrEmpty(ProviderName))
{
??? What goes here?
}
return true;
}
private bool DoSave()
{
// Save the information to DB
}
To be honest I am unsure of what should be coded in the block in the 'if (!string.IsNullOrEmpty(ProviderName))'
Also you said to add the ICommand code in the DataContext, so not sure if it is in the right place because When I open the Window, Save is enable and clicking it does nothing, even if all the fields have correct data in it.
Here is the ProviderNew xaml code for the button
<Button x:Name="btnSave" Height="25" Width="100" Content="Save" HorizontalAlignment="Right" Margin="0,0,120,0"
Grid.Row="2" Command="{Binding SaveCommand}" >
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="IsEnabled" Value="False" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding ElementName=txtBoxHelpDesk, Path=(Validation.HasError)}" Value="False" />
<Condition Binding="{Binding ElementName=txtBoxProviderName, Path=(Validation.HasError)}" Value="False" />
<Condition Binding="{Binding ElementName=txtBoxRechargeInstructions, Path=(Validation.HasError)}" Value="False" />
</MultiDataTrigger.Conditions>
<Setter Property="IsEnabled" Value="True" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
Your help is very much appreciated.
Regards,
Neill
Ok I managed to reproduce this problem, and it was bugging me that it wasn't working. But I figured it out.
The problem is that the converter is not being fired when the error changes, because the actual property (ProviderName) does not change due to the ValidationRule. You can see this behavior if you put a breakpoint in the Converter, the ValidationRule and the Property-setter.
So instead of using an
IValueConverter
, you should change the binding of thetbNameValidation
to the following:the important part being
Path=(Validation.Errors).CurrentItem.ErrorContent"
. This will ensure that the current error message is shown.Also, if you would like the "Please enter a value in all fields." message to show from the beginning, you should add
Mode=TwoWay
to the binding oftxtBoxProviderName
:EDIT for part2
to fix the enabled property of the button, you can use the same code as you have in your question, but instead of using the
Click
event to run your save-code, you can use theCommand
property instead:in your DataContext, create a property of the type
ICommand
, and bind the button to this:and the CanSave implementation can check if all expectations are met:
The
CanSave()
will determine the state of your button, because the CommandBinding will automatically take care of enabling the button according to theCanExecute
method of the bounded command. The logic of your Validator will come back here though, so it might be a good idea to find a way to reuse that code.more information about Relaying Commands