Skip to content

6. Examples

In this chapter, we will illustrate what we have seen previously through a series of examples.

6.1. Example 1

6.1.1. The Problem

This application must allow a user to calculate their taxes. We are considering the simplified case of a taxpayer who has only their salary to report (2004 figures for 2003 income):

  • We calculate the number of tax brackets for the employee as nbParts = nbEnfants / 2 + 1 if they are unmarried, and nbEnfants / 2 + 2 if they are married, where nbEnfants is the number of children.
  • If he has at least three children, he is entitled to an additional half-share
  • His taxable income R is calculated as R = 0.72 * S, where S is his annual salary
  • We calculate his family coefficient QF = R / nbParts
  • We calculate his tax I. Consider the following table:
4262
0
0
8382
0.0683
291.09
14,753
0.1914
1,322.92
23,888
0.2826
2,668.39
38,868
0.3738
4,846.98
47,932
0.4262
6,883.66
0
0.4809
9505.54

Each row has 3 fields. To calculate tax I, find the first row where QF <= field1. For example, if QF = 5000, the row found will be

    8382        0.0683        291.09

Tax I is then equal to 0.0683*R - 291.09*nbParts. If QF is such that the condition QF<=field1 is never met, then the coefficients from the last row are used. Here:

    0                0.4809    9505.54

which gives tax I = 0.4809*R - 9505.54*nbParts.

6.1.2. The application's MVC structure

The application's MVC structure will be as follows:

Image

The controller role will be handled by the [main.aspx] page. There will be three possible actions:

  • init: corresponds to the client’s first request. The controller will display the [formulaire.aspx] view
  • calcul: corresponds to the request to calculate the tax. If the data in the input form is correct, the tax is calculated using the business class [impots]. The controller returns the [form.aspx] view to the client as it was validated, along with the calculated tax. If the data in the input form is incorrect, the controller will return the [errors.aspx] view with a list of errors and a link to return to the form.
  • return: corresponds to returning to the form after an error. The controller displays the [form.aspx] view as it was validated before the error.

The [main.aspx] controller knows nothing about tax calculations. It is simply responsible for managing the client-server dialogue and executing the actions requested by the client. For the [calculate] action, it will rely on the [tax] business class.

6.1.3. The business class

The **impot** class will be defined as follows:


' imported namespaces
Imports System

' class
Namespace st.istia.univangers.fr
    Public Class impot
        Private limits(), coeffR(), coeffN() As Decimal

        ' constructor
        Public Sub New(ByRef source As impotsData)
            ' the data needed to calculate the tax
            ' come from an external source [source]
            ' we retrieve them - there may be an exception
            Dim data() As Object = source.getData
            limits = CType(data(0), Decimal())
            coeffR = CType(data(1), Decimal())
            coeffN = CType(data(2), Decimal())
        End Sub

        ' tax calculation
        Public Function calculate(ByVal married As Boolean, ByVal numChildren As Integer, ByVal salary As Long) As Long
            ' calculate the number of shares
            Dim nbParts As Decimal
            If married Then
                nbParts = CDec(nbChildren) / 2 + 2
            Else
                nbParts = CDec(nbChildren) / 2 + 1
            End If
            If nbChildren >= 3 Then
                nbParts += 0.5D
            End If
            ' Calculate taxable income & Family Quotient
            Dim income As Decimal = 0.72D * salary
            Dim QF As Decimal = income / numParts
            ' tax calculation
            limits((limits.Length - 1)) = QF + 1
            Dim i As Integer = 0
            While QF > limits(i)
                i += 1
            End While
            Return CLng(revenue * coeffR(i) - nbParts * coeffN(i))
        End Function
    End Class
End Namespace

A tax object by providing its constructor with a data source of type [impotsData]. This class has a public method [getData] that retrieves the three data arrays needed to calculate the tax, as described earlier. This method can handle an exception if the data could not be retrieved or if it is found to be incorrect. Once the [tax] object is created, its calculate method can be called repeatedly to calculate the taxpayer’s tax based on their marital status (married or single), number of children, and annual salary.

6.1.4. The data access class

The [impotsData] class is the class that provides access to the data. It is an abstract class. A derived class must be created for each new possible data source (arrays, flat files, databases, console, etc.). Its definition is as follows:


Imports System.Collections

Namespace st.istia.univangers.fr
    Public MustInherit Class impotsData
        Protected limites() As Decimal
        Protected coeffr() As Decimal
        Protected coeffn() As Decimal
        Protected checked As Boolean
        Protected valid As Boolean

        ' data access method
        Public MustOverride Function getData() As Object()

        ' data validation method
        Protected Function checkData() As Integer
            ' checks the acquired data
            ' there must be data
            valid = Not limits Is Nothing AndAlso Not coeffr Is Nothing AndAlso Not coeffn Is Nothing
            If Not valid Then Return 1
            ' must have 3 arrays of the same size
            If valid Then valid = limits.Length = coeffr.Length AndAlso limits.Length = coeffn.Length
            If Not valid Then Return 2
            ' the arrays must not be empty
            valid = limits.Length ≠ 0
            If Not valid Then Return 3
            ' each array must contain elements >= 0 and in ascending order
            valid = check(limits, limits.Length - 1) AndAlso check(coeffr, coeffr.Length) AndAlso check(coeffn, coeffn.Length)
            If Not valid Then Return 4
            ' everything is fine
            Return 0
        End Function

        ' checks the validity of an array's contents
        Protected Function check(ByRef array() As Decimal, ByVal n As Integer) As Boolean
            ' array must have its first n elements >= 0 and in strictly ascending order
            If array(0) < 0 Then Return False
            For i As Integer = 1 To n - 1
                If array(i) <= array(i - 1) Then Return False
            Next
            ' OK
            Return True
        End Function
    End Class
End Namespace

The class has the following protected attributes:

limits
array of tax bracket limits
coeffr
array of coefficients applied to taxable income
coeffn
table of coefficients applied to the number of shares
checked
Boolean indicating whether the data (limits, coeffr, coeffn) has been verified
valid
Boolean indicating whether the data (limits, coeffr, coeffn) is valid

The class has no constructor. It has an abstract method [getData] that derived classes must implement. The purpose of this method is to:

  • assign values to the three arrays limits, coeffr, coeffn
  • throw an exception if the data could not be acquired or if it turned out to be invalid.

The class provides the protected methods [checkData] and [check] that verify the validity of the attributes (limits, coeffr, coeffn). This relieves derived classes of the need to implement them. They will simply need to use them.

The first derived class we will use is as follows:


Imports System.Collections
Imports System

Namespace st.istia.univangers.fr
    Public Class impotsArray
        Inherits impotsData

        ' constructor without arguments
        Public Sub New()
            ' Initialize arrays with constants
            limits = New Decimal() {4262D, 8382D, 14753D, 23888D, 38868D, 47932D, 0D}
            coeffr = New Decimal() {0D, 0.0683D, 0.1914D, 0.2826D, 0.3738D, 0.4262D, 0.4809D}
            coeffn = New Decimal() {0D, 291.09D, 1322.92D, 2668.39D, 4846.98D, 6883.66D, 9505.54D}
            checked = True
            valid = True
        End Sub

        ' constructor with three input arrays
        Public Sub New(ByRef limits() As Decimal, ByRef coeffr() As Decimal, ByRef coeffn() As Decimal)
            ' store the data
            Me.limits = limits
            Me.coeffr = coeffr
            Me.coeffn = coeffn
            checked = False
        End Sub

        Public Overrides Function getData() As Object()
            ' check the data if necessary
            Dim error As Integer
            If Not checked Then error = checkData() : checked = True
            ' if invalid, then throw an exception
            If Not valid Then Throw New Exception("The tax bracket data is invalid (" + error.ToString + ")")
            ' otherwise return the three arrays
            Return New Object() {limits, coeffr, coeffn}
        End Function
    End Class
End Namespace

This class, named [impotsArray], has two constructors:

  • a constructor without arguments that initializes the attributes (limits, coeffr, coeffn) of the base class with hard-coded arrays
  • a constructor that initializes the attributes (limits, coeffr, coeffn) of the base class with arrays passed to it as parameters

The [getData] method, which allows external classes to retrieve the arrays (limits, coeffr, coeffn), simply verifies the validity of the three arrays using the [checkData] method of the base class. It throws an exception if the data is invalid.

6.1.5. Testing business classes and data access classes

It is important to include only business and data access classes that have been verified as correct in a web application. This way, the web application’s debugging phase can focus on the controller and view layers. A test program might look like the following:


' options
Option Strict On
Option Explicit On 

' namespaces
Imports System
Imports Microsoft.VisualBasic

Namespace st.istia.univangers.fr
    Module test
        Sub Main()
            ' Interactive tax calculation program
            ' The user enters three pieces of data via the keyboard: married, number of children, salary
            ' the program then displays the tax due
            Const syntax As String = "syntax: married noChildren salary" + ControlChars.Lf + "married: o for married, n for unmarried" + ControlChars.Lf + "noChildren: number of children" + ControlChars.Lf + "salary: annual salary in F"

            ' Create a tax object
            Dim taxObj As Tax = Nothing
            Try
                taxObj = New Tax(New TaxArray)
            Catch ex As Exception
                Console.Error.WriteLine(("The following error occurred: " + ex.Message))
                Environment.Exit(1)
            End Try
            ' infinite loop
            Dim married As String
            Dim numberOfChildren As Integer
            Dim salary As Long
            While True
                ' Request the parameters for the tax calculation
                Console.Out.Write("Tax calculation parameters in the format married nbChildren salary or nothing to stop:")
                Dim parameters As String = Console.In.ReadLine().Trim()
                ' anything to do?
                If parameters Is Nothing OrElse parameters = "" Then
                    Exit While
                End If
                ' Check the number of arguments in the entered line
                Dim error As Boolean = False
                Dim args As String() = parameters.Split(Nothing)
                Dim numParameters As Integer = args.Length
                If nbParameters <> 3 Then
                    Console.Error.WriteLine(syntax)
                    error = True
                End If
                ' Checking parameter validity
                If Not error Then
                    ' married
                    married = args(0).ToLower()
                    If married <> "y" And married <> "n" Then
                        error = True
                    End If
                    ' nbChildren
                    Try
                        nbChildren = Integer.Parse(args(1))
                        If nbChildren < 0 Then
                            Throw New Exception
                        End If
                    Catch
                        error = True
                    End Try
                    ' salary
                    Try
                        salary = Integer.Parse(args(2))
                        If salary < 0 Then
                            Throw New Exception
                        End If
                    Catch
                        error = True
                    End Try
                End If
                ' if the parameters are correct, calculate the tax
                If Not error Then
                    Console.Out.WriteLine(("tax=" & objTax.calculate(married = "o", numChildren, salary) & " euro(s)"))
                Else
                    Console.Error.WriteLine(syntax)
                End If
            End While
        End Sub
    End Module
End Namespace

The application prompts the user to enter the three pieces of information needed to calculate their tax:

  • marital status: o for married, n for unmarried
  • number of children
  • their annual salary

