Skip to content

6. 示例

在本章中,我们将通过一系列示例来阐释前文所述的内容。

6.1. 示例 1

6.1.1. 问题

该应用程序必须允许用户计算其应缴税款。我们考虑一种简化情况:纳税人仅需申报工资收入(2003年收入的2004年数据):

  • 我们通过公式 nbParts = nbEnfants / 2 + 1(未婚)或 nbEnfants / 2 + 2(已婚)来计算该雇员的税级数量,其中 nbEnfants 表示子女数量。
  • 若其子女数至少为三名,则有权额外获得半份免税额
  • 其应税收入 RR = 0.72 * S 计算,其中 S 为其年薪
  • 我们计算其家庭系数 QF = R / nbParts
  • 我们计算他的税额 I。请看下表:
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

每行有 3 个字段。要计算税款 I,请查找满足 QF <= 字段1 的第一行。例如,如果 QF = 5000,则找到的行将是

    8382        0.0683        291.09

税额 I 即等于 0.0683*R - 291.09*nbParts。若 QF 值导致条件 QF<=field1 永远无法满足,则采用最后一行中的系数。此处:

    0                0.4809    9505.54

由此得出税款 I = 0.4809*R - 9505.54*nbParts。

6.1.2. 应用程序的 MVC 结构

该应用程序的 MVC 结构如下:

Image

控制器角色将由 [main.aspx] 页面承担。将包含三个可能的操作:

  • init:对应客户端的首次请求。控制器将显示 [formulaire.aspx] 视图
  • calcul:对应计算税款的请求。若输入表单中的数据正确,则使用业务类 [impots] 计算税款。控制器将已验证的 [form.aspx] 视图连同计算出的税款一并返回给客户端。若输入表单中的数据有误,控制器将返回 [errors.aspx] 视图,其中包含错误列表及返回表单的链接。
  • return:对应于发生错误后返回表单。控制器将显示 [form.aspx] 视图,该视图与发生错误前经过验证的状态一致。

[main.aspx] 控制器并不涉及税费计算。它仅负责管理客户端与服务端之间的交互,并执行客户端请求的操作。对于 [calculate] 操作,它将依赖 [tax] 业务类。

6.1.3. 业务类

**impot** 类将定义如下:


' imported namespaces
Imports System
 
' class
Namespace st.istia.univangers.fr
    Public Class impot
        Private limites(), coeffR(), coeffN() As Decimal
 
        ' manufacturer
        Public Sub New(ByRef source As impotsData)
            ' data required for tax calculation
            ' come from an external source [source]
            ' we retrieve them - there may be an exception
            Dim data() As Object = source.getData
            limites = CType(data(0), Decimal())
            coeffR = CType(data(1), Decimal())
            coeffN = CType(data(2), Decimal())
        End Sub
 
        ' tAX CALCULATION
        Public Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Long) As Long
            ' calculating the number of shares
            Dim nbParts As Decimal
            If marié Then
                nbParts = CDec(nbEnfants) / 2 + 2
            Else
                nbParts = CDec(nbEnfants) / 2 + 1
            End If
            If nbEnfants >= 3 Then
                nbParts += 0.5D
            End If
            ' calculation of taxable income & family quota
            Dim revenu As Decimal = 0.72D * salaire
            Dim QF As Decimal = revenu / nbParts
            ' tAX CALCULATION
            limites((limites.Length - 1)) = QF + 1
            Dim i As Integer = 0
            While QF > limites(i)
                i += 1
            End While
            Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
        End Function
    End Class
End Namespace

通过向其构造函数提供类型为 [impotsData] 的数据源来创建一个税款对象。该类有一个公共方法 [getData],用于检索计算税款所需的三个数据数组,如前所述。如果无法检索数据或发现数据有误,该方法可以处理异常。 创建 [tax] 对象后,可以反复调用其 calculate 方法,根据纳税人的婚姻状况(已婚或单身)、子女数量和年薪来计算其应缴税额。

6.1.4. 数据访问类

[impotsData] 类是提供数据访问功能的类。它是一个抽象类。对于每种可能的新数据源(数组、平面文件、数据库、控制台等),都必须创建一个派生类。其定义如下:


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 valide As Boolean
 
        ' data access method
        Public MustOverride Function getData() As Object()
 
        ' data verification method
        Protected Function checkData() As Integer
            ' verifies acquired data
            ' we need data
            valide = Not limites Is Nothing AndAlso Not coeffr Is Nothing AndAlso Not coeffn Is Nothing
            If Not valide Then Return 1
            ' we must have 3 arrays of the same size
            If valide Then valide = limites.Length = coeffr.Length AndAlso limites.Length = coeffn.Length
            If Not valide Then Return 2
            ' tables must be non-empty
            valide = limites.Length <> 0
            If Not valide Then Return 3
            ' each array must contain elements >=0 in ascending order
            valide = check(limites, limites.Length - 1) AndAlso check(coeffr, coeffr.Length) AndAlso check(coeffn, coeffn.Length)
            If Not valide Then Return 4
            ' all is good
            Return 0
        End Function
 
        ' checks the validity of an array's contents
        Protected Function check(ByRef tableau() As Decimal, ByVal n As Integer) As Boolean
            ' array must have its first n elements >=0 and in strictly ascending order
            If tableau(0) < 0 Then Return False
            For i As Integer = 1 To n - 1
                If tableau(i) <= tableau(i - 1) Then Return False
            Next
            ' it's good
            Return True
        End Function
    End Class
End Namespace

该类具有以下受保护的属性:

limits
税率区间限额数组
coeffr
应用于应税收入的系数数组
coeffn
应用于股数系数的表格
已验证
布尔值,表示数据(限额、coeffr、coeffn)是否已通过验证
有效
布尔值,表示数据(limits、coeffr、coeffn)是否有效

该类没有构造函数。它有一个抽象方法 [getData],派生类必须实现该方法。该方法的目的是:

  • 为三个数组 limits、coeffr、coeffn 赋值
  • 若无法获取数据或数据被判定为无效,则抛出异常。

该类提供了受保护的方法 [checkData] 和 [check],用于验证属性(limits、coeffr、coeffn)的有效性。这免除了派生类实现这些方法的必要,它们只需直接调用即可。

我们将使用的第一个派生类如下:


Imports System.Collections
Imports System
 
Namespace st.istia.univangers.fr
    Public Class impotsArray
        Inherits impotsData
 
        ' constructor with no arguments
        Public Sub New()
            ' initializing tables with constants
            limites = 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
            valide = True
        End Sub
 
        ' builder with three input tables
        Public Sub New(ByRef limites() As Decimal, ByRef coeffr() As Decimal, ByRef coeffn() As Decimal)
            ' data storage
            Me.limites = limites
            Me.coeffr = coeffr
            Me.coeffn = coeffn
            checked = False
        End Sub
 
        Public Overrides Function getData() As Object()
            ' check data if necessary
            Dim erreur As Integer
            If Not checked Then erreur = checkData() : checked = True
            ' if invalid, then throw an exception
            If Not valide Then Throw New Exception("Les données des tranches d'impôts sont invalides (" + erreur.ToString + ")")
            ' otherwise we return the three tables
            Return New Object() {limites, coeffr, coeffn}
        End Function
    End Class
End Namespace

该类名为 [impotsArray],拥有两个构造函数:

  • 一个无参构造函数,它使用硬编码的数组初始化基类的属性(limits、coeffr、coeffn)
  • 一个构造函数,它使用作为参数传递的数组来初始化基类的属性(limits、coeffr、coeffn)

[getData] 方法允许外部类检索数组 (limits, coeffr, coeffn),该方法仅通过基类的 [checkData] 方法验证这三个数组的有效性。若数据无效,则抛出异常。

6.1.5. 测试业务类和数据访问类

在 Web 应用程序中,务必仅包含已验证正确的业务类和数据访问类。这样,Web 应用程序的调试阶段就可以专注于控制器和视图层。测试程序可能如下所示:


' options
Option Strict On
Option Explicit On 
 
' namespaces
Imports System
Imports Microsoft.VisualBasic
 
Namespace st.istia.univangers.fr
    Module test
        Sub Main()
            ' interactive tax calculator
            ' the user enters three data points on the keyboard: married nbEnfants salary
            ' the program then displays the tax payable
            Const syntaxe As String = "syntaxe : marié nbEnfants salaire" + ControlChars.Lf + "marié : o pour marié, n pour non marié" + ControlChars.Lf + "nbEnfants : nombre d'enfants" + ControlChars.Lf + "salaire : salaire annuel en F"
 
            ' tax object creation
            Dim objImpôt As impot = Nothing
            Try
                objImpôt = New impot(New impotsArray)
            Catch ex As Exception
                Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
                Environment.Exit(1)
            End Try
            ' infinite loop
            Dim marié As String
            Dim nbEnfants As Integer
            Dim salaire As Long
            While True
                ' tax calculation parameters are requested
                Console.Out.Write("Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :")
                Dim paramètres As String = Console.In.ReadLine().Trim()
                ' anything to do?
                If paramètres Is Nothing OrElse paramètres = "" Then
                    Exit While
                End If
                ' check the number of arguments in the input line
                Dim erreur As Boolean = False
                Dim args As String() = paramètres.Split(Nothing)
                Dim nbParamètres As Integer = args.Length
                If nbParamètres <> 3 Then
                    Console.Error.WriteLine(syntaxe)
                    erreur = True
                End If
                ' checking the validity of parameters
                If Not erreur Then
                    ' married
                    marié = args(0).ToLower()
                    If marié <> "o" And marié <> "n" Then
                        erreur = True
                    End If
                    ' nbEnfants
                    Try
                        nbEnfants = Integer.Parse(args(1))
                        If nbEnfants < 0 Then
                            Throw New Exception
                        End If
                    Catch
                        erreur = True
                    End Try
                    ' salary
                    Try
                        salaire = Integer.Parse(args(2))
                        If salaire < 0 Then
                            Throw New Exception
                        End If
                    Catch
                        erreur = True
                    End Try
                End If
                ' if the parameters are correct - the tax is calculated
                If Not erreur Then
                    Console.Out.WriteLine(("impôt=" & objImpôt.calculer(marié = "o", nbEnfants, salaire) & " euro(s)"))
                Else
                    Console.Error.WriteLine(syntaxe)
                End If
            End While
        End Sub
    End Module
End Namespace

该应用程序会提示用户输入计算税款所需的三个信息:

  • 婚姻状况:o 代表已婚,n 代表未婚
  • 子女人数
  • 年薪

税费计算是通过应用程序启动时创建的 [tax] 类型的对象来完成的:


            ' tax object creation
            Dim objImpôt As impot = Nothing
            Try
                objImpôt = New impot(New impotsArray)
            Catch ex As Exception
                Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
                Environment.Exit(1)
            End Try

