Thursday, December 14, 2017

Bindable password control with limited character sets

In my previous blog I showed how to implement a password control with a bindable text for use in MVVM projects. In this blog, I will show you how to encapsulate this functionality in a user control for easy re-use. I will also show you how to allow the consuming page to specify the type of characters that can be included in the password.

The idea is to take a simple textbox control, which already has a bindable text property, implement a password character set I found, and suppress all cut, copy, and paste operations. In addition we will add some new dependency properties that will control the characters that can be entered. This allows the consumer to have numeric or alpha only password, or different combinations.

Because the password control is simply a textbox control with some extra properties, we can easily implement it as a user control. We don't need the power of a custom control. This means we don't have to worry about template binding everything.

Let's start by creating a new UserControl project called CustomControls. I'm going to write the project in C# and target Framework 4.0.


Rename UserControl1 to BindablePassword.

We will base our control on a TextBox so replace the contents of BindablePassword.xaml with this...

<TextBox x:Class="CustomControls.BindablePassword"
             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"
         FontFamily="pack://application:,,,./Fonts/#Password"
         CommandManager.PreviewExecuted="TextBox_PreviewExecuted"
         PreviewTextInput="TextBox_PreviewTextInput"
         ContextMenu="{x:Null}">
</TextBox>


As you can see, we have specified a font family. We need to grab that font.
Add a folder to the project called Fonts. Get the Password.ttf file from https://github.com/davidagraf/passwd/blob/master/public/ttf/password.ttf and drop it into the Fonts folder. Make sure  Build Action = Resource and Copy to Output Directory = Do not copy.

TextBox_PreviewExecuted will suppress any cut, copy, or paste actions. This prevents someone copying the password from the textbox and pasting it into, say, notepad, thus revealing the password.

TextBox_PreviewTextInput will look at the character being typed and cancel the operation if it isn't the correct type.

ContextMenu prevents the TextBox's context menu being displayed because the default context menu has the Cut, Copy, and Paste commands in it.

Let's look at the code behind. This specifies three new dependency properties that control the types of characters that can be entered. The consumer can specify these properties in its XAML or code behind. As you can see, it would be very easy to define different classes of characters to support.

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace CustomControls
{
    public partial class BindablePassword : TextBox
    {
        static readonly DependencyProperty AllowDigitsProperty;
        static readonly DependencyProperty AllowAlphaProperty;
        static readonly DependencyProperty AllowSpecialProperty;

        public bool AllowDigits
        {
            get { return (bool)GetValue(AllowDigitsProperty); }
            set { SetValue(AllowDigitsProperty, value); }
        }

        public bool AllowAlpha
        {
            get { return (bool)GetValue(AllowAlphaProperty); }
            set { SetValue(AllowAlphaProperty, value); }
        }

        public bool AllowSpecial
        {
            get { return (bool)GetValue(AllowSpecialProperty); }
            set { SetValue(AllowSpecialProperty, value); }
        }

        public BindablePassword()
        {
            InitializeComponent();
        }

        static BindablePassword()
        {
            AllowDigitsProperty = DependencyProperty.Register("AllowDigits", typeof(bool), typeof(BindablePassword), new FrameworkPropertyMetadata(false));
            AllowAlphaProperty = DependencyProperty.Register("AllowAlpha", typeof(bool), typeof(BindablePassword), new FrameworkPropertyMetadata(false));
            AllowSpecialProperty = DependencyProperty.Register("AllowSpecial", typeof(bool), typeof(BindablePassword), new FrameworkPropertyMetadata(false));
        }

        private void TextBox_PreviewExecuted(object sender, ExecutedRoutedEventArgs e)
        {   // Suppress cut, copy, and paste
            if (e.Command == ApplicationCommands.Cut ||
                e.Command == ApplicationCommands.Copy ||
                e.Command == ApplicationCommands.Paste)
                e.Handled = true;
        }

        private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
        {   // Suppress unwanted characters
            String c = e.Text;

            if ("1234567890".Contains(c) && AllowDigits) return;
            if ("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".Contains(c) && AllowAlpha) return;
            if ("~`!@#$%^&*()_-+={[}]|\\:;\"'<,>.?/".Contains(c) && AllowSpecial) return;
            e.Handled = true;
        }
    }
}

At this point the project will build but it won't run because we need a valid startup project type. Add a WPF Application project called TestBindablePassword to the solution.


In the TestBindablePassword project, add a reference to CustomControls. Set TestBindablePassword as the default project. Your solution explorer now looks like this...



We will use the password control three times in our MainWindow, one for digits only, one for alpha only, and one for special characters only. Each password control will have its text property bound and a label to the right will be bound to the same property so you can see the password as you enter it. This would not be a good design in real life!

Change the MainWindow.xaml to look like this...

<Window x:Class="TestBindablePassword.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:TestBindablePassword"
        xmlns:custom="clr-namespace:CustomControls;assembly=CustomControls"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <StackPanel Orientation="Vertical">
        <StackPanel Orientation="Horizontal" Height="22">
            <TextBlock Text="Digits" Width="100"/>
            <custom:BindablePassword Text="{Binding DigitPassword, UpdateSourceTrigger=PropertyChanged}" Width="100" AllowDigits="True"/>
            <TextBlock Text="{Binding Path=DigitPassword}" Margin="20,0,0,0"/>
        </StackPanel>
        <StackPanel Orientation="Horizontal" Height="22">
            <TextBlock Text="Alpha" Width="100"/>
            <custom:BindablePassword Text="{Binding AlphaPassword, UpdateSourceTrigger=PropertyChanged}" Width="100" AllowAlpha="True"/>
            <TextBlock Text="{Binding Path=AlphaPassword}" Margin="20,0,0,0"/>
        </StackPanel>
        <StackPanel Orientation="Horizontal" Height="22">
            <TextBlock Text="Special" Width="100"/>
            <custom:BindablePassword Text="{Binding SpecialPassword, UpdateSourceTrigger=PropertyChanged}" Width="100" AllowSpecial="True"/>
            <TextBlock Text="{Binding Path=SpecialPassword}" Margin="20,0,0,0"/>
        </StackPanel>
    </StackPanel>
</Window>


In the code behind we simply have to provide properties to bind to.

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

namespace TestBindablePassword
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {

        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyPropertyChanged(String PropertyName = "")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
            }
        }

        private string _DigitPassword = "";
        public string DigitPassword
        {
            get { return _DigitPassword; }
            set
            {
                _DigitPassword = value;
                NotifyPropertyChanged("DigitPassword");
            }
        }

        private string _AlphaPassword = "";
        public string AlphaPassword
        {
            get { return _AlphaPassword; }
            set
            {
                _AlphaPassword = value;
                NotifyPropertyChanged("AlphaPassword");
            }
        }

        private string _SpecialPassword = "";
        public string SpecialPassword
        {
            get { return _SpecialPassword; }
            set
            {
                _SpecialPassword = value;
                NotifyPropertyChanged("SpecialPassword");
            }
        }

        public MainWindow()
        {
            InitializeComponent();
        }

    }
}

Now we have a reusable password control with a bindable text property. It looks like this...


No comments:

Post a Comment