The tax calculation is performed using an object of type [tax] created when the application launches:


            ' creation of a tax object
            Dim objTax As Tax = Nothing
            Try
                objTax = New Tax(New TaxArray)
            Catch ex As Exception
                Console.Error.WriteLine(("The following error occurred: " + ex.Message))
                Environment.Exit(1)
            End Try

As the data source, we use an object of type [impotsArray]. The constructor without arguments for this class is used, providing the three arrays (limites, coeffr, coeffn) with hard-coded values. Creating an [impot] object can theoretically throw an exception because, to create itself, the object will request the data (limites, coeffr, coeffn) from its data source, which was passed to it as a parameter, and this data retrieval may trigger an exception. However, in this case, the method used to obtain the data (hard-coded values) cannot cause an exception. However, we have left the exception handling in place to draw the reader’s attention to the possibility that the [impot] object might be constructed incorrectly.

Here is an example of the previous program in action:

dos>dir
04/05/2004  1:28 PM                1,337 impots.vb
April 21, 2004  8:23 AM                1,311 impotsArray.vb
04/21/2004  08:26                1,634 impotsData.vb
04/21/2004  08:42                2,490 testimpots1.vb

We compile all the classes [impot, impotsData, impotsArray] into a single assembly [impot.dll]:

dos>vbc /t:library /out:impot.dll impotsData.vb impotsArray.vb impots.vb
Microsoft (R) Visual Basic .NET Compiler version 7.10.3052.4
dos>dir
04/05/2004  1:28 PM                1,337 impots.vb
04/21/2004  08:23                1,311 impotsArray.vb
04/21/2004  08:26                1,634 impotsData.vb
04/21/2004  08:42                2,490 testimpots1.vb
04/21/2004  09:21                5,632 impots.dll

We compile the test program:

dos>vbc /r:impot.dll testimpots1.vb
Microsoft (R) Visual Basic .NET Compiler version 7.10.3052.4
dos>dir
04/05/2004  1:28 PM                1,337 impots.vb
04/21/2004  08:23                1,311 impotsArray.vb
04/21/2004  08:26                1,634 impotsData.vb
04/21/2004  08:42                2,490 testimpots1.vb
04/21/2004  09:21                5,632 impots.dll
04/21/2004  09:23                4,608 testimpots1.exe

We can run the tests:

dos>testimpots1
Tax calculation parameters in married format: number of children, salary, or nothing to stop :o 2 60000
tax=4,300 euro(s)
Tax calculation parameters for married couple format: number of children, salary or nothing to stop: n 2 60000
tax=6,872 euro(s)
Tax calculation parameters for married couples with n children, salary or nothing to stop at:

6.1.6. Web application views

The application will have two views: [form.aspx] and [errors.aspx]. Let’s illustrate how the application works using screenshots. The [form.aspx] view is displayed when the URL [main.aspx] is requested for the first time:

Image

The user fills out the form:

Image

and uses the [Calculate] button to get the following result:

Image

They may enter incorrect data:

Image

Clicking the [Calculate] button then returns a different response [errors.aspx]:

Image

They can use the [Back to Form] link above to return to the [form.aspx] view as it was before the error:

Image

6.1.7. The [form.aspx] view

The [form.aspx] page will be as follows:


<%@ page src="formulaire.aspx.vb" inherits="formulaire" AutoEventWireup="false"%>
<html>
    <head>
        <title>Tax</title>
    </head>
    <body>
        <P>Calculate Your Tax</P>
        <HR>
        <form method="post" action="main.aspx?action=calcul">
            <TABLE border="0">
                <TR>
                    <TD>Are you married?</TD>
                    <TD>
                        <INPUT type="radio" value="yes" name="rdMarie" <%=rdouichecked%>>Yes 
                      <INPUT type="radio"   value="no" name="rdMarie" <%=rdnonchecked%>>No
                     </TD>
                </TR>
                <TR>
                    <TD>Number of children</TD>
                    <TD><INPUT type="text" size="3" maxLength="3" name="txtEnfants" value="<%=txtEnfants%>"></TD>
                </TR>
                <TR>
                    <TD>Annual salary (EUR)</TD>
                    <TD><INPUT type="text" maxLength="12" size="12" name="txtSalary" value="<%=txtSalary%>"></TD>
                </TR>
                <TR>
                    <TD>Tax due:
                    </TD>
                    <TD><%=txtTax%></TD>
                </TR>
            </TABLE>
            <hr>
            <P>
                <INPUT type="submit" value="Calculate">
            </P>
        </form>
        <form method="post" action="main.aspx?action=delete">
                <INPUT type="submit" value="Clear">
        </form>
    </body>
</html>

The dynamic fields on this page are as follows:

rdouichecked
"checked" if the [yes] checkbox should be checked, "" otherwise
rdnonchecked
same for the [no] checkbox
txtChildren
value to be placed in the [txtChildren] input field
txtSalary
value to be entered in the [txtSalary] input field
txtTax
Value to enter in the [txtTax] input field

The page has two forms, each with a [submit] button. The [Calculate] button is the [submit] button for the following form:


        <form method="post" action="main.aspx?action=calcul">
...
            <P>
                <INPUT type="submit" value="Calculate">
            </P>
        </form>

We can see that the form parameters will be posted to the controller with [action=calcul]. The [Clear] button is the [submit] button for the following form:


        <form method="post" action="main.aspx?action=effacer">
                <INPUT type="submit" value="Clear">
        </form>

We can see that the form parameters will be posted to the controller with [action=effacer]. Here, the form has no parameters. Only the action matters.

The fields in [formulaire.aspx] are calculated by [formulaire.aspx.vb]:


Imports System.Collections.Specialized

Public Class form
    Inherits System.Web.UI.Page

    ' page fields
    Protected rdouichecked As String
    Protected rdnonchecked As String
    Protected txtChildren As String
    Protected txtSalary As String
    Protected txtTax As String

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' retrieve the previous request from the context
        Dim form As NameValueCollection = Context.Items("form")
        ' prepare the page to be displayed
        ' radio buttons
        rdouichecked = ""
        rdnonchecked = "checked"
        If form("rdMarie").ToString = "yes" Then
            rdouichecked = "checked"
            rdnonchecked = ""
        End If
        ' the rest
        txtChildren = CType(form("txtChildren"), String)
        txtSalary = CType(form("txtSalary"), String)
        txtTax = CType(Context.Items("txtTax"), String)
    End Sub
End Class

The fields in [main.aspx] are calculated based on two pieces of information placed by the controller in the page context:

  • Context.Items("form"): a NameValueCollection dictionary containing the values of the HTML fields [rdmarie,txtEnfants,txtSalaire]
  • Context.Items("txtImpot"): tax value

6.1.8. The [erreurs.aspx] view

The [erreurs.aspx] view displays any errors that may occur during the application's runtime. Its presentation code is as follows:


<%@ page src="erreurs.aspx.vb" inherits="erreurs" AutoEventWireup="false"%>
<HTML>
    <HEAD>
        <title>Tax</title>
    </HEAD>
    <body>
        <P>The following errors occurred:</P>
        <HR>
        
            <%=HTMLErrors%>
        
        
            <%=link%>
        
    </body>
</HTML>

The page has three dynamic fields:

HTMLErrors
HTML code for a list of errors
href
URL of a link
link
link text

These fields are calculated by the page's controller in [errors.aspx.vb]:


Imports System.Collections
Imports Microsoft.VisualBasic

Public Class errors
    Inherits System.Web.UI.Page

    ' page parameter
    Protected HTMLErrors As String = ""
    Protected href As String
    Protected link As String

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' retrieve the context items
        Dim errors As ArrayList = CType(context.Items("errors"), ArrayList)
        href = context.Items("href").ToString
        link = context.Items("link").ToString
        ' Generate the HTML code for the list
        Dim i As Integer
        For i = 0 To errors.Count - 1
            HTMLErrors += " " + Errors(i).ToString + "" + ControlChars.CrLf
        Next
    End Sub

End Class

The page controller retrieves information placed by the application controller in the page context:

