The accepted answer is simple and elegant. It states that it's the "easiest" way and I agree!
But as I understand it, the FormattedNumberTextBox
scheme as coded today relies on changes to the focused state of the control so there's at least one "possible" issue: if the user types some keys then hits Enter key there might not be a change of focus (by design or default).
So, this post just makes a few tweaks to an already excellent answer by handling [Enter] and adding another nice amenity - a settable/bindable Value
property that fires PropertyChanged
events when a new valid value is received (either by keyboard input or programmatically). At the same time, it ensures that a textbox that is ReadOnly
will always display the formatted value.
Focused entry or re-entry.
Response to Enter key
Handle the Enter key
This method also responds to an Escape key event by reverting to the last good formatted value.
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
switch (e.KeyData)
{
case Keys.Return:
e.SuppressKeyPress = e.Handled = true;
OnValidating(new CancelEventArgs());
break;
case Keys.Escape:
e.SuppressKeyPress = e.Handled = true;
formatValue();
break;
}
}
Define behavior for when TextBox
calls its built-in validation.
This performs format + SelectAll. If the new input string can't be parsed it simply reverts to the previous valid state.
protected override void OnValidating(CancelEventArgs e)
{
base.OnValidating(e);
if (Modified)
{
if (double.TryParse(Text, out double value))
{
Value = value;
}
formatValue();
_unmodified = Text;
Modified = false;
}
}
Ensure that a mouse click causes the full-resolution display:
- Whether or not the control gains focus as a result.
- Only if control is not read only.
Use BeginInvoke which doesn't block remaining mouse events in queue.
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
if (!(ReadOnly || Modified))
{
BeginInvoke(() =>
{
int selB4 = SelectionStart;
Text = Value == 0 ? "0.00" : $"{Value}";
Modified = true;
Select(Math.Min(selB4, Text.Length - 1), 0);
});
}
}
Implement the bindable Value
property for the underlying value
Allows setting the underlying value programmatically using textBoxFormatted.Value = 123.456789
.
class TextBoxFP : TextBox, INotifyPropertyChanged
{
public TextBoxFP()
{
_unmodified = Text = "0.00";
CausesValidation = true;
}
public double Value
{
get => _value;
set
{
if (!Equals(_value, value))
{
_value = value;
formatValue();
OnPropertyChanged();
}
}
}
double _value = 0;
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Manage the built-in Modified
property of the text box and the actual formatting.
string _unmodified;
protected override void OnTextChanged(EventArgs e)
{
base.OnTextChanged(e);
if(Focused)
{
Modified = !Text.Equals(_unmodified);
}
}
public string Format { get; set; } = "N2";
private void formatValue()
{
Text = Value.ToString(Format);
Modified = false;
BeginInvoke(() => SelectAll());
}
Testing
Bind the PropertyChanged
event and attempt various valid (1.2345) and invalid ("abc") entries.
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
textBoxFormatted.PropertyChanged += (sender, e) =>
{
if(e.PropertyName == nameof(TextBoxFP.Value))
{
textBoxBulk.Value = textBoxFormatted.Value * 100;
textBoxDiscount.Value = textBoxBulk.Value * - 0.10;
textBoxNet.Value = textBoxBulk.Value + textBoxDiscount.Value;
}
};
buttonTestValue.Click += (sender, e) => textBoxFormatted.Value = (double)Math.PI;
}
}
