Задача — «создать поле ввода пароля с возможностью показать или скрыть пароль». Требований к безопасности нет, поэтому работу с паролем можно вести в строковом виде.
На данный момент эту проблему можно решить довольно просто, используя шрифт с одним символом маски в TextBox. При срабатывании обычного CheckBox шрифт заменяется маскирующим шрифтом или наоборот. Но такой маскирующий шрифт необходимо подготовить заранее.
Но недавно дали дополнительное условие «возможность смены маскирующего символа». Самый простой способ изменить символ — в PasswordBox. Но у него нет возможности отключить маскирование и для привязки пароля необходимо использовать дополнительный код. На данный момент нам приходится использовать решение, в котором TextBox используется в режиме «без маскировки», а PasswordBox — в режиме «с маскированием».
Можете ли вы посоветовать что-то более оптимальное?





Я думаю, что хорошее и простое решение — использовать декоратор для визуализации маскирующих символов как наложение исходного ввода.
Покажите декоратору, чтобы он отображал маскирующие символы, скрывая пароль, установив кисть переднего плана на кисть фона.
В следующем примере показано, как использовать Adorner для украшения TextBox.
Я удалил часть кода, чтобы уменьшить сложность (исходный код библиотеки также содержал проверку входных символов и длины пароля, логику событий, маршрутизируемые команды, поддержку SecureString, низкоуровневое позиционирование курсора для поддержки любого семейства шрифтов в тех случаях, когда шрифт не моноширинный шрифт, символы пароля и маскирующие символы не выравниваются правильно и т. д., а гораздо более сложный вариант по умолчанию ControlTemplate). Поэтому текущая версия поддерживает только моноширинные шрифты. Поддерживаемые шрифты необходимо зарегистрировать в конструкторе. Вместо этого вы можете реализовать обнаружение моноширинных шрифтов.
Однако это полностью рабочий пример (с Пасхой!).
UnsecurePasswodBox.cs
public class UnsecurePasswodBox : TextBox
{
public bool IsShowPasswordEnabled
{
get => (bool)GetValue(IsShowPasswordEnabledProperty);
set => SetValue(IsShowPasswordEnabledProperty, value);
}
public static readonly DependencyProperty IsShowPasswordEnabledProperty = DependencyProperty.Register(
"IsShowPasswordEnabled",
typeof(bool),
typeof(UnsecurePasswodBox),
new FrameworkPropertyMetadata(default(bool), OnIsShowPasswordEnabledChaged));
public char CharacterMaskSymbol
{
get => (char)GetValue(CharacterMaskSymbolProperty);
set => SetValue(CharacterMaskSymbolProperty, value);
}
public static readonly DependencyProperty CharacterMaskSymbolProperty = DependencyProperty.Register(
"CharacterMaskSymbol",
typeof(char),
typeof(UnsecurePasswodBox),
new PropertyMetadata('●', OnCharacterMaskSymbolChanged));
private FrameworkElement? part_ContentHost;
private AdornerLayer? adornerLayer;
private UnsecurePasswordBoxAdorner? maskingAdorner;
private Brush foregroundInternal;
private bool isChangeInternal;
private readonly HashSet<string> supportedMonospaceFontFamilies;
private FontFamily fallbackFont;
static UnsecurePasswodBox()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(UnsecurePasswodBox),
new FrameworkPropertyMetadata(typeof(UnsecurePasswodBox)));
TextProperty.OverrideMetadata(
typeof(UnsecurePasswodBox),
new FrameworkPropertyMetadata(OnTextChanged));
ForegroundProperty.OverrideMetadata(
typeof(UnsecurePasswodBox),
new FrameworkPropertyMetadata(propertyChangedCallback: null, coerceValueCallback: OnCoerceForeground));
FontFamilyProperty.OverrideMetadata(
typeof(UnsecurePasswodBox),
new FrameworkPropertyMetadata(propertyChangedCallback: null, coerceValueCallback: OnCoerceFontFamily));
}
public UnsecurePasswodBox()
{
this.Loaded += OnLoaded;
// Only use a monospaced font
this.supportedMonospaceFontFamilies = new HashSet<string>()
{
"Consolas",
"Courier New",
"Lucida Console",
"Cascadia Mono",
"Global Monospace",
"Cascadia Code",
};
this.fallbackFont = new FontFamily("Consolas");
this.FontFamily = fallbackFont;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
this.Loaded -= OnLoaded;
FrameworkElement adornerDecoratorChild = this.part_ContentHost ?? this;
this.adornerLayer = AdornerLayer.GetAdornerLayer(adornerDecoratorChild);
if (this.adornerLayer is null)
{
throw new InvalidOperationException("No AdornerDecorator found in parent visual tree");
}
this.maskingAdorner = new UnsecurePasswordBoxAdorner(adornerDecoratorChild, this)
{
Foreground = Brushes.Black
};
HandleInputMask();
}
private static void OnIsShowPasswordEnabledChaged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var unsecurePasswordBox = (UnsecurePasswodBox)d;
unsecurePasswordBox.HandleInputMask();
Keyboard.Focus(unsecurePasswordBox);
}
private static void OnCharacterMaskSymbolChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
=> ((UnsecurePasswodBox)d).RefreshMask();
private static void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
=> ((UnsecurePasswodBox)d).RefreshMask();
private static object OnCoerceForeground(DependencyObject d, object baseValue)
{
var unsecurePasswordBox = (UnsecurePasswodBox)d;
// Reject external font color change while in masking mode
// as this would reveal the password.
// But store new value and make it available when exiting masking mode.
if (!unsecurePasswordBox.isChangeInternal && !unsecurePasswordBox.IsShowPasswordEnabled)
{
unsecurePasswordBox.foregroundInternal = baseValue as Brush;
}
return unsecurePasswordBox.isChangeInternal
? baseValue
: unsecurePasswordBox.IsShowPasswordEnabled
? baseValue
: unsecurePasswordBox.Foreground;
}
private static object OnCoerceFontFamily(DependencyObject d, object baseValue)
{
var unsecurePasswordBox = (UnsecurePasswodBox)d;
var desiredFontFamily = baseValue as FontFamily;
return desiredFontFamily is not null
&& unsecurePasswordBox.supportedMonospaceFontFamilies.Contains(desiredFontFamily.Source)
? baseValue
: unsecurePasswordBox.FontFamily;
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.part_ContentHost = GetTemplateChild("PART_ContentHost") as FrameworkElement;
}
private void HandleInputMask()
{
this.isChangeInternal = true;
if (this.IsShowPasswordEnabled)
{
this.adornerLayer?.Remove(this.maskingAdorner);
ShowText();
}
else
{
HideText();
this.adornerLayer?.Add(this.maskingAdorner);
}
this.isChangeInternal = false;
}
private void ShowText()
=> SetCurrentValue(ForegroundProperty, this.foregroundInternal);
private void HideText()
{
this.foregroundInternal = this.Foreground;
SetCurrentValue(ForegroundProperty, this.Background);
}
private void RefreshMask()
{
if (!this.IsShowPasswordEnabled)
{
this.maskingAdorner?.Update();
}
}
private class UnsecurePasswordBoxAdorner : Adorner
{
public Brush Foreground { get; set; }
private readonly UnsecurePasswodBox unsecurePasswodBox;
private const int DefaultSystemTextPadding = 2;
public UnsecurePasswordBoxAdorner(UIElement adornedElement, UnsecurePasswodBox unsecurePasswodBox) : base(adornedElement)
{
this.IsHitTestVisible = false;
this.unsecurePasswodBox = unsecurePasswodBox;
}
public void Update()
=> InvalidateVisual();
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
var typeface = new Typeface(
this.unsecurePasswodBox.FontFamily,
this.unsecurePasswodBox.FontStyle,
this.unsecurePasswodBox.FontWeight,
this.unsecurePasswodBox.FontStretch,
this.unsecurePasswodBox.fallbackFont);
double pixelsPerDip = VisualTreeHelper.GetDpi(this).PixelsPerDip;
ReadOnlySpan<char> maskedInput = MaskInput(this.unsecurePasswodBox.Text);
var maskedText = new FormattedText(
maskedInput.ToString(),
CultureInfo.CurrentCulture,
this.unsecurePasswodBox.FlowDirection,
typeface,
this.unsecurePasswodBox.FontSize,
this.Foreground,
pixelsPerDip)
{
MaxTextWidth = ((FrameworkElement)this.AdornedElement).ActualWidth + UnsecurePasswordBoxAdorner.DefaultSystemTextPadding,
Trimming = TextTrimming.None
};
var textOrigin = new Point(0, 0);
textOrigin.Offset(this.unsecurePasswodBox.Padding.Left + UnsecurePasswordBoxAdorner.DefaultSystemTextPadding, 0);
drawingContext.DrawText(maskedText, textOrigin);
}
private ReadOnlySpan<char> MaskInput(ReadOnlySpan<char> input)
{
if (input.Length == 0)
{
return input;
}
char[] textMask = new char[input.Length];
Array.Fill(textMask, this.unsecurePasswodBox.CharacterMaskSymbol);
return new ReadOnlySpan<char>(textMask);
}
}
}
Общий.xaml
<Style TargetType = "local:UnsecurePasswodBox">
<Setter Property = "BorderBrush"
Value = "{x:Static SystemColors.ActiveBorderBrush}" />
<Setter Property = "BorderThickness"
Value = "1" />
<Setter Property = "Background"
Value = "White" />
<Setter Property = "Template">
<Setter.Value>
<ControlTemplate TargetType = "local:UnsecurePasswodBox">
<Border Background = "{TemplateBinding Background}"
BorderBrush = "{TemplateBinding BorderBrush}"
BorderThickness = "{TemplateBinding BorderThickness}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width = "Auto" />
</Grid.ColumnDefinitions>
<AdornerDecorator Grid.Column = "0">
<ScrollViewer x:Name = "PART_ContentHost" />
</AdornerDecorator>
<ToggleButton Grid.Column = "1"
IsChecked = "{Binding RelativeSource = {RelativeSource TemplatedParent}, Path=IsShowPasswordEnabled}"
Background = "Transparent"
VerticalContentAlignment = "Center"
Padding = "4,0">
<ToggleButton.Content>
<TextBlock Text = ""
FontFamily = "Segoe MDL2 Assets" />
</ToggleButton.Content>
<ToggleButton.Template>
<ControlTemplate TargetType = "ToggleButton">
<ContentPresenter Margin = "{TemplateBinding Padding}"
VerticalAlignment = "{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment = "{TemplateBinding HorizontalContentAlignment}" />
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Вы можете попробовать еще раз.
Я предлагаю использовать UserControl, который использует исходный PasswordBox и альтернативный TextBox, управляемый ToggleButton. Пользовательский элемент управления XAML:
<UserControl x:Class = "Problem300324A.DualPasswordBox"
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:Problem300324A"
mc:Ignorable = "d"
d:DesignHeight = "450" d:DesignWidth = "800" Name = "Parent">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key = "BolleanToVisibilityConverter" />
<local:InvertBooleanToVisibilityConverter x:Key = "InvertBooleanToVisibilityConverter"/>
<local:ShowHideConverter x:Key = "ShowHideConverter"/>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<PasswordBox Name = "password"
PasswordChanged = "PasswordBox_PasswordChanged"
Visibility = "{Binding ElementName=Parent,Path=ShowPassword,Converter = {StaticResource InvertBooleanToVisibilityConverter}}"
/>
<TextBox Name = "text" Grid.Column = "0"
TextChanged = "text_TextChanged"
Text = "{Binding ElementName=Parent,Path=Password}"
Visibility = "{Binding ElementName=Parent,Path=ShowPassword,Converter = {StaticResource BolleanToVisibilityConverter}}"
/>
<ToggleButton Name = "toggle" Grid.Column = "1"
IsChecked = "{Binding ElementName=Parent,Path=ShowPassword,Mode=TwoWay}" Width = "40"
Content = "{Binding ElementName=Parent,Path=ShowPassword,Converter = {StaticResource ShowHideConverter}}"
/>
</Grid>
</UserControl>
Код позади:
public partial class DualPasswordBox : UserControl
{
public DualPasswordBox()
{
InitializeComponent();
}
public static readonly DependencyProperty ShowPasswordProperty =
DependencyProperty.Register("ShowPassword", typeof(bool), typeof(DualPasswordBox));
public bool ShowPassword
{
get { return (bool)GetValue(ShowPasswordProperty); }
set { SetValue(ShowPasswordProperty, value); }
}
private void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
{
if (text.Text != password.Password)
{
text.Text = password.Password;
text.CaretIndex = text.Text.Length;
}
}
private void text_TextChanged(object sender, TextChangedEventArgs e)
{
if (text.Text != password.Password)
{
password.Password = text.Text;
password
.GetType().
GetMethod("Select", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(password, new object[] { text.Text.Length, 0 });
}
}
}
Используя пример:
<local:DualPasswordBox ShowPassword = "True" Height = "40" />
Естественно, код представляет собой нарушение безопасности, которое необходимо оценить.
На данный момент мы также используем подобное решение. Я об этом написал в своем вопросе. Я считаю это решение неоптимальным и «сложным». Но другой лучшей альтернативы мы пока не нашли.
К сожалению, ваш код содержит ошибки в классе UnsecurePasswordBoxAdorner. Скорее всего они произошли при копировании кода в сообщение. Буду очень признателен, если исправите эти ошибки.