I've put together a form in which a user can dynamically generate a customer order with one or more order positions. For each position, there are several attributes like amount, product name, price, discount etc.
My main problem is: What is the best way to deal with invalid values for the input fields? For example if a user types "X" into the amount field instead of 1, 2 or whatever.
The basic idea was to let the user enter everything they want - but the order can only be saved once every input field contains valid data. If not, all invalid fields will be highlighted so the user knows what he did wrong.
So far, this seems to work just fine but my idea was to also have a Customer_Order object which would be updated everytime the user changes the value of an input field.
Obviously I could not do that if I want to allow the user to enter Strings like "X" into Integer or Decimal fields... so it seems to me that I have 2 options:
A: Either restrict the input fields and programatically turn invalid values into zeros (For example: User enters "abc" into price field -> String will be converted to 0,00)
OR B: keep my original plan with not so strict input regulations and NOT have a Customer_Order object that is always kept up to date. I would instead create the object from scratch and fill it with all the data from the input fields when the user finishes the order.
My problem with A is that I would like to keep the input fields as non-strict as possible. If a user types in something invalid, they should SEE what they typed in instead of the program changing the value. And my problem with B is that having an always up-to-date object of the customer order makes it easier to calulate prices on the fly. If I don't have that object, I would have to read out and parse all the necessary input fields every time I want to calculate something.
I'm not that experienced with GUIs so I really don't know if I'm missing something here... what would be the most elegant way to handle this? Is it generally a bad idea to have an always up-to-date object in the background at all times?
One option is to only allow valid keys. This can be done by utilizing the KeyDown event handler.
Create a new Windows Forms App (.NET Framework) project
Add a TextBox to the form (name: textBoxAmount)
Open Solution Explorer
In VS menu, click View
Select Solution Explorer
Open Properties Window
In VS menu, click View
Select Properties Window
Add TextBox KeyDown event handler
In Properties Window, select textBoxAmount from the drop-down
Click
Double-click KeyDown
Add module (name: HelperInput.vb)
Click Project
Select Add Module... (name: HelperInput.vb)
Click OK
HelperInput.vb:
Imports System.Globalization
Module HelperInput
Public Sub TBKeyDownMonetaryValue(sender As Object, e As System.Windows.Forms.KeyEventArgs)
Dim tb As Control = DirectCast(sender, Control) 'TextBox
Dim isKeyAllowed As Boolean = False
Dim nfInfo As NumberFormatInfo = CultureInfo.CurrentUICulture.NumberFormat
Debug.WriteLine($"currency symbol: {nfInfo.CurrencySymbol} decimal separator: {nfInfo.CurrencyDecimalSeparator} number group separator: {nfInfo.NumberGroupSeparator} currency group separator: {nfInfo.CurrencyGroupSeparator}")
If Not Control.ModifierKeys = Keys.Shift Then
Select Case e.KeyCode
Case Keys.Enter
isKeyAllowed = True
Case Keys.Back
isKeyAllowed = True
Case Keys.Delete
isKeyAllowed = True
Case Keys.NumPad0
isKeyAllowed = True
Case Keys.NumPad1
isKeyAllowed = True
Case Keys.NumPad2
isKeyAllowed = True
Case Keys.NumPad3
isKeyAllowed = True
Case Keys.NumPad4
isKeyAllowed = True
Case Keys.NumPad5
isKeyAllowed = True
Case Keys.NumPad6
isKeyAllowed = True
Case Keys.NumPad7
isKeyAllowed = True
Case Keys.NumPad8
isKeyAllowed = True
Case Keys.NumPad9
isKeyAllowed = True
Case Keys.D0
isKeyAllowed = True
Case Keys.D1
isKeyAllowed = True
Case Keys.D2
isKeyAllowed = True
Case Keys.D3
isKeyAllowed = True
Case Keys.D4
isKeyAllowed = True
Case Keys.D5
isKeyAllowed = True
Case Keys.D6
isKeyAllowed = True
Case Keys.D7
isKeyAllowed = True
Case Keys.D8
isKeyAllowed = True
Case Keys.D9
isKeyAllowed = True
Case Else
isKeyAllowed = False
End Select
End If
'only allow one currency decimal separator
If e.KeyCode = Keys.Oemcomma AndAlso nfInfo.CurrencyDecimalSeparator = "," AndAlso (String.IsNullOrEmpty(tb.Text) OrElse Not tb.Text.Contains(nfInfo.CurrencyDecimalSeparator)) Then
isKeyAllowed = True
ElseIf e.KeyCode = Keys.OemPeriod AndAlso nfInfo.CurrencyDecimalSeparator = "." AndAlso (String.IsNullOrEmpty(tb.Text) OrElse Not tb.Text.Contains(nfInfo.CurrencyDecimalSeparator)) Then
isKeyAllowed = True
End If
If Not isKeyAllowed Then
e.Handled = True
e.SuppressKeyPress = True
End If
End Sub
End Module
Form1.vb:
Public Class Form1
Private Sub TextBoxAmount_KeyDown(sender As Object, e As KeyEventArgs) Handles TextBoxAmount.KeyDown
HelperInput.TBKeyDownMonetaryValue(sender, e)
End Sub
End Class
Resources:
NumberFormatInfo Class
Is it possible to make a richtextbox only able to contain one line? I want it to have the wraptext ability but I can't have multiple lines in the file it will generate.
You can set its AcceptsReturn attribute to false that should only allow one line because it won't let the text return.
If for some reason your properties window doesn't have the AcceptsReturn property, you can add if e.KeyCode = Keys.Enter Then e.SuppressKeyPress = True under the event KeyDown (as suggested by Jimi).
Simply think first,how do we get to the next line ? By pressing Return or Enter.So,in the KeyPress event of the RichTextbox you can simply use :
If e.KeyChar = Keys.Return Then
e.Handled = True
End if
Now this has a major drawback and that is : What if a user copy pastes a multi-line text into the richtextbox ?
To fix that, you can simply apply the following code in the TextChanged event :
Private Sub Rtb_TextChanged()
Dim lcount as Integer = rtb.Lines.Count
Dim i As Integer
If lcount > 1 Then
For i = 2 to lcount - 1
Dim index As Integer = rtb.GetFirstCharIndexFromLine(i)
Dim count As Integer = rtb.GetFirstCharIndexFromLine(i + 1) - start_index
rtb.Text = rtb.Text.Remove(index, count)
Next
End if
End Sub
Hope this helps :)
It looks very strange, but I can't find an online solution for my problem! At least in VB.NET.
Here's the deal:
I have a TextBox in a form (limited to numbers by a KeyPress event) and want to keep two decimal places as long as the user inputs his data.
For example, if the TextBox is blank, then, when the user presses, let's say, "2", the TextBox shows "0,02". Then, if the user presses "7", the TextBox shows "0,27". Then again, by pressing "6", it shows "2,76" and so on...
I managed to do this for one decimal place with the code:
Select Case Me.TextBox.Text
Case ""
Case ","
Me.TextBox.Text = ""
Case Else
Me.TextBox.Text = Strings.Left(Replace(Me.TextBox.Text, ",", ""), Strings.Len(Replace(Me.TextBox.Text, ",", "")) - 1) & "," & Strings.Right(Replace(Me.TextBox.Text, ",", ""), 1)
Me.TextBox.SelectionStart = Len(Me.TextBox.Text)
End Select
Please note that: 1. This code's running on a TextChanged event; 2. I'm from Portugal and here we use a comma (",") instead of a dot (".") for the decimal separator.
Could you help me to adjust my piece of code to work properly with two decimal places?
Any help will be very appreciated. And, as always, thank you all in advance.
Here's a custom class I've made which does what you require:
Public Class FactorDecimal
Private _value As String = "0"
Public DecimalPlaces As Integer
Public Sub AppendNumber(ByVal Character As Char)
If Char.IsNumber(Character) = False Then Throw New ArgumentException("Input must be a valid numerical character!", "Character")
_value = (_value & Character).TrimStart("0"c)
End Sub
Public Sub RemoveRange(ByVal Index As Integer, ByVal Length As Integer)
If _value.Length >= Me.DecimalPlaces + 1 AndAlso _
Index + Length > _value.Length - Me.DecimalPlaces Then Length -= 1 'Exclude decimal point.
If Index + Length >= _value.Length Then Length = _value.Length - Index 'Out of range checking.
_value = _value.Remove(Index, Length)
If _value.Length = 0 Then _value = "0"
End Sub
Public Overrides Function ToString() As String
Dim Result As Decimal
If Decimal.TryParse(_value, Result) = True Then
'Divide Result by (10 ^ DecimalPlaces) in order to get the amount of decimal places we want.
'For example: 2 decimal places => Result / (10 ^ 2) = Result / 100 = x,xx.
Return (Result / (10 ^ Me.DecimalPlaces)).ToString("0." & New String("0"c, Me.DecimalPlaces))
End If
Return "<parse error>"
End Function
Public Sub New(ByVal DecimalPlaces As Integer)
If DecimalPlaces <= 0 Then DecimalPlaces = 1
Me.DecimalPlaces = DecimalPlaces
End Sub
End Class
It works by letting you append numbers to form a long string of numerical characters (for example 3174 + 8 = 31748), then when you call ToString() it does the following:
It parses the long number string into a decimal (ex. "31748" => 31748.0)
It divides the decimal by 10 raised to the power of the amount of decimals you want (for example: 2 decimals => 31748.0 / 102 = 317.48).
Finally it calls ToString() on the decimal with the format 0.x - where x is a repeating amount of zeros depending on how many decimals you want (ex. 2 decimals => 0.00).
NOTE: This solution adapts to the current system's culture settings and will therefore automatically use the decimal point defined in that culture. For example in an American (en-US) system it will use the dot: 317.48, whereas in a Swedish (sv-SE) or Portuguese (pt-PT) system it will use the comma: 317,48.
You can use it like this:
Dim FactorDecimal1 As New FactorDecimal(2)
Private Sub TextBox1_KeyPress(sender As Object, e As KeyPressEventArgs) Handles TextBox1.KeyPress
If Char.IsNumber(e.KeyChar) = False Then
e.Handled = True 'Input was not a number.
Return
End If
FactorDecimal1.AppendNumber(e.KeyChar)
TextBox1.Text = FactorDecimal1.ToString()
End Sub
Private Sub TextBox1_KeyDown(sender As Object, e As KeyEventArgs) Handles TextBox1.KeyDown
Dim TargetTextBox As TextBox = DirectCast(sender, TextBox)
e.SuppressKeyPress = True
Select Case e.KeyData 'In order to not block some standard keyboard shortcuts (ignoring paste since the pasted text won't get verified).
Case Keys.Control Or Keys.C
TargetTextBox.Copy()
Case Keys.Control Or Keys.X
TargetTextBox.Cut()
Case Keys.Control Or Keys.A
TargetTextBox.SelectAll()
Case Keys.Back, Keys.Delete 'Backspace or DEL.
FactorDecimal1.RemoveRange(TextBox1.SelectionStart, If(TextBox1.SelectionLength = 0, 1, TextBox1.SelectionLength))
TextBox1.Text = FactorDecimal1.ToString()
Case Else
e.SuppressKeyPress = False 'Allow all other key presses to be passed on to the KeyPress event.
End Select
End Sub
Online test: http://ideone.com/fMcKJr
Hope this helps!
Thank you #Visual Vincent. Your method works fine. However I managed to find a simpler way of doing what I asked by the following code:
Private Sub TextBox_KeyPress(sender As Object, e As KeyPressEventArgs) Handles TextBox.KeyPress
If Not Char.IsDigit(e.KeyChar) And Not Char.IsControl(e.KeyChar) Then
e.Handled = True
End If
End Sub
Private Sub TextBox_TextChanged(sender As Object, e As EventArgs) Handles TextBox.TextChanged
Select Case Val(Replace(Me.TextBox.Text, ",", "."))
Case 0 : Me.TextBox.Text = ""
Case Else
Me.TextBox.Text = Format(Val(Replace(Me.TextBox.Text, ",", "")) / 100, "0.00")
Me.TextBox.SelectionStart = Len(Me.TextBox.Text)
End Select
End Sub
This piece of code look suspicious simple to me. For now it works fine and does the trick exactly how I wanted. Maybe there's something missing to me, or maybe I wasn't clear enough on the description of my goal.
If you find any flaw on my method, please feel free to point it! I'll appreciate it very much.
I have a text box in which i don't want decimal, so I am restricting user to key in decimal by checking in keydown event.
However, this is causing issue with ">" sign as user is not able to enter ">" sign in text box.
If Not e.Handled AndAlso e.Key = Key.Decimal OrElse e.PlatformKeyCode = 190 OrElse e.PlatformKeyCode = 110 Then
If e.Key <> Key.Back Then
e.Handled = True
End If
End If
Any inputs how can i solve?
I have various input textboxes on my form. And based on entries made in each of the input textboxes, I update the display textbox. However, using AppendText message of textbox. I have my input textboxes set to accept only numeric values. So for each character inputted correctly, I update the display Textbox. Problem I have is when user selects Key.Back or Key.Delete, what I would like to do is do a Delete of last character in the display textbox, but I am yet to get this right. How do I delete the last character in the string that is contained in a TextBox using AppendText() or any of the utility functions available in that control please?
If (e.KeyChar = ChrW(Keys.Back)) Then
txtDisplay.Text -= 1
txtDisplay.Update()
ElseIf (Not (Char.IsDigit(e.KeyChar) Or Char.IsControl(e.KeyChar) Or (e.KeyChar = "."))) Then
e.Handled = True
Else
txtDisplay.AppendText(e.KeyChar)
End If
Use this in KeypPress event handler of txtDisplay
If e.KeyChar = ChrW(Keys.Back) Then
If txtDisplay.Text.Length > 0 Then
txtDisplay.Text = txtDisplay.Text.Substring(0, txtDisplay.TextLength - 1)
txtDisplay.SelectionStart = txtDisplay.Text.Length
End If
ElseIf (Not (Char.IsDigit(e.KeyChar) Or Char.IsControl(e.KeyChar) Or (e.KeyChar = "."))) Then
e.Handled = True
End If