user2426627 user2426627 - 1 month ago 8
C# Question

Passwordbox Validation.ErrorTemplate not displayed

I am trying to show Validation.ErrorTemplate of the passwordBox. However, it is not showing. On the same form I have a username textbox and the error template
is displayed correctly on that control.

Below is the Xaml for the password in a datatempalte.

<PasswordBox Grid.Row="3" DataContext="{Binding Path=DataContext, RelativeSource={RelativeSource AncestorType=ContentControl}}">
<PasswordBox.Style>
<Style>
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<DockPanel LastChildFill="True">
<TextBlock DockPanel.Dock="Right" Foreground="Red" FontSize="14" FontWeight="Bold">*</TextBlock>
<Border BorderBrush="Red" BorderThickness="1">
<AdornedElementPlaceholder></AdornedElementPlaceholder>
</Border>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</PasswordBox.Style>
<i:Interaction.Behaviors>
<behavior:PasswordBoxBehaviorBinding SPassword="{Binding Path=Password, ValidatesOnNotifyDataErrors=True}" />
</i:Interaction.Behaviors>
</PasswordBox>


Below is the attached property I am using.

public class PasswordBoxBehaviorBinding : Behavior<PasswordBox>
{
public SecureString SPassword
{
get
{
return (SecureString)GetValue(PasswordProperty);
}
set
{
SetValue(PasswordProperty, value);
}
}
public static readonly DependencyProperty PasswordProperty = DependencyProperty.Register(
"SPassword",
typeof(SecureString),
typeof(PasswordBoxBehaviorBinding),
new PropertyMetadata(null));

protected override void OnAttached()
{
AssociatedObject.PasswordChanged += AssociatedObject_PasswordChanged;
base.OnAttached();
}

protected override void OnDetaching()
{
AssociatedObject.PasswordChanged += AssociatedObject_PasswordChanged;
base.OnDetaching();
}
private void AssociatedObject_PasswordChanged(object sender, System.Windows.RoutedEventArgs e)
{
var binding = BindingOperations.GetBindingExpression(this, PasswordProperty);
if ( binding != null )
{
if ( binding.ResolvedSource != null )
{
PropertyInfo property = binding.ResolvedSource.GetType().GetProperty(binding.ParentBinding.Path.Path);
if (property != null)
{
property.SetValue(binding.ResolvedSource, AssociatedObject.SecurePassword);
}
}
}
}
}


I implemented INotifyDataError interface in the base view model.

public class ViewModelBase : BindableBase, INotifyDataErrorInfo
{
private IDictionary<string, List<string>> errors = new Dictionary<string, List<string>>();

public bool HasErrors
{
get
{
return this.errors.Count > 0;
}
}

public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

public IEnumerable GetErrors(string propertyName)
{
if ( this.errors.ContainsKey(propertyName) )
{
return this.errors[propertyName];
}
return null;
}

public void AddError(string propertyName, string error)
{
this.errors[propertyName] = new List<string> { error };
this.RaiseErrorsChanged(propertyName);
}

public void RemoveError(string propertyName)
{
if ( this.errors.ContainsKey(propertyName) )
{
this.errors.Remove(propertyName);
}
this.RaiseErrorsChanged(propertyName);
}

private void RaiseErrorsChanged(string propertyName)
{
if ( this.ErrorsChanged != null )
{
this.ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
}
}

Answer

The problem is, that the errors are raised on the DependencyObject that hosts the data-bound properties where the validation errors occurs. In your case <behavior:PasswordBoxBehaviorBinding SPassword="{Binding Path=Password, ValidatesOnNotifyDataErrors=True}" /> means, that you can read your errors inside the behavior.

At that point, I also want to advise against the strange hack that you do to the binding of SPassword. Just set the value normally:

private void AssociatedObject_PasswordChanged(object sender, System.Windows.RoutedEventArgs e)
{
    SPassword = AssociatedObject.SecurePassword;
    // use debugger to verify, that the validation errors exist. Otherwise, no need for the following line of code
    var behaviorErrors = Validation.GetErrors(this);
}

Unfortunately, I haven't found, how to promote the Validation.Errors from the attached behavior to the host control in an elegant way. So basically, your options would be to somehow chain-bind the errors from the behavior to the passwordbox or to create an extra binding to your property, since this binding will use the same validation mechanism and thus set the Validation.Errors on the PasswordBox. I decided to bind the viewmodel Password to PasswordBox.Tag for error propagation purposes.

<PasswordBox Width="200" Height="100" Tag="{Binding Password,ValidatesOnNotifyDataErrors=True,Mode=OneWay}">
    <i:Interaction.Behaviors>
        <behavior:PasswordBoxBehaviorBinding SPassword="{Binding Password}"/>
    </i:Interaction.Behaviors>
</PasswordBox>

Note, that I removed the binding error validation from the binding in behavior, because it's not useful anyway and I added the binding error validation for the Tag binding.

One more thing: I changed the SPassword property to bind twoway by default:

public static readonly DependencyProperty PasswordProperty = DependencyProperty.Register(
    "SPassword",
    typeof(SecureString),
    typeof(PasswordBoxBehaviorBinding),
    new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

Otherwise, make sure to set the binding mode appropriately.