作为数据源,我们使用一个 [impotsArray] 类型的对象。 使用该类的无参构造函数,并为三个数组(limites、coeffr、coeffn)提供硬编码的值。创建 [impot] 对象理论上可能会抛出异常,因为该对象在创建自身时会从作为参数传递给它的数据源中请求数据(limites、coeffr、coeffn),而此数据检索可能会触发异常。 不过,在此情况下,用于获取数据(硬编码值)的方法不会引发异常。但我们保留了异常处理机制,旨在提醒读者注意 [impot] 对象可能因构造错误而导致的潜在问题。

以下是上述程序的运行示例:

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

我们将所有类 [impot, impotsData, impotsArray] 编译成一个程序集 [impot.dll]:

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

我们编译测试程序:

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

我们可以运行测试:

dos>testimpots1
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 60000
impôt=4300 euro(s)
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :n 2 60000
impôt=6872 euro(s)
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :

6.1.6. Web 应用程序视图

该应用程序将包含两个视图:[form.aspx] 和 [errors.aspx]。让我们通过屏幕截图来说明应用程序的工作原理。当首次请求 URL [main.aspx] 时,将显示 [form.aspx] 视图:

Image

用户填写表单:

Image

并点击 [Calculate] 按钮,得到以下结果:

Image

用户可能输入了错误数据:

Image

此时点击 [Calculate] 按钮将返回不同的响应页面 [errors.aspx]:

Image

用户可以使用上方的 [返回表单] 链接,回到出错前的 [form.aspx] 视图:

Image

6.1.7. [form.aspx] 视图

[form.aspx] 页面将如下所示:


<%@ page src="formulaire.aspx.vb" inherits="formulaire" AutoEventWireup="false"%>
<html>
    <head>
        <title>Impôt</title>
    </head>
    <body>
        <P>Calcul de votre impôt</P>
        <HR>
        <form method="post" action="main.aspx?action=calcul">
            <TABLE border="0">
                <TR>
                    <TD>Etes-vous marié(e)</TD>
                    <TD>
                        <INPUT type="radio" value="oui" name="rdMarie" <%=rdouichecked%>>Oui 
                      <INPUT type="radio"  value="non" name="rdMarie" <%=rdnonchecked%>>Non
                     </TD>
                </TR>
                <TR>
                    <TD>Nombre d'enfants</TD>
                    <TD><INPUT type="text" size="3" maxLength="3" name="txtEnfants" value="<%=txtEnfants%>"></TD>
                </TR>
                <TR>
                    <TD>Salaire annuel (euro)</TD>
                    <TD><INPUT type="text" maxLength="12" size="12" name="txtSalaire" value="<%=txtSalaire%>"></TD>
                </TR>
                <TR>
                    <TD>Impôt à payer :
                    </TD>
                    <TD><%=txtImpot%></TD>
                </TR>
            </TABLE>
            <hr>
            <P>
                <INPUT type="submit" value="Calculer">
            </P>
        </form>
        <form method="post" action="main.aspx?action=effacer">
                <INPUT type="submit" value="Effacer">
        </form>
    </body>
</html>

此页面上的动态字段如下:

rdouichecked
如果 [yes] 复选框应被选中,则返回 "checked";否则返回 ""
rdnonchecked
[否]复选框也是如此
txtChildren
要放入 [txtChildren] 输入字段的值
txtSalary
要输入到 [txtSalary] 输入字段中的值
txtTax
要输入到 [txtTax] 输入字段中的值

该页面包含两个表单,每个表单都有一个 [submit] 按钮。[Calculate] 按钮是以下表单的 [submit] 按钮:


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

我们可以看到,表单参数将通过 [action=calcul] 提交到控制器。 [Clear] 按钮是以下表单的 [submit] 按钮:


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

我们可以看到,表单参数将通过 [action=effacer] 提交到控制器。在此,表单没有参数。只有 action 参数是关键。

[formulaire.aspx] 中的字段由 [formulaire.aspx.vb] 进行计算:


Imports System.Collections.Specialized
 
Public Class formulaire
    Inherits System.Web.UI.Page
 
    ' page fields
    Protected rdouichecked As String
    Protected rdnonchecked As String
    Protected txtEnfants As String
    Protected txtSalaire As String
    Protected txtImpot As String
 
    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' we retrieve the previous request in the
        Dim form As NameValueCollection = Context.Items("formulaire")
        ' prepare the page to be displayed
        ' radio buttons
        rdouichecked = ""
        rdnonchecked = "checked"
        If form("rdMarie").ToString = "oui" Then
            rdouichecked = "checked"
            rdnonchecked = ""
        End If
        ' the rest
        txtEnfants = CType(form("txtEnfants"), String)
        txtSalaire = CType(form("txtSalaire"), String)
        txtImpot = CType(Context.Items("txtImpot"), String)
    End Sub
End Class

[main.aspx] 中的字段是根据控制器放置在页面上下文中的两项信息计算得出的:

  • Context.Items("form"):一个 NameValueCollection 字典,包含 HTML 字段 [rdmarie,txtEnfants,txtSalaire] 的值
  • Context.Items("txtImpot"):税额

6.1.8. [erreurs.aspx] 视图

[erreurs.aspx] 视图用于显示应用程序运行期间可能发生的任何错误。其呈现代码如下:


<%@ page src="erreurs.aspx.vb" inherits="erreurs" AutoEventWireup="false"%>
<HTML>
    <HEAD>
        <title>Impôt</title>
    </HEAD>
    <body>
        <P>Les erreurs suivantes se sont produites :</P>
        <HR>
        <ul>
            <%=erreursHTML%>
        </ul>
        <a href="<%=href%>">
            <%=lien%>
        </a>
    </body>
</HTML>

该页面包含三个动态字段:

HTMLErrors
错误列表的 HTML 代码
href
链接的 URL
link
链接文本

这些字段由页面控制器在 [errors.aspx.vb] 中计算得出:


Imports System.Collections
Imports Microsoft.VisualBasic
 
Public Class erreurs
    Inherits System.Web.UI.Page
 
    ' page parameter
    Protected erreursHTML As String = ""
    Protected href As String
    Protected lien As String
 
    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' retrieve context elements
        Dim erreurs As ArrayList = CType(context.Items("erreurs"), ArrayList)
        href = context.Items("href").ToString
        lien = context.Items("lien").ToString
        ' we generate the HTML code from the list
        Dim i As Integer
        For i = 0 To erreurs.Count - 1
            erreursHTML += "<li> " + erreurs(i).ToString + "</li>" + ControlChars.CrLf
        Next
    End Sub
 
End Class

页面控制器检索应用程序控制器放置在页面上下文中的信息:

