WPF TextBox to enter decimal values
Asked Answered
F

15

26

Is there any decent way to get a WPF control which is bound to a decimal value?

When I just bind the TextBox or DataGridTextColumn to a decimal, data entry is a problem.

<TextBox Text="{Binding MyDecimal, UpdateSourceTrigger=PropertyChanged, 
    ValidatesOnDataErrors=True}"/>

When I try to enter "0,5" in this TextBox I'll get "5" as a result. It is nearly impossible to enter "0,5" at all (apart from entering 1,5 and replacing the "1" with a "0").

When I use StringFormat, data entry is only slightly improved:

<TextBox Text="{Binding MyDecimal, StringFormat=F1, UpdateSourceTrigger=PropertyChanged,
    ValidatesOnDataErrors=True}"/>

Now, when I try to enter "0,5" I'll end up with "0,5,0", which still is wrong but at least I can remove the trailing ",0" without much difficulty.

Still, entering decimal types using WPF is very awkward, because these TextBoxes are very prone to data entry errors, which is a real pain especially for values!

So what am I supposed to use for decimal data entry in WPF? Or does Microsoft not support decimal data??

Ferwerda answered 4/6, 2013 at 9:26 Comment(0)
G
30

I currently use this behavior for digital and decimal input:

public class TextBoxInputBehavior : Behavior<TextBox>
{
    const NumberStyles validNumberStyles = NumberStyles.AllowDecimalPoint |
                                               NumberStyles.AllowThousands |
                                               NumberStyles.AllowLeadingSign;
    public TextBoxInputBehavior()
    {
        this.InputMode = TextBoxInputMode.None;
        this.JustPositivDecimalInput = false;
    }

    public TextBoxInputMode InputMode { get; set; }


    public static readonly DependencyProperty JustPositivDecimalInputProperty =
     DependencyProperty.Register("JustPositivDecimalInput", typeof(bool),
     typeof(TextBoxInputBehavior), new FrameworkPropertyMetadata(false));

    public bool JustPositivDecimalInput
    {
        get { return (bool)GetValue(JustPositivDecimalInputProperty); }
        set { SetValue(JustPositivDecimalInputProperty, value); }
    }

    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.PreviewTextInput += AssociatedObjectPreviewTextInput;
        AssociatedObject.PreviewKeyDown += AssociatedObjectPreviewKeyDown;

        DataObject.AddPastingHandler(AssociatedObject, Pasting);

    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.PreviewTextInput -= AssociatedObjectPreviewTextInput;
        AssociatedObject.PreviewKeyDown -= AssociatedObjectPreviewKeyDown;

        DataObject.RemovePastingHandler(AssociatedObject, Pasting);
    }

    private void Pasting(object sender, DataObjectPastingEventArgs e)
    {
        if (e.DataObject.GetDataPresent(typeof(string)))
        {
            var pastedText = (string)e.DataObject.GetData(typeof(string));

            if (!this.IsValidInput(this.GetText(pastedText)))
            {
                System.Media.SystemSounds.Beep.Play();
                e.CancelCommand();
            }
        }
        else
        {
            System.Media.SystemSounds.Beep.Play();
            e.CancelCommand();
        }
     }

     private void AssociatedObjectPreviewKeyDown(object sender, KeyEventArgs e)
     {
        if (e.Key == Key.Space)
        {
            if (!this.IsValidInput(this.GetText(" ")))
            {
                System.Media.SystemSounds.Beep.Play();
                e.Handled = true;
            }
        }
     }

     private void AssociatedObjectPreviewTextInput(object sender, TextCompositionEventArgs e)
     {
        if (!this.IsValidInput(this.GetText(e.Text)))
        {
            System.Media.SystemSounds.Beep.Play();
            e.Handled = true;
        }
     }

     private string GetText(string input)
     {
        var txt = this.AssociatedObject;

        int selectionStart = txt.SelectionStart;
        if (txt.Text.Length < selectionStart) 
            selectionStart = txt.Text.Length;

        int selectionLength = txt.SelectionLength;
        if (txt.Text.Length < selectionStart + selectionLength) 
            selectionLength = txt.Text.Length - selectionStart;

        var realtext = txt.Text.Remove(selectionStart, selectionLength);

        int caretIndex = txt.CaretIndex;
        if (realtext.Length < caretIndex) 
            caretIndex = realtext.Length;

        var newtext = realtext.Insert(caretIndex, input);

        return newtext;
     }

     private bool IsValidInput(string input)
     {
        switch (InputMode)
        {
            case TextBoxInputMode.None:
                return true;
            case TextBoxInputMode.DigitInput:
                return CheckIsDigit(input);

            case TextBoxInputMode.DecimalInput:
                decimal d;
                //wen mehr als ein Komma
                if (input.ToCharArray().Where(x => x == ',').Count() > 1)
                    return false;


                if (input.Contains("-"))
                {
                     if (this.JustPositivDecimalInput) 
                        return false;


                     if (input.IndexOf("-",StringComparison.Ordinal) > 0) 
                          return false;

                      if(input.ToCharArray().Count(x=>x=='-') > 1)
                          return false;

                        //minus einmal am anfang zulässig
                       if (input.Length == 1) 
                           return true;
                    }

                    var result = decimal.TryParse(input, validNumberStyles, CultureInfo.CurrentCulture, out d);
                    return result;



            default: throw new ArgumentException("Unknown TextBoxInputMode");

        }
        return true;
     }

     private bool CheckIsDigit(string wert)
     {
        return wert.ToCharArray().All(Char.IsDigit);
     }
}

 public enum TextBoxInputMode
 {
  None,
  DecimalInput,
  DigitInput
  }

