0
votes

I am creating an ATM application for a project. I am using VB.net to update the balance on MS access to a new balance to reflect the actions of the withdraw function on the application. Currently the new balance appears in the 'txtNewBalance' however it doesn't update on the database itself and in-turn can't be used on a separate deposit page as the original balance appears and not the updated balance after withdrawal of some of the balance. The following is my code:

Public Class frmWithdraw

    Dim objConnection As New OleDb.OleDbConnection("Provider=Microsoft.ACE.OLEDB.12.0; Data Source=ATM2.accdb")

    Dim objAccountDA As New OleDb.OleDbDataAdapter("Select * from tblAccount", objConnection)
    Dim objAccountCB As New OleDb.OleDbCommandBuilder(objAccountDA)
    Dim AccountDataSet As New DataSet()

    Public Sub StoreDetails()
        Dim objRow As DataRow

        objRow = AccountDataSet.Tables("tblAccount").Rows.Find("AccounNum")

        objRow.Item("AccountNum") = txtAccountNum.Text
        objRow.Item("AccountBalance") = txtBalance.Text

    End Sub

    Public Sub Retrieve()

        objAccountDA.FillSchema(AccountDataSet, SchemaType.Source, "tblAccount")
        objAccountDA.Fill(AccountDataSet, "tblAccount")

        txtAccountNum.Text = frmLogin.EmployeeNO

        FillAccountDetails()
        'FillUserDetails()
    End Sub

    Private Sub frmWithdraw_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        'TODO: This line of code loads data into the 'ATM2DataSet.tblAccount' table. You can move, or remove it, as needed.
        ' Me.TblAccountTableAdapter.Fill(Me.ATM2DataSet.tblAccount)
        Retrieve()
    End Sub

    Public Sub FillAccountDetails()
        Dim objRow As DataRow
        objRow = AccountDataSet.Tables("tblAccount").Rows.Find(txtAccountNum.Text.ToString)
        txtBalance.Text = objRow.Item("Balance").ToString

    End Sub

    Private Sub btnCancel_Click(sender As Object, e As EventArgs) Handles btnCancel.Click
        Me.Hide()
        frmMain.Show()
    End Sub

    Private Sub btnWithdraw_Click(sender As Object, e As EventArgs) Handles btnWithdraw.Click

        If txtOutput.Text = "" Or txtOutput.Text = "0" Or txtOutput.Text >= 300 Then
            MessageBox.Show("Invalid amount")
            txtOutput.Clear()

        End If

        txtNewBal.Text = Val(txtBalance.Text) - Val(txtOutput.Text)
        objAccountDA.Update(AccountDataSet, "tblAccount")
        AccountDataSet.AcceptChanges()
        Dim objCurrentRow As DataRow
        objCurrentRow = AccountDataSet.Tables("tblAccount").Rows.Find(txtAccountNum.Text.ToString)
        objCurrentRow("Balance") = Val(txtNewBal.Text.ToString)

        Retrieve()
        MessageBox.Show("Withdrawal Confirmed")

        Me.Hide()
        frmMain.Show()

    End Sub

    Private Sub TblAccountBindingNavigatorSaveItem_Click(sender As Object, e As EventArgs)
        Me.Validate()
        Me.TblAccountBindingSource.EndEdit()
        Me.TableAdapterManager.UpdateAll(Me.ATM2DataSet)

    End Sub

    Private Sub btn20_Click(sender As Object, e As EventArgs) Handles btn20.Click
        txtOutput.Text = 20 * numAmount.Text
    End Sub

    Private Sub btn40_Click(sender As Object, e As EventArgs) Handles btn40.Click
        txtOutput.Text = 40 * numAmount.Text
    End Sub

    Private Sub btn60_Click(sender As Object, e As EventArgs) Handles btn60.Click
        txtOutput.Text = 60 * numAmount.Text
    End Sub

    Private Sub btn80_Click(sender As Object, e As EventArgs) Handles btn80.Click
        txtOutput.Text = 80 * numAmount.Text
    End Sub

    Private Sub btn100_Click(sender As Object, e As EventArgs) Handles btn100.Click
        txtOutput.Text = 100 * numAmount.Text
    End Sub

    Private Sub HelpToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles HelpToolStripMenuItem.Click
        Me.Hide()
        frmHelp.Show()
    End Sub

End Class
2
You code is a bit confused, and seems to be mixing weakly and strongly typed datatable methodologies. I can't see anywhere that you actually decrement the datatable amount, but I also suspect that the dataset being sent for update and the dataset showing things on the form, is a different dataset..Caius Jard
Normally, aggregate data should not be saved but calculated when needed. There are ways to show opening balance on report without saving and updating field.June7
Set Option Strict Onpreciousbetine
Cannot stress @preciousbetine comment enough; MS should ship VB.NET with Strict on. They should also ditch the DLLs intended to make VB6 code work with copy and paste, like Val, CInt, MsgBox etc - these things all have new proper names nowCaius Jard

