다음을 통해 공유


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.