The XAML usage looks like this:

<TextBox Text="{Binding Sum}">
    <i:Interaction.Behaviors>
        <Behaviors:TextBoxInputBehavior InputMode="DecimalInput"/>
    </i:Interaction.Behaviors>
</TextBox>
Gustative answered 4/6, 2013 at 9:33 Comment(13)
Will have a look at this, thanks! Do you know how to use it with a DataGridTextColumn?Ferwerda
I saw some strange behavior (it allows any character) when you put a minus sign first, otherwise is a nice solution! I'm going to try to find the bug in your code but I think it could take some time, since is a little bit extense. Thanks for your solution.Flynn
Thanks, but don't work ok when add StringFormat:n0 to it. How to use it with StringFormat?Tachymetry
i dont use stringformat. is use a converter for this stuff instead. ext="{Binding Path=MyValue, Mode=TwoWay, UpdateSourceTrigger=LostFocus, ValidatesOnExceptions=true, ValidatesOnDataErrors=true, NotifyOnValidationError=True, TargetNullValue={x:Static System:String.Empty}, Converter={StaticResource MyStringToDecimalConverter},ConverterParameter=#\,##0.00}"Gustative
160 lines of code for only accepting numbers in textbox... I prefer KISS - keep it simple and stupid. PreviewTextInput and handled = true could be read and understood much faster...Pasquinade
@Markus for Copy&Paste or Whitespace you need some more code :) but you dont have to use the 160 lines. its just one solution.Gustative
To get Behavior<TextBox> add a reference > extensions > System.Windows.InteractivitySalman
The XAML is giving me this: Interaction is not supported in a Windows Presentation Foundation (WPF) project.Salman
@Salman add a reference to System.Windows.InteractivityCarinthia
What is the namespace prefix "i" defined as?Certitude
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"Certitude
Great solution, better than the whole new control some are suggesting since this way you won't have to apply styling for the various theme(s) you're supporting, which is tons of work. However, if you refactored it to an attached property, we wouldn't have to import System.Windows.Interactions and it would be far less verbose to apply. :)Funnyman
As a note in .net 5 or greater this is now in a nuget package called Microsoft.Xaml.Behaviors.Wpf.Carolus
A
15
    private void DecimalTextBox_PreviewTextInput(object sender, System.Windows.Input.TextCompositionEventArgs e)
    {
        bool approvedDecimalPoint = false;

        if (e.Text == ".")
        {
            if (!((TextBox)sender).Text.Contains("."))
                approvedDecimalPoint = true;
        }

        if (!(char.IsDigit(e.Text, e.Text.Length - 1) || approvedDecimalPoint))
            e.Handled = true;
    }