2 Answers

1
votes

Currently the new balance appears in the 'txtNewBalance' however it doesn't update on the database itself and in-turn can't be used on a separate deposit page as the original balance appears and not the updated balance after withdrawal of some of the balance.

I don't see txtNewBalance in your code, so I am not sure how you retrieve and maintain the value of this control. I suppose it is another form, that may be non-modal and possibly out of sync with the activity carried out in the other forms.

The code does not look very safe overall and I see a lot of opportunity for things to go wrong.

I am worried with this bit of code:

If txtOutput.Text = "" Or txtOutput.Text = "0" Or txtOutput.Text >= 300 Then
    MessageBox.Show("Invalid amount")
    txtOutput.Clear()

End If

Here you check txtOutput, but even if the value entered is incorrect the rest of the code will execute nonetheless. MessageBox suspends the execution but does not end it. If you want to stop execution, you should add Exit Sub right after txtOutput.Clear(). I think this is what you intended. What happens here, is that you proceed anyway and update AccountDataSet no matter what, and you change txtNewBal too. I am not sure about the final result, but it's extremely likely to be wrong. That could be your problem, and that may not be the only one.

In fact, this check is incorrect because you are only testing for 3 specific conditions, but there are many more that could occur: just think whitespace or anything else.


To further elaborate on the possible security issues that may occur consider this code in the Retrieve function:

txtAccountNum.Text = frmLogin.EmployeeNO

A better way would be to define EmployeeNO as a global variable right after login and pass that variable as a parameter to your function, rather than repeatedly checking the other form to fetch this value.

Imagine that after login, a malicious person goes back to the login form and changes the value of EmployeeNO (presumably a textbox). Your Retrieve function will then use a new value, that could be that of another employee.

'Normally' that should not happen. But you use the Show function for your forms and not ShowDialog, that means your forms are not modal. If there is flaw in your code and the Hide does fail for some reason, you could find yourself in a situation where more than one form is visible at the same time and it is possible to switch from one form to another to trick your application into doing bad things.


Other remarks:

In all cases I would strongly advise to log all actions to a file. It is important to keep an activity trail, especially when there is money or real goods involved.

Would you do business with a bank that is unable to explain where and when you withdrew money from your account ?

Any changes should be committed to database immediately. Don't keep changes in a dataset for any longer than strictly necessary. Storing data in memory is just too dangerous. If there is a power cut you could lose data and your accounts may be in an inconsistent state.

If you have to update more than than one record, it will be important to use transactions to preserve the integrity of your data. Imagine that you credit one account but your code crashes before you can deduct the corresponding amount from the source account. You are left with a big discrepancy in your accounts. What is even worse is the inability to explain where the discrepancy comes from.

Regarding validation of user input in your code: are you really using regular textboxes ? A better alternative would be the NumericUpDown control. It behaves like a textbox but will limit input to numbers, in a range that you can define in advance. That also improves your UI a little bit.

Are you even handling exceptions in your code ?

To sum up here are some of things you might want to do:

  • improve validation techniques
  • choose the best UI controls for the job
  • log everything
  • have strong exception handling - if in doubt stop your application rather than risk doing something wrong
  • maybe read a few tutorials, because many people have done similar stuff and there is no need to reinvent the wheel every time. On Stack Overflow alone there is plenty of vetted code, not to mention repositories like Github where you can see how applications are built. You can take inspiration from the things that exist. I myself have learned a lot simply by watching the more experienced developers. It is important to learn best practices early and not develop bad habits that will always bite you later.

An ATM application sounds like serious stuff, especially if you are going to service a number of people and you are not the sole user. You don't want to make them angry. You certainly cannot afford to be sloppy. So take your time and document yourself. Good luck.

1
votes

OK, so your project seems to have a strongly typed dataset called ATM2DataSet, so I'll regularize your code to use that; don't use weakly typed datasets if you're using strongly typed ones - there's no need. Strongly typed ones are better and easier to use

Here's what I mean:

Me.ATM2DataSet.tblAccount(0).Balance += 100 'strongly typed

'weakly typed, not good
Me.someDataSetIMadeJustNow.Tables("tblAccount").Rows(0).Item("Balance")= DirectCast(Me.someDataSetIMadeJustNow.Tables("tblAccount").Rows(0).Item("Balance"), Integer) + 100 

With the weakly typed dataset you're retrieving table and column names by strings or numeric indexes, and you get Object types returned. With the strongly typed dataset, you have named properties (tblAccount, Balance) that are the correct datatype already without any casting

