验证规则无法使用2个验证规则正确更新

Validation Rule not updating correctly with 2 validation rules

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:

  1. Type A - Works, Displays nothings
  2. Hit Backspace for empty string - Works, displays validation error message "Please enter a value in all fields"
  3. Type ! - Does not work - It should display "Invalid characters were found", but still displays "Please enter a value in all fields."
  4. Backspace to empty string- Technically works because it still displays first error "Please enter a value in all fields."
  5. Type A - Works, no error message
  6. Type ! - Fine, displays message "Invalid characters were found.

Same happens the other way round.

Open Window

  1. Type ! - Fine, displays "Invalid characters were found.
  2. 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 the tbNameValidation to the following:

<TextBlock x:Name="tbNameValidation"  
           Text="{Binding ElementName=txtBoxProviderName, 
                  Path=(Validation.Errors).CurrentItem.ErrorContent}">

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 of txtBoxProviderName:

 <Binding ElementName="This" 
          Path="ProviderName" 
          UpdateSourceTrigger="PropertyChanged"
          Mode="TwoWay">

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 the Command property instead:

in your DataContext, create a property of the type ICommand, and bind the button to this:

<Button Command="{Binding SaveCommand}" .... />

private ICommand saveCommand; 
public ICommand SaveCommand { 
    get { 
        if (saveCommand == null) {
            saveCommand = new RelayCommand(p => DoSave(), p=> CanSave() ); 
         } 
         return saveCommand; 
      } 
 }

and the CanSave implementation can check if all expectations are met:

private bool CanSave(){
   if (!string.IsNullOrEmpty(ProviderName)){
       //.... etc
   }
 }

The CanSave() will determine the state of your button, because the CommandBinding will automatically take care of enabling the button according to the CanExecute 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

Thank you so much. It works great now. You already answered what was going to be my second question. To have the Rule Validation work from the beginning, because I have a Save button that is not enabled if a Rule Validation fails. The Mode="TwoWay" however does not do that. Isn't Two-Way the default mode anyway?
you can use the Validation.HasError property to control the state of the Save-button, either through Binding or through a Trigger
Please see Edit 1 on post, too short for all the info here. Thanks
see edit, hope this helps you. Validation has always been a tricky thing in WPF imho...
See edit 2, Thanks