Amazonas answered 8/4, 2015 at 17:49 Comment(5)
Explain why this code will resolve OP's problem instead of that accepted answer...Dried
This will also work as an alternative. Since a decimal number must accomodate the "." character, it is tested for. Only one instance is allowed. A lot less code.Amazonas
This is a much better answer and WAY less code than the accepted answer which is overkill for what was requested. The space can easily be fixed by adding a TextChanged event and adding the following code: if (myTextBox.Text.Contains(" ")) { myTextBox.Text = myTextBox.Text.Replace(" ", ""); myTextBox.CaretIndex = myTextBox.Text.Length; }Neotype
thinking of Globalization, this will fail for 'non-dot' decimal separator, comma in german e.g..Metacenter
@Amazonas Here's a suggestion to improve this code: If the input is a dot and the TextBox.Text is empty so disapprove the decimal point in order to avoid entering a dot as the first input character.Norry
I
7

I also came across this issue; with UpdateSourceTrigger=PropertyChanged it seems that the binding tries to update the text as you are typing it. To fix this issue we changed our input fields so that UpdateSourceTrigger=LostFocus, e.g.:

<TextBox Text="{Binding MyDecimal, UpdateSourceTrigger=LostFocus, ValidatesOnDataErrors=True, StringFormat=n1}" />

You can define your own validation errors by using the IDataErrorInfo interface. You just need to add the following to your backing model:

 public class MyModel : IDataErrorInfo
 {
    /* my properties */

    public string Error { get { return null; } }
    public string this[string name]
    {
       get
       {
          switch (name)
          {
             case "MyDecimal":
                return NumberHelper.IsValidValue(MyDecimal) ? message : null;
             default: return null;
          }
       }
    }
    private string message = "Invalid value";
 }
Indicant answered 23/1, 2014 at 12:7 Comment(5)
LostFocus is the default UpdateSourceTrigger value for the Text property. Not setting it at all will have the same effect.Hale
@dotsamuelswan, yes, but this is trying to answer a question, I want it to be obvious what is going on.Indicant
Problem is: the UpdateSourceTrigger was set for a reason, because otherwise the binding value might not be updated when the user invokes a button using a keyboard shortcut.Ferwerda
@Sam, ah, in that case this is probably not going to help. I will leave it up though because in the case where you don't need PropertyChanged, this is a simple solution, IMO.Indicant
I am trying to implement this into my project but don't understand where the Number.Helper comes from. Could you please share the full code so it is understandable? I pay with an upvote.Paronomasia
B
6

The WPF Extended toolkit has a DecimalUpDown control that may suit your needs. It's free to use, and it's better to use this than to try and roll your own.

As for validating the input on it, there are a number of ways of applying validation, here is one detailed in MSDN. I detail another approach for custom bindable validation in two posts on my blog (you would apply the validation to the Value property binding on the DecimalUpDown control).

Bullion answered 4/6, 2013 at 9:32 Comment(2)
New controls mean applying all the themes you support to them, which is a ton of work. A minimalist attached property or Blend behavior is almost always preferred when the desired action is simple.Funnyman
Links are broken.Couthie
I
6

As of .NET 4.5, there is a Easier fix, add a "Delay" to the binding

 <TextBox  Text="{Binding MyDouble, UpdateSourceTrigger=PropertyChanged, Delay=1000}" />

Users now have 1 second (1000ms) before the binding system would attempt to replace the period (changing "1." to "1"). Which should give them time to enter in additional characters after the '.' so that it doesn't get removed.

Iyar answered 25/2, 2021 at 18:13 Comment(0)
V
4

I implemented my own TextBox. It updates the source, when there is a number in the text, otherwise not. On lost Focus, I read the source property. All you have to do is replace the TextBox with this class and bind the "Number" Property which is of type double.

public class DoubleTextBox: TextBox
{
    public DoubleTextBox()
    {
        TextChanged += DoubleTextBox_TextChanged;
        LostFocus += DoubleTextBox_LostFocus;
    }

    void DoubleTextBox_LostFocus(object sender, System.Windows.RoutedEventArgs e)
    {
        Text = Number.ToString("N2");
    }

