VB.NET Writing better code Part 2
Introduction
Part 1 of this series covered best practices while writing code with Visual Basic .NET which leads to this part of the series which provides several common tasks, how they are generally handled by the average developer or hobbyist that never intend to become developers. Both groups will think of something they need then begin coding without any plan then at one time or another run into roadblocks. By considering what is presented below can assist with those coders who will never plan out or consider an architecture before writing a line of code.
Although the majority of examples are working with Windows forms what is being presented applies to any type of project e.g. WPF, UWP, ASP.NET and class projects.
Assertions
Always assume consider asserting code which may fail in production like a database connection.
Protecting users from users
Rather than assuming values entered into a control will always be correct its best to assert that values entered are correct along with are valid values for a particular task. For instance, an input should be of type Integer, the first assertion should be can the value in a (for this example) TextBox can be converted to an Integer.
Initially this can be done with subscribing to KeyPress event of the TextBox.
Private Sub IdentifierTextBox_KeyPress(sender As Object, e As KeyPressEventArgs) _
Handles SetValue1TextBox.KeyPress
If (Not Char.IsControl(e.KeyChar)) AndAlso (Not Char.IsDigit(e.KeyChar)) Then
e.Handled = True
End If
End Sub
Although the code will ensure only 0-9 can be entered there is nothing to prevent a user from pasting non-integer values into the TextBox. This is were proper planning ahead of time comes into play. A better option would be to select a standard control like NumericUpDown or a custom TextBox as shown below.
Imports System.Windows.Forms
Public Class NumericTextbox
Inherits TextBox
Const WM_PASTE As Integer = &H302
<System.ComponentModel.Browsable(False)>
Public ReadOnly Property AsDouble As Double
Get
If String.IsNullOrWhiteSpace(Text) Then
Return 0
Else
Return CDbl(Text)
End If
End Get
End Property
Public ReadOnly Property AsDecimal As Decimal
Get
If String.IsNullOrWhiteSpace(Text) Then
Return 0
Else
Return CDec(Text)
End If
End Get
End Property
Public ReadOnly Property AsInteger As Integer
Get
If String.IsNullOrWhiteSpace(Text) Then
Return 0
Else
Return CInt(Text)
End If
End Get
End Property
Public ReadOnly Property IsInteger As Boolean
Get
If Integer.TryParse(Text, Nothing) Then
Return True
Else
Return False
End If
End Get
End Property
Protected Overrides Sub OnKeyPress(e As KeyPressEventArgs)
Dim Value As String = Text
Value = Value.Remove(SelectionStart, SelectionLength)
Value = Value.Insert(SelectionStart, e.KeyChar)
e.Handled = Value.LastIndexOf("-", StringComparison.Ordinal) > 0 Or
Not (Char.IsControl(e.KeyChar) OrElse Char.IsDigit(e.KeyChar) OrElse (e.KeyChar = "."c And
Not Text.Contains(".") Or e.KeyChar = "."c And SelectedText.Contains(".")) OrElse
(e.KeyChar = "-"c And SelectionStart = 0))
MyBase.OnKeyPress(e)
End Sub
Protected Overrides Sub WndProc(ByRef m As Message)
If m.Msg = WM_PASTE Then
Dim value As String = Text
value = value.Remove(SelectionStart, SelectionLength)
value = value.Insert(SelectionStart, Clipboard.GetText)
Dim result As Decimal = 0
If Not Decimal.TryParse(value, result) Then
Return
End If
End If
MyBase.WndProc(m)
End Sub
End Class
Great, this TextBox will provide a control to permit numeric only and prevent pasting from the Windows Clipboard. If this was found after placing 10 plus TextBoxes on a form is now a problem as each standard TextBox must be removed and each time one is removed any underlying code is broken for a while.
This is why one should not sit down and write code without considering ramifications as per the above example.
Connect to data on other machines
Programmers and hobbyist coders will write an application, connect to a Microsoft Access database with a hard-coded path to a folder that does not exists on another machine or connection to a server-based database e.g. SQL-Server Express edition then when a connection is not valid can lock up a machine while the data provider attempts to connection to a non-existing database or database server. This is because no thought has been done to consider that the database file path does not exists or that the database server does not exist, SQL-Server is not installed are the top reasons for failure points.
When dealing with local database files like Microsoft Access always assert for file existence using System.IO.File.Exists before attempting to open the database. A commercial application should wrap opening a database with try-catch statement to ensure that the database can be opened, and the table(s) are accessible. When working with time consuming data operations implement a cancellation process which can interrupt the process or simply abort the process.
To test a connection create methods which only perform a test to connect. The following code snippets show performing a test to open a database synchronously and asynchronously.
Public Async Function TestConnectionAsync() As Task(Of Boolean)
Using cn As New SqlConnection With {.ConnectionString = ConnectionString}
Try
Await cn.OpenAsync()
Return True
Catch ex As Exception
Return False
End Try
End Using
End Function
Public Function TestConnection() As Boolean
Using cn As New SqlConnection With {.ConnectionString = ConnectionString}
Try
cn.Open()
Return True
Catch ex As Exception
Return False
End Try
End Using
End Function
Usage can be found in the following form in the GitHub repository for this article.
Another consideration is to ensure sensitive data is protected. For simple protection for SQL-Server see the following article and then research various types of connections and setting permissions to a SQL-Server database before there is a project which requires very secure connections.
Working with placing data elements on a form
Its common place to read data from a file, usually delimited by a comma, tab or fixed position. A developer has been given the task of reading a delimited text file and placing elements from each line in a text file. Elements are fixed e.g. four elements per line. Mistaken number one is assuming each element is the correct type and that there are exactly x amount of elements per line and no empty lines.
Any time a task involves touching any text file this should ring bells that there are possibilities of problems, yet the average coder does not think about this. In the following example only, a few considerations are looked at with an outcome that might very well have a coder have to toss out all their code and start over as halfway through the project their manager injects new requirements. If the coder/developer asked enough questions this would solved wasting hours coding in a wrong direction.
First pass they realize sometimes data is not always perfect, read the text file which has several lines but with only enough TextBox controls for one line. The next evolution is to create a class which has properties which represent elements in a line.
Public Class Item
Public Sub New()
End Sub
Public Sub New(line As String)
Dim parts = line.Split(","c).ToIntegerArray()
If parts.Length = 4 Then
Value1 = parts(0)
Value2 = parts(1)
Value3 = parts(2)
Value4 = parts(3)
End If
End Sub
Public Property Value1() As Integer
Public Property Value2() As Integer
Public Property Value3() As Integer
Public Property Value4() As Integer
End Class
The above class and the class which follow are placed in their own file as placing code in a form is unwise if the code is to be used in another form or even another project.
Public Class FileOperations
Public Function Read() As List(Of Item)
Dim lines = File.ReadAllLines("TextFile2.txt").Where(
Function(line)
Dim parts = line.Split(","c)
If line.Length > 0 AndAlso parts.Length = 4 Then
Return True
Else
Return False
End If
End Function).Select(Function(fileItem) New Item(fileItem))
Return lines.ToList()
End Function
End Class
TextBox controls are placed on a form TextBox1, TextBox2, TextBox3, TextBox4 (which have no meaning to the actual data which may be find here but a bad practice when another task involves many more TextBox controls).
Public Class Form2
Private Sub Binding1Button_Click(sender As Object, e As EventArgs) Handles Binding1Button.Click
Dim operations = New Example1.Classes1.FileOperations
Dim item = operations.Read().FirstOrDefault()
TextBox1.Text = item.Value1.ToString()
TextBox2.Text = item.Value2.ToString()
TextBox3.Text = item.Value3.ToString()
TextBox4.Text = item.Value3.ToString()
End Sub
End Class
The text file
23,55,81,10
11,57,82,87
14,58,89,11
4,8,9,1
88,0,90,91
Results
Now the manager says "how can I look at all lines one by one?" or "how can I look at all lines one by one, search for a specific line and even edit values?"
Coders/developers with no plan generally think that an array where each element has values to place into a TextBox and will use the element index as the line number yet this has issues if there are blank lines that are not allowed. This tends to become a struggle taking days, weeks of frustration. If time was taken ahead of time with no pressure a viable solution may present itself.
The following is not all encompassing, just enough to provide a great head start with room to grow.
Learning points which will assist with the solution which will be used in the next evolution of this task.
- Working with a BindingSource, many developers never heard of them, have heard of them but unsure why they are needed.
- Data binding without strong typed datasets which tend to be what most code samples use for examples of data binding.
- Using INotifyPropertyChanged, to many developers are foreign to what this Interface is capable of.
The next step is to a) create a temporary property for the line number which can be thought of as a primary key or perhaps there is a primary key already in which will be in this case.
The text file has a primary key where on one line the key is incorrect, line two which has a letter rather than a number.
1,23,55,81,10
o,11,57,82,87
3,14,58,89,11
4,4,8,9,1
5,88,0,90,91
Class now has a property for the key element in position zero.
Public Class Test
Public Sub New(line As String)
Dim parts = line.Split(","c).ToIntegerArray()
If parts.Length = 5 Then
Id = parts(0)
Value1 = parts(1)
Value2 = parts(2)
Value3 = parts(3)
Value4 = parts(4)
End If
End Sub
Public Property Id() As Integer
Public Property Value1() As Integer
Public Property Value2() As Integer
Public Property Value3() As Integer
Public Property Value4() As Integer
End Class
To be able to work through presenting all lines a BindingSource is used to provide navigation in tangent with a BindingNavigator. Note only one property is setup with data binding where data binding provides a control, in this case a TextBox to present a single property of the current item.
Public Class Form2
Private BindingSource As New BindingSource
Private Sub Binding1Button_Click(sender As Object, e As EventArgs) _
Handles Binding1Button.Click
Dim operations = New Example1.Classes1.FileOperations
BindingSource.DataSource = operations.Read()
BindingNavigator1.BindingSource = BindingSource
Value1TextBox.DataBindings.Add("Text", BindingSource, "Value1")
End Sub
End Class
As mentioned above a new requirement is to permit editing so to do this the Current property of the BIndingSource which is of type Object needs to be cast to Item. To test this out the following code will be used.
Private Sub SetCurrentValue1Button_Click(sender As Object, e As EventArgs) _
Handles SetCurrentValue1Button.Click
CType(BindingSource.Current, Item).Value1 = 2
End Sub
Running the code causes no errors but the TextBox is not updated. A common way to learn what happen is to use Console.WriteLine
CType(BindingSource.Current, Item).Value1 = 2
Console.WriteLine(CType(BindingSource.Current, Item).Value1)
The second line reports that Value1 is 2 but not the TextBox. This is because even though there is a binding between TextBox and a property does not mean the TextBox will be notified of the change which is the problem.
A hack is to add ResetCurrentItem of the BindingSource.
CType(BindingSource.Current, Item).Value1 = 2
BindingSource.ResetCurrentItem()
This in turn notifies any data bindings there was a change. Why is it a hack? Because the developer randomly search for "why did my TextBox value not update. . .. " and the answers were from developers who were not educated enough or the developer searching found solutions which appeared to complex, "over their head" and they think "I will take the simple way out".
"I will take the simple way out" can be good in many cases but than there are cases where taking the simple path results in what is commonly known as spaghetti code which usually makes maintaining project code nearly impossible.
The takeaway here is to not simply begin coding without understanding what is expected for the final product. This elevates having to patch up existing code which is now classified as spaghetti code or to throw a way existing code and start over.
With research and taking time to learn about INotificationChanged for Windows Forms or two-way binding for XAML based solutions will be costly up front having never used it before but provided a much better product.
The following version of the class used prior has been rigged up with notification change. Note only Value1 has been setup for notification change, all properties can be done also, only one property has been done for easy learning.
Public Class Item
Implements INotifyPropertyChanged
Private _value1 As Integer
Public Sub New()
End Sub
Public Sub New(line As String)
Dim parts = line.Split(","c).ToIntegerArray()
If parts.Length = 5 Then
Id = parts(0)
Value1 = parts(1)
Value2 = parts(2)
Value3 = parts(3)
Value4 = parts(4)
End If
End Sub
Public Property Id() As Integer
Public Property Value1() As Integer
Get
Return _value1
End Get
Set(ByVal value As Integer)
If Not (value = _value1) Then
_value1 = value
NotifyPropertyChanged()
End If
End Set
End Property
Public Property Value2() As Integer
Public Property Value3() As Integer
Public Property Value4() As Integer
Public ReadOnly Property Array() As Integer()
Get
Return New Integer() {Value1, Value2, Value3, Value4}
End Get
End Property
Public Overrides Function ToString() As String
Return Id.ToString()
End Function
Public Event PropertyChanged As PropertyChangedEventHandler _
Implements INotifyPropertyChanged.PropertyChanged
Private Sub NotifyPropertyChanged(<CallerMemberName()>
Optional ByVal propertyName As String = Nothing)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
End Class
Note: When placing Implements INotifiationChanged to a class Visual Studio will light up the line request to implement missing members, allow Visual Studio to perform this task. Then note that the procedure NotifyPropertyChange will look different than the one above, for your implementation use the default Visual Studio provided or the above model which no property name need be passed as CallerMemberName will have the property name.
For the form code which has extras on the data binding. Open the project once the source code for this article has been downloaded or cloned via Visual Studio Team Explorer and work through the code.
Imports Example1.LanguageExtensions
Imports Item = Example1.Classes.Item
Public Class Form1
Private BindingSource As New BindingSource
Private Sub Binding1Button_Click(sender As Object, e As EventArgs) Handles Binding1Button.Click
BindingNavigator1.BindingSource = Nothing
Dim operations = New Example1.Classes.FileOperations
Dim itemList = operations.Read()
If Not String.IsNullOrWhiteSpace(IdentifierTextBox.Text) Then
Dim singleItem = itemList.FirstOrDefault(Function(item) item.Id = CInt(IdentifierTextBox.Text))
If singleItem IsNot Nothing Then
BindingSource.DataSource = singleItem
Else
TextBoxList.ForEach(Sub(tb)
tb.Text = ""
End Sub)
Exit Sub
End If
Else
BindingNavigator1.BindingSource = BindingSource
BindingSource.DataSource = itemList
End If
'
' Binding must be cleared if this event is used multiple times
'
IdentifierLabel.DataBindings.Clear()
Value1TextBox.DataBindings.Clear()
Value2TextBox.DataBindings.Clear()
Value3TextBox.DataBindings.Clear()
Value4TextBox.DataBindings.Clear()
IdentifierLabel.DataBindings.Add("Text", BindingSource, "id")
'
' setup formatting to pad with leading zeros
'
Value1TextBox.DataBindings.Add("Text", BindingSource, "Value1", True,
DataSourceUpdateMode.OnValidation, vbNullString, "D3")
'
' Alternate method for binding to a property
'
Dim value2Binding As Binding = New Binding("Text", BindingSource, "Value2")
AddHandler value2Binding.Format, AddressOf SimpleFormattingToEmptyForValue2
Value2TextBox.DataBindings.Add(value2Binding)
Value3TextBox.DataBindings.Add("Text", BindingSource, "Value3")
Value4TextBox.DataBindings.Add("Text", BindingSource, "Value4")
End Sub
''' <summary>
''' Demonstrates formatting a value using binding format event, in
''' this case when a value is o show (empty).
''' </summary>
''' <param name="sender"></param>
''' <param name="e"></param>
Private Sub SimpleFormattingToEmptyForValue2(sender As Object, e As ConvertEventArgs)
If e.DesiredType Is GetType(String) Then
Dim value = CInt(e.Value)
If value = 0 Then
e.Value = "(empty)"
End If
End If
End Sub
''' <summary>
''' Only permit integer values
''' </summary>
''' <param name="sender"></param>
''' <param name="e"></param>
Private Sub IdentifierTextBox_KeyPress(sender As Object, e As KeyPressEventArgs) _
Handles IdentifierTextBox.KeyPress, SetValue1TextBox.KeyPress
If (Not Char.IsControl(e.KeyChar)) AndAlso (Not Char.IsDigit(e.KeyChar)) Then
e.Handled = True
End If
End Sub
Private Sub SetCurrentValue1Button_Click(sender As Object, e As EventArgs) _
Handles SetCurrentValue1Button.Click
CType(BindingSource.Current, Item).Value1 = CInt(SetValue1TextBox.Text)
End Sub
End Class
Input validation
When working with data input many times validation is done haphazardly or never considered until late into coding. By haphazardly, code is written in one form and copied and pasted into another form with no consideration for if the code pasted into another form does proper validation. To remedy this take time to learn how to validate user inputs in classes which any form can used or when dealing with WPF the same applies where data annotations can be used in both windows forms and WPF projects, see the following article for learning how to work with data annotations using VB.NET. The following screenshot comes from the following TechNet article.
Summary
In this article the focus has been “think” before writing a single line of code to prevent losing time over misunderstandings about a project business requirement where business requirements may be for writing solutions for a company, something to sell or for personal use. By first analyzing needs to complete a software solution will save time and produce a better product.
See also
VB.NET Writing better code part 1
.NET: Defensive data programming part 1
VB.NET Writing better code Part 2.NET: Defensive data programming Part 4 (a) Data Annotation
VB.NET: SQL Injection Protection using parameterized queries
Source code
A Microsoft Visual Studio solution created with version 2017 which works with version 2017 and higher, for lower releases of Visual Studio minor adjustments will be required. One of the project requires SQL-Server available, the project has a script to create a database with tables and data.