Public Class frmWithdraw

    'don't need any of those things you dim'd


    Public Sub PopulateTextBoxes()

        Me.tblAccountTableAdapter.Fill(Me.ATM2DataSet.tblAccount)

        Dim ro = Me.ATM2DataSet.tblAccount.FindByAccountNum(txtAccountNum.Text)

        If ro IsNot Nothing Then ro.Balance = Convert.ToInt32(txtBalance.Text)

    End Sub

    Private Sub frmWithdraw_Load(sender As Object, e As EventArgs) Handles MyBase.Load

        txtAccountNum.Text = frmLogin.EmployeeNO

        txtAccountNum.ReadOnly = true ' stop problems if the user edits it

        PopulateTextBoxes()

    End Sub

    Private Sub btnCancel_Click(sender As Object, e As EventArgs) Handles btnCancel.Click
        Me.Hide()
        frmMain.Show()
    End Sub

    Private Sub btnWithdraw_Click(sender As Object, e As EventArgs) Handles btnWithdraw.Click

        'parse the amount the user wants to withdraw (in output.Text)
        Dim amount as Integer

        'if you use a numericUpDown with a min of 0 and max of 300 it makes this a LOT easier - no more invalid amounts or parsing!
        If Not Integer.TryParse(txtOutput.Text, ByRef amount) OrElse amount < 0 OrElse amount >= 300  Then

            MessageBox.Show("Invalid amount")
            txtOutput.Clear()
            Return
        End If


        Dim ro = Me.ATM2DataSet.FindByAccountNum(txtAccountNum.Text)

        If amount > ro.Balance Then 
          'overdraft? deny?
        End If

        ro.Balance -= amount 'decrement balance

        Me.TblAccountTableAdapter.Update(ro) 'save to db

        MessageBox.Show("Withdrawal Confirmed")

        Me.Hide()
        frmMain.Show()

    End Sub


    'you should turn on Option Strict, and not treat strings and numbers the same
    Private Sub btn20_Click(sender As Object, e As EventArgs) Handles btn20.Click
        txtOutput.Text = (20 * Convert.ToInt32(numAmount.Text)).ToString()
    End Sub

    Private Sub btn40_Click(sender As Object, e As EventArgs) Handles btn40.Click
        txtOutput.Text = (40 * Convert.ToInt32(numAmount.Text)).ToString()
    End Sub

    Private Sub btn60_Click(sender As Object, e As EventArgs) Handles btn60.Click
        txtOutput.Text = (60 * Convert.ToInt32(numAmount.Text)).ToString()
    End Sub

    Private Sub btn80_Click(sender As Object, e As EventArgs) Handles btn80.Click
        txtOutput.Text = (80 * Convert.ToInt32(numAmount.Text)).ToString()
    End Sub

    Private Sub btn100_Click(sender As Object, e As EventArgs) Handles btn100.Click
        txtOutput.Text = (100 * Convert.ToInt32(numAmount.Text)).ToString()
    End Sub

    Private Sub HelpToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles HelpToolStripMenuItem.Click
        Me.Hide()
        frmHelp.Show()
    End Sub

End Class

Now, I don't guarantee I've got all the flow of the program right here, because there are things I don't know, but I'm trying to demonstrate how to use the dataset and tableadapters on your form, and show you that making more DataSets and OleDbCOmmandXXX objects is totally unnecessary; your form already has everything it needs to read and wrote the db with the ATM2DataSet and xxxTableAdapter.

We load the form, and the only thing to do is put the accountNumber in the relevant box and load up the balance

The next thing you need to look at, is you(i) download the whole DB into the dataset and then use the FindByXXX method to search the datatable by its primary key. THe better thing to do is modify the definition of the query in the tableadapter (open the ATM2Dataset file in your solution explorer) to be like this (or add another query that does this):

SELECT * FROM tablAccount WHERE accountNum = ?

The tableadapter will thus acquire another parameter to its fill method where you can specify the account number to laod. I don't know i your accountNum is a string or an int, but it would, after making your SQL query capable of searching on parameters, look like this:

'you choose the fill by name during the wizard
Me.tblAccountTableAdapter.FillByAccNum(Me.ATM2DataSet.tblAccount, txtAccountNum.Text)

'or if it's an integer
Me.tblAccountTableAdapter.FillByAccNum(Me.ATM2DataSet.tblAccount, Convert.ToInt32(txtAccountNum.Text))

This will only download the account you're interested in, instead of all the entire db table. If account Num is the primary key, then there will be only one row downloaded, so you can access it by index:

Me.ATM2DataSet.tblAccount[0].Balance -= 250 'make a withdraw of 250

This means you can make the appropriate changes to the PopulateTextboxes method. If you eventually get sick of putting data in and out of textboxes, look up databinding. It's real easy with strongly typed datasets - just drag things out of the Data Sources window (View menu>> Other windows) onto the form. Voila; textboxes that are conencted to the ATM2DataSet appear, and they keep their values up to date/when you type in them they auto change the dataset. All you neeed to is save the results. Oh, and understand that a BindingSource sits between the textbox (which only shows one record) and the dataset (which holds multiple records) and maintains the concept of "Current"