    void DoubleTextBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        double zahl;
        if (string.IsNullOrWhiteSpace(Text))
        {
            Number = 0;
        }
        else if (double.TryParse(Text, out zahl))
        {
            Number = Double.Parse(zahl.ToString("N2"));
        }
        else
        {
            ValidationError validationError =
                new ValidationError(new ExceptionValidationRule(), GetBindingExpression(NumberProperty));

            validationError.ErrorContent = "Keine gültige Zahl";

            Validation.MarkInvalid(
                GetBindingExpression(NumberProperty),
                validationError);

        }
    }

    public double Number
    {
        get { return (double)this.GetValue(NumberProperty); }
        set { this.SetValue(NumberProperty, value); }
    }

    public static readonly DependencyProperty NumberProperty = DependencyProperty.Register(
        "Number", typeof(double), typeof(DoubleTextBox), 
        new FrameworkPropertyMetadata
            (
                0d,
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault
            )
    );
}
Varia answered 1/11, 2014 at 21:30 Comment(1)
I face issues when I try to type "123.40" - its not accepting "0"Priorate
S
3

Im new, so I cant comment his answer, but I fixed the negative number issues in blindmeis's code.

Just modify the

if (input.Contains("-"))

section of IsValidInput() to...

                if (input.Contains("-"))
                {
                    if (this.JustPositivDecimalInput)
                        return false;

                    //minus einmal am anfang zulässig
                    //minus once at the beginning
                    if (input.IndexOf("-", StringComparison.Ordinal) == 0 && input.ToCharArray().Count(x => x == '-') == 1)
                    {
                        if(input.Length == 1)
                        {
                            //INPUT IS "-"
                            return true;
                        }
                        else if (input.Length == 2)
                        {
                            //VALIDATE NEGATIVE DECIMALS...INPUT IS "-."
                            if (input.IndexOf(".", StringComparison.Ordinal) == 1)
                            {
                                return true;
                            }
                        }
                        else 
                        {
                            return decimal.TryParse(input, validNumberStyles, CultureInfo.CurrentCulture, out d);
                        }
                    }
                }
Spleen answered 3/1, 2014 at 17:40 Comment(0)
S
3

if you want the textbox to only allow decimal then write previewinputtext event for that textbox. then in that event write this code

decimal result;
e.Handled=!decimal.TryParse((sender as TextBox).Text + e.Text, out result)
Slit answered 7/8, 2017 at 9:6 Comment(0)
I
3

I know that this post is old but it comes in first on Google Search for this problem. As I had error with system.windows.interactivity package (old version of this package) I continued my search.

This post on MSDN fixed my problem and it's a one line solutionjust before initializecomponent on the main window like this:

    Public Sub New()

    ' This call is required by the designer.
    FrameworkCompatibilityPreferences.KeepTextBoxDisplaySynchronizedWithTextProperty = False
    InitializeComponent()

    ' Add any initialization after the InitializeComponent() call.

End Sub

Hope this will help other google searchers.

Irretrievable answered 9/7, 2019 at 21:12 Comment(0)
B
2

This regex works

private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
  {
   Regex regex = new Regex("^[.][0-9]+$|^[0-9]*[.]{0,1}[0-9]*$");
   e.Handled = !regex.IsMatch((sender as TextBox).Text.Insert((sender as TextBox).SelectionStart,e.Text));
  }
Bifoliolate answered 17/1, 2018 at 17:51 Comment(2)
Thanks, this works Could you please expalin this line Regex("^[.][0-9]+$|^[0-9]*[.]{0,1}[0-9]*$");Katelyn
How to allow negatives?Leftward
I
2

This will allow only decimals to be entered into the textbox and nothing else.

The viewmodel looks like this:

    private string _decimalVal = "0";
    public string decimalVal
    {
        get { return _decimalVal.ToString(); }
        set
        {
            if (string.IsNullOrEmpty(value) || value == "-")
                SetProperty(ref _decimalVal, value);
            else if (Decimal.TryParse(value, out decimal newVal))
            {
                if (newVal == 0)
                    value = "0";

                SetProperty(ref _decimalVal, value = (value.Contains(".")) ? Convert.ToDecimal(value).ToString("0.00") : value);
            }
        }
    }

The XAML usage looks like this:

<TextBox Text="{Binding decimalVal,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" />
Ideation answered 28/6, 2018 at 16:31 Comment(0)
H
0

I found that using only PreviewTextInput event only caused issues when you wanted to enter a negative number after you had enter some digits 1->12->123->-123(moved cursor back)

In PreviewTextInput event moving the caret this wont work (sender as TextBox).Text + e.Text

Used the following to get regex expression link as a base Decimal number regular expression, where digit after decimal is optional

