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();
}
}
}