[ACCEPTED]-How to define TextBox input restrictions?-input-filtering
I've done this in the past with an attached 2 behavior, which can be used like this:
<TextBox b:Masking.Mask="^\p{Lu}*$"/>
The 1 attached behavior code looks like this:
/// <summary>
/// Provides masking behavior for any <see cref="TextBox"/>.
/// </summary>
public static class Masking
{
private static readonly DependencyPropertyKey _maskExpressionPropertyKey = DependencyProperty.RegisterAttachedReadOnly("MaskExpression",
typeof(Regex),
typeof(Masking),
new FrameworkPropertyMetadata());
/// <summary>
/// Identifies the <see cref="Mask"/> dependency property.
/// </summary>
public static readonly DependencyProperty MaskProperty = DependencyProperty.RegisterAttached("Mask",
typeof(string),
typeof(Masking),
new FrameworkPropertyMetadata(OnMaskChanged));
/// <summary>
/// Identifies the <see cref="MaskExpression"/> dependency property.
/// </summary>
public static readonly DependencyProperty MaskExpressionProperty = _maskExpressionPropertyKey.DependencyProperty;
/// <summary>
/// Gets the mask for a given <see cref="TextBox"/>.
/// </summary>
/// <param name="textBox">
/// The <see cref="TextBox"/> whose mask is to be retrieved.
/// </param>
/// <returns>
/// The mask, or <see langword="null"/> if no mask has been set.
/// </returns>
public static string GetMask(TextBox textBox)
{
if (textBox == null)
{
throw new ArgumentNullException("textBox");
}
return textBox.GetValue(MaskProperty) as string;
}
/// <summary>
/// Sets the mask for a given <see cref="TextBox"/>.
/// </summary>
/// <param name="textBox">
/// The <see cref="TextBox"/> whose mask is to be set.
/// </param>
/// <param name="mask">
/// The mask to set, or <see langword="null"/> to remove any existing mask from <paramref name="textBox"/>.
/// </param>
public static void SetMask(TextBox textBox, string mask)
{
if (textBox == null)
{
throw new ArgumentNullException("textBox");
}
textBox.SetValue(MaskProperty, mask);
}
/// <summary>
/// Gets the mask expression for the <see cref="TextBox"/>.
/// </summary>
/// <remarks>
/// This method can be used to retrieve the actual <see cref="Regex"/> instance created as a result of setting the mask on a <see cref="TextBox"/>.
/// </remarks>
/// <param name="textBox">
/// The <see cref="TextBox"/> whose mask expression is to be retrieved.
/// </param>
/// <returns>
/// The mask expression as an instance of <see cref="Regex"/>, or <see langword="null"/> if no mask has been applied to <paramref name="textBox"/>.
/// </returns>
public static Regex GetMaskExpression(TextBox textBox)
{
if (textBox == null)
{
throw new ArgumentNullException("textBox");
}
return textBox.GetValue(MaskExpressionProperty) as Regex;
}
private static void SetMaskExpression(TextBox textBox, Regex regex)
{
textBox.SetValue(_maskExpressionPropertyKey, regex);
}
private static void OnMaskChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
var textBox = dependencyObject as TextBox;
var mask = e.NewValue as string;
textBox.PreviewTextInput -= textBox_PreviewTextInput;
textBox.PreviewKeyDown -= textBox_PreviewKeyDown;
DataObject.RemovePastingHandler(textBox, Pasting);
if (mask == null)
{
textBox.ClearValue(MaskProperty);
textBox.ClearValue(MaskExpressionProperty);
}
else
{
textBox.SetValue(MaskProperty, mask);
SetMaskExpression(textBox, new Regex(mask, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace));
textBox.PreviewTextInput += textBox_PreviewTextInput;
textBox.PreviewKeyDown += textBox_PreviewKeyDown;
DataObject.AddPastingHandler(textBox, Pasting);
}
}
private static void textBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
var textBox = sender as TextBox;
var maskExpression = GetMaskExpression(textBox);
if (maskExpression == null)
{
return;
}
var proposedText = GetProposedText(textBox, e.Text);
if (!maskExpression.IsMatch(proposedText))
{
e.Handled = true;
}
}
private static void textBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
var textBox = sender as TextBox;
var maskExpression = GetMaskExpression(textBox);
if (maskExpression == null)
{
return;
}
//pressing space doesn't raise PreviewTextInput - no idea why, but we need to handle
//explicitly here
if (e.Key == Key.Space)
{
var proposedText = GetProposedText(textBox, " ");
if (!maskExpression.IsMatch(proposedText))
{
e.Handled = true;
}
}
}
private static void Pasting(object sender, DataObjectPastingEventArgs e)
{
var textBox = sender as TextBox;
var maskExpression = GetMaskExpression(textBox);
if (maskExpression == null)
{
return;
}
if (e.DataObject.GetDataPresent(typeof(string)))
{
var pastedText = e.DataObject.GetData(typeof(string)) as string;
var proposedText = GetProposedText(textBox, pastedText);
if (!maskExpression.IsMatch(proposedText))
{
e.CancelCommand();
}
}
else
{
e.CancelCommand();
}
}
private static string GetProposedText(TextBox textBox, string newText)
{
var text = textBox.Text;
if (textBox.SelectionStart != -1)
{
text = text.Remove(textBox.SelectionStart, textBox.SelectionLength);
}
text = text.Insert(textBox.CaretIndex, newText);
return text;
}
}
I've improved Kent Boogaart's answer by 7 also handling the following actions that 6 previously could cause the pattern to be 5 violated:
- Backspace
- Selecting and dragging text in a way that can violate the pattern
- Cut command
For example, Kent Boogaart's answer 4 allowed the user to enter "ac" by first 3 entering "abc" and afterwards delete the 2 "b" with the backspace which violates the 1 following regex
^(a|ab|abc)$
Usage (unchanged):
<TextBox b:Masking.Mask="^\p{Lu}*$"/>
Mask class:
public static class Masking
{
private static readonly DependencyPropertyKey _maskExpressionPropertyKey = DependencyProperty.RegisterAttachedReadOnly("MaskExpression",
typeof(Regex),
typeof(Masking),
new FrameworkPropertyMetadata());
/// <summary>
/// Identifies the <see cref="Mask"/> dependency property.
/// </summary>
public static readonly DependencyProperty MaskProperty = DependencyProperty.RegisterAttached("Mask",
typeof(string),
typeof(Masking),
new FrameworkPropertyMetadata(OnMaskChanged));
/// <summary>
/// Identifies the <see cref="MaskExpression"/> dependency property.
/// </summary>
public static readonly DependencyProperty MaskExpressionProperty = _maskExpressionPropertyKey.DependencyProperty;
/// <summary>
/// Gets the mask for a given <see cref="TextBox"/>.
/// </summary>
/// <param name="textBox">
/// The <see cref="TextBox"/> whose mask is to be retrieved.
/// </param>
/// <returns>
/// The mask, or <see langword="null"/> if no mask has been set.
/// </returns>
public static string GetMask(TextBox textBox)
{
if (textBox == null)
{
throw new ArgumentNullException("textBox");
}
return textBox.GetValue(MaskProperty) as string;
}
/// <summary>
/// Sets the mask for a given <see cref="TextBox"/>.
/// </summary>
/// <param name="textBox">
/// The <see cref="TextBox"/> whose mask is to be set.
/// </param>
/// <param name="mask">
/// The mask to set, or <see langword="null"/> to remove any existing mask from <paramref name="textBox"/>.
/// </param>
public static void SetMask(TextBox textBox, string mask)
{
if (textBox == null)
{
throw new ArgumentNullException("textBox");
}
textBox.SetValue(MaskProperty, mask);
}
/// <summary>
/// Gets the mask expression for the <see cref="TextBox"/>.
/// </summary>
/// <remarks>
/// This method can be used to retrieve the actual <see cref="Regex"/> instance created as a result of setting the mask on a <see cref="TextBox"/>.
/// </remarks>
/// <param name="textBox">
/// The <see cref="TextBox"/> whose mask expression is to be retrieved.
/// </param>
/// <returns>
/// The mask expression as an instance of <see cref="Regex"/>, or <see langword="null"/> if no mask has been applied to <paramref name="textBox"/>.
/// </returns>
public static Regex GetMaskExpression(TextBox textBox)
{
if (textBox == null)
{
throw new ArgumentNullException("textBox");
}
return textBox.GetValue(MaskExpressionProperty) as Regex;
}
private static void SetMaskExpression(TextBox textBox, Regex regex)
{
textBox.SetValue(_maskExpressionPropertyKey, regex);
}
private static void OnMaskChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
var textBox = dependencyObject as TextBox;
var mask = e.NewValue as string;
textBox.PreviewTextInput -= textBox_PreviewTextInput;
textBox.PreviewKeyDown -= textBox_PreviewKeyDown;
DataObject.RemovePastingHandler(textBox, Pasting);
DataObject.RemoveCopyingHandler(textBox, NoDragCopy);
CommandManager.RemovePreviewExecutedHandler(textBox, NoCutting);
if (mask == null)
{
textBox.ClearValue(MaskProperty);
textBox.ClearValue(MaskExpressionProperty);
}
else
{
textBox.SetValue(MaskProperty, mask);
SetMaskExpression(textBox, new Regex(mask, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace));
textBox.PreviewTextInput += textBox_PreviewTextInput;
textBox.PreviewKeyDown += textBox_PreviewKeyDown;
DataObject.AddPastingHandler(textBox, Pasting);
DataObject.AddCopyingHandler(textBox, NoDragCopy);
CommandManager.AddPreviewExecutedHandler(textBox, NoCutting);
}
}
private static void NoCutting(object sender, ExecutedRoutedEventArgs e)
{
if(e.Command == ApplicationCommands.Cut)
{
e.Handled = true;
}
}
private static void NoDragCopy(object sender, DataObjectCopyingEventArgs e)
{
if (e.IsDragDrop)
{
e.CancelCommand();
}
}
private static void textBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
var textBox = sender as TextBox;
var maskExpression = GetMaskExpression(textBox);
if (maskExpression == null)
{
return;
}
var proposedText = GetProposedText(textBox, e.Text);
if (!maskExpression.IsMatch(proposedText))
{
e.Handled = true;
}
}
private static void textBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
var textBox = sender as TextBox;
var maskExpression = GetMaskExpression(textBox);
if (maskExpression == null)
{
return;
}
string proposedText = null;
//pressing space doesn't raise PreviewTextInput, reasons here http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/446ec083-04c8-43f2-89dc-1e2521a31f6b?prof=required
if (e.Key == Key.Space)
{
proposedText = GetProposedText(textBox, " ");
}
// Same story with backspace
else if(e.Key == Key.Back)
{
proposedText = GetProposedTextBackspace(textBox);
}
if (proposedText != null && proposedText != string.Empty && !maskExpression.IsMatch(proposedText))
{
e.Handled = true;
}
}
private static void Pasting(object sender, DataObjectPastingEventArgs e)
{
var textBox = sender as TextBox;
var maskExpression = GetMaskExpression(textBox);
if (maskExpression == null)
{
return;
}
if (e.DataObject.GetDataPresent(typeof(string)))
{
var pastedText = e.DataObject.GetData(typeof(string)) as string;
var proposedText = GetProposedText(textBox, pastedText);
if (!maskExpression.IsMatch(proposedText))
{
e.CancelCommand();
}
}
else
{
e.CancelCommand();
}
}
private static string GetProposedTextBackspace(TextBox textBox)
{
var text = GetTextWithSelectionRemoved(textBox);
if (textBox.SelectionStart > 0 && textBox.SelectionLength == 0)
{
text = text.Remove(textBox.SelectionStart-1, 1);
}
return text;
}
private static string GetProposedText(TextBox textBox, string newText)
{
var text = GetTextWithSelectionRemoved(textBox);
text = text.Insert(textBox.CaretIndex, newText);
return text;
}
private static string GetTextWithSelectionRemoved(TextBox textBox)
{
var text = textBox.Text;
if (textBox.SelectionStart != -1)
{
text = text.Remove(textBox.SelectionStart, textBox.SelectionLength);
}
return text;
}
}
I changed VitalyB's code to support Color 11 Themes. Instead of blocking user input if 10 it doesn't meet the RegEx script, it just 9 highlights the text box. The text box will 8 be the theme default without interaction, and 7 then defaults to a light green or red depending 6 on the value after the input is set. You 5 can also set the fail and pass colors programatically 4 with:
b:ColorMasking.PassColor = "Hexadecimal Value"
b:ColorMasking.FailColor = "Hexadecimal Value"
The class is below:
public class ColorMasking : DependencyObject
{
private static readonly DependencyPropertyKey _maskExpressionPropertyKey = DependencyProperty.RegisterAttachedReadOnly("MaskExpression",
typeof(Regex),
typeof(ColorMasking),
new FrameworkPropertyMetadata());
/// <summary>
/// Identifies the <see cref="Mask"/> dependency property.
/// </summary>
///
public static readonly DependencyProperty PassColorProperty = DependencyProperty.RegisterAttached("PassColor",
typeof(string),
typeof(ColorMasking),
new PropertyMetadata("#99FF99"));
public static void SetPassColor(DependencyObject obj, string passColor)
{
obj.SetValue(PassColorProperty, passColor);
}
public static string GetPassColor(DependencyObject obj)
{
return (string)obj.GetValue(PassColorProperty);
}
public static readonly DependencyProperty FailColorProperty = DependencyProperty.RegisterAttached("FailColor",
typeof(string),
typeof(ColorMasking),
new PropertyMetadata("#FFCCFF"));
public static void SetFailColor(DependencyObject obj, string failColor)
{
obj.SetValue(FailColorProperty, failColor);
}
public static string GetFailColor(DependencyObject obj)
{
return (string)obj.GetValue(FailColorProperty);
}
public static readonly DependencyProperty MaskProperty = DependencyProperty.RegisterAttached("Mask",
typeof(string),
typeof(ColorMasking),
new FrameworkPropertyMetadata(OnMaskChanged));
private static void OnPassColorChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
var textBox = dependencyObject as TextBox;
var color = e.NewValue as string;
textBox.SetValue(PassColorProperty, color);
}
/// <summary>
/// Identifies the <see cref="MaskExpression"/> dependency property.
/// </summary>
public static readonly DependencyProperty MaskExpressionProperty = _maskExpressionPropertyKey.DependencyProperty;
/// <summary>
/// Gets the mask for a given <see cref="TextBox"/>.
/// </summary>
/// <param name="textBox">
/// The <see cref="TextBox"/> whose mask is to be retrieved.
/// </param>
/// <returns>
/// The mask, or <see langword="null"/> if no mask has been set.
/// </returns>
public static string GetMask(TextBox textBox)
{
if (textBox == null)
{
throw new ArgumentNullException("textBox");
}
return textBox.GetValue(MaskProperty) as string;
}
/// <summary>
/// Sets the mask for a given <see cref="TextBox"/>.
/// </summary>
/// <param name="textBox">
/// The <see cref="TextBox"/> whose mask is to be set.
/// </param>
/// <param name="mask">
/// The mask to set, or <see langword="null"/> to remove any existing mask from <paramref name="textBox"/>.
/// </param>
public static void SetMask(TextBox textBox, string mask)
{
if (textBox == null)
{
throw new ArgumentNullException("textBox");
}
textBox.SetValue(MaskProperty, mask);
}
/// <summary>
/// Gets the mask expression for the <see cref="TextBox"/>.
/// </summary>
/// <remarks>
/// This method can be used to retrieve the actual <see cref="Regex"/> instance created as a result of setting the mask on a <see cref="TextBox"/>.
/// </remarks>
/// <param name="textBox">
/// The <see cref="TextBox"/> whose mask expression is to be retrieved.
/// </param>
/// <returns>
/// The mask expression as an instance of <see cref="Regex"/>, or <see langword="null"/> if no mask has been applied to <paramref name="textBox"/>.
/// </returns>
public static Regex GetMaskExpression(TextBox textBox)
{
if (textBox == null)
{
throw new ArgumentNullException("textBox");
}
return textBox.GetValue(MaskExpressionProperty) as Regex;
}
private static void SetMaskExpression(TextBox textBox, Regex regex)
{
textBox.SetValue(_maskExpressionPropertyKey, regex);
}
private static void OnMaskChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
var textBox = dependencyObject as TextBox;
var mask = e.NewValue as string;
textBox.PreviewTextInput -= textBox_PreviewTextInput;
textBox.PreviewKeyDown -= textBox_PreviewKeyDown;
DataObject.RemovePastingHandler(textBox, Pasting);
DataObject.RemoveCopyingHandler(textBox, NoDragCopy);
CommandManager.RemovePreviewExecutedHandler(textBox, NoCutting);
if (mask == null)
{
textBox.ClearValue(MaskProperty);
textBox.ClearValue(MaskExpressionProperty);
}
else
{
textBox.SetValue(MaskProperty, mask);
SetMaskExpression(textBox, new Regex(mask, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace));
textBox.PreviewTextInput += textBox_PreviewTextInput;
textBox.PreviewKeyDown += textBox_PreviewKeyDown;
DataObject.AddPastingHandler(textBox, Pasting);
DataObject.AddCopyingHandler(textBox, NoDragCopy);
CommandManager.AddPreviewExecutedHandler(textBox, NoCutting);
}
}
private static void NoCutting(object sender, ExecutedRoutedEventArgs e)
{
if (e.Command == ApplicationCommands.Cut)
{
e.Handled = true;
}
}
private static void NoDragCopy(object sender, DataObjectCopyingEventArgs e)
{
if (e.IsDragDrop)
{
e.CancelCommand();
}
}
private static void textBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
var textBox = sender as TextBox;
var maskExpression = GetMaskExpression(textBox);
string passHex = (string)textBox.GetValue(PassColorProperty);
string failHex = (string)textBox.GetValue(FailColorProperty);
Color passColor = Extensions.ToColorFromHex(passHex);
Color failColor = Extensions.ToColorFromHex(failHex);
if (maskExpression == null)
{
return;
}
var proposedText = GetProposedText(textBox, e.Text);
if (!maskExpression.IsMatch(proposedText))
{
textBox.Background = new SolidColorBrush(failColor);
}
else
{
textBox.Background = new SolidColorBrush(passColor);
}
}
private static void textBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
var textBox = sender as TextBox;
var maskExpression = GetMaskExpression(textBox);
string passHex = (string)textBox.GetValue(PassColorProperty);
string failHex = (string)textBox.GetValue(FailColorProperty);
Color passColor = Extensions.ToColorFromHex(passHex);
Color failColor = Extensions.ToColorFromHex(failHex);
if (maskExpression == null)
{
return;
}
string proposedText = null;
//pressing space doesn't raise PreviewTextInput, reasons here http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/446ec083-04c8-43f2-89dc-1e2521a31f6b?prof=required
if (e.Key == Key.Space)
{
proposedText = GetProposedText(textBox, " ");
}
// Same story with backspace
else if (e.Key == Key.Back)
{
proposedText = GetProposedTextBackspace(textBox);
}
if (proposedText != null && !maskExpression.IsMatch(proposedText))
{
textBox.Background = new SolidColorBrush(failColor);
}
else
{
textBox.Background = new SolidColorBrush(passColor);
}
}
private static void Pasting(object sender, DataObjectPastingEventArgs e)
{
TextBox textBox = sender as TextBox;
var maskExpression = GetMaskExpression(textBox);
string passHex = (string)textBox.GetValue(PassColorProperty);
string failHex = (string)textBox.GetValue(FailColorProperty);
Color passColor = Extensions.ToColorFromHex(passHex);
Color failColor = Extensions.ToColorFromHex(failHex);
if (maskExpression == null)
{
return;
}
if (e.DataObject.GetDataPresent(typeof(string)))
{
var pastedText = e.DataObject.GetData(typeof(string)) as string;
var proposedText = GetProposedText(textBox, pastedText);
if (!maskExpression.IsMatch(proposedText))
{
textBox.Background = new SolidColorBrush(failColor);
}
else
{
textBox.Background = new SolidColorBrush(passColor);
}
}
else
{
textBox.Background = new SolidColorBrush(failColor);
}
}
private static string GetProposedTextBackspace(TextBox textBox)
{
var text = GetTextWithSelectionRemoved(textBox);
if (textBox.SelectionStart > 0)
{
text = text.Remove(textBox.SelectionStart - 1, 1);
}
return text;
}
private static string GetProposedText(TextBox textBox, string newText)
{
var text = GetTextWithSelectionRemoved(textBox);
text = text.Insert(textBox.CaretIndex, newText);
return text;
}
private static string GetTextWithSelectionRemoved(TextBox textBox)
{
var text = textBox.Text;
if (textBox.SelectionStart != -1)
{
text = text.Remove(textBox.SelectionStart, textBox.SelectionLength);
}
return text;
}
}
To run, the script 3 requires a class written by Aaron C, explained 2 here: Silverlight/WPF sets ellipse with hexadecimal colour shown here: http://www.wiredprairie.us/blog/index.php/archives/659
The code is below in 1 case the website is ever moved:
public static class Extensions
{
public static void SetFromHex(this Color c, string hex)
{
Color c1 = ToColorFromHex(hex);
c.A = c1.A;
c.R = c1.R;
c.G = c1.G;
c.B = c1.B;
}
public static Color ToColorFromHex(string hex)
{
if (string.IsNullOrEmpty(hex))
{
throw new ArgumentNullException("hex");
}
// remove any "#" characters
while (hex.StartsWith("#"))
{
hex = hex.Substring(1);
}
int num = 0;
// get the number out of the string
if (!Int32.TryParse(hex, System.Globalization.NumberStyles.HexNumber, null, out num))
{
throw new ArgumentException("Color not in a recognized Hex format.");
}
int[] pieces = new int[4];
if (hex.Length > 7)
{
pieces[0] = ((num >> 24) & 0x000000ff);
pieces[1] = ((num >> 16) & 0x000000ff);
pieces[2] = ((num >> 8) & 0x000000ff);
pieces[3] = (num & 0x000000ff);
}
else if (hex.Length > 5)
{
pieces[0] = 255;
pieces[1] = ((num >> 16) & 0x000000ff);
pieces[2] = ((num >> 8) & 0x000000ff);
pieces[3] = (num & 0x000000ff);
}
else if (hex.Length == 3)
{
pieces[0] = 255;
pieces[1] = ((num >> 8) & 0x0000000f);
pieces[1] += pieces[1] * 16;
pieces[2] = ((num >> 4) & 0x000000f);
pieces[2] += pieces[2] * 16;
pieces[3] = (num & 0x000000f);
pieces[3] += pieces[3] * 16;
}
return Color.FromArgb((byte)pieces[0], (byte)pieces[1], (byte)pieces[2], (byte)pieces[3]);
}
}
private void TextBox1_SelectionChanged(object sender, RoutedEventArgs e)
{
string txt = TextBox1.Text;
if (txt != "")
{
TextBox1.Text = Regex.Replace(TextBox1.Text, "[^0-9]", "");
if (txt != TextBox1.Text)
{
TextBox1.Select(TextBox1.Text.Length, 0);
}
}
}
0
Here is yet another version of the existing 11 solutions here. It's a Behavior simulating 10 a "TextChanging" event providing the old 9 text, the new text and a flag to cancel 8 the change. This way we can implement whatever 7 filter we want.
I got rid of the "PreviewKeyDown" handler 6 used only for the space character. TextBox 5 seems to manage everything with routed commands 4 and "Space" does have its own commmand, though 3 it's not public. I also added support for 2 additionals keyboard shortcuts like "Ctrl+Backspace" (delete 1 previous word).
public class TextChangingBehavior : Behavior<TextBox>
{
public event EventHandler<TextChangingEventArgs> TextChanging;
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.PreviewTextInput += OnPreviewTextInput;
CommandManager.AddPreviewExecutedHandler(AssociatedObject, OnPreviewExecutedHandler);
DataObject.AddCopyingHandler(AssociatedObject, OnCopying);
DataObject.AddPastingHandler(AssociatedObject, OnPasting);
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.PreviewTextInput -= OnPreviewTextInput;
CommandManager.RemovePreviewExecutedHandler(AssociatedObject, OnPreviewExecutedHandler);
DataObject.RemoveCopyingHandler(AssociatedObject, OnCopying);
DataObject.RemovePastingHandler(AssociatedObject, OnPasting);
}
#region Text
private enum CharCategory
{
LetterOrDigit,
Whitespace,
Other
}
private CharCategory GetCharCategory(char c)
{
if (char.IsLetterOrDigit(c))
return CharCategory.LetterOrDigit;
else if (char.IsWhiteSpace(c))
return CharCategory.Whitespace;
else
return CharCategory.Other;
}
private string GetText(string input = null)
{
var box = AssociatedObject;
var text = box.Text ?? string.Empty;
if (input != null)
{
// Delete selection
var deleteCount = box.SelectionLength;
if (deleteCount > 0)
text = text.Remove(box.SelectionStart, deleteCount);
// Insert input
if (input.Length > 0)
text = text.Insert(box.CaretIndex, input);
}
return text;
}
#endregion
private void OnPreviewExecutedHandler(object sender, ExecutedRoutedEventArgs e)
{
var box = AssociatedObject;
var selectionExists = box.SelectionLength > 0;
var caretIndex = box.CaretIndex;
string newText = null;
if (e.Command == ApplicationCommands.Cut)
{
if (selectionExists)
newText = GetText(string.Empty);
}
else if (e.Command == EditingCommands.Backspace)
{
if (selectionExists)
newText = GetText(string.Empty);
else if (caretIndex > 0)
newText = GetText().Remove(caretIndex - 1, 1);
}
else if (e.Command == EditingCommands.Delete)
{
if (selectionExists)
newText = GetText(string.Empty);
else
{
newText = GetText();
if (caretIndex >= newText.Length)
newText = null;
else
newText = newText.Remove(caretIndex, 1);
}
}
else if (e.Command == EditingCommands.DeletePreviousWord)
{
if (selectionExists)
newText = GetText(string.Empty);
else if (caretIndex > 0)
{
newText = GetText();
var startIndex = caretIndex;
// Include whitespaces
do
startIndex--;
while (startIndex > 0 && char.IsWhiteSpace(newText[startIndex]));
// Include the next block
var currentCategory = GetCharCategory(newText[startIndex]);
while (startIndex > 0 && GetCharCategory(newText[startIndex - 1]) == currentCategory)
startIndex--;
newText = newText.Remove(startIndex, caretIndex - startIndex);
}
}
else if (e.Command == EditingCommands.DeleteNextWord)
{
if (selectionExists)
newText = GetText(string.Empty);
else
{
newText = GetText();
if (caretIndex >= newText.Length)
newText = null;
else
{
var endIndex = caretIndex + 1;
// Include the current block
var currentCategory = GetCharCategory(newText[caretIndex]);
while (endIndex < newText.Length && GetCharCategory(newText[endIndex]) == currentCategory)
endIndex++;
// Include whitespaces
while (endIndex < newText.Length && char.IsWhiteSpace(newText[endIndex]))
endIndex++;
newText = newText.Remove(caretIndex, endIndex - caretIndex);
}
}
}
else if (e.Command is RoutedUICommand cmd && new[] { "Space", "ShiftSpace" }.Contains(cmd.Name))
{
newText = GetText(" ");
}
if (newText != null && OnProcessChange(newText))
e.Handled = true;
}
private void OnCopying(object sender, DataObjectCopyingEventArgs e)
{
if (e.IsDragDrop)
{
if (OnProcessChange(GetText(string.Empty)))
e.CancelCommand();
}
}
private void OnPasting(object sender, DataObjectPastingEventArgs e)
{
if (e.DataObject.GetDataPresent(typeof(string)))
{
if (OnProcessChange(GetText((string)e.DataObject.GetData(typeof(string)))))
e.CancelCommand();
}
}
private void OnPreviewTextInput(object sender, TextCompositionEventArgs e)
{
if (!string.IsNullOrEmpty(e.Text))
{
if (OnProcessChange(GetText(e.Text)))
e.Handled = true;
}
}
private bool OnProcessChange(string newValue)
{
var oldValue = GetText();
if (string.Equals(oldValue, newValue, StringComparison.Ordinal))
return false;
else
{
var args = new TextChangingEventArgs(oldValue, newValue);
OnTextChanging(args);
return args.Cancel;
}
}
protected virtual void OnTextChanging(TextChangingEventArgs e)
{
TextChanging?.Invoke(this, e);
}
}
public class TextChangingEventArgs : EventArgs
{
public string OldValue { get; }
public string NewValue { get; }
public bool Cancel { get; set; }
public TextChangingEventArgs(string oldValue, string newValue)
{
OldValue = oldValue;
NewValue = newValue;
}
}
More Related questions
We use cookies to improve the performance of the site. By staying on our site, you agree to the terms of use of cookies.