Context.Items("errors"
ArrayList object containing the list of error messages to display
Context.Items("href"
URL of a link
Context.Items("link"
link text

Now that we know what the application user sees, we can move on to writing the application's controller.

6.1.9. The controllers [global.asax, main.aspx]

Let’s review the MVC architecture of our application:

Image

ClientApplication logic

The controller [main.aspx] must handle three actions:

  • init: corresponds to the client's first request. The controller displays the [formulaire.aspx] view
  • calcul: corresponds to the tax calculation request. If the data in the input form is correct, the tax is calculated using the business class [taxes]. The controller returns the [form.aspx] view to the client as it was validated, along with the calculated tax. If the data in the input form is incorrect, the controller returns the [errors.aspx] view with a list of errors and a link to return to the form.
  • return: corresponds to returning to the form after an error. The controller displays the [form.aspx] view as it was validated before the error.

It is also known that every request to the application passes through the [global.asax] controller, if it exists. We therefore have, at the application’s entry point, a chain of two controllers:

  • [global.asax], which, due to the ASP.NET architecture, receives all requests to the application
  • [main.aspx], which, by the developer’s choice, also receives all requests to the application

The need for [main.aspx] stems from the fact that we will have a session to manage. We have seen that [global.asax] is not suitable as a controller in this case. We could do without [global.asax] entirely here. However, we will use it to execute code when the application starts. The MVC diagram above shows that we will need to create a [tax] object to calculate the tax. There is no need to create this object multiple times; once is sufficient. We will therefore create it at application startup during the [Application_Start] event handled by the [global.asax] controller. The code for this is as follows:

[global.asax]

<%@ Application src="Global.asax.vb" Inherits="Global" %>

[global.asax.vb]


Imports System
Imports System.Web
Imports System.Web.SessionState
Imports st.istia.univangers.fr

Public Class Global
    Inherits System.Web.HttpApplication

    Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' Create an 'impot' object
        Dim objTaxAs Tax
        Try
            objImpot = New impot(New impotsArray)
            ' Add the object to the application
            Application("objImpot") = objImpot
            ' no error
            Application("error") = False
        Catch ex As Exception
            'An error occurred; we note it in the application
            Application("error") = True
        End Try
    End Sub
End Class

Once created, the [impot] object is stored in the application. This is where requests from different clients will retrieve it. Since the creation of the [impot] object may fail, we handle any exceptions and set an [error] key in the application to indicate whether or not an error occurred during the creation of the [impot] object.

The controller code [main.aspx, main.aspx.vb] will be as follows:

[main.aspx]

<%@ page src="main.aspx.vb" inherits="main" AutoEventWireup="false"%>

[main.aspx.vb]


Imports System
Imports System.Collections.Specialized
Imports System.Collections
Imports st.istia.univangers.fr

Public Class main
    Inherits System.Web.UI.Page

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' First, we check if the application was able to initialize correctly
        If CType(Application("error"), Boolean) Then
            ' we redirect to the error page
            Dim errors As New ArrayList
            errors.Add("Application temporarily unavailable...")
            context.Items("errors") = errors
            context.Items("link") = ""
            context.Items("href") = ""
            Server.Transfer("errors.aspx")
        End If
        ' retrieve the action to be performed
        Dim action As String
        If Request.QueryString("action") Is Nothing Then
            action = "init"
        Else
            action = Request.QueryString("action").ToString.ToLower
        End If
        ' execute the action
        Select Case action
            Case "init"
                ' initialize application
                initAppli()
            Case "calculation"
                ' calculate tax
                calculateTax()
            Case "return"
                ' return to form
                returnToForm()
            Case "clear"
                ' initialize application
                initApp()
            Case Else
                ' unknown action = init
                initAppli()
        End Select
    End Sub

    Private Sub initAppli()
        ' Display the pre-filled form
        Context.Items("form") = initForm()
        Context.Items("txtImpot") = ""
        Server.Transfer("form.aspx", True)
    End Sub

    Private Function initForm() As NameValueCollection
        ' Initialize the form
        Dim form As New NameValueCollection
        form.Set("rdMarie", "no")
        form.Set("txtChildren", "")
        form.Set("txtSalary", "")
        Return form
    End Function

    Private Sub calculateTax()
        ' Check the validity of the entered data
        Dim errors As ArrayList = checkData()
        ' if there are errors, report them
        If errors.Count <> 0 Then
            ' Save the entries
            Session.Item("form") = Request.Form
            ' prepare the error page
            context.Items("href") = "main.aspx?action=return"
            context.Items("link") = "Back to form"
            context.Items("errors") = errors
            Server.Transfer("errors.aspx")
        End If
        ' No errors here - calculating the tax
        Dim tax As Long = CType(Application("objTax"), tax).calculate( _
        Request.Form("rdMarie") = "yes", _
        CType(Request.Form("txtChildren"), Integer), _
        CType(Request.Form("txtSalary"), Long))
        ' display the results page
        context.Items("txtImpot") = impot.ToString + " euro(s)"
        context.Items("form") = Request.Form
        Server.Transfer("formulaire.aspx", True)
    End Sub

    Private Sub returnForm()
        ' the form is displayed with values retrieved from the session
        Context.Items("form") = Session.Item("form")
        Context.Items("txtImpot") = ""
        Server.Transfer("form.aspx", True)
    End Sub

    Private Function checkData() As ArrayList
        ' initially no errors
        Dim errors As New ArrayList
        Dim error As Boolean = False
        ' Married radio button
        Try
            Dim rdMarried As String = Request.Form("rdMarried").ToString
            If rdMarried <> "yes" And rdMarried <> "no" Then
                Throw New Exception
            End If
        Catch
            errors.Add("You did not specify your marital status")
        End Try
        ' number of children
        Try
            Dim txtChildren As String = Request.Form("txtChildren").ToString
            Dim nbChildren As Integer = CType(txtChildren, Integer)
            If nbChildren < 0 Then Throw New Exception
        Catch
            errors.Add("The number of children is incorrect")
        End Try
        ' salary
        Try
            Dim txtSalary As String = Request.Form("txtSalary").ToString
            Dim salary As Integer = CType(txtSalary, Long)
            If salary < 0 Then Throw New Exception
        Catch
            errors.Add("The annual salary is incorrect")
        End Try
        ' return the list of errors
        Return errors
    End Function
End Class

The controller starts by checking that the application has initialized correctly:


        ' First, we check if the application was able to initialize correctly
        If CType(Application("error"), Boolean) Then
            ' we redirect to the error page
            Dim errors As New ArrayList
            errors.Add("Application temporarily unavailable...")
            context.Items("errors") = errors
            context.Items("link") = ""
            context.Items("href") = ""
            Server.Transfer("errors.aspx")
        End If

If the controller detects that the application failed to initialize correctly (the [impot] object required for the calculation could not be created), it displays the error page with the appropriate parameters. Here, there is no need to place the return link on the form since the entire application is unavailable. A general error message is placed in [Context.Items("errors")] of type [ArrayList].

If the controller determines that the application is operational, it then analyzes the action it is asked to execute via the [action] parameter. We have encountered this mode of operation many times by now. The processing of each type of action is delegated to a function.

6.1.9.1. The init and delete actions

These two actions must display the empty input form. Recall that this form (see views) has two parameters:

  • Context.Items("form"): a [NameValueCollection] dictionary containing the values of the HTML fields [rdmarie,txtEnfants,txtSalaire]
  • Context.Items("txtImpot"): tax value

The [initAppli] function initializes these two parameters to display an empty form.

6.1.9.2. The calculation action

This action must calculate the tax due based on the data entered in the form and return the form pre-filled with the entered values and the calculated tax amount. The [calculImpot] function responsible for this task begins by verifying that the form data is correct:

  • the [rdMarie] field must be present and have the value [yes] or [no]
  • the [txtEnfants] field must be present and be an integer >=0
  • the [txtSalaire] field must be present and be an integer >=0

If the entered data is invalid, the controller displays the [erreurs.aspx] view after first setting the expected values for it in the context:

  • Error messages are placed in an [ArrayList] object, which is then added to the context [Context.Items("errors")]
  • The URL of the return link and the text of that link are also placed in the context.

Before handing control over to the [erreurs.aspx] page, which will send the response to the client, the values entered in the form (Request.Form) are placed in the session, associated with the "form" key. This will allow a subsequent request to retrieve them.

One might wonder here whether it is useful to verify that the fields [rdMarie, txtEnfants, txtSalaire] are present in the request sent by the client. This is unnecessary if we are certain that our client is a browser that has received the view [formulaire.aspx] containing these fields. We can never be certain of this. We will show an example a little later where the client is the [curl] application we have already encountered. We will query the application without sending the fields it expects and see how it reacts. This is a rule that has been stated several times and which we reiterate here: an application must never make assumptions about the type of client querying it. To be safe, it must assume that it could be queried by a programmed application that might send it unexpected parameter strings. It must behave correctly in all cases.

In our case, we verified that the fields [rdMarie, txtEnfants, txtSalaire] were present in the request but did not check whether it could contain others. In this application, they would be ignored. Nevertheless, again as a security measure, it would be useful to log this type of request in a log file and send an alert to the application administrator so that they are aware the application is receiving "strange" requests. By analyzing these in the log file, they could detect a potential attack on the application and then take the necessary measures to protect it.

If the expected data is correct, the controller initiates the tax calculation using the [tax] object stored in the application. It then stores the two pieces of information expected by the [form.aspx] view in the context:

  • Context.Items("formulaire"): a [NameValueCollection] dictionary containing the values of the HTML fields [rdmarie,txtEnfants,txtSalaire], here [Request.Form)], i.e., the values previously entered in the form
  • Context.Items("txtImpot"): the tax value that has just been calculated

The attentive reader may have wondered while reading the above: since the [impot] object created at application startup is shared among all requests, could there be access conflicts leading to corruption of the [impot] object’s data? To answer this question, we need to return to the code of the [impot] class. The requests call the [impot].calculateTax method to obtain the tax due. So this is the code we need to examine:

        Public Function calculate(ByVal married As Boolean, ByVal numChildren As Integer, ByVal salary As Long) As Long
            ' Calculate the number of shares
            Dim nbParts As Decimal
            If married Then
                nbParts = CDec(nbEnfants) / 2 + 2
            Else
                nbParts = CDec(nbChildren) / 2 + 1
            End If
            If nbChildren >= 3 Then
                nbParts += 0.5D
            End If
            ' Calculate taxable income & Family Quotient
            Dim income As Decimal = 0.72D * salary
            Dim QF As Decimal = income / nbParts
            ' calculate tax
            limits((limits.Length - 1)) = QF + 1
            Dim i As Integer = 0
            While QF > limits(i)
                i += 1
            End While
            Dim amount As Long = CLng(income * coeffR(i) - nbParts * coeffN(i))
            Return impot
        End Function

Suppose a thread is executing the previous method and is interrupted. Another thread then executes the method. What are the risks? To find out, we added the following code:


            Dim impot As Long = CLng(revenue * coeffR(i) - nbParts * coeffN(i))
            ' wait 10 seconds
            Thread.Sleep(10000)
            Return impot

Thread 1, after calculating the value [impot1] of the local variable [impot], is interrupted. Thread 2 then executes and calculates a new value [impot2] for the same variable [impot] before being interrupted. Thread 1 regains control. What does it find in the local variable [impot]? Since this variable is local to a method, it is stored in a memory structure called the stack. This stack is part of the thread’s context, which is saved when the thread is suspended. When thread 2 starts up, its context is set up with a new stack and therefore a new local variable [impot]. When thread 2 is suspended in turn, its context will also be saved. When thread 1 is restarted, its context is restored, including its stack. It then retrieves its local variable [impot] and not that of thread 2. We are therefore in a situation where there are no access conflicts between requests. The tests conducted with the 10-second pause described above confirmed that simultaneous requests did indeed yield the expected result.

6.1.9.3. The return action

This action corresponds to clicking the [Back to Form] link on the [errors.aspx] view to return to the [form.aspx] view, which is pre-filled with the values previously entered and saved in the session. The [returnForm] function retrieves this information. The two parameters expected by the [form.aspx] view are initialized:

  • Context.Items("form") with the values previously entered and saved in the session
  • Context.Items("txtImpot") with the empty string

6.1.10. Testing the web application

All of the above files are placed in a folder named <application-path>.

Image

In this folder, a [bin] subfolder is created, in which the [impot.dll] assembly—generated from the compilation of the business class files: [impots.vb, impotsData.vb, impotsArray.vb]—is placed. The required compilation command is shown below:

dos>vbc /t:library /out:impot.dll impotsData.vb impotsArray.vb impots.vb
Microsoft (R) Visual Basic .NET Compiler version 7.10.3052.4
dos>dir
04/05/2004  13:28                1,337 impots.vb
04/21/2004  08:23                1,311 impotsArray.vb
04/21/2004  08:26                1,634 impotsData.vb
04/21/2004  09:21                5,632 impots.dll

The [impot.dll] file above must be placed in <application-path>\bin so that the web application can access it. The Cassini server is launched with the parameters (<application-path>,/impots1). Using a browser, we request the URL [http://localhost/impots1/main.aspx]:

Image

We fill out the form:

Image

Then we start the tax calculation using the [Calculate] button. We get the following response:

Image

Then we enter incorrect data:

Image

Clicking the [Calculate] button yields the following response:

Image

Clicking the [Back to Form] link takes us back to the form as it was when it was submitted:

Image

Finally, clicking the [Clear] button resets the page:

Image

6.1.11. Using the [curl] client

It is important to test web applications with clients other than browsers. If you send a browser a form with parameters to be posted when it is submitted, the browser will send the values of those parameters back to the server. Another client might not do this, and then the server would receive a request with missing parameters. It must know what to do in this case. Another example is client-side data validation. If the form contains data to be validated, this validation can be performed on the client side using scripts included in the document containing the form. The browser will only submit the form if all client-side validated data is valid. One might then be tempted, on the server side, to assume that we will receive validated data and not want to perform this validation a second time. That would be a mistake. Indeed, a client other than a browser could send invalid data to the server, and the web application might then behave unexpectedly. We will illustrate these points using the [curl] client.

First, we request the URL [http://localhost/impots1/main.aspx]:

dos>curl --include --url http://localhost/impots1/main.aspx

HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 15:18:10 GMT
Set-Cookie: ASP.NET_SessionId=ivthkl45tjdjrzznevqsf255; path=/
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 982
Connection: Close


<html>
    <head>
        <title>Tax</title>
    </head>
    <body>
        <P>Calculate Your Tax</P>
        <HR width="100%" SIZE="1">
        <form method="post" action="main.aspx?action=calcul">
            <TABLE border="0">
                <TR>
                    <TD>Are you married?</TD>
                    <TD>
                        <INPUT type="radio" value="yes" name="rdMarie" >Yes <INPUT type="radio"  value="no" name="rdMarie" checked>No</TD>
                </TR>
                <TR>
                    <TD>Number of children</TD>
                    <TD><INPUT type="text" size="3" maxLength="3" name="txtEnfants" value=""></TD>
                </TR>
                <TR>
                    <TD>Annual salary (euros)</TD>
                    <TD><INPUT type="text" maxLength="12" size="12" name="txtSalary" value=""></TD>
                </TR>
                <TR>
                    <TD>Tax due:
                    </TD>
                    <TD></TD>
                </TR>
            </TABLE>
            <hr>
            <P>
                <INPUT type="submit" value="Calculate">
            </P>
        </form>
        <form method="post" action="main.aspx?action=clear">
                <INPUT type="submit" value="Delete">
        </form>
    </body>
</html>

The server sent us the form's HTML code. In the HTTP headers, we have the session cookie. We will use it in subsequent requests to maintain the session. Let's request the [calcul] action without providing any parameters:

dos>curl --cookie ASP.NET_SessionId=ivthkl45tjdjrzznevqsf255 --include --url  http://localhost/impots1/main.aspx?action=calcul 

HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 15:22:42 GMT
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 380
Connection: Close


<HTML>
    <HEAD>
        <title>Tax</title>
    </HEAD>
    <body>
        <P>The following errors occurred:</P>
        <HR>

            You did not specify your marital status
The number of children is incorrect
The annual salary is incorrect

        <a href="main.aspx?action=return">
            Back to the form

    </body>
</HTML>

We can see that the web application returned the [errors] view with three error messages for the three missing parameters. Now let's send some incorrect parameters:

dos>curl --cookie ASP.NET_SessionId=ivthkl45tjdjrzznevqsf255 --include --data rdMarie=xx --data txtEnfants=xx --data txtSalaire=xx --url http://localhost/impots1/main.aspx?action=calcul 

HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 15:25:50 GMT
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 380
Connection: Close


<HTML>
    <HEAD>
        <title>Tax</title>
    </HEAD>
    <body>
        <P>The following errors occurred:</P>
        <HR>

            You did not specify your marital status
The number of children is incorrect
The annual salary is incorrect

        <a href="main.aspx?action=return">
            Back to the form

    </body>
</HTML>

The three errors were correctly detected. Now let's send valid parameters:

dos>curl --cookie ASP.NET_SessionId=ivthkl45tjdjrzznevqsf255 --include --data rdMarie=yes --data txtChildren=2 --data txtSalary=60000 --url http://localhost/impots1/main.aspx?action=calcul 

HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 15:28:24 GMT
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 1000
Connection: Close


<html>
    <head>
        <title>Tax</title>
    </head>
    <body>
        <P>Calculate Your Tax</P>
        <HR width="100%" SIZE="1">
        <form method="post" action="main.aspx?action=calcul">
            <TABLE border="0">
                <TR>
                    <TD>Are you married?</TD>
                    <TD>
                        <INPUT type="radio" value="yes" name="rdMarie" checked>Yes <INPUT type="radio"  value="no" name="rdMarie" >No</TD>
                </TR>
                <TR>
                    <TD>Number of children</TD>
                    <TD><INPUT type="text" size="3" maxLength="3" name="txtEnfants" value="2"></TD>
                </TR>
                <TR>
                    <TD>Annual salary (euros)</TD>
                    <TD><INPUT type="text" maxLength="12" size="12" name="txtSalary" value="60000"></TD>
                </TR>
                <TR>
                    <TD>Tax due:
                    </TD>
                    <TD>4,300 euros</TD>
                </TR>
            </TABLE>
            <hr>
            <P>
                <INPUT type="submit" value="Calculate">
            </P>
        </form>
        <form method="post" action="main.aspx?action=clear">
                <INPUT type="submit" value="Delete">
        </form>
    </body>
</html>

We have successfully retrieved the tax due: 4,300 euros. The lesson from this example is that we must not be misled by the fact that we are writing a web application intended for clients that are browsers. A web application is a TCP/IP service, and this network protocol does not reveal the nature of a service’s client application. Therefore, we cannot know whether a web application’s client is a browser or not. We therefore follow two rules:

  • Upon receiving a request from a client, we make no assumptions about the client and verify that the expected parameters in the request are present and valid
  • we construct a response intended for browsers, which generally consists of HTML documents

A web application can be built to serve different clients simultaneously, such as browsers and mobile phones. We can then include a new parameter in each request indicating the client type. Thus, a browser will request a tax calculation via a request to the URL http://machine/impots/main.aspx?client=navigateur&action=calcul, while a mobile phone will send a request to the URL http://machine/impots/main.aspx?client=mobile&action=calcul. The MVC structure facilitates the development of such an application. It takes the following form:

Image

The [Business Classes, Data Access Classes] block remains unchanged. This is because it is a client-agnostic component. The [Controller] block changes only slightly but must account for a new parameter in the request: the [client] parameter, which indicates the type of client being handled. The [Views] block must generate views for each type of client. It might be worthwhile to account for the presence of the [client] parameter in the query right from the application’s design phase, even if the short- or medium-term goal is limited to browsers. If the application later needs to support a new type of client, only views tailored to that client need to be written.

6.2. Example 2

6.2.1. The Problem

Here, we propose to address the same problem as before but by modifying the data source for the [tax] object created by the web application. In the previous version, the data source provided values from arrays hard-coded into the code. This time, the new data source will retrieve them from an ODBC data source associated with a MySQL database.

6.2.2. The ODBC data source

The data will be located in a table named [IMPOTS] in a MySQL database named [dbimpots]. The contents of this table will be as follows:

Image

The database owner is the user [admimpots] with password [mdpimpots]. We associate an ODBC data source with this database. Before doing so, let’s first review the different ways to access a database using the .NET platform.

There are many databases available for Windows platforms. To access them, applications use programs called drivers.

Image

In the diagram above, the driver has two interfaces:

  • the I1 interface presented to the application
  • the I2 interface to the database

To prevent an application written for database B1 from having to be rewritten if migrating to a different database B2, standardization efforts have been made on interface I1. If databases using "standardized" drivers are employed, database B1 will be provided with driver P1, database B2 with driver P2, and the I1 interface of these two drivers will be identical. Thus, the application will not need to be rewritten. For example, you can migrate an Access database to a MySQL database without changing the application.

There are two types of standardized drivers:

  • ODBC (Open DataBase Connectivity) drivers
  • OLE DB (Object Linking and Embedding DataBase) drivers

ODBC drivers provide access to databases. The data sources for OLE DB drivers are more varied: databases, email systems, directories, etc. There are no limits. Any data source can be the subject of an OLE DB driver if a vendor decides to do so. The benefit is obviously significant: you have uniform access to a wide variety of data.

The .NET 1.1 platform comes with three types of data access classes:

  1. SQL Server.NET classes, for accessing Microsoft SQL Server databases
  2. the Ole Db.NET classes, for accessing databases from DBMSs that offer an OLE DB driver
  3. ODBC.NET classes, for accessing databases from DBMSs that offer an ODBC driver

The MySQL DBMS has long had an ODBC driver. This is the one we’ll use now. In Windows, we select [Start Menu/Control Panel/Administrative Tools/32-bit ODBC Sources]. Depending on the Windows version, this path may vary slightly. This opens the following application, which will allow us to create our ODBC source:

Image

We will create a System data source, i.e., a data source that any user of the machine can use. Therefore, above, we select the [System Data Source] tab. The page displayed has an [Add] button, which we use to create a new ODBC data source:

Image

The wizard asks you to select the ODBC driver to use. Windows comes with a number of pre-installed ODBC drivers. The MySQL ODBC driver is not included in this set. You must therefore install it first. You can find it online by typing the search terms "MySQL ODBC" or "MyODBC" into a search engine. Here, we have installed the [MySQL ODBC 3.51] driver. We select it and click [Finish]:

Image

You will need to provide the following information:

Data Source Name
the name that will identify the ODBC data source. Any Windows application will be able to access the source using this name
Description
Any text describing the data source
Host Name
The name of the machine hosting the MySQL DBMS. Here, it is the local machine. It could be a remote machine. This would allow a Windows application to access a remote database without any special coding. This is a major advantage of the ODBC source.
Database Name
A MySQL DBMS can manage multiple databases. Here, we specify which one we want to manage: dbimpots
User
The name of a user registered within the MySQL database management system. Access to the data source will be performed under this user’s name. Here: admimpots
Password
This user's password. Here: mdpimpots
Port
MySQL DBMS working port. By default, this is port 3306. We have not changed it

Once this is done, we test the validity of our connection settings using the [Test Data Source] button:

Image

Once this is done, we are confident in our ODBC data source. We can now use it. We click [OK] as many times as necessary to exit the ODBC wizard.

If the reader does not have the MySQL DBMS, they can download it for free at [http://www.mysql.com]. Below, we outline the steps to create an ODBC source with Access. The first steps are identical to those described previously. We add a new system data source:

Image

The selected driver will be [Microsoft Access Driver]. Click [Finish] to proceed to the ODBC source definition:

Image

The information to be provided is as follows:

Data source name
The name that will identify the ODBC data source. Any Windows application will be able to access the source using this name
Description
Any text describing the data source
Database
The full name of the Access file to be used

6.2.3. A new data access class

Let’s revisit the MVC structure of our application:

Image

In the diagram above, the [impotsData] class is responsible for retrieving data. In this case, it will retrieve data from the MySQL database [dbimpots]. As we learned in the previous version of this application, [impotsData] is an abstract class that must be derived whenever we want to adapt it to a new data source. Let’s review the structure of this abstract class:


Imports System.Collections

Namespace st.istia.univangers.fr
    Public MustInherit Class impotsData
        Protected limites() As Decimal
        Protected coeffr() As Decimal
        Protected coeffn() As Decimal
        Protected checked As Boolean
        Protected valid As Boolean

        ' data access method
        Public MustOverride Function getData() As Object()

        ' data verification method
        Protected Function checkData() As Integer
            ' checks the acquired data
...
        End Function

        ' Checks the validity of an array's contents
        Protected Function check(ByRef array() As Decimal, ByVal n As Integer) As Boolean
        ...
        End Function
    End Class
End Namespace

The class that derives from [impotsData] must implement two methods:

  • a constructor if the default constructor of [impotsData] is not suitable
  • the [getData] method, which returns the three arrays (limites, coeffr, coeffn)

We create the [impotsODBC] class, which will retrieve the data (limits,coeffr,coeffn) from an ODBC source whose name we will provide:


Imports System.Data.Odbc
Imports System.Data
Imports System.Collections
Imports System

Namespace st.istia.univangers.fr
    Public Class impotsODBC
        Inherits impotsData

        ' instance variables
        Protected DSNimpots As String

        ' constructor
        Public Sub New(ByVal DSNimpots As String)
            ' Note the three pieces of information
            Me.DSNimpots = DSNimpots
        End Sub

        Public Overrides Function getdata() As Object()
            ' Initialize the three arrays limits, coeffr, and coeffn based on
            ' the contents of the [impots] table in the DSNimpots ODBC database
            ' limits, coeffr, and coeffn are the three columns of this table
            ' may throw various exceptions

            Dim connectString As String = "DSN=" + DSNimpots + ";"         ' connection string to the database
            Dim impotsConn As OdbcConnection = Nothing         ' the connection
            Dim sqlCommand As OdbcCommand = Nothing         ' the SQL command
            ' the SELECT query
            Dim selectCommand As String = "select limits,coeffr,coeffn from taxes"
            ' arrays to retrieve the data
            Dim aLimits As New ArrayList
            Dim aCoeffR As New ArrayList
            Dim aCoeffN As New ArrayList
            Try
                ' Attempt to access the database
                impotsConn = New OdbcConnection(connectString)
                impotsConn.Open()
                ' create a command object
                sqlCommand = New OdbcCommand(selectCommand, impotsConn)
                ' Execute the query
                Dim myReader As OdbcDataReader = sqlCommand.ExecuteReader()
                ' Process the retrieved table
                While myReader.Read()
                    ' the data from the current row is placed in the arrays
                    aLimites.Add(myReader("limites"))
                    aCoeffR.Add(myReader("coeffr"))
                    aCoeffN.Add(myReader("coeffn"))
                End While
                ' Release resources
                myReader.Close()
                taxConn.Close()
            Catch e As Exception
                Throw New Exception("Database access error (" + e.Message + ")")
            End Try
            ' dynamic arrays are converted to static arrays
            Me.limits = New Decimal(aLimits.Count - 1) {}
            Me.coeffr = New Decimal(aLimites.Count - 1) {}
            Me.coeffn = New Decimal(aLimites.Count - 1) {}
            Dim i As Integer
            For i = 0 To aLimites.Count - 1
                limits(i) = Decimal.Parse(aLimits(i).ToString())
                coeffR(i) = Decimal.Parse(aCoeffR(i).ToString())
                coeffN(i) = Decimal.Parse(aCoeffN(i).ToString())
            Next i
            ' Check the acquired data
            Dim error As Integer = checkData()
            ' if data is invalid, throw an exception
            If Not valid Then Throw New Exception("The tax bracket data is invalid (" + error.ToString + ")")
            ' otherwise, return the three arrays
            Return New Object() {limits, coeffr, coeffn}
        End Function
    End Class
End Namespace

Let's take a look at the constructor:


        ' constructor
        Public Sub New(ByVal DSNimpots As String)
            ' note the three pieces of information
            Me.TaxDSN = TaxDSN
        End Sub

It takes as a parameter the name of the ODBC data source containing the data to be retrieved. The constructor simply stores this name. The [getData] method is responsible for reading the data from the [impots] table and placing it into three arrays (limites, coeffr, coeffn). Let's comment on the code:

  • the parameters for connecting to the ODBC data source are defined, but the connection is not open
            ' connection string to the database
            Dim connectString As String = "DSN=" + DSNimpots + ";"
            ' create a database connection object - this connection is not open
            Dim impotsConn As OdbcConnection = New OdbcConnection(connectString)
  • Define three [ArrayList] objects to retrieve data from the [impots] table:

            ' arrays to retrieve the data
            Dim aLimits As New ArrayList
            Dim aCoeffR As New ArrayList
            Dim aCoeffN As New ArrayList
  • All database access code is enclosed in a try/catch block to handle any access errors. We open the connection to the database:

                ' we attempt to access the database
                impotsConn = New OdbcConnection(connectString)
                impotsConn.Open()
  • We execute the [select] command on the open connection. We obtain an [OdbcDataReader] object that will allow us to iterate through the rows of the table resulting from the select:

                ' create a command object
                Dim sqlCommand As OdbcCommand = New OdbcCommand(selectCommand, impotsConn)
                ' Execute the query
                Dim myReader As OdbcDataReader = sqlCommand.ExecuteReader()
  • We iterate through the result table, row by row. To do this, we use the [Read] method of the [OdbcDataReader] object obtained previously. This method does two things:
    • It advances one row in the table. Initially, the cursor is positioned before the first row
    • it returns the Boolean value [true] if it was able to advance, [false] otherwise, the latter case indicating that all rows have been processed.

The columns of the current row of the [OdbcDataReader] object are obtained via OdbcDataReader. This returns an object representing the value of the column. We iterate through the entire table to populate its contents into the three [ArrayList] objects:


                ' Processing the retrieved table
                While myReader.Read()
                    ' the data from the current row is placed in the arrays
                    aLimites.Add(myReader("limites"))
                    aCoeffR.Add(myReader("coeffr"))
                    aCoeffN.Add(myReader("coeffn"))
  • Once this is done, we release the resources associated with the connection:
                ' releasing resources
                myReader.Close()
                impotsConn.Close()
  • The contents of the three [ArrayList] objects are transferred to three standard arrays:

            ' dynamic arrays are converted to static arrays
            limits = New Decimal(aLimits.Count - 1) {}
            coeffr = New Decimal(aLimites.Count - 1) {}
            coeffn = New Decimal(aLimits.Count - 1) {}
            Dim i As Integer
            For i = 0 To aLimits.Count - 1
                limits(i) = CType(aLimits(i), Decimal)
                coeffR(i) = CType(aCoeffR(i), Decimal)
                coeffN(i) = CType(aCoeffN(i), Decimal)
            Next i
  • Once the data from the [impots] table has been loaded into the three arrays, all that remains is to verify their contents using the [checkData] method of the base class [impotsData]:
            ' we check the acquired data
            Dim error As Integer = checkData()
            ' if data is invalid, then throw an exception
            If Not valid Then Throw New Exception("The tax bracket data is invalid (" + error.ToString + ")")
            ' otherwise, return the three arrays
            Return New Object() {limits, coeffr, coeffn}

6.2.4. Tests for the data access class

A test program could look like this:

Option Explicit On 
Option Strict On

' namespaces
Imports System
Imports Microsoft.VisualBasic

Namespace st.istia.univangers.fr

    ' test page
    Module testimports
        Sub Main(ByVal arguments() As String)
            ' interactive tax calculation program
            ' The user enters three pieces of information via the keyboard: marital status, number of children, salary
            ' the program then displays the tax due
            Const syntax1 As String = "pg DSNimpots"
            Const syntax2 As String = "syntax: married noChildren salary" + ControlChars.Lf + "married: o for married, n for unmarried" + ControlChars.Lf + "noChildren: number of children" + ControlChars.Lf + "salary: annual salary in F"

            ' Check program parameters
            If arguments.Length <> 1 Then
                ' error message
                Console.Error.WriteLine(syntax1)
                ' end
                Environment.Exit(1)
            End If
            ' retrieve the arguments
            Dim DSNimpots As String = arguments(0)

            ' create a tax object
            Dim taxObj As Tax = Nothing
            Try
                objTax = New Tax(New TaxODBC(TaxDSN))
            Catch ex As Exception
                Console.Error.WriteLine(("The following error occurred: " + ex.Message))
                Environment.Exit(2)
            End Try

            ' infinite loop
            While True
                ' initially no errors
                Dim error As Boolean = False

                ' Requesting tax calculation parameters
                Console.Out.Write("Tax calculation parameters in the format: married, number of children, salary, or 'nothing' to exit:")
                Dim parameters As String = Console.In.ReadLine().Trim()

                ' anything to do?
                If parameters Is Nothing Or parameters = "" Then
                    Exit While
                End If

                ' Check the number of arguments in the entered line
                Dim args As String() = parameters.Split(Nothing)
                Dim nbParameters As Integer = args.Length
                If nbParameters <> 3 Then
                    Console.Error.WriteLine(syntax2)
                    error = True
                End If
                Dim groom As String
                Dim nbChildren As Integer
                Dim salary As Integer
                If Not error Then
                    ' Checking the validity of the
                    ' married
                    married = args(0).ToLower()
                    If married <> "o" And married <> "n" Then
                        Console.Error.WriteLine((syntax2 + ControlChars.Lf + "Invalid 'married' argument: enter 'y' or 'n'")
                        error = True
                    End If
                    ' nbChildren
                    nbChildren = 0
                    Try
                        nbChildren = Integer.Parse(args(1))
                        If nbChildren < 0 Then
                            Throw New Exception
                        End If
                    Catch
                        Console.Error.WriteLine(syntax2 + "\nInvalid numberOfChildren argument: enter a positive integer or zero")
                        error = True
                    End Try
                    ' salary
                    salary = 0
                    Try
                        salary = Integer.Parse(args(2))
                        If salary < 0 Then
                            Throw New Exception
                        End If
                    Catch
                        Console.Error.WriteLine(syntax2 + "\nInvalid salary argument: enter a positive integer or zero")
                        error = True
                    End Try
                End If
                If Not error Then
                    ' parameters are correct - calculate the tax
                    Console.Out.WriteLine(("tax=" & objTax.calculate(married = "o", numChildren, salary).ToString + " euro(s)"))
                End If
            End While
        End Sub
    End Module
End Namespace

The application is launched with a parameter:

  • DSNimpots: name of the ODBC data source to be used

The tax calculation is performed using an object of type [tax] created when the application is launched:


            ' creation of a tax object
            Dim objTax As Tax = Nothing
            Try
                objTax = New Tax(New TaxODBC(DSNTaxes))
            Catch ex As Exception
                Console.Error.WriteLine(("The following error occurred: " + ex.Message))
                Environment.Exit(1)
            End Try

Once initialized, the application repeatedly prompts the user to enter the three pieces of information needed to calculate their tax:

  • marital status: o for married, n for unmarried
  • the number of children
  • annual salary

All classes are compiled:

dos>vbc /r:system.dll /r:system.data.dll /t:library /out:impot.dll impots.vb impotsArray.vb impotsData.vb impotsODBC.vb
dos>dir
04/01/2004  7:34 PM             7,168 impots.dll
04/01/2004  7:31 PM             1,360 impots.vb
04/21/2004  08:23             1,311 impotsArray.vb
04/21/2004  08:26             1,634 impotsData.vb
04/01/2004  7:34 PM             2,735 impotsODBC.vb
04/01/2004  7:32 PM             3,210 testimpots.vb

The test program is then compiled:

dos>vbc /r:impot.dll testimpots.vb
dir>dir
04/01/2004  7:34 PM             7,168 impots.dll
04/01/2004  7:31 PM             1,360 impots.vb
04/21/2004  08:23             1,311 impotsArray.vb
04/21/2004  08:26             1,634 impotsData.vb
04/01/2004  7:34 PM             2,735 impotsODBC.vb
04/01/2004  7:34 PM             6,144 testimpots.exe
04/01/2004  7:32 PM             3,210 testimpots.vb

The test program is first run with the MySQL ODBC data source:

dos>testimpots odbc-mysql-dbimpots
Tax calculation parameters in married format: number of children, salary, or nothing to stop :o 2 60000
tax=4,300 euro(s)

We switch the ODBC source to an Access source:

dos>testimpots odbc-access-dbimpots
Tax calculation parameters in married format: number of children, salary or nothing to stop :o 2 60000
tax=4,300 F

6.2.5. Web application views

These are the same as in the previous application: formulaire.aspx and erreurs.aspx

6.2.6. Application controllers [global.asax, main.aspx]

Only the [global.asax] controller needs to be modified. It is responsible for creating the [impot] object when the application starts. The constructor of this object has a single parameter: the [impotsData] object, which is responsible for retrieving the data. This parameter therefore changes for each new type of data source. The [global.asax.vb] controller becomes the following:


Imports System
Imports System.Web
Imports System.Web.SessionState
Imports st.istia.univangers.fr
Imports System.Configuration

Public Class Global
    Inherits System.Web.HttpApplication

    Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' Create an 'impot' object
        Dim objImpot As impot
        Try
            objImpot = New impot(New impotsODBC(ConfigurationSettings.AppSettings("DSNimpots")))
            ' Add the object to the application
            Application("objImpot") = objImpot
            ' no error
            Application("error") = False
        Catch ex As Exception
            'An error occurred; we note it in the application
            Application("error") = True
            Application("message") = ex.Message
        End Try
    End Sub
End Class

The data source for the [impot] object is now an [impotODBC] object. This object takes the DSN name of the ODBC data source to be used as a parameter. Rather than hard-coding this name in the code, we place it in the application's [web.config] configuration file:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <appSettings>
        <add key="DSNimpots" value="odbc-mysql-dbimpots" />
    </appSettings>
</configuration>

We know that the value of a key C in the <appSettings> section of the [web.config] file is retrieved in the application code using [ConfigurationSettings.AppSettings(C)].

To determine the cause of the exception, we log the exception message in the application so that it remains available for queries. The [main.aspx.vb] control will include this message in its error list:


    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' First, we check if the application was able to initialize correctly
        If CType(Application("error"), Boolean) Then
            ' we redirect to the error page
            Dim errors As New ArrayList
            errors.Add("Application temporarily unavailable...(" + Application("message").ToString + ")")
            context.Items("errors") = errors
            context.Items("link") = ""
            context.Items("href") = ""
            Server.Transfer("errors.aspx")
        End If
        ' retrieve the action to be performed
...

6.2.7. Summary of changes

The application is ready to be tested. Let's list the changes made to the previous version:

  1. a new data access class has been created
  2. the controller [global.asax.vb] has been modified in two places: construction of the [impot] object and logging of the message related to any exception in the application
  3. The [main.aspx.vb] controller has been modified in one place to display the previous exception message
  4. A [web.config] file has been added

The modification work was done primarily in 1, i.e., outside the web application. This was made possible by the application’s MVC architecture, which separates the controller from the business classes. That is the whole point of this architecture. It could be shown that with an appropriate [web.config] file, any modification to the application controller could have been avoided. It is possible to specify in [web.config] the name of the data access class to be dynamically instantiated—such as —as well as the various parameters required for this instantiation. With this information, [global.asax] can instantiate the data access object. Changing the data source then amounts to:

  • creating the data access class for that source if it does not yet exist
  • modifying the [web.config] file to allow the dynamic creation of an instance of this class in [global.asax]

6.2.8. Testing the web application

All of the above files are placed in a folder named <application-path>.

Image

In this folder, a [bin] subfolder is created, in which the [impot.dll] assembly—generated from the compilation of the business class files: [impots.vb, impotsData.vb, impotsArray.vb, impotsODBC.vb]—is placed. The required compilation command is shown below:

dos>vbc /r:system.dll /r:system.data.dll /t:library /out:impot.dll impots.vb impotsArray.vb impotsData.vb impotsODBC.vb
dos>dir
04/01/2004  19:34             7,168 impots.dll
04/01/2004  7:31 PM             1,360 impots.vb
04/21/2004  08:23             1,311 impotsArray.vb
04/21/2004  08:26             1,634 impotsData.vb
04/01/2004  7:34 PM             2,735 impotsODBC.vb
04/01/2004  7:32 PM             3,210 testimpots.vb

The [impot.dll] file above must be placed in <application-path>\bin so that the web application can access it. The Cassini server is launched with the parameters (<application-path>,/impots2). The tests yield the same results as in the previous version, as the presence of the database is transparent to the user. To demonstrate this presence, however, we ensure that the ODBC source is unavailable by stopping the MySQL DBMS and request the URL [http://localhost/impots2/main.aspx]. We receive the following response:

Image

6.3. Example 3

6.3.1. The Problem

Here, we propose to address the same problem by again modifying the data source of the [impot] object created by the web application. This time, the new data source will be an ACCESS database accessed via an OLEDB driver. Our goal is to demonstrate another way to access a database.

6.3.2. The OLEDB Data Source

The data will be located in a table named [IMPOTS] in an ACCESS database. The contents of this table will be as follows:

Image

6.3.3. The Data Access Class

Let’s revisit the MVC structure of our application:

Image

  • In the diagram above, the [impotsData] class is responsible for retrieving the data. This time, it will need to do so from an OLEDB source.

We create the [impotsOLEDB] class, which will retrieve the data (limits, coeffr, coeffn) from an ODBC source that we will name:


Imports System.Data
Imports System.Collections
Imports System
Imports System.Xml
Imports System.Data.OleDb

Namespace st.istia.univangers.fr
    Public Class impotsOLEDB
        Inherits impotsData

        ' instance variables
        Protected connectionString As String

        ' constructor
        Public Sub New(ByVal connectionString As String)
            ' store the three pieces of information
            Me.connectionString = connectionString
        End Sub

        Public Overrides Function getData() As Object()
            ' initializes the three arrays limits, coeffr, and coeffn based on
            ' the contents of the [impots] table in the OLEDB database [connectionString]
            ' limits, coeffr, and coeffn are the three columns of this table
            ' may throw various exceptions

            ' we create a DataAdapter object to read data from the OLEDB source
            Dim adapter As New OleDbDataAdapter("select limits,coeffr,coeffn from taxes", connectionString)
            ' we create an in-memory representation of the result of the SELECT statement
            Dim content As New DataTable("taxes")
            Try
                adapter.Fill(content)
            Catch e As Exception
                Throw New Exception("Database access error (" + e.Message + ")")
            End Try
            ' Retrieve the contents of the tax table
            Dim taxRows As DataRowCollection = content.Rows
            ' resize the destination arrays
            Me.limits = New Decimal(taxRows.Count - 1) {}
            Me.coeffr = New Decimal(taxRows.Count - 1) {}
            Me.coeffn = New Decimal(taxRows.Count - 1) {}
            ' transfer the contents of the tax table to the arrays
            Dim i As Integer
            Dim row As DataRow
            Try
                For i = 0 To TaxLines.Count - 1
                    ' row i of the table
                    row = taxRows.Item(i)
                    ' retrieve the contents of the row
                    limits(i) = CType(row.Item(0), Decimal)
                    coeffr(i) = CType(row.Item(1), Decimal)
                    coeffn(i) = CType(row.Item(2), Decimal)
                Next
            Catch
                Throw New Exception("The tax bracket data is not of the correct type")
            End Try
            ' Check the acquired data
            Dim error As Integer = checkData()
            ' if data is invalid, then throw an exception
            If Not valid Then Throw New Exception("The tax bracket data is invalid (" + error.ToString + ")")
            ' otherwise, return the three arrays
            Return New Object() {limits, coeffr, coeffn}
        End Function
    End Class
End Namespace

Let's take a look at the constructor:


        ' constructor
        Public Sub New(ByVal connectionString As String)
            ' Note the following three pieces of information
            Me.connectionString = connectionString
        End Sub

It receives as a parameter the connection string for the OLEDB source containing the data to be retrieved. The constructor simply stores it. A connection string contains all the parameters required by the OLEDB driver to connect to the OLEDB source. It is generally quite complex. To find the connection string for ACCESS databases, you can use the [WebMatrix] tool. Launch this tool. It provides a window for connecting to a data source:

Using the icon indicated by the arrow above, you can create a connection to two types of Microsoft databases: SQL Server and ACCESS. Let’s choose ACCESS:

Image

We used the [...] button to select the ACCESS database. We confirm the wizard. In the [Data] tab, icons represent the connection:

Image

Now, let’s create a new .aspx file via [Files/New File]:

Image

We get a blank page on which we can design our web interface:

Image

Let’s drag the [impots] table from the [Data] tab onto the sheet above. We get the following result:

Image

Right-click on the [AccessDataSourceControl] object below to access its properties:

Image

The OLEDB connection string to the ACCESS database is provided by the [ConnectionString] property above:

Provider=Microsoft.Jet.OLEDB.4.0; Ole DB Services=-4; Data Source=D:\data\serge\devel\aspnet\poly\chap5\impots\3\impots.mdb

We can see that this string consists of a fixed part and a variable part, which is simply the name of the ACCESS file. We will use this fact to generate the connection string to our OLEDB data source.

Let’s now return to our [impotsOLEDB] class. The [getData] method is responsible for reading data from the [impots] table and placing it into three arrays (limites, coeffr, coeffn). Let’s comment on its code:

  • We define the [DataAdapter] object, which will allow us to transfer the result of an SQL SELECT query into memory. To do this, we define the [select] query to be executed and associate it with the [DataAdapter] object. The constructor of the [DataAdapter] also requires the connection string it will use to connect to the OLEDB source

            ' We create a DataAdapter object to read data from the OLEDB source
            Dim adapter As New OleDbDataAdapter("select limits,coeffr,coeffn from taxes", connectionString)
  • We execute the [select] command using the [Fill] method of the [DataAdapter] object. The result of the [select] is loaded into a [DataTable] object created for this purpose. A [DataTable] object is the in-memory representation of a database table, i.e., a set of rows and columns. We handle an exception that may occur if, for example, the connection string is incorrect.

            ' We create an in-memory representation of the select result
            Dim content As New DataTable("taxes")
            Try
                adapter.Fill(content)
            Catch e As Exception
                Throw New Exception("Database access error (" + e.Message + ")")
            End Try
  • In [content], we have the [taxes] table returned by the [select] statement. A [DataTable] object is a table, i.e., a set of rows. These are accessible via the [rows] property of [DataTable]:

            ' Retrieve the contents of the impots table
            Dim taxRows As DataRowCollection = content.Rows
  • Each element of the [taxRows] collection is a [DataRow] object representing a row in the table. This row has columns accessible via the [DataRow] object through its [Item] property. [DataRow].[Item(i)] is column number i of the [DataRow]. By iterating through the collection of rows (the DataRows collection of taxRows) and the collection of columns for each row, we can retrieve the entire table:
            ' we dimension the receiving arrays
            Me.limits = New Decimal(taxLines.Count - 1) {}
            Me.coeffr = New Decimal(taxLines.Count - 1) {}
            Me.coeffn = New Decimal(taxLines.Count - 1) {}
            ' Transfer the contents of the 'impots' table to the arrays
            Dim i As Integer
            Dim row As DataRow
            Try
                For i = 0 To taxLines.Count - 1
                    ' Row i of the table
                    row = taxRows.Item(i)
                    ' retrieve the row's contents
                    limits(i) = CType(row.Item(0), Decimal)
                    coeffr(i) = CType(row.Item(1), Decimal)
                    coeffn(i) = CType(row.Item(2), Decimal)
                Next
            Catch
                Throw New Exception("The tax bracket data is not of the correct type")
            End Try
  • Once the data from the [taxes] table has been loaded into the three arrays, all that remains is to verify their contents using the [checkData] method of the base class [taxData]:
            ' Check the acquired data
            Dim error As Integer = checkData()
            ' if data is invalid, throw an exception
            If Not Valid Then Throw New Exception("The tax bracket data is invalid (" + error.ToString + ")")
            ' otherwise, return the three arrays
            Return New Object() {limits, coeffr, coeffn}

6.3.4. Tests for the data access class

A test program could look like this:

Option Explicit On 
Option Strict On

' namespaces
Imports System
Imports Microsoft.VisualBasic

Namespace st.istia.univangers.fr

    ' test page
    Module testimports
        Sub Main(ByVal arguments() As String)
            ' interactive tax calculation program
            ' the user enters three pieces of data via the keyboard: married, number of children, salary
            ' the program then displays the tax due
            Const syntax1 As String = "pg bdACCESS"
            Const syntax2 As String = "syntax: married nbChildren salary" + ControlChars.Lf + "married: o for married, n for unmarried" + ControlChars.Lf + "nbChildren: number of children" + ControlChars.Lf + "salary: annual salary in F"

            ' Checking program parameters
            If arguments.Length <> 1 Then
                ' error message
                Console.Error.WriteLine(syntax1)
                ' end
                Environment.Exit(1)
            End If
            ' retrieve the arguments
            Dim path As String = arguments(0)
            ' prepare the connection string
            Dim connectionString As String = "Provider=Microsoft.Jet.OLEDB.4.0; Ole DB Services=-4; Data Source=" + path

            ' Create a tax object
            Dim taxObject As TaxObject = Nothing
            Try
                objTax = New Tax(New TaxOLEDB(connectionString))
            Catch ex As Exception
                Console.Error.WriteLine(("The following error occurred: " + ex.Message))
                Environment.Exit(2)
            End Try

            ' infinite loop
            While True
                ' initially no errors
                Dim error As Boolean = False

                ' Requesting tax calculation parameters
                Console.Out.Write("Tax calculation parameters in the format: married, number of children, salary, or 'nothing' to exit:")
                Dim parameters As String = Console.In.ReadLine().Trim()

                ' anything to do?
                If parameters Is Nothing Or parameters = "" Then
                    Exit While
                End If

                ' Check the number of arguments in the entered line
                Dim args As String() = parameters.Split(Nothing)
                Dim numParams As Integer = args.Length
                If nbParameters  3 Then
                    Console.Error.WriteLine(syntax2)
                    error = True
                End If
                Dim husband As String
                Dim nbChildren As Integer
                Dim salary As Integer
                If Not error Then
                    ' Checking the validity of the
                    ' married
                    married = args(0).ToLower()
                    If married <> "o" And married <> "n" Then
                        Console.Error.WriteLine((syntax2 + ControlChars.Lf + "Invalid married argument: enter o or n"))
                        error = True
                    End If
                    ' nbChildren
                    nbChildren = 0
                    Try
                        nbChildren = Integer.Parse(args(1))
                        If nbChildren < 0 Then
                            Throw New Exception
                        End If
                    Catch
                        Console.Error.WriteLine(syntax2 + "\nInvalid numberOfChildren argument: enter a positive integer or zero")
                        error = True
                    End Try
                    ' salary
                    salary = 0
                    Try
                        salary = Integer.Parse(args(2))
                        If salary < 0 Then
                            Throw New Exception
                        End If
                    Catch
                        Console.Error.WriteLine(syntax2 + "\nInvalid salary argument: enter a positive integer or zero")
                        error = True
                    End Try
                End If
                If Not error Then
                    ' the parameters are correct - calculate the tax
                    Console.Out.WriteLine(("tax=" & objTax.calculate(married = "o", numChildren, salary).ToString + " euro(s)"))
                End If
            End While
        End Sub
    End Module
End Namespace

The application is launched with a parameter:

  • bdACCESS: name of the ACCESS file to be used

The tax calculation is performed using an object of type [tax] created when the application is launched:


            ' retrieve the arguments
            Dim path As String = arguments(0)
            ' prepare the connection string
            Dim connectionString As String = "Provider=Microsoft.Jet.OLEDB.4.0; Ole DB Services=-4; Data Source=" + path

            ' create a tax object
            Dim taxObject As TaxObject = Nothing
            Try
                objTax = New Tax(New TaxOLEDB(connectionString))
            Catch ex As Exception
                Console.Error.WriteLine(("The following error occurred: " + ex.Message))
                Environment.Exit(2)
            End Try

The connection string to the OLEDB source was constructed using information obtained with [WebMatrix].

Once initialized, the application repeatedly prompts the user to enter the three pieces of information needed to calculate their tax:

  • marital status: o for married, n for unmarried
  • number of children
  • their annual salary

All classes are compiled:

dos>vbc /r:system.dll /r:system.data.dll /t:library /out:impot.dll impots.vb impotsArray.vb impotsData.vb impotsOLEDB.vb
dos>vbc /r:impot.dll testimpots.vb

The [impots.mdb] file is placed in the test application folder, and the application is launched as follows:

dos>testimpots impots.mdb
Tax calculation parameters in married format: number of children, salary, or nothing to stop :o 2 60000
tax=4,300 euro(s)

You can run the application with an incorrect ACCESS file:

dos>testimpots xx
The following error occurred: Database access error (File 'D:\data\serge\devel\aspnet\poly\chap5\impots\3\xx' not found.)

6.3.5. The web application views

These are the same as in the previous application: formulaire.aspx and erreurs.aspx

6.3.6. Application controllers [global.asax, main.aspx]

Only the [global.asax] controller needs to be modified. It is responsible for creating the [impot] object when the application starts. The constructor of this object has a single parameter: the [impotsData] object responsible for retrieving the data. This parameter therefore changes since we are switching data sources. The [global.asax.vb] controller becomes the following:


Imports System
Imports System.Web
Imports System.Web.SessionState
Imports st.istia.univangers.fr
Imports System.Configuration

Public Class Global
    Inherits System.Web.HttpApplication

    Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' Create an 'impot' object
        Dim objImpot As impot
        Try
            objImpot = New impot(New impotsOLEDB(ConfigurationSettings.AppSettings("connectionString")))
            ' Add the object to the application
            Application("objImpot") = objImpot
            ' no error
            Application("error") = False
        Catch ex As Exception
            'An error occurred; we log it in the application
            Application("error") = True
            Application("message") = ex.Message
        End Try
    End Sub
End Class

The data source for the [impot] object is now an [impotOLEDB] object. This object takes as a parameter the connection string for the OLEDB data source to be used. This string is stored in the application's [web.config] configuration file:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <appSettings>
        <add key="connectionString" 
        value="Provider=Microsoft.Jet.OLEDB.4.0; Ole DB Services=-4; Data Source=D:\data\serge\devel\aspnet\poly\chap5\impots2\impots.mdb" />
    </appSettings>
</configuration>

The [main.aspx] controller remains unchanged.

6.3.7. Summary of changes

The application is ready for testing. Let’s list the changes made to the previous version:

  1. a new data access class has been created
  2. the [global.asax.vb] controller has been modified in one place: construction of the [impot] object
  3. A [web.config] file has been added

6.3.8. Testing the web application

All of the above files are placed in a folder named <application-path>.

Image

In this folder, a [bin] subfolder is created, in which the [impot.dll] assembly—generated from compiling the business class files: [impots.vb, impotsData.vb, impotsArray.vb, impotsOLEDB.vb]—is placed. The required compilation command is shown below:

dos>vbc /r:system.dll /r:system.data.dll /t:library /out:impot.dll impots.vb impotsArray.vb impotsData.vb impotsOLEDB.vb

The [impot.dll] file generated by this command must be placed in <application-path>\bin so that the web application can access it. The Cassini server is launched with the parameters (<application-path>,/impots3). The tests yield the same results as in the previous version.

6.4. Example 4

6.4.1. The Problem

We now propose to transform our application into a tax calculation simulation application. A user will be able to perform successive tax calculations, and these will be presented to them on a new view resembling this:

Image

6.4.2. The application’s MVC structure

The application’s MVC structure becomes as follows:

Image

A new view [simulations.aspx] appears, of which we have just provided a screenshot. The data access class will be the [impotsODBC] class from Example 2.

6.4.3. The web application views

The [erreurs.aspx] view remains unchanged. The [formulaire.aspx] view changes slightly. In fact, the tax amount no longer appears on this view. It is now on the [simulations.aspx] view. Thus, upon startup, the page presented to the user is as follows:

Image

Additionally, the [form] view includes a JavaScript script that validates the entered data before sending it to the server, as shown in the following example:

Image

The presentation code is as follows:


<%@ page src="formulaire.aspx.vb" inherits="formulaire" AutoEventWireup="false"%>
<html>
    <head>
        <title>Tax</title>
        <script language="javascript">
        function calculate(){
          // Check parameters before sending them to the server
        with(document.frmImpots){
          //number of children
          fields = /^\s*(\d+)\s*$/ .exec(txtChildren.value);
          if(champs==null){
            // the template is not validated
            alert("The number of children was not provided or is incorrect");
            txtChildren.focus();
            return;
          }//if
          //salary
          fields = /^\s*(\d+)\s*$/ .exec(txtSalary.value);
          if(fields==null){
            // the pattern does not match
            alert("Salary was not provided or is incorrect");
            txtSalary.focus();
            return;
          }//if
          // OK—send the form to the server
          submit();
        }//with
      }//calculate  
        </script>
    </head>
    <body>
        <P>Calculate your tax</P>
        <HR width="100%" SIZE="1">
        <form name="frmImpots" method="post" action="main.aspx?action=calcul">
            <TABLE border="0">
                <TR>
                    <TD>Are you married?</TD>
                    <TD>
                        <INPUT type="radio" value="yes" name="rdMarie" <%=rdouichecked%>>Yes 
                      <INPUT type="radio"   value="no" name="rdMarie" <%=rdnonchecked%>>No</TD>
                </TR>
                <TR>
                    <TD>Number of children</TD>
                    <TD><INPUT type="text" size="3" maxLength="3" name="txtEnfants" value="<%=txtEnfants%>"></TD>
                </TR>
                <TR>
                    <TD>Annual salary (euros)</TD>
                    <TD><INPUT type="text" maxLength="12" size="12" name="txtSalaire" value="<%=txtSalaire%>"></TD>
                </TR>
            </TABLE>
            <hr>
            <P>
                <INPUT type="button" value="Calculate" onclick="calculate()">
            </P>
        </form>
        <form method="post" action="main.aspx?action=clear">
            <INPUT type="submit" value="Clear">
        </form>
    </body>
</html>

The dynamic fields on the page are the same as in previous versions. The dynamic field for the tax amount has been removed. The [Calculate] button is no longer a [submit] button. It is a [button], and when clicked, the JavaScript function [calculate()] is executed:


                <INPUT type="button" value="Calculate" onclick="calculate()">

We have given the form the name [frmImpots] so that we can reference it in the [calculer] script:


        <form name="frmImpots" method="post" action="main.aspx?action=calcul">

The JavaScript function [calculate] uses regular expressions to validate the fields in the [document.frmImpots.txtEnfants] and [document.frmImpots.txtSalaire] forms. If the entered values are correct, they are sent to the server via [document.frmImpots.submit()].

The presentation page obtains its dynamic fields from the following controller [formulaire.aspx.vb]:


Imports System.Collections.Specialized

Public Class form
    Inherits System.Web.UI.Page

    ' page fields
    Protected rdouichecked As String
    Protected rdnonchecked As String
    Protected txtChildren As String
    Protected txtSalary As String
    Protected txtTax As String

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' retrieve the previous request from the context
        Dim form As NameValueCollection = Context.Items("form")
        ' prepare the page to be displayed
        ' radio buttons
        rdouichecked = ""
        rdnonchecked = "checked"
        If form("rdMarie").ToString = "yes" Then
            rdouichecked = "checked"
            rdnonchecked = ""
        End If
        ' the rest
        txtChildren = CType(form("txtChildren"), String)
        txtSalary = CType(form("txtSalary"), String)
    End Sub
End Class

The controller [formulaire.aspx.vb] is identical to previous versions except that it no longer needs to retrieve the [txtImpot] field from the context, as this field has been removed from the page.

The [simulations.aspx] view appears as follows:

Image

and corresponds to the following presentation code:


<%@ page src="simulations.aspx.vb" inherits="simulations" autoeventwireup="false" %>
<HTML>
    <HEAD>
        <title>Simulations</title>
    </HEAD>
    <body>
        <P>Simulation Results</P>
        <HR width="100%" SIZE="1">
        
            
                
                    Married
                
                    Children
                
                    Annual salary (euros)
                
                    Tax due (euros)
            
            <%=simulationsHTML%>
        
        
        <a href="<%=href%>">
            <%=link%>
        
    </body>
</HTML>

This code contains three dynamic fields:

simulationsHTML
HTML code for a list of simulations in the form of HTML table rows
href
URL of a link
link
link text

They are generated by the controller [simulations.aspx.vb]:


Imports System.Collections
Imports Microsoft.VisualBasic

Public Class simulations
    Inherits System.Web.UI.Page

    Protected simulationsHTML As String = ""
    Protected href As String
    Protected link As String

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        'Retrieve the simulations from the context
        Dim simulations As ArrayList = CType(context.Items("simulations"), ArrayList)
        ' each simulation is an array of 4 string elements
        Dim simulation() As String
        Dim i, j As Integer
        For i = 0 To simulations.Count - 1
            simulation = CType(simulations(i), String())
            simulationsHTML += ""
            For j = 0 To simulation.Length - 1
                simulationsHTML += "" + simulation(j) + ""
            Next
            simulationsHTML += "" + ControlChars.CrLf
        Next
        ' retrieve the other elements of the context
        href = context.Items("href").ToString
        link = context.Items("link").ToString
    End Sub
End Class

The page controller retrieves information placed by the application controller in the page context:

Context.Items("simulations")
ArrayList object containing the list of simulations to display. Each element is an array of 4 strings representing the simulation's information (married, children, salary, tax).
Context.Items("href")
URL of a link
Context.Items("link")
Link text

6.4.4. The controllers [global.asax, main.aspx]

Let’s review the MVC architecture of our application:

Image

The controller [main.aspx] must handle three actions:

  • init: corresponds to the client’s first request. The controller displays the view [form.aspx]
  • calcul: corresponds to the tax calculation request. If the data in the input form is correct, the tax is calculated using the business class [impotsODBC]. The controller returns the [simulations.aspx] view to the client with the result of the current simulation plus all previous ones. If the data in the input form is incorrect, the controller returns the [erreurs.aspx] view with the list of errors and a link to return to the form.
  • return: corresponds to returning to the form after an error. The controller displays the [form.aspx] view as it was validated before the error.

In this new version, only the [calcul] action has changed. Indeed, if the data is valid, it must lead to the [simulations.aspx] view, whereas previously it led to the [form.aspx] view. The [main.aspx.vb] controller becomes the following:


Imports System
...

Public Class main
    Inherits System.Web.UI.Page

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' First, we check if the application was able to initialize correctly
...
        ' we execute the action
        Select Case action
            Case "init"
                ' initialize application
                initAppli()
            Case "calculation"
                ' calculate tax
                calculateTax()
            Case "return"
                ' return to form
                returnToForm()
            "Clear" checkbox
                ' initialize application
                initAppli()
            Else
                ' unknown action = init
                initAppli()
        End Select
    End Sub

...
    Private Sub calculateTax()
        ' Save the entries
        Session.Item("form") = Request.Form
        ' Check the validity of the entered data
        Dim errors As ArrayList = checkData()
        ' if there are errors, display a message
        If errors.Count <> 0 Then
            ' prepare the error page
            context.Items("href") = "main.aspx?action=return"
            context.Items("link") = "Back to form"
            context.Items("errors") = errors
            Server.Transfer("errors.aspx")
        End If
        ' No errors here - calculating the tax
        Dim tax As Long = CType(Application("objTax"), tax).calculate( _
        Request.Form("rdMarie") = "yes", _
        CType(Request.Form("txtChildren"), Integer), _
        CType(Request.Form("txtSalary"), Long))
        ' add the result to the existing simulations
        Dim simulations As ArrayList
        If Not Session.Item("simulations") Is Nothing Then
            simulations = CType(Session.Item("simulations"), ArrayList)
        Else
            simulations = New ArrayList
        End If
        ' Add the current simulation
        Dim simulation() As String = New String() {Request.Form("rdMarie").ToString, _
        Request.Form("txtEnfants").ToString, Request.Form("txtSalaire").ToString, _
        tax.ToString}
        simulations.Add(simulation)
        ' Add the simulations to the session and context
        context.Items("simulations") = simulations
        Session.Item("simulations") = simulations
        ' display the results page
        context.Items("href") = "main.aspx?action=return"
        context.Items("link") = "Back to form"
        Server.Transfer("simulations.aspx", True)
    End Sub
...
End Class

We have included only what is necessary above to understand the changes found exclusively in the [calculImpots] function:

  • First, the function saves the form [Request.Form] to the session so that the form can be regenerated in the state in which it was validated. This must be done in all cases, since whether the operation results in the [erreurs.aspx] response or the [simulations.aspx] response, we return to the form via the [Back to form] link. To restore the form correctly, its values must have been saved in the session beforehand.
  • If the entered data is correct, the function adds the current simulation (married, children, salary, tax) to the list of simulations. This list is found in the session associated with the "simulations" key.
  • The list of simulations is stored in the session for future use. It is also placed in the current context because that is where the [simulations.aspx] view expects to find it
  • The [simulations.aspx] view is displayed once the other information it requires has been placed in the context

6.4.5. Summary of Changes

The application is ready for testing. Let’s list the changes made to previous versions:

  1. A new view has been created
  2. The [main.aspx.vb] controller has been modified in one place: handling of the [calcul] action

6.4.6. Testing the web application

The reader is invited to perform the tests. Here is a reminder of the procedure. All application files are placed in a folder named <application-path>. Within this folder, a subfolder [bin] is created, containing the assembly [impot.dll] generated from the compilation of the business class files: [impots.vb, impotsData.vb, impotsArray.vb, impotsODBC.vb]. The [impot.dll] file produced by this command must be placed in <application-path>\bin so that the web application can access it. The Cassini server is launched with the parameters (<application-path>,/impots4).

6.5. Conclusion

The previous examples have demonstrated, in a concrete scenario, mechanisms commonly used in web development. We have consistently used the MVC architecture for its educational value. We could have handled these same examples differently and perhaps more simply without this architecture. However, it offers significant advantages as soon as the application becomes somewhat complex with multiple pages.

We could continue our examples in various ways. Here are a few:

  • The user might want to save their simulations over time. They could run simulations on day D and retrieve them on day D+3, for example. One possible solution to this problem is the use of cookies. We know that the session token between the server and a client is transmitted via this mechanism. We could also use this mechanism to transmit the simulations between the client and the server.
    • At the same time the server sends the page containing the simulation results, it sends a cookie in its HTTP headers containing a string representing the simulations. Since these are in an [ArrayList] object, this object must be converted to a [String]. The server would set a lifespan for the cookie, for example 30 days.
    • The client browser stores the received cookies in a file and sends them back each time it makes a request to a server that sent them, provided they are still valid (lifetime not exceeded). The server will receive a [String] character string for the simulations, which it must convert into an [ArrayList] object.

Cookies are managed by [Response.Cookies] when sent to the client and by [Request.Cookies] when received on the server.

  • The mechanism described above can become quite resource-intensive if there are a large number of simulations. Furthermore, it is common for a user to periodically clear their cookies by deleting them all, even if they otherwise allow their browser to use them. So sooner or later, the simulation cookies will be lost. We may therefore want to store these on the server rather than on the client, in a database for example. To link simulations to a specific user, the application could start with an authentication phase requiring a username and password, which are themselves stored in a database or any other type of data repository.
  • We might also want to secure the operation of our application. It currently makes two assumptions:
    1. the user always goes through the controller [main.aspx]
    2. and in that case, it always uses the actions available on the page that was sent to it

What happens, for example, if the user directly requests the URL [http://localhost/impots4/formulaire.aspx]? This scenario is unlikely since the user is unaware of this URL. However, it must be accounted for. It can be handled by the application controller [global.asax], which processes all requests made to the application. It can thus verify that the requested resource is indeed [main.aspx].

A more likely scenario is that a user does not use the actions available on the page the server sent them. For example, what happens if the user directly requests the URL [http://localhost/impots4/main.aspx?action=retour] without first filling out the form? Let’s try it. We get the following response:

Image

The server crashes. This is normal. For the [return] action, the controller expects to find a [NameValueCollection] object in the session representing the form values it needs to display. It does not find them. The controller mechanism provides an elegant solution to this problem. For each request, the [main.aspx] controller can verify that the requested action is indeed one of the actions from the page previously sent to the user. We can use the following mechanism:

  • Before sending its response to the client, the controller stores information identifying this page in the client’s session
  • when it receives a new request from the client, it verifies that the requested action indeed belongs to the last page sent to that client
  • The information linking pages and the actions allowed on those pages can be entered into the application’s [web.config] configuration file.
  • Experience shows that application controllers share a broad common foundation and that it is possible to build a generic controller, with its specialization for a given application handled via a configuration file. This is the approach taken, for example, by the [Struts] tool in the field of Java web programming.