There are several threads out there about error validation. However, if it comes to put things together for my code, I fail. I am way of from understanding WPF. Thus, your help, advise and review is much appreciated.
Edit: Some edits have been done, after thinking about @EldHasp comments. I will mark them, so they are visible.
Threads like this do give me an idea what should be done:
If an error is validated in the PasswordBox
the error message fails to show and the red box is showing up, too. I am validating on the property Password
being empty.
Edit: As explained in the comment, I was storing the password in a string
. After reading the related threads, again, I have changed that and replaced the type string
by the type SecureString
. The ViewModel's property UiPassword is now a SecureString
. And the property Password in the BindablePasswordBox
is of this type, also. The code behind file of BindablePasswordBox
is adjusted to handle these changes. The current situation is shown in the new screenshot of the login page. There is no visible error validation at all on the password. However, this is still what I want to achieve - while using PasswordBox
and SecureString
.
If an error is validated on the TextBox
the error message shows up and the red box is not showing up. If at least one character is entered into the TextBox
the error message disappears. I am validating on the property Text
being empty.
My current issue showed up, when I replaced the TextBox
to enter a password by a PasswordBox
. Then everything that worked with the TextBox
does not work at all. See this implementation on my UserControl
(unabridged code below):
<TextBox Grid.Column="2" Grid.Row="0"
x:Name="txtUserName"
MinWidth="200"
Text="{Binding UiUserName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" />
<!-- Password components; TODO - the DataErrors are not displayed properly -->
<Label Grid.Column="1" Grid.Row="1" Content="Password" />
<components:BindablePasswordBox Grid.Column="2" Grid.Row="1"
x:Name="txtPassword"
MinWidth="200"
Password="{Binding UiPassword, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" />
App.xaml
<Application x:Class="PaperDeliveryWpf.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PaperDeliveryWpf">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/ShellBodyStyle.xaml" />
<ResourceDictionary Source="Resources/ShellFooterStyle.xaml" />
<ResourceDictionary Source="Resources/ShellHeaderStyle.xaml" />
<ResourceDictionary Source="Resources/ShellTitleStyle.xaml" />
<ResourceDictionary Source="Resources/TextBoxStyle.xaml" />
<ResourceDictionary Source="Resources/PasswordBoxStyle.xaml" />
<ResourceDictionary Source="Resources/ButtonStyle.xaml" />
<ResourceDictionary Source="Resources/DataTemplate.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
DataTemplate.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:usercontrols="clr-namespace:PaperDeliveryWpf.UserControls"
xmlns:viewmodels="clr-namespace:PaperDeliveryWpf.ViewModels">
<DataTemplate DataType="{x:Type viewmodels:LoginViewModel}">
<usercontrols:LoginUserControl />
</DataTemplate>
</ResourceDictionary>
BindablePasswordBox.xaml
<UserControl x:Class="PaperDeliveryWpf.UserControls.Components.BindablePasswordBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:PaperDeliveryWpf.UserControls.Components"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<PasswordBox x:Name="passwordBox" PasswordChanged="BindablePasswordBox_PasswordChanged"/>
</UserControl>
PasswordBoxStyle.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="PasswordBox">
<!-- Sets basic look of the PasswordBox -->
<Setter Property="Padding" Value="2 1" />
<Setter Property="BorderBrush" Value="LightGray" />
<!-- Removes the red border around the PasswordBox, if validation found errors -->
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<AdornedElementPlaceholder />
</ControlTemplate>
</Setter.Value>
</Setter>
<!-- Enables the UI to show the error messages -->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<StackPanel>
<Border Padding="{TemplateBinding Padding}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="3">
<ScrollViewer x:Name="PART_ContentHost" />
</Border>
<ItemsControl ItemsSource="{TemplateBinding Validation.Errors}" Margin="0 5 0 5">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Foreground="Red" Text="{Binding ErrorContent}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
BindablePasswordBox.xaml.cs
(this is updated code; to see the original content, please refer to the history of this thread)
using System.Security;
using System.Windows;
using System.Windows.Controls;
namespace PaperDeliveryWpf.UserControls.Components;
public partial class BindablePasswordBox : UserControl
{
public SecureString Password
{
get { return (SecureString)GetValue(PasswordProperty); }
set { SetValue(PasswordProperty, value); }
}
public static readonly DependencyProperty PasswordProperty =
DependencyProperty.Register("Password", typeof(SecureString), typeof(BindablePasswordBox));
public BindablePasswordBox()
{
InitializeComponent();
passwordBox.PasswordChanged += BindablePasswordBox_PasswordChanged;
}
private void BindablePasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
{
Password = passwordBox.SecurePassword;
}
}
LoginUserControl.xaml
<UserControl x:Class="PaperDeliveryWpf.UserControls.LoginUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:PaperDeliveryWpf.UserControls"
xmlns:viewModels="clr-namespace:PaperDeliveryWpf.ViewModels"
xmlns:components="clr-namespace:PaperDeliveryWpf.UserControls.Components"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=viewModels:LoginViewModel}"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="boolToVisibility" />
</UserControl.Resources>
<DockPanel>
<!-- Header -->
<Label DockPanel.Dock="Top" Content="Login Page" HorizontalAlignment="Center" FontSize="26" FontWeight="Bold" Margin="20" />
<!-- Body -->
<StackPanel DockPanel.Dock="Top">
<Grid HorizontalAlignment="Center" FocusManager.FocusedElement="{Binding ElementName=txtUserName}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- UserName components -->
<Label Grid.Column="1" Grid.Row="0" Content="User Name" />
<TextBox Grid.Column="2" Grid.Row="0"
x:Name="txtUserName"
MinWidth="200"
Text="{Binding UiUserName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" />
<!-- Password components; TODO - the DataErrors are not displayed properly -->
<Label Grid.Column="1" Grid.Row="1" Content="Password" />
<components:BindablePasswordBox Grid.Column="2" Grid.Row="1"
x:Name="txtPassword"
MinWidth="200"
Password="{Binding UiPassword, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" />
<!-- Optional design; this is not hiding the entered values in the textbox -->
<!--<TextBox Grid.Column="2" Grid.Row="1"
x:Name="txtPassword"
MinWidth="200"
Text="{Binding UiPassword, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" />-->
</Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Content="Login" Command="{Binding LoginButtonCommand}" />
<Button Content="Cancel" Command="{Binding CancelButtonCommand}" />
</StackPanel>
</StackPanel>
<!-- Footer -->
</DockPanel>
</UserControl>
ShellView.xaml
<Window x:Class="PaperDeliveryWpf.Views.ShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:PaperDeliveryWpf.Views"
xmlns:viewmodels="clr-namespace:PaperDeliveryWpf.ViewModels"
xmlns:userControls="clr-namespace:PaperDeliveryWpf.UserControls"
Title="ShellView" Height="450" Width="800"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=viewmodels:ShellViewModel}">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="boolToVisibility" />
</Window.Resources>
<Grid>
<!-- *** The main structure of the window is: Titel(Row=0), Header(Row=1), Body(Row=2) and Footer(Row=3) *** -->
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- *** The UI's body *** -->
<ScrollViewer Grid.Row="2" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<!-- *** The ShellViewModel is hosting the logic, which UserControl shall be loaded *** -->
<StackPanel Grid.Column="1">
<ContentControl Content="{Binding CurrentView, ValidatesOnNotifyDataErrors=False}" />
</StackPanel>
</Grid>
</ScrollViewer>
</Grid>
</Window>
In my opinion, you approached the implementation of the task in a fundamentally wrong way. PasswordBox is designed for SECURE password management. You extract it into a string and thereby destroy all security. Judging by your implementation, you only need to MASK the password input. And there is no need to work safely with it. In this case, you just need to replace the font in the TextBox and all your problems will be solved automatically.
An example of such an implementation:
namespace PasswordFont.ViewModels
{
public class LauncherViewModel
{
public string? Password { get; set; }
}
}
<Window.Resources>
<vms:LauncherViewModel x:Key="viewModel" Pin="123456789"/>
<FontFamily x:Key="passowrdFont">pack://application:,,,/Resources/Fonts/#password</FontFamily>
</Window.Resources>
<Window x:Class="PasswordFont.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:PasswordFont"
xmlns:vms="clr-namespace:PasswordFont.ViewModels"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800"
FontSize="30">
<Window.Resources>
<vms:LauncherViewModel x:Key="viewModel" Password="123456789"/>
<FontFamily x:Key="passowrdFont">pack://application:,,,/Resources/Fonts/#password</FontFamily>
<ImageSource x:Key="open">pack://application:,,,/Resources/Icons/eye_open_icon.png</ImageSource>
<ImageSource x:Key="closed">pack://application:,,,/Resources/Icons/eye_closed_icon.png</ImageSource>
<DataTemplate x:Key="imageDataTemplate" DataType="ImageSource">
<Image Source="{Binding}"/>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid VerticalAlignment="Center" HorizontalAlignment="Center"
Width="200">
<TextBox x:Name="pinBox" Text="{Binding Pin, UpdateSourceTrigger=PropertyChanged}">
<TextBox.Style>
<Style TargetType="TextBox">
<Style.Triggers>
<DataTrigger Binding="{Binding IsChecked, ElementName=passwordHide}"
Value="True">
<Setter Property="FontFamily" Value="{DynamicResource passowrdFont}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
<ToggleButton HorizontalAlignment="Right"
x:Name="passwordHide"
IsChecked="True"
ContentTemplate="{DynamicResource imageDataTemplate}" Height="{Binding ActualHeight, ElementName=pinBox, Mode=OneWay}">
<FrameworkElement.Resources>
<Style TargetType="ToggleButton">
<Setter Property="ToolTip" Value="Показать"/>
<Setter Property="Content" Value="{DynamicResource open}"/>
<Style.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="ToolTip" Value="Скрыть"/>
<Setter Property="Content" Value="{DynamicResource closed}"/>
</Trigger>
</Style.Triggers>
</Style>
</FrameworkElement.Resources>
</ToggleButton>
</Grid>
</Grid>
</Window>
Project source codes: PasswordFont.7z
Topic with explanation (I warn you - in Russian): Implementation of a TextBox with the Hide/Show Password function [WPF, Eld Hasp]
On the merits of the question you asked.
"Catching" errors is the logic of bindings.
You have implemented a binding of the ViewModel.UiPassword property to the BindablePasswordBox.Password property. Therefore the error will be shown in the BindablePasswordBox. In PasswordBox you pass a value by direct explicit assignment (SetValue) passwordBox.Password = Password;
. Since there is no binding used in this assignment, passwordBox
will not receive an error message.
But you control the display of the error, specifically for passwordBox
.
Solution:
The best option, as I described above, is to use a TextBox with a masking font.
If it is fundamentally important for you to correct your implementation, then the style <Style TargetType="PasswordBox">
should be set not for PasswordBox, but for your UserControl <Style TargetType="components:BindablePasswordBox">
.
I found this thread on PasswordBox: [How to bind to a PasswordBox in MVVM] (stackoverflow.com/a/45422711/20737189). Quite a lot to think about.
This is also a VERY bad approach. The whole point of using PassworBox is to avoid string and work exclusively with SecureString. And this needs to be done at all levels - starting from View, passing through ViewModel, Model, Repository and ending in the Database Server. Using string for a password anywhere completely destroys the whole concept of using PassworBox. In this case, you should use a TextBox with a masking font. An example of working with PassworBox with encrypted password transmission can be found in my topic here (sorry, but again in Russian): Attached Properties for PasswordBox: Password binding and secure password handling
If you cannot understand the meaning of the article with a translator, I can translate it into English, but I’m afraid it won’t be possible to post such a translation on Stackoverflow. They don't like general topics here.