Determined @"^[+-]?\d*.?\d*$" this worked the best for me.

    string previousText = "";
    int previousCaretIndex = 0;
    private void txtB_PreviewTextInput(object sender, TextCompositionEventArgs e)
    {   
        previousText = ((System.Windows.Controls.TextBox)sender).Text;
        previousCaretIndex = ((System.Windows.Controls.TextBox)sender).CaretIndex;
    }

    private void txtB_TextChanged(object sender, TextChangedEventArgs e)
    {
        if(!Regex.IsMatch(((System.Windows.Controls.TextBox)sender).Text, @"^[+-]?\d*\.?\d*$"))
        {
            ((System.Windows.Controls.TextBox)sender).Text = previousText;
            ((System.Windows.Controls.TextBox)sender).CaretIndex = previousCaretIndex;
            e.Handled = true;
        }
    }
Hazardous answered 20/4, 2020 at 1:32 Comment(0)
N
0

By this approach will prevent copying and pasting non integer and non decimal values to the TextBox which I don't see in any of the other answers:

private void TextBox_PreviewTextInput(object sender, System.Windows.Input.TextCompositionEventArgs e)
{
    var textBoxText = ((System.Windows.Controls.TextBox)sender).Text;
    var regex = new System.Text.RegularExpressions.Regex(@"^\d+\.?\d*$");
    if (textBoxText.Length > 0)
    {
        textBoxText += e.Text;
        e.Handled = !regex.IsMatch(textBoxText);
    }
    else
    {
        e.Handled = !regex.IsMatch(e.Text);
    }
}

private void TextBox_PreviewExecuted(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
{
    if (e.Command == System.Windows.Input.ApplicationCommands.Paste)
    {
        if (System.Windows.Clipboard.ContainsText())
        {
            e.Handled = !new System.Text.RegularExpressions.Regex(@"^\d+\.?\d*$").IsMatch(System.Windows.Clipboard.GetText());
        }
    }
}

// In case user copies and pastes 2 times or more.
// E.G. "1.0" might give "1.01.0" and so on.
// E.G. if the regex expression is for the range of 1-100.
// Then user might delete some numbers from the input which would give "0" or "00" etc.
private void TextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
    var textBox = (System.Windows.Controls.TextBox)sender;
    if (!new System.Text.RegularExpressions.Regex(@"^\d+\.?\d*$").IsMatch(textBox.Text.Trim()))
    {
        textBox.Clear();
    }
}

XAML:

<TextBox PreviewTextInput="TextBox_PreviewTextInput" CommandManager.PreviewExecuted="TextBox_PreviewExecuted" TextChanged="TextBox_TextChanged" HorizontalAlignment="Left" VerticalAlignment="Top" Width="120"/>

By the way if you want to change its behavior in order to accept another patterns such as regex expressions, you can just change the regex expression: @"^\d+\.?\d*$" to something else that fits your needs, this approach seems way more simple and reliable.

EDIT

In some cases depending on the regex expression, e.g. a date time regex expression for HH:mm:ss where TextChanged would not accept something like 00: as you type trying to achieve 00:20:00 that would stop in the third digit 00:, so in this case if you don't have a better regex expression then instead of using TextChanged use the following:

private void TextBox_LostFocus(object sender, System.Windows.RoutedEventArgs e)
{
    var textBox = (System.Windows.Controls.TextBox)sender;
    var textBoxText = textBox.Text.Trim();
    if (textBoxText.Length == 0)
    {
        this.error = false; // It can be true too, depends on your logic.
    }
    else
    {
        this.error = !new System.Text.RegularExpressions.Regex(@"^\d+\.?\d*$").IsMatch(textBoxText);

        if (this.error)
        {
            textBox.Background = System.Windows.Media.Brushes.Red;
        }
        else
        {
            textBox.ClearValue(System.Windows.Controls.TextBox.BackgroundProperty);
        }
    }
}

The error variable is a member variable that you should use to validate at the end of your form, e.g. by clicking on a button.

Norry answered 29/5, 2021 at 4:12 Comment(0)
R
0

Here is my solution partially based on other answers. Control "DoubleTextBox" contains property "DecimalCount" that can be used to set the number of decimals. Copying/pasting, MVVM and selection problems also handled. It hasn't been fully tested yet and can contain bugs. If so, I'm going to update the post later.