Context.Items("errors"
包含待显示错误消息列表的 ArrayList 对象
Context.Items("href"
链接的 URL
Context.Items("link"
链接文本

既然我们已经知道应用程序用户看到的内容,就可以继续编写应用程序的控制器了。

6.1.9. 控制器 [global.asax, main.aspx]

让我们回顾一下应用程序的 MVC 架构:

Image

应用程序逻辑客户端

控制器 [main.aspx] 必须处理三个操作:

  • init:对应客户端的首次请求。控制器显示 [formulaire.aspx] 视图
  • calcul:对应税费计算请求。若输入表单中的数据正确,则使用业务类 [taxes] 计算税费。控制器将已通过验证的 [form.aspx] 视图连同计算出的税费一并返回给客户端。若输入表单中的数据有误,控制器将返回包含错误列表及返回表单链接的 [errors.aspx] 视图。
  • return:对应于发生错误后返回表单。控制器将显示 [form.aspx] 视图,其内容与发生错误前经过验证时的状态一致。

此外,众所周知,只要存在 [global.asax] 控制器,对应用程序的每次请求都会经过它。因此,在应用程序的入口点处,存在两个控制器的链:

  • [global.asax],由于 ASP.NET 架构的缘故,它接收所有发往应用程序的请求
  • [main.aspx],根据开发者的选择,它同样接收所有发往应用程序的请求

需要 [main.aspx] 的原因在于我们需要管理会话。我们已经看到,在此情况下 [global.asax] 不适合作为控制器。这里我们完全可以不使用 [global.asax]。不过,我们将利用它来在应用程序启动时执行代码。 上方的 MVC 图示表明,我们需要创建一个 [tax] 对象来计算税额。该对象无需多次创建,创建一次即可。因此,我们将在应用程序启动时,通过 [global.asax] 控制器处理的 [Application_Start] 事件来创建它。相关代码如下:

[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 objImpot As impot
        Try
            objImpot = New impot(New impotsArray)
            ' put the object in the application
            Application("objImpot") = objImpot
            ' no error
            Application("erreur") = False
        Catch ex As Exception
            'there has been an error, we note it in the application
            Application("erreur") = True
        End Try
    End Sub
End Class

创建完成后,[import] 对象将存储在应用程序中。来自不同客户端的请求将从这里获取该对象。由于 [import] 对象的创建可能会失败,因此我们处理任何异常,并在应用程序中设置一个 [error] 键,以指示 [import] 对象的创建过程中是否发生了错误。

控制器代码 [main.aspx, main.aspx.vb] 如下所示:

[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 of all, we check whether the application has initialized correctly
        If CType(Application("erreur"), Boolean) Then
            ' redirects to error page
            Dim erreurs As New ArrayList
            erreurs.Add("Application momentanément indisponible...")
            context.Items("erreurs") = erreurs
            context.Items("lien") = ""
            context.Items("href") = ""
            Server.Transfer("erreurs.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"
                ' init application
                initAppli()
            Case "calcul"
                ' tax calculation
                calculImpot()
            Case "retour"
                ' back to form
                retourFormulaire()
            Case "effacer"
                ' init application
                initAppli()
            Case Else
                ' unknown action = init
                initAppli()
        End Select
    End Sub
 
    Private Sub initAppli()
        ' the pre-filled form is displayed
        Context.Items("formulaire") = initForm()
        Context.Items("txtImpot") = ""
        Server.Transfer("formulaire.aspx", True)
    End Sub
 
    Private Function initForm() As NameValueCollection
        ' initialize the form
        Dim form As New NameValueCollection
        form.Set("rdMarie", "non")
        form.Set("txtEnfants", "")
        form.Set("txtSalaire", "")
        Return form
    End Function

    Private Sub calculImpot()
        ' check the validity of the data entered
        Dim erreurs As ArrayList = checkData()
        ' if there are errors, we report them
        If erreurs.Count <> 0 Then
            ' save entries
            Session.Item("formulaire") = Request.Form
            ' prepare the error page
            context.Items("href") = "main.aspx?action=retour"
            context.Items("lien") = "Retour au formulaire"
            context.Items("erreurs") = erreurs
            Server.Transfer("erreurs.aspx")
        End If
        ' no errors here - the tax is calculated
        Dim impot As Long = CType(Application("objImpot"), impot).calculer( _
        Request.Form("rdMarie") = "oui", _
        CType(Request.Form("txtEnfants"), Integer), _
        CType(Request.Form("txtSalaire"), Long))
        ' the result page is displayed
        context.Items("txtImpot") = impot.ToString + " euro(s)"
        context.Items("formulaire") = Request.Form
        Server.Transfer("formulaire.aspx", True)
    End Sub
 
    Private Sub retourFormulaire()
        ' displays the form with values taken from the session
        Context.Items("formulaire") = Session.Item("formulaire")
        Context.Items("txtImpot") = ""
        Server.Transfer("formulaire.aspx", True)
    End Sub
 
    Private Function checkData() As ArrayList
        ' initially no errors
        Dim erreurs As New ArrayList
        Dim erreur As Boolean = False
        ' married radio button
        Try
            Dim rdMarie As String = Request.Form("rdMarie").ToString
            If rdMarie <> "oui" And rdMarie <> "non" Then
                Throw New Exception
            End If
        Catch
            erreurs.Add("Vous n'avez pas indiqué votre statut marital")
        End Try
        ' no. of children
        Try
            Dim txtEnfants As String = Request.Form("txtEnfants").ToString
            Dim nbEnfants As Integer = CType(txtEnfants, Integer)
            If nbEnfants < 0 Then Throw New Exception
        Catch
            erreurs.Add("Le nombre d'enfants est incorrect")
        End Try
        ' salary
        Try
            Dim txtSalaire As String = Request.Form("txtSalaire").ToString
            Dim salaire As Integer = CType(txtSalaire, Long)
            If salaire < 0 Then Throw New Exception
        Catch
            erreurs.Add("Le salaire annuel est incorrect")
        End Try
        ' return the list of errors
        Return erreurs
    End Function
End Class

控制器首先检查应用程序是否已正确初始化:


        ' first of all, we check whether the application has initialized correctly
        If CType(Application("erreur"), Boolean) Then
            ' redirects to error page
            Dim erreurs As New ArrayList
            erreurs.Add("Application momentanément indisponible...")
            context.Items("erreurs") = erreurs
            context.Items("lien") = ""
            context.Items("href") = ""
            Server.Transfer("erreurs.aspx")
        End If

如果控制器检测到应用程序初始化失败(无法创建计算所需的 [import] 对象),则会显示包含相应参数的错误页面。由于整个应用程序均不可用,因此无需在表单上放置返回链接。一条通用错误消息会被放置在类型为 [ArrayList] 的 [Context.Items("errors")] 中。

如果控制器判定应用程序可正常运行,则会通过 [action] 参数分析被要求执行的操作。我们此前已多次遇到这种运行模式。每种操作的处理都委托给一个函数。

6.1.9.1. init 和 delete 操作

这两个操作必须显示空的输入表单。请注意,该表单(参见视图部分)包含两个参数:

  • Context.Items("form"):一个 [NameValueCollection] 字典,包含 HTML 字段 [rdmarie,txtEnfants,txtSalaire] 的值
  • Context.Items("txtImpot"): 税额

[initAppli] 函数会初始化这两个参数以显示空表单。

6.1.9.2. 计算操作

此操作必须根据表单中输入的数据计算应缴税款,并返回预先填入输入值及计算出的税额的表单。负责此任务的 [calculImpot] 函数首先会验证表单数据是否正确:

  • [rdMarie] 字段必须存在,且值为 [yes] 或 [no]
  • [txtEnfants] 字段必须存在,且为 >=0 的整数
  • [txtSalaire] 字段必须存在,且为大于等于 0 的整数

如果输入的数据无效,控制器会在上下文中先设置预期值,然后显示 [erreurs.aspx] 视图:

  • 错误消息被放入一个 [ArrayList] 对象中,随后该对象被添加到上下文中 [Context.Items("errors")]
  • 返回链接的 URL 及其文本也会被放入上下文中。

在将控制权移交给 [erreurs.aspx] 页面(该页面将向客户端发送响应)之前,表单中输入的值(Request.Form)会被存入会话中,并关联 "form" 键。这将允许后续请求检索这些值。

这里有人可能会质疑,是否需要验证客户端发送的请求中是否包含字段 [rdMarie, txtEnfants, txtSalaire]。如果我们确信客户端是一个已接收包含这些字段的视图 [formulaire.aspx] 的浏览器,那么这便没有必要。但我们永远无法对此确信无疑。 稍后我们将展示一个示例,其中客户端是我们之前接触过的 [curl] 应用程序。 我们将不发送应用程序预期的字段来查询它,并观察其反应。这是一条已被多次强调且在此重申的规则:应用程序绝不能对查询它的客户端类型做出任何假设。为了安全起见,它必须假设可能会被某个编程应用程序查询,而该应用程序可能会发送意料之外的参数字符串。在任何情况下,它都必须表现得正确。

在本例中,我们已验证请求中包含 [rdMarie, txtEnfants, txtSalaire] 字段,但未检查是否可能包含其他字段。 在该应用程序中,这些字段会被忽略。然而,作为一项安全措施,将此类请求记录到日志文件中并向应用程序管理员发送警报是很有用的,以便他们知晓应用程序正在接收“异常”请求。通过分析日志文件中的这些记录,他们可以检测到针对应用程序的潜在攻击,并采取必要措施加以防护。

如果预期数据正确,控制器将使用应用程序中存储的 [tax] 对象启动税费计算。然后,它将 [form.aspx] 视图所需的两项信息存储在上下文中:

  • Context.Items("formulaire"):一个 [NameValueCollection] 字典,包含 HTML 字段 [rdmarie,txtEnfants,txtSalaire](此处指 [Request.Form])的值,即之前在表单中输入的值
  • Context.Items("txtImpot"):刚刚计算出的税额

细心的读者在阅读上述内容时可能有所疑惑:既然在应用程序启动时创建的 [impot] 对象在所有请求之间共享,是否会因访问冲突导致 [impot] 对象的数据损坏?要回答这个问题,我们需要回到 [impot] 类的代码。 请求会调用 [impot].calculateTax 方法来获取应缴税额。因此,我们需要检查的代码如下:

        Public Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Long) As Long
            ' calculating the number of shares
            Dim nbParts As Decimal
            If marié Then
                nbParts = CDec(nbEnfants) / 2 + 2
            Else
                nbParts = CDec(nbEnfants) / 2 + 1
            End If
            If nbEnfants >= 3 Then
                nbParts += 0.5D
            End If
            ' calculation of taxable income & family quota
            Dim revenu As Decimal = 0.72D * salaire
            Dim QF As Decimal = revenu / nbParts
            ' tAX CALCULATION
            limites((limites.Length - 1)) = QF + 1
            Dim i As Integer = 0
            While QF > limites(i)
                i += 1
            End While
            Dim impot As Long = CLng(revenu * coeffR(i) - nbParts * coeffN(i))
            Return impot
        End Function

假设一个线程正在执行上述方法时被中断,随后另一个线程接手执行该方法。这会带来哪些风险?为了解答这个问题,我们添加了以下代码:


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

线程 1 在计算完局部变量 [impot] 的值 [impot1] 后被中断。随后线程 2 开始执行,并在被中断前为同一个变量 [impot] 计算出新值 [impot2]。线程 1 重新获得控制权。 它在局部变量 [impot] 中发现了什么?由于该变量是方法的局部变量,因此它存储在一个称为栈的内存结构中。该栈是线程上下文的一部分,在线程暂停时会被保存。当线程 2 启动时,其上下文会配置一个新的栈,因此也会有一个新的局部变量 [impot]。 当线程 2 随后被挂起时,其上下文也会被保存。当线程 1 重启时,其上下文(包括栈)被恢复。此时它检索的是自己的局部变量 [impot],而非线程 2 的。因此,我们处于请求之间不存在访问冲突的状态。如上所述,通过 10 秒暂停进行的测试证实,并发请求确实产生了预期的结果。

6.1.9.3. 返回操作

此操作对应于点击 [errors.aspx] 视图上的 [返回表单] 链接,以返回 [form.aspx] 视图,该视图已预先填充了先前输入并保存在会话中的值。[returnForm] 函数负责检索这些信息。为 [form.aspx] 视图预期的两个参数被初始化:

  • Context.Items("form") 设置为先前输入并保存在会话中的值
  • Context.Items("txtImpot") 设置为空字符串

6.1.10. 测试 Web 应用程序

将上述所有文件放置在名为 <application-path> 的文件夹中。

Image

在此文件夹中创建一个名为 [bin] 的子文件夹,并将 [impot.dll] 程序集(由业务类文件 [impots.vb、impotsData.vb、impotsArray.vb] 编译生成)放置其中。所需的编译命令如下:

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

上述 [impot.dll] 文件必须放置在 <application-path>\bin 目录下,以便 Web 应用程序能够访问它。Cassini 服务器通过参数 (<application-path>,/impots1) 启动。使用浏览器,我们请求 URL [http://localhost/impots1/main.aspx]:

Image

填写表单:

Image

然后通过 [Calculate] 按钮开始计算税款。我们得到以下响应:

Image

接着输入错误数据:

Image

点击 [Calculate] 按钮将得到以下响应:

Image

点击 [返回表单] 链接将带我们回到表单提交时的状态:

Image

最后,点击 [清除] 按钮将重置页面:

Image

6.1.11. 使用 [curl] 客户端

使用浏览器以外的客户端测试 Web 应用程序非常重要。如果向浏览器发送一个表单,并在提交时包含待提交的参数,浏览器会将这些参数的值发回给服务器。而其他客户端可能不会这样做,此时服务器收到的请求中可能会缺少参数。服务器必须知道在这种情况下该如何处理。 另一个例子是客户端数据验证。如果表单包含待验证的数据,可以通过表单所在文档中包含的脚本在客户端进行验证。只有当所有经过客户端验证的数据均有效时,浏览器才会提交表单。 因此,在服务器端,人们可能会误以为将收到经过验证的数据,从而不想再次进行验证。这将是一个错误。事实上,除浏览器以外的其他客户端可能会向服务器发送无效数据,从而导致 Web 应用程序出现意外行为。我们将使用 [curl] 客户端来说明这些要点。

首先,我们请求 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>Impôt</title>
    </head>
    <body>
        <P>Calcul de votre impôt</P>
        <HR width="100%" SIZE="1">
        <form method="post" action="main.aspx?action=calcul">
            <TABLE border="0">
                <TR>
                    <TD>Etes-vous marié(e)</TD>
                    <TD>
                        <INPUT type="radio" value="oui" name="rdMarie" >Oui <INPUT type="radio"  value="non" name="rdMarie" checked>Non</TD>
                </TR>
                <TR>
                    <TD>Nombre d'enfants</TD>
                    <TD><INPUT type="text" size="3" maxLength="3" name="txtEnfants" value=""></TD>
                </TR>
                <TR>
                    <TD>Salaire annuel (euro)</TD>
                    <TD><INPUT type="text" maxLength="12" size="12" name="txtSalaire" value=""></TD>
                </TR>
                <TR>
                    <TD>Impôt à payer :
                    </TD>
                    <TD></TD>
                </TR>
            </TABLE>
            <hr>
            <P>
                <INPUT type="submit" value="Calculer">
            </P>
        </form>
        <form method="post" action="main.aspx?action=effacer">
                <INPUT type="submit" value="Effacer">
        </form>
    </body>
</html>

服务器向我们发送了表单的 HTML 代码。在 HTTP 头部中,我们看到了会话 Cookie。我们将在后续请求中使用它来维持会话。现在,让我们不带任何参数地请求 [calcul] 操作:

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>Impôt</title>
    </HEAD>
    <body>
        <P>Les erreurs suivantes se sont produites :</P>
        <HR>
        <ul>
            <li> Vous n'avez pas indiqué votre statut marital</li>
<li> Le nombre d'enfants est incorrect</li>
<li> Le salaire annuel est incorrect</li>
        </ul>
        <a href="main.aspx?action=retour">
            Retour au formulaire
        </a>
    </body>
</HTML>

我们可以看到,Web 应用程序返回了 [errors] 视图,其中包含针对三个缺失参数的三条错误消息。现在,让我们发送一些错误的参数:

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>Impôt</title>
    </HEAD>
    <body>
        <P>Les erreurs suivantes se sont produites :</P>
        <HR>
        <ul>
            <li> Vous n'avez pas indiqué votre statut marital</li>
<li> Le nombre d'enfants est incorrect</li>
<li> Le salaire annuel est incorrect</li>
        </ul>
        <a href="main.aspx?action=retour">
            Retour au formulaire
        </a>
    </body>
</HTML>

这三个错误已被正确检测到。现在让我们发送有效的参数:

dos>curl --cookie ASP.NET_SessionId=ivthkl45tjdjrzznevqsf255 --include --data rdMarie=oui --data txtEnfants=2 --data txtSalaire=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>Impôt</title>
    </head>
    <body>
        <P>Calcul de votre impôt</P>
        <HR width="100%" SIZE="1">
        <form method="post" action="main.aspx?action=calcul">
            <TABLE border="0">
                <TR>
                    <TD>Etes-vous marié(e)</TD>
                    <TD>
                        <INPUT type="radio" value="oui" name="rdMarie" checked>Oui <INPUT type="radio"  value="non" name="rdMarie" >Non</TD>
                </TR>
                <TR>
                    <TD>Nombre d'enfants</TD>
                    <TD><INPUT type="text" size="3" maxLength="3" name="txtEnfants" value="2"></TD>
                </TR>
                <TR>
                    <TD>Salaire annuel (euro)</TD>
                    <TD><INPUT type="text" maxLength="12" size="12" name="txtSalaire" value="60000"></TD>
                </TR>
                <TR>
                    <TD>Impôt à payer :
                    </TD>
                    <TD>4300 euro(s)</TD>
                </TR>
            </TABLE>
            <hr>
            <P>
                <INPUT type="submit" value="Calculer">
            </P>
        </form>
        <form method="post" action="main.aspx?action=effacer">
                <INPUT type="submit" value="Effacer">
        </form>
    </body>
</html>

我们已成功获取应缴税款:4,300 欧元。本例的启示是:我们绝不能因正在编写面向浏览器客户端的 Web 应用程序而产生误解。Web 应用程序本质上是一种 TCP/IP 服务,而该网络协议不会透露服务客户端应用程序的性质。因此,我们无法确定 Web 应用程序的客户端是否为浏览器。因此,我们遵循两条规则:

  • 收到客户端请求时,我们不对客户端做任何假设,并验证请求中是否包含预期参数且这些参数有效
  • 构建面向浏览器的响应,该响应通常由 HTML 文档组成

Web 应用程序可以设计为同时服务于不同的客户端,例如浏览器和手机。此时,我们可以在每个请求中添加一个新参数来标识客户端类型。 因此,浏览器将通过向 URL http://machine/impots/main.aspx?client=navigateur&action=calcul 发送请求来计算税费,而手机则会向 URL http://machine/impots/main.aspx?client=mobile&action=calcul 发送请求。MVC 架构有助于此类应用程序的开发,其结构如下:

Image

[业务类、数据访问类] 模块保持不变,因为它是与客户端无关的组件。[控制器] 模块仅需稍作调整,但必须处理请求中的新参数:[client] 参数,该参数用于标识当前处理的客户端类型。 [视图] 部分必须为每种客户端类型生成相应的视图。即使短期或中期目标仅限于浏览器,在应用程序的设计阶段就考虑查询中 [client] 参数的存在也是值得的。如果应用程序日后需要支持新的客户端类型,只需编写针对该客户端定制的视图即可。

6.2. 示例 2

6.2.1. 问题

在此,我们建议通过修改 Web 应用程序创建的 [tax] 对象的数据源来解决与之前相同的问题。在之前的版本中,数据源提供的值来自硬编码在代码中的数组。这次,新的数据源将从与 MySQL 数据库关联的 ODBC 数据源中检索这些值。

6.2.2. ODBC 数据源

数据将存储在名为 [dbimpots] 的 MySQL 数据库中,位于名为 [IMPOTS] 的表中。该表的内容如下:

Image

数据库所有者为用户 [admimpots],密码为 [mdpimpots]。我们将一个 ODBC 数据源与该数据库关联。在进行此操作之前,让我们先回顾一下使用 .NET 平台访问数据库的各种方法。

Windows 平台上有许多可用的数据库。为了访问它们,应用程序会使用称为驱动程序的程序。

Image

在上图中,驱动程序具有两个接口:

  • 面向应用程序的 I1 接口
  • 面向数据库的 I2 接口

为避免将针对数据库 B1 编写的应用程序迁移至其他数据库 B2 时需要重写,已对接口 I1 进行了标准化工作。如果使用“标准化”驱动程序的数据库,数据库 B1 将配备驱动程序 P1,数据库 B2 将配备驱动程序 P2,且这两个驱动程序的 I1 接口将完全一致。因此,应用程序无需重写。 例如,您可以将 Access 数据库迁移到 MySQL 数据库,而无需更改应用程序。

标准化驱动程序有两种类型:

  • ODBC(开放数据库连接)驱动程序
  • OLE DB(对象链接与嵌入数据库)驱动程序

ODBC 驱动程序提供对数据库的访问。OLE DB 驱动程序的数据源更为多样:数据库、电子邮件系统、目录等。没有限制。只要供应商决定这样做,任何数据源都可以成为 OLE DB 驱动程序的对象。其优势显而易见:您可以统一访问各种各样的数据。

.NET 1.1 平台提供了三种数据访问类:

  1. SQL Server.NET 类,用于访问 Microsoft SQL Server 数据库
  2. Ole Db.NET 类,用于访问提供 OLE DB 驱动程序的数据库管理系统 (DBMS) 中的数据库
  3. ODBC.NET 类,用于访问提供 ODBC 驱动程序的数据库管理系统(DBMS)

MySQL 数据库管理系统(DBMS)早已提供了 ODBC 驱动程序。这就是我们现在要使用的驱动程序。在 Windows 中,我们选择 [开始菜单/控制面板/管理工具/32 位 ODBC 数据源]。根据 Windows 版本的不同,此路径可能会略有差异。这将打开以下应用程序,允许我们创建 ODBC 数据源:

Image

我们将创建一个系统数据源,即该计算机上的任何用户均可使用的数据源。因此,在上方我们选择 [系统数据源] 选项卡。显示的页面上有一个 [添加] 按钮,我们通过它来创建新的 ODBC 数据源:

Image

向导会要求您选择要使用的 ODBC 驱动程序。Windows 系统自带了若干预装的 ODBC 驱动程序,但 MySQL ODBC 驱动程序并不包含在其中。因此,您必须先安装该驱动程序。 您可以在搜索引擎中输入“MySQL ODBC”或“MyODBC”进行在线搜索。在此,我们已安装了 [MySQL ODBC 3.51] 驱动程序。我们选择它并点击 [完成]:

Image

您需要提供以下信息:

数据源名称
用于标识 ODBC 数据源的名称。任何 Windows 应用程序均可通过此名称访问该数据源
描述
描述该数据源的任意文本
主机名
托管 MySQL 数据库管理系统 (DBMS) 的机器名称。此处指本地机器,但也可能是远程机器。这使得 Windows 应用程序无需任何特殊编码即可访问远程数据库。这是 ODBC 数据源的一大优势。
数据库名称
MySQL 数据库管理系统可管理多个数据库。此处需指定要管理的数据库:dbimpots
用户
在 MySQL 数据库管理系统中注册的用户名。将以该用户名访问数据源。此处为:admimpots
密码
该用户的密码。此处:mdpimpots
端口
MySQL 数据库管理系统的工作端口。默认端口为 3306。我们未对其进行更改

完成上述设置后,我们通过 [测试数据源] 按钮验证连接设置的有效性:

Image

完成上述操作后,我们对该 ODBC 数据源已充满信心。现在可以使用它了。我们根据需要多次单击 [确定] 以退出 ODBC 向导。

如果读者尚未安装 MySQL 数据库管理系统,可前往 [http://www.mysql.com] 免费下载。下面,我们将概述使用 Access 创建 ODBC 数据源的步骤。前几步与之前描述的完全相同。我们添加一个新的系统数据源:

Image

所选驱动程序为 [Microsoft Access Driver]。点击 [完成] 进入 ODBC 数据源定义界面:

Image

需填写的信息如下:

数据源名称
用于标识该 ODBC 数据源的名称。任何 Windows 应用程序均可通过此名称访问该数据源
描述
描述数据源的任意文本
数据库
要使用的 Access 文件的完整名称

6.2.3. 一个新的数据访问类

让我们重新审视应用程序的 MVC 结构:

Image

在上图中,[impotsData] 类负责检索数据。在此情况下,它将从 MySQL 数据库 [dbimpots] 中检索数据。正如我们在该应用程序的上一版本中所学到的,[impotsData] 是一个抽象类,每当我们想要将其适配到新的数据源时,都必须继承该类。让我们回顾一下这个抽象类的结构:


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 valide As Boolean
 
        ' data access method
        Public MustOverride Function getData() As Object()
 
        ' data verification method
        Protected Function checkData() As Integer
            ' verifies acquired data
...
        End Function
 
        ' checks the validity of an array's contents
        Protected Function check(ByRef tableau() As Decimal, ByVal n As Integer) As Boolean
        ...
        End Function
    End Class
End Namespace

从 [impotsData] 派生的类必须实现两个方法:

  • 如果 [impotsData] 的默认构造函数不合适,则需提供构造函数
  • [getData] 方法,该方法返回三个数组(limites、coeffr、coeffn)

我们创建 [impotsODBC] 类,该类将从我们指定的 ODBC 数据源中检索数据 (limits, coeffr, coeffn):


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
 
        ' manufacturer
        Public Sub New(ByVal DSNimpots As String)
            ' we note the three pieces of information
            Me.DSNimpots = DSNimpots
        End Sub
 
        Public Overrides Function getdata() As Object()
            ' initializes the three limit tables, coeffr, coeffn from
            ' the contents of the [impots] table in the ODBC DSNimpots database
            ' limites, coeffr, coeffn are the three columns of this table
            ' can launch various exceptions
 
            Dim connectString As String = "DSN=" + DSNimpots + ";"         ' base connection chain
            Dim impotsConn As OdbcConnection = Nothing         ' the connection
            Dim sqlCommand As OdbcCommand = Nothing         ' the SQL command
            ' the SELECT query
            Dim selectCommand As String = "select limites,coeffr,coeffn from impots"
            ' tables to retrieve data
            Dim aLimites 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()
                ' Using the recovered table
                While myReader.Read()
                    ' the data of the current line are put in the tables
                    aLimites.Add(myReader("limites"))
                    aCoeffR.Add(myReader("coeffr"))
                    aCoeffN.Add(myReader("coeffn"))
                End While
                ' freeing up resources
                myReader.Close()
                impotsConn.Close()
            Catch e As Exception
                Throw New Exception("Erreur d'accès à la base de données (" + e.Message + ")")
            End Try
            ' dynamic tables are placed in static tables
            Me.limites = New Decimal(aLimites.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
                limites(i) = Decimal.Parse(aLimites(i).ToString())
                coeffR(i) = Decimal.Parse(aCoeffR(i).ToString())
                coeffN(i) = Decimal.Parse(aCoeffN(i).ToString())
            Next i
            ' verify acquired data
            Dim erreur As Integer = checkData()
            ' if invalid data, throws an exception
            If Not valide Then Throw New Exception("Les données des tranches d'impôts sont invalides (" + erreur.ToString + ")")
            ' otherwise we return the three tables
            Return New Object() {limites, coeffr, coeffn}
        End Function
    End Class
End Namespace

让我们来看看构造函数:


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

它将包含待检索数据的 ODBC 数据源名称作为参数。构造函数仅将此名称存储起来。[getData] 方法负责从 [impots] 表中读取数据,并将其放入三个数组(limites、coeffr、coeffn)中。让我们对代码进行说明:

  • 连接 ODBC 数据源的参数已定义,但连接尚未建立
            ' base connection chain
            Dim connectString As String = "DSN=" + DSNimpots + ";"
            ' a database connection object is created - this connection is not open
            Dim impotsConn As OdbcConnection = New OdbcConnection(connectString)
  • 定义三个 [ArrayList] 对象以从 [impots] 表中检索数据:

            ' tables to retrieve data
            Dim aLimites As New ArrayList
            Dim aCoeffR As New ArrayList
            Dim aCoeffN As New ArrayList
  • 所有数据库访问代码均包含在 try/catch 代码块中,以处理任何访问错误。我们打开与数据库的连接:

                ' on tente d'accéder à la base de données
                impotsConn = New OdbcConnection(connectString)
                impotsConn.Open()
  • 我们在已打开的连接上执行 [select] 命令。我们获取一个 [OdbcDataReader] 对象,该对象将允许我们遍历 select 语句返回的表中的行:

                ' on crée un objet command
                Dim sqlCommand As OdbcCommand = New OdbcCommand(selectCommand, impotsConn)
                ' on exécute la requête
                Dim myReader As OdbcDataReader = sqlCommand.ExecuteReader()
  • 我们逐行遍历结果表。为此,我们使用之前获取的 [OdbcDataReader] 对象的 [Read] 方法。该方法有两个作用:
    • 在表中向前移动一行。初始时,光标位于第一行之前
    • 如果能够向前移动一行,则返回布尔值 [true];否则返回 [false],后者表示所有行均已处理完毕。

通过 OdbcDataReader 可以获取 [OdbcDataReader] 对象当前行的列。这将返回一个表示该列值的对象。我们遍历整个表,将其内容填充到三个 [ArrayList] 对象中:


                ' Exploitation de la table récupérée
                While myReader.Read()
                    ' les données de la ligne courante sont mis dans les tableaux
                    aLimites.Add(myReader("limites"))
                    aCoeffR.Add(myReader("coeffr"))
                    aCoeffN.Add(myReader("coeffn"))
  • 完成上述操作后,我们释放与连接相关的资源:
                ' libération des ressources
                myReader.Close()
                impotsConn.Close()
  • 三个 [ArrayList] 对象的内容被转移到三个标准数组中:

            ' dynamic tables are placed in static tables
            limites = New Decimal(aLimites.Count - 1) {}
            coeffr = New Decimal(aLimites.Count - 1) {}
            coeffn = New Decimal(aLimites.Count - 1) {}
            Dim i As Integer
            For i = 0 To aLimites.Count - 1
                limites(i) = CType(aLimites(i), Decimal)
                coeffR(i) = CType(aCoeffR(i), Decimal)
                coeffN(i) = CType(aCoeffN(i), Decimal)
            Next i
  • 一旦 [impots] 表中的数据已加载到这三个数组中,剩下的就是使用基类 [impotsData] 的 [checkData] 方法来验证其内容:
            ' verify acquired data
            Dim erreur As Integer = checkData()
            ' if invalid data, throws an exception
            If Not valide Then Throw New Exception("Les données des tranches d'impôts sont invalides (" + erreur.ToString + ")")
            ' otherwise we return the three tables
            Return New Object() {limites, coeffr, coeffn}

6.2.4. 数据访问类的测试

测试程序可能如下所示:

Option Explicit On 
Option Strict On

' namespaces
Imports System
Imports Microsoft.VisualBasic

Namespace st.istia.univangers.fr

    ' test pg
    Module testimpots
        Sub Main(ByVal arguments() As String)
            ' interactive tax calculator
            ' the user enters three data points on the keyboard: married nbEnfants salary
            ' the program then displays the tax payable
            Const syntaxe1 As String = "pg DSNimpots"
            Const syntaxe2 As String = "syntaxe : marié nbEnfants salaire" + ControlChars.Lf + "marié : o pour marié, n pour non marié" + ControlChars.Lf + "nbEnfants : nombre d'enfants" + ControlChars.Lf + "salaire : salaire annuel en F"

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

            ' tax object creation
            Dim objImpot As impot = Nothing
            Try
                objImpot = New impot(New impotsODBC(DSNimpots))
            Catch ex As Exception
                Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
                Environment.Exit(2)
            End Try

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

                ' tax calculation parameters are requested
                Console.Out.Write("Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :")
                Dim paramètres As String = Console.In.ReadLine().Trim()

                ' anything to do?
                If paramètres Is Nothing Or paramètres = "" Then
                    Exit While
                End If

                ' check the number of arguments in the input line
                Dim args As String() = paramètres.Split(Nothing)
                Dim nbParamètres As Integer = args.Length
                If nbParamètres <> 3 Then
                    Console.Error.WriteLine(syntaxe2)
                    erreur = True
                End If
                Dim marié As String
                Dim nbEnfants As Integer
                Dim salaire As Integer
                If Not erreur Then
                    ' checking the validity of parameters
                    ' married
                    marié = args(0).ToLower()
                    If marié <> "o" And marié <> "n" Then
                        Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument marié incorrect : tapez o ou n"))
                        erreur = True
                    End If
                    ' nbEnfants
                    nbEnfants = 0
                    Try
                        nbEnfants = Integer.Parse(args(1))
                        If nbEnfants < 0 Then
                            Throw New Exception
                        End If
                    Catch
                        Console.Error.WriteLine(syntaxe2 + "\nArgument nbEnfants incorrect : tapez un entier positif ou nul")
                        erreur = True
                    End Try
                    ' salary
                    salaire = 0
                    Try
                        salaire = Integer.Parse(args(2))
                        If salaire < 0 Then
                            Throw New Exception
                        End If
                    Catch
                        Console.Error.WriteLine(syntaxe2 + "\nArgument salaire incorrect : tapez un entier positif ou nul")
                        erreur = True
                    End Try
                End If
                If Not erreur Then
                    ' parameters are correct - tax is calculated
                    Console.Out.WriteLine(("impôt=" & objImpot.calculer(marié = "o", nbEnfants, salaire).ToString + " euro(s)"))
                End If
            End While
        End Sub
    End Module
End Namespace

应用程序带有一个参数启动:

  • DSNimpots:要使用的 ODBC 数据源名称

税费计算使用在应用程序启动时创建的 [tax] 类型对象进行:


            ' tax object creation
            Dim objImpôt As impot = Nothing
            Try
                objImpot = New impot(New impotsODBC(DSNimpots))
            Catch ex As Exception
                Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
                Environment.Exit(1)
            End Try

初始化完成后,应用程序会反复提示用户输入计算税款所需的三个信息:

  • 婚姻状况:o 表示已婚,n 表示未婚
  • 子女数量
  • 年薪

所有类均已编译:

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

随后编译测试程序:

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

首先使用 MySQL ODBC 数据源运行测试程序:

dos>testimpots odbc-mysql-dbimpots
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 60000
impôt=4300 euro(s)

我们将 ODBC 数据源切换为 Access 数据源:

dos>testimpots odbc-access-dbimpots
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 60000
impôt=4300 F

6.2.5. Web应用程序视图

与上一应用程序相同:formulaire.aspx 和 erreurs.aspx

6.2.6. 应用程序控制器 [global.asax, main.aspx]

仅需修改 [global.asax] 控制器。它负责在应用程序启动时创建 [impot] 对象。该对象的构造函数有一个参数:[impotsData] 对象,负责检索数据。因此,该参数会随着每种新的数据源类型而变化。 [global.asax.vb] 控制器修改如下:


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")))
            ' put the object in the application
            Application("objImpot") = objImpot
            ' no error
            Application("erreur") = False
        Catch ex As Exception
            'there has been an error, we note it in the application
            Application("erreur") = True
            Application("message") = ex.Message
        End Try
    End Sub
End Class

[impot] 对象的数据源现在是一个 [impotODBC] 对象。该对象将要使用的 ODBC 数据源的 DSN 名称作为参数。我们不将此名称硬编码在代码中,而是将其放在应用程序的 [web.config] 配置文件中:


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

我们知道,[web.config] 文件中 <appSettings> 部分的键 C 的值,可在应用程序代码中通过 [ConfigurationSettings.AppSettings(C)] 进行获取。

为确定异常原因,我们将在应用程序中记录异常消息,以便后续查询。 [main.aspx.vb] 控件将把此消息包含在其错误列表中:


    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' first of all, we check whether the application has initialized correctly
        If CType(Application("erreur"), Boolean) Then
            ' redirects to error page
            Dim erreurs As New ArrayList
            erreurs.Add("Application momentanément indisponible...(" + Application("message").ToString + ")")
            context.Items("erreurs") = erreurs
            context.Items("lien") = ""
            context.Items("href") = ""
            Server.Transfer("erreurs.aspx")
        End If
        ' retrieve the action to be performed
...

6.2.7. 更改摘要

该应用程序已准备就绪,可以进行测试了。下面列出与上一版本相比所做的更改:

  1. 创建了一个新的数据访问类
  2. 控制器 [global.asax.vb] 进行了两处修改:[Import] 对象的构造以及应用程序中任何异常相关消息的日志记录
  3. [main.aspx.vb] 控制器在一处进行了修改,用于显示上述异常消息
  4. 已添加 [web.config] 文件

修改工作主要在 1 中完成,即在 Web 应用程序外部。这得益于应用程序的 MVC 架构,该架构将控制器与业务类分离。这正是该架构的核心要义。可以证明,通过配置适当的 [web.config] 文件,本可以完全避免对应用程序控制器进行任何修改。 可以在 [web.config] 中指定要动态实例化的数据访问类的名称(例如),以及实例化所需的各种参数。有了这些信息,[global.asax] 就可以实例化数据访问对象。因此,更改数据源只需:

  • 如果该数据源对应的数据访问类尚不存在,则创建该类
  • 修改 [web.config] 文件,以便在 [global.asax] 中动态创建该类的实例

6.2.8. 测试 Web 应用程序

上述所有文件均放置在名为 <application-path> 的文件夹中。

Image

在此文件夹中创建一个名为 [bin] 的子文件夹,并将由业务类文件 [impots.vb、impotsData.vb、impotsArray.vb、impotsODBC.vb] 编译生成的 [impot.dll] 程序集放置其中。所需的编译命令如下:

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

上述 [impot.dll] 文件必须放置在 <application-path>\bin 目录下,以便 Web 应用程序能够访问它。 Cassini 服务器通过参数 (<application-path>,/impots2) 启动。由于数据库的存在对用户是透明的,因此测试结果与上一版本相同。不过,为了验证数据库是否存在,我们通过停止 MySQL 数据库管理系统来确保 ODBC 数据源不可用,并请求 URL [http://localhost/impots2/main.aspx]。我们收到以下响应:

Image

6.3. 示例 3

6.3.1. 问题

在此,我们建议通过再次修改 Web 应用程序创建的 [impot] 对象的数据源来解决同一问题。这次,新的数据源将是一个通过 OLEDB 驱动程序访问的 ACCESS 数据库。我们的目标是演示访问数据库的另一种方式。

6.3.2. OLEDB 数据源

数据将位于 ACCESS 数据库中名为 [IMPOTS] 的表中。该表的内容如下:

Image

6.3.3. 数据访问类

让我们重新审视应用程序的 MVC 结构:

Image

  • 在上图中,[impotsData] 类负责检索数据。这次,它需要从 OLEDB 数据源中获取数据。

我们创建 [impotsOLEDB] 类,该类将从一个名为 [impotsOLEDB] 的 ODBC 数据源中检索数据(limits、coeffr、coeffn):


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 chaineConnexion As String
 
        ' manufacturer
        Public Sub New(ByVal chaineConnexion As String)
            ' we note the three pieces of information
            Me.chaineConnexion = chaineConnexion
        End Sub
 
        Public Overrides Function getData() As Object()
            ' initializes the three limit tables, coeffr, coeffn from
            ' the contents of the [impots] table in the OLEDB [chaineConnexion] database
            ' limits, coeffr, coeffn are the three columns of this table
            ' can launch various exceptions
 
            ' create a DataAdapter object to read data from source OLEDB
            Dim adaptateur As New OleDbDataAdapter("select limites,coeffr,coeffn from impots", chaineConnexion)
            ' create a memory image of the select result
            Dim contenu As New DataTable("impots")
            Try
                adaptateur.Fill(contenu)
            Catch e As Exception
                Throw New Exception("Erreur d'accès à la base de données (" + e.Message + ")")
            End Try
            ' retrieve the contents of the impots table
            Dim lignesImpots As DataRowCollection = contenu.Rows
            ' dimensioning of reception panels
            Me.limites = New Decimal(lignesImpots.Count - 1) {}
            Me.coeffr = New Decimal(lignesImpots.Count - 1) {}
            Me.coeffn = New Decimal(lignesImpots.Count - 1) {}
            ' transfer the contents of the impots table to the tables
            Dim i As Integer
            Dim ligne As DataRow
            Try
                For i = 0 To lignesImpots.Count - 1
                    ' table line i
                    ligne = lignesImpots.Item(i)
                    ' retrieve the contents of the line
                    limites(i) = CType(ligne.Item(0), Decimal)
                    coeffr(i) = CType(ligne.Item(1), Decimal)
                    coeffn(i) = CType(ligne.Item(2), Decimal)
                Next
            Catch
                Throw New Exception("Les données des tranches d'impôts n'ont pas le bon type")
            End Try
            ' verify acquired data
            Dim erreur As Integer = checkData()
            ' if invalid data, throws an exception
            If Not valide Then Throw New Exception("Les données des tranches d'impôts sont invalides (" + erreur.ToString + ")")
            ' otherwise we return the three tables
            Return New Object() {limites, coeffr, coeffn}
        End Function
    End Class
End Namespace

让我们来看看构造函数:


        ' manufacturer
        Public Sub New(ByVal chaineConnexion As String)
            ' we note the three pieces of information
            Me.chaineConnexion = chaineConnexion
        End Sub

它接收一个参数,即包含待检索数据的 OLEDB 数据源的连接字符串。构造函数仅将其存储起来。连接字符串包含 OLEDB 驱动程序连接到 OLEDB 数据源所需的所有参数,通常相当复杂。要查找 ACCESS 数据库的连接字符串,可以使用 [WebMatrix] 工具。启动该工具,它会提供一个用于连接数据源的窗口:

使用上图箭头所指的图标,您可以建立与两种 Microsoft 数据库的连接:SQL Server 和 ACCESS。我们选择 ACCESS:

Image

我们使用 [...] 按钮选择了 ACCESS 数据库。确认向导。在 [数据] 选项卡中,图标代表该连接:

Image

现在,让我们通过 [文件/新建文件] 创建一个新的 .aspx 文件:

Image

系统将显示一个空白页面,我们可以在此设计 Web 界面:

Image

将 [Data] 选项卡中的 [impots] 表拖放到上方的工作表中。结果如下:

Image

右键单击下方的 [AccessDataSourceControl] 对象以访问其属性:

Image

连接到 ACCESS 数据库的 OLEDB 连接字符串由上方的 [ConnectionString] 属性提供:

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

我们可以看到,该字符串由固定部分和变量部分组成,变量部分即为 ACCESS 文件的名称。我们将利用这一特性来生成连接到 OLEDB 数据源的连接字符串。

现在让我们回到 [impotsOLEDB] 类。[getData] 方法负责从 [impots] 表中读取数据,并将其放入三个数组(limites、coeffr、coeffn)中。让我们对它的代码进行说明:

  • 我们定义了 [DataAdapter] 对象,它将允许我们将 SQL SELECT 查询的结果传输到内存中。为此,我们定义了要执行的 [select] 查询,并将其与 [DataAdapter] 对象关联起来。[DataAdapter] 的构造函数还要求提供用于连接 OLEDB 源的连接字符串

            ' on crée un objet DataAdapter pour lire les données de la source OLEDB
            Dim adaptateur As New OleDbDataAdapter("select limites,coeffr,coeffn from impots", chaineConnexion)
  • 我们使用 [DataAdapter] 对象的 [Fill] 方法执行 [select] 命令。该 [select] 语句的结果会被加载到为此目的创建的 [DataTable] 对象中。一个 [DataTable] 对象是数据库表在内存中的表示形式,即一组行和列。我们处理可能发生的异常,例如连接字符串不正确时。

            ' on crée une image en mémoire du résultat du select
            Dim contenu As New DataTable("impots")
            Try
                adaptateur.Fill(contenu)
            Catch e As Exception
                Throw New Exception("Erreur d'accès à la base de données (" + e.Message + ")")
            End Try
  • 在 [content] 中,我们拥有由 [select] 语句返回的 [taxes] 表。一个 [DataTable] 对象即是一张表,也就是一组行。可以通过 [DataTable] 的 [rows] 属性访问这些行:

            ' retrieve the contents of the impots table
            Dim lignesImpots As DataRowCollection = contenu.Rows
  • [taxRows] 集合的每个元素都是一个 [DataRow] 对象,代表表中的一行。该行的列可通过 [DataRow] 对象的 [Item] 属性访问。 [DataRow].[Item(i)] 表示 [DataRow] 的第 i 列。通过遍历行集合(即 taxRows 的 DataRows 集合)以及每行的列集合,我们可以获取整个表格:
            ' dimensioning of reception panels
            Me.limites = New Decimal(lignesImpots.Count - 1) {}
            Me.coeffr = New Decimal(lignesImpots.Count - 1) {}
            Me.coeffn = New Decimal(lignesImpots.Count - 1) {}
            ' transfer the contents of the impots table to the tables
            Dim i As Integer
            Dim ligne As DataRow
            Try
                For i = 0 To lignesImpots.Count - 1
                    ' table line i
                    ligne = lignesImpots.Item(i)
                    ' retrieve the contents of the line
                    limites(i) = CType(ligne.Item(0), Decimal)
                    coeffr(i) = CType(ligne.Item(1), Decimal)
                    coeffn(i) = CType(ligne.Item(2), Decimal)
                Next
            Catch
                Throw New Exception("Les données des tranches d'impôts n'ont pas le bon type")
            End Try
  • 将 [taxes] 表中的数据加载到三个数组后,剩下的就是使用基类 [taxData] 的 [checkData] 方法验证其内容:
            ' verify acquired data
            Dim erreur As Integer = checkData()
            ' if invalid data, throws an exception
            If Not valide Then Throw New Exception("Les données des tranches d'impôts sont invalides (" + erreur.ToString + ")")
            ' otherwise we return the three tables
            Return New Object() {limites, coeffr, coeffn}

6.3.4. 数据访问类的测试

测试程序可能如下所示:

Option Explicit On 
Option Strict On

' namespaces
Imports System
Imports Microsoft.VisualBasic

Namespace st.istia.univangers.fr

    ' test pg
    Module testimpots
        Sub Main(ByVal arguments() As String)
            ' interactive tax calculator
            ' the user enters three data points on the keyboard: married nbEnfants salary
            ' the program then displays the tax payable
            Const syntaxe1 As String = "pg bdACCESS"
            Const syntaxe2 As String = "syntaxe : marié nbEnfants salaire" + ControlChars.Lf + "marié : o pour marié, n pour non marié" + ControlChars.Lf + "nbEnfants : nombre d'enfants" + ControlChars.Lf + "salaire : salaire annuel en F"

            ' checking program parameters
            If arguments.Length <> 1 Then
                ' error msg
                Console.Error.WriteLine(syntaxe1)
                ' end
                Environment.Exit(1)
            End If
            ' retrieve the arguments
            Dim chemin As String = arguments(0)
            ' prepare the connection chain
            Dim chaineConnexion As String = "Provider=Microsoft.Jet.OLEDB.4.0; Ole DB Services=-4; Data Source=" + chemin

            ' tax object creation
            Dim objImpot As impot = Nothing
            Try
                objImpot = New impot(New impotsOLEDB(chaineConnexion))
            Catch ex As Exception
                Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
                Environment.Exit(2)
            End Try

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

                ' tax calculation parameters are requested
                Console.Out.Write("Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :")
                Dim paramètres As String = Console.In.ReadLine().Trim()

                ' anything to do?
                If paramètres Is Nothing Or paramètres = "" Then
                    Exit While
                End If

                ' check the number of arguments in the input line
                Dim args As String() = paramètres.Split(Nothing)
                Dim nbParamètres As Integer = args.Length
                If nbParamètres <> 3 Then
                    Console.Error.WriteLine(syntaxe2)
                    erreur = True
                End If
                Dim marié As String
                Dim nbEnfants As Integer
                Dim salaire As Integer
                If Not erreur Then
                    ' checking the validity of parameters
                    ' married
                    marié = args(0).ToLower()
                    If marié <> "o" And marié <> "n" Then
                        Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument marié incorrect : tapez o ou n"))
                        erreur = True
                    End If
                    ' nbEnfants
                    nbEnfants = 0
                    Try
                        nbEnfants = Integer.Parse(args(1))
                        If nbEnfants < 0 Then
                            Throw New Exception
                        End If
                    Catch
                        Console.Error.WriteLine(syntaxe2 + "\nArgument nbEnfants incorrect : tapez un entier positif ou nul")
                        erreur = True
                    End Try
                    ' salary
                    salaire = 0
                    Try
                        salaire = Integer.Parse(args(2))
                        If salaire < 0 Then
                            Throw New Exception
                        End If
                    Catch
                        Console.Error.WriteLine(syntaxe2 + "\nArgument salaire incorrect : tapez un entier positif ou nul")
                        erreur = True
                    End Try
                End If
                If Not erreur Then
                    ' parameters are correct - tax is calculated
                    Console.Out.WriteLine(("impôt=" & objImpot.calculer(marié = "o", nbEnfants, salaire).ToString + " euro(s)"))
                End If
            End While
        End Sub
    End Module
End Namespace

应用程序通过以下参数启动:

  • bdACCESS:要使用的 ACCESS 文件名

税费计算使用在应用程序启动时创建的 [tax] 类型对象进行:


            ' retrieve the arguments
            Dim chemin As String = arguments(0)
            ' prepare the connection chain
            Dim chaineConnexion As String = "Provider=Microsoft.Jet.OLEDB.4.0; Ole DB Services=-4; Data Source=" + chemin
 
            ' tax object creation
            Dim objImpot As impot = Nothing
            Try
                objImpot = New impot(New impotsOLEDB(chaineConnexion))
            Catch ex As Exception
                Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
                Environment.Exit(2)
            End Try

连接到 OLEDB 源的连接字符串是使用通过 [WebMatrix] 获取的信息构建的。

初始化完成后,应用程序会反复提示用户输入计算税款所需的三个信息:

  • 婚姻状况:o 表示已婚,n 表示未婚
  • 子女数量
  • 年薪

所有类均已编译:

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

将 [impots.mdb] 文件放置在测试应用程序文件夹中,并按以下方式启动应用程序:

dos>testimpots impots.mdb
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 60000
impôt=4300 euro(s)

您可以使用不正确的 ACCESS 文件运行该应用程序:

dos>testimpots xx
L'erreur suivante s'est produite : Erreur d'accès à la base de données (Ficher 'D:\data\serge\devel\aspnet\poly\chap5\impots\3\xx' introuvable.)

6.3.5. Web 应用程序视图

这些与前一个应用程序中的相同:formulaire.aspx 和 erreurs.aspx

6.3.6. 应用程序控制器 [global.asax, main.aspx]

只需修改 [global.asax] 控制器。它在应用程序启动时负责创建 [impot] 对象。该对象的构造函数有一个参数:负责检索数据的 [impotsData] 对象。因此,由于我们正在切换数据源,该参数会发生变化。[global.asax.vb] 控制器变为如下内容:


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("chaineConnexion")))
            ' put the object in the application
            Application("objImpot") = objImpot
            ' no error
            Application("erreur") = False
        Catch ex As Exception
            'there has been an error, we note it in the application
            Application("erreur") = True
            Application("message") = ex.Message
        End Try
    End Sub
End Class

[impot] 对象的数据源现在是一个 [impotOLEDB] 对象。该对象将要使用的 OLEDB 数据源的连接字符串作为参数。该字符串存储在应用程序的 [web.config] 配置文件中:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <appSettings>
        <add key="chaineConnexion" 
        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>

[main.aspx] 控制器保持不变。

6.3.7. 更改摘要

应用程序已准备就绪,可以进行测试。下面列出与上一版本相比所做的更改:

  1. 创建了一个新的数据访问类
  2. [global.asax.vb] 控制器在某处进行了修改:[import] 对象的构造
  3. 已添加 [web.config] 文件

6.3.8. 测试 Web 应用程序

上述所有文件均放置在名为 <application-path> 的文件夹中。

Image

在此文件夹中创建了一个 [bin] 子文件夹,并将 [impot.dll] 程序集(由业务类文件 [impots.vb、impotsData.vb、impotsArray.vb、impotsOLEDB.vb] 编译生成)放置其中。所需的编译命令如下:

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

此命令生成的 [impot.dll] 文件必须放置在 <application-path>\bin 目录下,以便 Web 应用程序能够访问它。Cassini 服务器需使用参数 (<application-path>,/impots3) 启动。测试结果与上一版本相同。

6.4. 示例 4

6.4.1. 问题

现在,我们建议将应用程序转换为一个税费计算模拟应用程序。用户将能够进行连续的税费计算,计算结果将以类似于以下样式的新视图呈现:

Image

6.4.2. 应用程序的 MVC 结构

该应用程序的 MVC 结构如下:

Image

出现了一个新的视图 [simulations.aspx],我们刚刚提供了其屏幕截图。数据访问类将是示例 2 中的 [importsODBC] 类。

6.4.3. Web 应用程序的视图

[erreurs.aspx]视图保持不变。[formulaire.aspx]视图则略有改动。实际上,税额不再显示在此视图中,而是移至[simulations.aspx]视图。因此,启动时向用户展示的页面如下所示:

Image

此外,[form]视图中包含一段JavaScript脚本,用于在将数据发送至服务器前对其进行验证,如下例所示:

Image

呈现代码如下:


<%@ page src="formulaire.aspx.vb" inherits="formulaire" AutoEventWireup="false"%>
<html>
    <head>
        <title>Impôt</title>
        <script language="javascript">
        function calculer(){
          // vérification des paramètres avant de les envoyer au serveur
        with(document.frmImpots){
          //nbre d'enfants
          champs=/^\s*(\d+)\s*$/.exec(txtEnfants.value);
          if(champs==null){
            // le modéle n'est pas vérifié
            alert("Le nombre d'enfants n'a pas été donné ou est incorrect");
            txtEnfants.focus();
            return;
          }//if
          //salaire
          champs=/^\s*(\d+)\s*$/.exec(txtSalaire.value);
          if(champs==null){
            // le modéle n'est pas vérifié
            alert("Le salaire n'a pas été donné ou est incorrect");
            txtSalaire.focus();
            return;
          }//if
          // c'est bon - on envoie le formulaire au serveur
          submit();
        }//with
      }//calculer  
        </script>
    </head>
    <body>
        <P>Calcul de votre impôt</P>
        <HR width="100%" SIZE="1">
        <form name="frmImpots" method="post" action="main.aspx?action=calcul">
            <TABLE border="0">
                <TR>
                    <TD>Etes-vous marié(e)</TD>
                    <TD>
                        <INPUT type="radio" value="oui" name="rdMarie" <%=rdouichecked%>>Oui 
                      <INPUT type="radio"  value="non" name="rdMarie" <%=rdnonchecked%>>Non</TD>
                </TR>
                <TR>
                    <TD>Nombre d'enfants</TD>
                    <TD><INPUT type="text" size="3" maxLength="3" name="txtEnfants" value="<%=txtEnfants%>"></TD>
                </TR>
                <TR>
                    <TD>Salaire annuel (euro)</TD>
                    <TD><INPUT type="text" maxLength="12" size="12" name="txtSalaire" value="<%=txtSalaire%>"></TD>
                </TR>
            </TABLE>
            <hr>
            <P>
                <INPUT type="button" value="Calculer" onclick="calculer()">
            </P>
        </form>
        <form method="post" action="main.aspx?action=effacer">
            <INPUT type="submit" value="Effacer">
        </form>
    </body>
</html>

页面上的动态字段与之前版本相同。税额的动态字段已被移除。[计算] 按钮不再是 [提交] 按钮,而是一个 [按钮],点击时将执行 JavaScript 函数 [calculate()]:


                <INPUT type="button" value="Calculer" onclick="calculer()">

我们为表单命名为 [frmImpots],以便在 [calculer] 脚本中引用它:


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

JavaScript 函数 [calculate] 使用正则表达式来验证 [document.frmImpots.txtEnfants] 和 [document.frmImpots.txtSalaire] 表单中的字段。如果输入的值正确,则通过 [document.frmImpots.submit()] 将其发送至服务器。

展示页面从以下控制器 [formulaire.aspx.vb] 获取其动态字段:


Imports System.Collections.Specialized
 
Public Class formulaire
    Inherits System.Web.UI.Page
 
    ' page fields
    Protected rdouichecked As String
    Protected rdnonchecked As String
    Protected txtEnfants As String
    Protected txtSalaire As String
    Protected txtImpot As String
 
    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' we retrieve the previous request in the
        Dim form As NameValueCollection = Context.Items("formulaire")
        ' prepare the page to be displayed
        ' radio buttons
        rdouichecked = ""
        rdnonchecked = "checked"
        If form("rdMarie").ToString = "oui" Then
            rdouichecked = "checked"
            rdnonchecked = ""
        End If
        ' the rest
        txtEnfants = CType(form("txtEnfants"), String)
        txtSalaire = CType(form("txtSalaire"), String)
    End Sub
End Class

控制器 [formulaire.aspx.vb] 与之前的版本完全相同,只是不再需要从上下文中检索 [txtImpot] 字段,因为该字段已从页面中删除。

[simulations.aspx] 视图如下所示:

Image

并对应以下呈现代码:


<%@ page src="simulations.aspx.vb" inherits="simulations" autoeventwireup="false" %>
<HTML>
    <HEAD>
        <title>simulations</title>
    </HEAD>
    <body>
        <P>Résultats des simulations</P>
        <HR width="100%" SIZE="1">
        <table>
            <tr>
                <th>
                    Marié</th>
                <th>
                    Enfants</th>
                <th>
                    Salaire annuel (euro)</th>
                <th>
                    Impôt à payer (euro)</th>
            </tr>
            <%=simulationsHTML%>
        </table>
        <p></p>
        <a href="<%=href%>">
            <%=lien%>
        </a>
    </body>
</HTML>

此代码包含三个动态字段:

simulationsHTML
以 HTML 表格行形式呈现的模拟列表的 HTML 代码
href
链接的URL
link
链接文本

它们由控制器 [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 lien As String
 
    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        'simulations are retrieved 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 += "<tr>"
            For j = 0 To simulation.Length - 1
                simulationsHTML += "<td>" + simulation(j) + "</td>"
            Next
            simulationsHTML += "</tr>" + ControlChars.CrLf
        Next
        ' recover the other elements of the context
        href = context.Items("href").ToString
        lien = context.Items("lien").ToString
    End Sub
End Class

页面控制器检索应用程序控制器放置在页面上下文中的信息:

Context.Items("simulations")
包含待显示模拟列表的 ArrayList 对象。每个元素是一个由 4 个字符串组成的数组,代表模拟的信息(已婚、子女、薪资、税款)。
Context.Items("href")
链接的 URL
Context.Items("link")
链接文本

6.4.4. 控制器 [global.asax, main.aspx]

让我们回顾一下应用程序的 MVC 架构:

Image

控制器 [main.aspx] 必须处理三个操作:

  • init:对应客户端的首次请求。控制器将显示视图 [form.aspx]
  • calcul:对应税费计算请求。如果输入表单中的数据正确,则使用业务类 [impotsODBC] 计算税费。控制器将 [simulations.aspx] 视图返回给客户端,其中包含当前模拟结果以及所有之前的模拟结果。如果输入表单中的数据不正确,控制器将返回 [erreurs.aspx] 视图,其中包含错误列表以及返回表单的链接。
  • return:对应于发生错误后返回表单。控制器将显示 [form.aspx] 视图,其内容为发生错误前已通过验证的状态。

在此新版本中,仅 [calcul] 操作发生了变化。实际上,如果数据有效,它应跳转至 [simulations.aspx] 视图,而此前则是跳转至 [form.aspx] 视图。[main.aspx.vb] 控制器代码如下:


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 of all, we check whether the application has initialized correctly
...
        ' execute the action
        Select Case action
            Case "init"
                ' init application
                initAppli()
            Case "calcul"
                ' tax calculation
                calculImpot()
            Case "retour"
                ' back to form
                retourFormulaire()
            Case "effacer"
                ' init application
                initAppli()
            Case Else
                ' unknown action = init
                initAppli()
        End Select
    End Sub
 
...
    Private Sub calculImpot()
        ' save entries
        Session.Item("formulaire") = Request.Form
        ' check the validity of the data entered
        Dim erreurs As ArrayList = checkData()
        ' if there are errors, we report them
        If erreurs.Count <> 0 Then
            ' prepare the error page
            context.Items("href") = "main.aspx?action=retour"
            context.Items("lien") = "Retour au formulaire"
            context.Items("erreurs") = erreurs
            Server.Transfer("erreurs.aspx")
        End If
        ' no errors here - the tax is calculated
        Dim impot As Long = CType(Application("objImpot"), impot).calculer( _
        Request.Form("rdMarie") = "oui", _
        CType(Request.Form("txtEnfants"), Integer), _
        CType(Request.Form("txtSalaire"), Long))
        ' the result is added to 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 current simulation
        Dim simulation() As String = New String() {Request.Form("rdMarie").ToString, _
        Request.Form("txtEnfants").ToString, Request.Form("txtSalaire").ToString, _
        impot.ToString}
        simulations.Add(simulation)
        ' put simulations in session and context
        context.Items("simulations") = simulations
        Session.Item("simulations") = simulations
        ' the result page is displayed
        context.Items("href") = "main.aspx?action=retour"
        context.Items("lien") = "Retour au formulaire"
        Server.Transfer("simulations.aspx", True)
    End Sub
...
End Class

上文仅列出了理解 [calculImpots] 函数中独有更改所需的必要内容:

  • 首先,该函数将表单 [Request.Form] 保存到会话中,以便在表单通过验证的状态下重新生成。这在所有情况下都必须执行,因为无论操作结果是返回 [erreurs.aspx] 还是 [simulations.aspx],我们都会通过 [返回表单] 链接回到表单。要正确恢复表单,必须事先将其值保存在会话中。
  • 如果输入的数据正确,该函数会将当前的模拟方案(婚姻状况、子女、薪资、税费)添加到模拟方案列表中。该列表位于与“simulations”键关联的会话中。
  • 模拟列表将存储在会话中以备将来使用。它也会被放入当前上下文中,因为 [simulations.aspx] 视图期望在此处找到它
  • 一旦 [simulations.aspx] 视图所需的其他信息被放入上下文中,该视图即会被显示

6.4.5. 更改摘要

应用程序已准备就绪,可以进行测试。让我们列出与之前版本相比所做的更改:

  1. 创建了一个新视图
  2. [main.aspx.vb] 控制器在某处进行了修改:处理 [calcul] 操作

6.4.6. 测试 Web 应用程序

欢迎读者进行测试。以下是操作流程的提醒。所有应用程序文件都放置在名为 <application-path> 的文件夹中。在此文件夹内创建了一个名为 [bin] 的子文件夹,其中包含由业务类文件 [impots.vb, impotsData.vb, impotsArray.vb, impotsODBC.vb] 编译生成的程序集 [impot.dll]。 必须将此命令生成的 [impot.dll] 文件放置在 <application-path>\bin 目录下,以便 Web 应用程序能够访问它。Cassini 服务器需使用参数 (<application-path>,/impots4) 启动。

6.5. 结论

前面的示例在具体场景中演示了 Web 开发中常用的机制。出于教学目的,我们始终采用了 MVC 架构。如果没有这种架构,我们本可以采用不同的方式处理这些示例,甚至可能更简单。然而,一旦应用程序变得稍微复杂一些,包含多个页面时,MVC 架构便能提供显著的优势。

我们可以以多种方式继续扩展这些示例。以下是其中几种:

  • 用户可能希望保存随时间推移的模拟结果。例如,用户可以在第 D 天运行模拟,并在第 D+3 天检索结果。解决此问题的可行方案之一是使用 Cookie。我们知道,服务器与客户端之间的会话令牌正是通过此机制传输的。我们也可以利用该机制在客户端与服务器之间传输模拟数据。
    • 当服务器发送包含模拟结果的页面时,会在 HTTP 头部同时发送一个 Cookie,其中包含一个代表模拟数据的字符串。由于这些数据存储在 [ArrayList] 对象中,因此必须将该对象转换为 [String]。服务器会为该 Cookie 设置有效期,例如 30 天。
    • 客户端浏览器将接收到的 Cookie 存储在文件中,并在每次向发送这些 Cookie 的服务器发起请求时将其发回,前提是这些 Cookie 仍然有效(未超过有效期)。服务器将收到一个表示模拟结果的 [String] 字符串,并必须将其转换为 [ArrayList] 对象。

Cookie 在发送至客户端时由 [Response.Cookies] 管理,在服务器接收时由 [Request.Cookies] 管理。

  • 如果模拟数量庞大,上述机制可能会消耗大量资源。此外,用户通常会定期清除所有 Cookie,即使他们通常允许浏览器使用 Cookie。因此,模拟 Cookie 迟早会丢失。 因此,我们可能希望将这些Cookie存储在服务器端而非客户端,例如存储在数据库中。为了将模拟与特定用户关联,应用程序可以从一个需要用户名和密码的身份验证阶段开始,这些凭据本身存储在数据库或任何其他类型的数据存储库中。
  • 我们可能还希望确保应用程序运行的安全性。当前系统基于以下两个假设:
    1. 用户始终通过控制器 [main.aspx] 进行操作
    2. 且在此情况下,它始终使用被发送给它的页面上可用的操作

例如,如果用户直接请求 URL [http://localhost/impots4/formulaire.aspx] 会发生什么?这种情况不太可能发生,因为用户并不知道这个 URL。但必须对此进行处理。这可以通过应用程序控制器 [global.asax] 来处理,该控制器负责处理发往应用程序的所有请求。因此,它可以验证所请求的资源是否确实是 [main.aspx]。

更常见的情况是,用户未使用服务器发送给他们的页面上提供的操作。例如,如果用户未先填写表单就直接请求 URL [http://localhost/impots4/main.aspx?action=retour],会发生什么?让我们试一试。我们会得到以下响应:

Image

服务器崩溃了。这是正常的。对于 [return] 操作,控制器期望在会话中找到一个 [NameValueCollection] 对象,该对象代表它需要显示的表单值。但它没有找到这些值。控制器机制为这个问题提供了一个优雅的解决方案。对于每个请求,[main.aspx] 控制器都可以验证所请求的操作是否确实是之前发送给用户的页面上的操作之一。我们可以使用以下机制:

  • 在向客户端发送响应之前,控制器会将标识此页面的信息存储在客户端的会话中
  • 当收到来自客户端的新请求时,它会验证所请求的操作是否确实属于上次发送给该客户端的页面
  • 将页面与页面上允许的操作关联的信息可以写入应用程序的 [web.config] 配置文件中。
  • 实践表明,应用程序控制器具有广泛的共同基础,因此可以构建一个通用控制器,并通过配置文件来处理其针对特定应用程序的特殊化。例如,在 Java Web 编程领域,[Struts] 工具就采用了这种方法。