I'm building a form that captures number of hours worked. This is a rewrite of a legacy application that didn't use proper controls to capture input, so one of the things that I'm doing is replacing TextBox controls used for capturing the hours with NumericUpDown controls instead.
I need some custom logic for these controls that deviate slightly from the default UI. For example, I can accept up to 2 decimal places, but it should show the least number of decimals possible. The other thing is that if the value is zero, then don't show any value.
In the designer, I have each NumericUpDown's BackgroundColor and ForeColor set to Window since their value starts at zero and then there's logic in the Enter/Leave that changes the ForeColor:
Code:
Private Sub NumericUpDowns_Enter(sender As Object, e As EventArgs) Handles NumericUpDownHours.Enter, NumericUpDownOvertime.Enter, NumericUpDownPremiumPay.Enter
Dim nud As NumericUpDown = TryCast(sender, NumericUpDown)
If (nud Is Nothing) Then
Return
End If
nud.DecimalPlaces = 2 ' allow for 2 decimals
nud.ForeColor = ForeColor ' show the value to the user
Dim displayedValue As String = nud.Value.ToString("F2")
nud.Select(0, displayedValue.Length) ' select the control
End Sub
Private Sub NumericUpDowns_Leave(sender As Object, e As EventArgs) Handles NumericUpDownHours.Leave, NumericUpDownOvertime.Leave, NumericUpDownPremiumPay.Leave
Dim nud As NumericUpDown = TryCast(sender, NumericUpDown)
If (nud Is Nothing) Then
Return
End If
If (nud.Value = 0) Then
' hide the value from the user
nud.ForeColor = nud.BackColor
End If
Dim decimalPlaces As Integer = 0 ' show 0 decimal places
Dim value As String = nud.Value.ToString("F2")
If (value(value.Length - 1) <> "0"c) Then
decimalPlaces = 2 ' show 2 decimal places
ElseIf (value(value.Length - 2) <> "0"c) Then
decimalPlaces = 1 ' show 1 decimal places
End If
nud.DecimalPlaces = decimalPlaces
End Sub
The problem I'm running into is that NumericUpDownPremiumPay's Enabled property is driven by the state of a checkbox and when the NumericUpDown is disabled then the ForeColor is the disabled color instead of the BackColor.
If you look at this picture, now how Hours and Overtime look empty, but Premium Pay does not.
Edit - Eff it, I'm tired of trying to get the image to render in thread. Just open the link and get redirected I guess.
If (nud.Value = 0) Then
' hide the value from the user
If (nud.Enabled = True) Then
nud.ForeColor = nud.BackColor
Else
'Some sort of appropriate manipluation of ForeColor or BackColor to handle a disabled NUD
End If
End If
That's what I was doing, but the system was not respecting the ForeColor when it was disabled.
Rather than hacking back/fore colors, I created a custom control that overrides the UpdateEditText method:
Code:
Namespace Controls
Public Class NumericTextBox
Inherits NumericUpDown
Public Property HideValueOnZero As Boolean
Private _maximumDecimalPlaces As Integer?
Public Property MaximumDecimalPlaces As Integer?
Get
Return _maximumDecimalPlaces
End Get
Set(value As Integer?)
If (value.HasValue AndAlso value.Value < 0) Then
Throw New ArgumentOutOfRangeException(NameOf(value))
End If
_maximumDecimalPlaces = value
End Set
End Property
Public Sub New()
HideValueOnZero = True
_maximumDecimalPlaces = 2
End Sub
Protected Overrides Sub OnEnabledChanged(e As EventArgs)
MyBase.OnEnabledChanged(e)
UpdateEditText()
End Sub
Protected Overrides Sub OnEnter(e As EventArgs)
MyBase.OnEnter(e)
If (_maximumDecimalPlaces.HasValue) Then
ToggleDecimals(_maximumDecimalPlaces.Value)
End If
SelectValue()
UpdateEditText()
End Sub
Protected Overrides Sub OnLeave(e As EventArgs)
MyBase.OnLeave(e)
UpdateEditText()
If (_maximumDecimalPlaces.HasValue) Then
ToggleDecimals()
End If
End Sub
Protected Overrides Sub OnValueChanged(e As EventArgs)
MyBase.OnValueChanged(e)
UpdateEditText()
End Sub
Protected Overrides Sub UpdateEditText()
MyBase.UpdateEditText()
If (HideValueOnZero AndAlso Value = 0 AndAlso Not ContainsFocus) Then
Dim internalTextBox As TextBox = GetInternalTextBox()
If (internalTextBox IsNot Nothing) Then
internalTextBox.Text = String.Empty
End If
End If
End Sub
Private Function GetInternalTextBox() As TextBox
Return Controls.OfType(Of TextBox).FirstOrDefault()
End Function
Private Sub SelectValue()
Dim displayedValue As String = Value.ToString("F2")
MyBase.Select(0, displayedValue.Length)
End Sub
Private Sub ToggleDecimals(Optional decimals As Integer? = Nothing)
If (decimals.HasValue) Then
DecimalPlaces = decimals.Value
Return
End If
Dim places As Integer = 0
Dim valueAsString As String = Value.ToString("F2")
If (valueAsString(valueAsString.Length - 1) <> "0"c) Then
places = 2
ElseIf (valueAsString(valueAsString.Length - 2) <> "0"c) Then
places = 1
End If
DecimalPlaces = places
End Sub
End Class
End Namespace