XAML:

xmlns:local_validators="clr-namespace:YourApp.validators"
xmlns:local_converters="clr-namespace:YourApp.converters"

..

<local_controls:DoubleTextBox x:Name="tbPresetDose" DecimalCount="{Binding PresetDoseDecimalPointsCount}">
    <TextBox.Resources>
        <local_converters:DecimalPlaceStringFormatConverter x:Key="decimalPlaceStringFormatConverter"/>
    </TextBox.Resources>
    <TextBox.Text>
        <MultiBinding Converter="{StaticResource decimalPlaceStringFormatConverter}">
            <Binding Path="PresetDose"/>
            <Binding Path="PresetDoseDecimalPointsCount"/>
        </MultiBinding>
    </TextBox.Text>
</local_controls:DoubleTextBox>

DoubleTextBox control:

public class DoubleTextBox : TextBox
{
    public DoubleTextBox()
    {
        DataObject.AddPastingHandler(this, OnPaste);
        PreviewTextInput += DoubleTextBoxPreviewTextInput;
    }
    private void OnPaste(object sender, DataObjectPastingEventArgs e)
    {
        if (e.DataObject.GetDataPresent(typeof(string)))
        {
            var pastedText = (string)e.DataObject.GetData(typeof(string));

            if (!IsValidInput(pastedText))
            {
                System.Media.SystemSounds.Beep.Play();
                e.CancelCommand();
            }
        }
        else
        {
            System.Media.SystemSounds.Beep.Play();
            e.CancelCommand();
        }
    }
    private void DoubleTextBoxPreviewTextInput(object sender, System.Windows.Input.TextCompositionEventArgs e)
    {
        String text;

        if (!String.IsNullOrEmpty(this.SelectedText))
        {
            text = this.Text.Remove(this.SelectionStart, this.SelectionLength);
            text = text.Insert(this.CaretIndex, e.Text);
        } 
        else
        {
            text = this.Text.Insert(this.CaretIndex, e.Text);
        }

        e.Handled = !IsValidInput(text);
    }
    public bool IsValidInput(string value)
    {
        if (String.IsNullOrEmpty(value))
            return false;

        string decimalNumberPattern = @"^[0-9]+(,[0-9]{0," + DecimalCount + @"})?$";
        var regex = new Regex(decimalNumberPattern);
        bool bResult = regex.IsMatch(value);
        return bResult;
    }
    public void DecimalCountChanged()
    {
        try
        {
            double doubleValue = double.Parse(Text, System.Globalization.CultureInfo.InvariantCulture);
            Text = doubleValue.ToString("N" + DecimalCount);
        }
        catch
        {
            Text = "";
        }
    }
    public double DecimalCount
    {
        get { return (double)this.GetValue(DecimalCountProperty); }
        set
        { 
            this.SetValue(DecimalCountProperty, value);
            DecimalCountChanged();
        }
    }
    public static readonly DependencyProperty DecimalCountProperty = DependencyProperty.Register(
        "DecimalCount", typeof(double), typeof(DoubleTextBox),
        new FrameworkPropertyMetadata
            (
                0d,
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault
            )
    );
}

DecimalPlaceStringFormatConverter:

public class DecimalPlaceStringFormatConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (!decimal.TryParse(values[0].ToString(), out decimal value))
            return values[0].ToString();

        if (!int.TryParse(values[1].ToString(), out int decimalPlaces))
            return value;

        if (values.Length == 2)
            return string.Format($"{{0:F{decimalPlaces}}}", value);
        else
            return value;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        object dResult = DependencyProperty.UnsetValue;
        string strValue = value as string;
        double parcedDouble;
        if (double.TryParse(strValue, out parcedDouble))
        {
            dResult = parcedDouble;
        }

        return new object[] { dResult };
    }
}

ViewModel:

private short _presetDoseDecimalPointsCount = 2;

..

public short PresetDoseDecimalPointsCount
{
    get => this._presetDoseDecimalPointsCount;
    set
    {
        if (value != _presetDoseDecimalPointsCount)
        {
            _presetDoseDecimalPointsCount = value;
            OnPropertyChanged();
        }
    }
}
Ryannryazan answered 16/11, 2021 at 8:33 Comment(0)
H
0

