I have values that are in format [double-type-value][unit] where unit can be "g" or "mg" (g for grams and mg for milligrams). Is there a way to allow user to enter text in TextBox ONLY in that format. For example to be like mini textbox that accept only numbers and mini combobox where values are "g" or "mg" in normal textbox or something else? It would be nice for unit to have default value to be "g" before something is typed in textbox so user don't have to type g or mg at the end of the textbox everty time if there are more textboxes.
EDIT I’m using MVVM pattern, so code behind is violating it.
Due the nature of this input i suggest you to create a little CustomControl
, more specific a TextBox
which is capable of limiting the Input
and convert the Text
to the according value -> a GramTextBox
.
The GramTextBox
has a DependencyProperty
called Gram
which represents the value of the entered Text
and can be bound to a ViewModel
(NOTE: The binding must contain Mode=TwoWay
due the GramTextBox
tries to update the bound Source
).
Code
public sealed class GramTextBox : TextBox
{
//Constructor
public GramTextBox() : base()
{
Text = "0g"; //Initial value
TextChanged += OnTextChanged;
DataObject.AddPastingHandler(this, OnPaste);
}
//Style override (get the Style of a TextBox for the GramTextBox)
static GramTextBox()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(GramTextBox), new FrameworkPropertyMetadata(typeof(TextBox)));
}
//Define a DependencyProperty to make it bindable (dont forget 'Mode=TwoWay' due the bound value is updated from this GramTextBox)
[Category("Common"), Description("Converted double value from the entered Text in gram")]
[Browsable(true)]
[Bindable(true)]
public double Gram
{
get { return (double)GetValue(PathDataProperty); }
set { SetCurrentValue(PathDataProperty, value); }
}
public static DependencyProperty PathDataProperty = DependencyProperty.Register("Gram", typeof(double), typeof(GramTextBox), new PropertyMetadata(0d));
//Extract the Gram value when Text has changed
private void OnTextChanged(object sender, TextChangedEventArgs e)
{
ExtractGram(Text);
}
//Suppress space input
protected override void OnPreviewKeyDown(KeyEventArgs e)
{
e.Handled = e.Key == Key.Space;
}
//Check text inputs
protected override void OnPreviewTextInput(TextCompositionEventArgs e)
{
e.Handled = !IsValidText(Text.Insert(CaretIndex, e.Text));
}
//check paste inputs
private void OnPaste(object sender, DataObjectPastingEventArgs e)
{
//Check if pasted object is string
if(e.SourceDataObject.GetData(typeof(string)) is string text)
{
//Check if combined string is valid
if(!IsValidText(Text.Insert(CaretIndex, text))) { e.CancelCommand(); }
}
else { e.CancelCommand(); }
}
//Check valid format for extraction (supports incomplete inputs like 0.m -> 0g)
private bool IsValidText(string text)
{
return Regex.IsMatch(text, @"^([0-9]*?\.?[0-9]*?m?g?)$");
}
//Extract value from entered string
private void ExtractGram(string text)
{
//trim all unwanted characters (only allow 0-9 dots and m or g)
text = Regex.Replace(text, @"[^0-9\.mg]", String.Empty);
//Expected Format -> random numbers, dots and couple m/g
//trim all text after the letter g
text = text.Split('g')[0];
//Expected Format -> random numbers, dots and m
//trim double dots (only one dot is allowed)
text = Regex.Replace(text, @"(?<=\..*)(\.)", String.Empty);
//Expected Format -> random numbers with one or more dots and m
//Check if m is at the end of the string to indicate milli (g was trimmed earlier)
bool isMilli = text.EndsWith("m");
//Remove all m, then only a double number should remain
text = text.Replace("m", String.Empty);
//Expected Format -> random numbers with possible dot
//trim all leading zeros
text = text.TrimStart(new char[] { '0' });
//Expected Format -> random numbers with possible dot
//Check if dot is at the beginning
if (text.StartsWith(".")) { text = $"0{text}"; }
//Expected Format -> random numbers with possible dot
//Check if dot is at the end
if (text.EndsWith(".")) { text = $"{text}0"; }
//Expected Format -> random numbers with possible dot
//Try to convert the remaining String to a Number, if it fails -> 0
Double.TryParse(text, out double result);
//Update Gram Property (divide when necessary)
Gram = (isMilli) ? result / 1000d : result;
}
}
Usage
Put this Class
in YOURNAMESPACE
and in the XAML
add a namespace alias
xmlns:cc="clr-namespace:YOURNAMESPACE"
Now the GramTextBox
can be used like this
<cc:GramTextBox Gram="{Binding VMDoubleProperty, Mode=TwoWay}" ... />
it will update the bound Property
in ViewModel
every time the Text
of the GramTextBox
changes (eg. valid inputs from keyboard/paste etc).
Notes
It is intended that nonsense inputs like .00g
, 0.0m
, .mg
set the Gram
Property
to 0
(like a fallback value).
Personal Note
Thanks @Pavel for the PasteHandler
Edit
To use this GramTextBox
in a DataGrid
, you can override the CellTemplate
of the Column
:
<DataGrid AutoGenerateColumns="False" ... >
<DataGrid.Columns>
<!-- Put some other Columns here like DataGridTextColumn -->
<DataGridTemplateColumn Header="Mass">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<cc:GramTextBox Gram="{Binding VMDoubleProperty, Mode=TwoWay}" ... />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!-- Put some other Columns here -->
</DataGrid.Columns>
</DataGrid>