Search code examples
c#wpfstring-formatting

custom string format text input wpf


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.


Solution

  • 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 DependencyPropertycalled 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>