I'll show you my solution, maybe it will help someone. The DecimalTextBox control contains a Decimals property that can be used to set the number of decimal places: "None, One, Two, Three, Four." If there are errors, write, I will update the post.

XAML:

xmlns:controls="clr-namespace:YourApp.Controls"

<controls:DecimalTextBox Text="{Binding UpdateSourceTrigger=Explicit,Path=Your_Value}" Decimals="Two"/>

DecimalTextBox control:

public class DecimalTextBox : TextBox
{  
    public DecimalTextBox()
    {
        this.PreviewTextInput += DecimalTextBox_PreviewTextInput;
        this.PreviewKeyDown += DecimalTextBox_PreviewKeyDown;
        this.TextChanged += DecimalTextBox_TextChanged;
        this.MouseDoubleClick += DecimalTextBox_MouseDoubleClick;
        this.LostFocus += DecimalTextBox_LostFocus;
    }

    // Enum for the number of decimal places
    public enum DecimalPlaces
    {
        None,
        One,
        Two,
        Three,
        Four,
    }

    // DependencyProperty for the number of decimal places
    public static readonly DependencyProperty DecimalsProperty = DependencyProperty.Register(
       "Decimals",
       typeof(DecimalPlaces),
       typeof(DecimalTextBox),
       new PropertyMetadata(DecimalPlaces.None)
    );

    // Property for the number of decimal places
    public DecimalPlaces Decimals
    {
        get { return (DecimalPlaces)GetValue(DecimalsProperty); }
        set { SetValue(DecimalsProperty, value); }
    }

    private void DecimalTextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
    {
        // Check if the input is a number or a period
        if (!char.IsDigit(e.Text, 0) && e.Text != ".")
        {
            e.Handled = true;
        }

    }
    private void DecimalTextBox_PreviewKeyDown(object sender, KeyEventArgs e)
    {
        // Check if the key is a comma
        if (e.Key == Key.OemComma)
        {
            e.Handled = true;
            this.Text = this.Text.Insert(this.CaretIndex, "."); 
        }
        
    }
    private void DecimalTextBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        var text = new StringBuilder(this.Text);

        if (this.Text.StartsWith("0") && this.Text.Length > 1 && this.Text[1] != '.')
        {
            text.Remove(0, 1);
        }
        if (this.Text.Contains(" ") &&  this.Decimals != DecimalPlaces.None )
        {
            text.Replace(" ", "."); 
        }
        else
        {
            text.Replace(" ", "");
        }

        var decimalIndex = this.Text.IndexOf(".");

        if (decimalIndex != -1 && this.Text.Length - decimalIndex > (int)this.Decimals + 1) 
        {
            text.Remove(decimalIndex + (int)this.Decimals + 1, text.Length - decimalIndex - (int)this.Decimals - 1); // Remove the extra digits
        }

        if (this.Text.Count(f => f == '.') > 1)
        {
             text.Remove(text.ToString().LastIndexOf("."), 1); // Remove the extra decimal point
        }

        this.Text = text.ToString();
        this.CaretIndex = this.Text.Length;
    }
    private void DecimalTextBox_MouseDoubleClick(object sender, MouseButtonEventArgs e)
    {
        this.SelectAll(); // Select the entire text
    }
    private void DecimalTextBox_LostFocus(object sender, RoutedEventArgs e)
    {
        var text = new StringBuilder(this.Text);
        if (!this.Text.Contains(".") && this.Decimals != DecimalPlaces.None)
        {
            // Add decimal places to the number
            text.Append("." + new string('0', (int)this.Decimals));
        }

        if (this.Text == ".")
        {
            // Replace the text with "0"
            text.Clear();
            text.Append("0" + "." + new string('0', (int)this.Decimals));
        }
        // Check if the text starts with a decimal point
        if (this.Text.StartsWith("."))
        {
            // Add "0" before the decimal point
            text.Insert(0, "0");
        }
        if (this.Text.EndsWith(".") && this.Decimals != DecimalPlaces.None)
        {
            // Add "0" after the decimal point
            text.Append(new string('0', (int)this.Decimals));
        }
        else if(this.Text.EndsWith(".") && this.Decimals == DecimalPlaces.None)
        {
            text.Remove(text.ToString().LastIndexOf("."), 1);
        }

        this.Text = text.ToString();
    }

}
Huston answered 1/12, 2023 at 5:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.