Friday, December 8, 2017

WPF Password control with bindable text

If you have included a password control in a MVVM solution you know that Microsoft has not made the password property a dependency property. This makes the password control pretty much useless.

It occurred to me that a regular text box control with a custom font would work as long as I prevented cut and paste operations on it. For the purpose of this demonstration I also limit the password to digits because that's the requirement I have to satisfy for my current project.

Let's start off by creating a new WPF application project called "Password".


I got my font from https://github.com/davidagraf/passwd/blob/master/public/ttf/password.ttf, thank you David. Create a folder in the project called Fonts and drop the ttf file into the folder. Make sure  Build Action = Resource and Copy to Output Directory = Do not copy.

I'm going to show you a window with several components. The interesting one is the TextBox. The FontFamily path is important to get right. The PreviewExecuted event handler suppresses edit keyboard shortcuts, the PreviewKeyDown suppresses non digit key presses, and the ContextMenu suppresses the text box's default context sensitive menu (because it contains shortcuts to edit commands).

<Window x:Class="Password.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:Password"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <StackPanel Orientation="Horizontal" Height="20">
        <TextBlock Text="Password: "/>
        <TextBox FontFamily="pack://application:,,,/Fonts/#Password" Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}"
                 Width="100" CommandManager.PreviewExecuted="TextBox_PreviewExecuted" PreviewKeyDown="TextBox_PreviewKeyDown"
                 ContextMenu="{x:Null}" MaxLength="6"/>
        <TextBlock Text="{Binding Password}" Margin="10,0,0,0"/>
    </StackPanel>
</Window>


It's very important we include the font in the application. If we depended on the user having the font installed, and they didn't, then the password would appear in clear text.

The code behind implements INotifyPropertyChanged and defines a password property. It also contains the event handlers referenced in the XAML.

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;

namespace Password
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private String _Password;
        public String Password
        {
            get { return _Password; }
            set
            {
                if (!value.Equals(_Password))
                {
                    _Password = value;
                    NotifyPropertyChanged("Password");
                }
            }
        }

        public MainWindow()
        {
            InitializeComponent();
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyPropertyChanged(String propertyName = "")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
        private void TextBox_PreviewExecuted(object sender, ExecutedRoutedEventArgs e)
        {
            // Suppress copy and paste
            if (e.Command == ApplicationCommands.Cut ||
                e.Command == ApplicationCommands.Copy ||
                e.Command == ApplicationCommands.Paste)
                e.Handled = true;
        }

        private void TextBox_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            // Allow cursor movement and backspace
            if (e.Key == Key.Back || e.Key == Key.Left || e.Key == Key.Right)
                return;

            String c = e.Key.ToString().Replace("NumPad", "").Replace("D", "");

            // Suppress any keypress that is not a digit
            if (c.Length != 1)
                e.Handled = true;
            else
             if (!char.IsDigit(Convert.ToChar(c)))
                e.Handled = true;
        }
    }
}


No comments:

Post a Comment