Skip to content

6. Przykłady

W niniejszym rozdziale zamierzamy zilustrować omówione wcześniej zagadnienia za pomocą serii przykładów.

6.1. Przykład 1

6.1.1. Problem

Ta aplikacja ma umożliwić użytkownikowi obliczenie wysokości podatku. Rozważamy uproszczony przypadek podatnika, który ma do zadeklarowania wyłącznie swoje wynagrodzenie (dane z 2004 r. dotyczące dochodów z 2003 r.):

  • liczba udziałów pracownika obliczana jest jako nbParts = nbEnfants/2 + 1, jeśli jest on osobą stanu wolnego, nbEnfants/2 + 2, jeśli jest żonaty, gdzie nbEnfants to liczba jego dzieci.
  • jeśli ma co najmniej troje dzieci, otrzymuje dodatkową połowę udziału
  • oblicza się jego dochód podlegający opodatkowaniu R = 0,72 * S, gdzie S to jego roczne wynagrodzenie
  • oblicza się jego współczynnik rodzinny QF = R / nbParts
  • oblicza się jego podatek I. Rozważmy następującą tabelę:
4262
0
0
8382
0,0683
291,09
14753
0,1914
1322,92
23 888
0,2826
2668,39
38 868
0,3738
4846,98
47932
0,4262
6883,66
0
0,4809
9505,54

Każdy wiersz zawiera 3 pola. Aby obliczyć podatek I, należy znaleźć pierwszy wiersz, w którym QF <= pole1. Na przykład, jeśli QF = 5000, znajdziemy wiersz

    8382        0.0683        291.09

Podatek I wynosi wówczas 0,0683*R – 291,09*nbParts. Jeśli QF jest takie, że warunek QF<=pole1 nigdy nie jest spełniony, wówczas stosuje się współczynniki z ostatniego wiersza. W tym przypadku:

    0                0.4809    9505.54

co daje podatek I = 0,4809*R – 9505,54*nbParts.

6.1.2. Struktura MVC w aplikacji

Struktura MVC aplikacji będzie wyglądać następująco:

Image

Rolę kontrolera pełni strona [main.aspx]. Możliwe są trzy akcje:

  • init: odpowiada pierwszemu żądaniu klienta. Kontroler wyświetli widok [formulaire.aspx]
  • calcul: odpowiada żądaniu obliczenia podatku. Jeśli dane z formularza wprowadzania danych są poprawne, podatek jest obliczany za pomocą klasy biznesowej [impots]. Kontroler zwraca klientowi widok [formulaire.aspx] w postaci, w jakiej został zatwierdzony, wraz z obliczonym podatkiem. Jeśli dane z formularza wprowadzania danych są nieprawidłowe, kontroler zwróci widok [erreurs.aspx] wraz z listą błędów i linkiem umożliwiającym powrót do formularza.
  • powrót: odpowiada powrotowi do formularza po wystąpieniu błędu. Kontroler wyświetla widok [formulaire.aspx] w stanie, w jakim został zatwierdzony przed wystąpieniem błędu.

Kontroler [main.aspx] nie zajmuje się obliczaniem podatków. Odpowiada jedynie za zarządzanie komunikacją klient-serwer oraz za wykonanie czynności żądanych przez klienta. W przypadku akcji [calcul] będzie on korzystał z klasy biznesowej [impot].

6.1.3. Klasa biznesowa

Klasa podatkowa zostanie zdefiniowana w następujący sposób:


' importowanych przestrzeni nazw
Imports System

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

        ' konstruktor
        Public Sub New(ByRef source As impotsData)
            ' dane niezbędne do obliczenia podatku
             ' pochodzą z zewnętrznego źródła [source]
             ' są pobierane – może wystąpić wyjątek
            Dim data() As Object = source.getData
            limites = CType(data(0), Decimal())
            coeffR = CType(data(1), Decimal())
            coeffN = CType(data(2), Decimal())
        End Sub

         ' obliczenie podatku
        Public Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Long) As Long
             ' obliczenie liczby udziałów
            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
             ' obliczenie dochodu podlegającego opodatkowaniu i współczynnika rodzinnego
            Dim revenu As Decimal = 0.72D * salaire
            Dim QF As Decimal = revenu / nbParts
             ' obliczenie podatku
            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

Obiekt podatku tworzy się poprzez przekazanie do jego konstruktora źródła danych typu [impotsData]. Klasa ta posiada metodę publiczną [getData], która pozwala uzyskać trzy tabele danych niezbędne do obliczenia podatku, które zostały przedstawione wcześniej. Metoda ta może obsłużyć wyjątek, jeśli dane nie mogły zostać pobrane lub jeśli okażą się nieprawidłowe. Po utworzeniu obiektu [impot] można wielokrotnie wywoływać jego metodę calculer, która oblicza podatek podatnika na podstawie jego stanu cywilnego (żonaty lub nie), liczby dzieci oraz rocznego wynagrodzenia.

6.1.4. Klasa dostępu do danych

Klasa [impotsData] umożliwia dostęp do danych. Jest to klasa abstrakcyjna. Dla każdego nowego potencjalnego źródła danych (tabele, pliki tekstowe, bazy danych, konsola itp.) należy utworzyć klasę pochodną. Jej definicja jest następująca:


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

        ' metoda dostępu do danych
        Public MustOverride Function getData() As Object()

        ' metoda weryfikacji danych
        Protected Function checkData() As Integer
            ' weryfikacja pobranych danych
            ' muszą być dostępne dane
            valide = Not limites Is Nothing AndAlso Not coeffr Is Nothing AndAlso Not coeffn Is Nothing
            If Not valide Then Return 1
            ' muszą istnieć 3 tabele tej samej wielkości
            If valide Then valide = limites.Length = coeffr.Length AndAlso limites.Length = coeffn.Length
            If Not valide Then Return 2
            ' tabele muszą być niepuste
            valide = limites.Length <> 0
            If Not valide Then Return 3
            ' każda tablica musi zawierać elementy >=0 w porządku rosnącym
            valide = check(limites, limites.Length - 1) AndAlso check(coeffr, coeffr.Length) AndAlso check(coeffn, coeffn.Length)
            If Not valide Then Return 4
            ' wszystko jest w porządku
            Return 0
        End Function

        ' sprawdza poprawność zawartości tablicy
        Protected Function check(ByRef tableau() As Decimal, ByVal n As Integer) As Boolean
            ' tablica musi mieć pierwsze n elementów >=0 i uporządkowanych w ściśle rosnącej kolejności
            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
            ' w porządku
            Return True
        End Function
    End Class
End Namespace

Klasa posiada następujące atrybuty chronione:

limites
tabela progów podatkowych
coeffr
tabela współczynników stosowanych do dochodu podlegającego opodatkowaniu
coeffn
tabela współczynników stosowanych do liczby udziałów
checked
wartość logiczna wskazująca, czy dane (limity, współczynnik1, współczynnik2) zostały zweryfikowane
valide
wartość logiczna wskazująca, czy dane (limity, współczynnikr, współczynnikn) są prawidłowe

Klasa nie posiada konstruktora. Posiada metodę abstrakcyjną [getData], którą klasy pochodne będą musiały zaimplementować. Zadaniem tej metody jest:

  • przypisanie wartości do trzech tablic: limity, współczynniki_r, współczynniki_n
  • wygenerowanie wyjątku, jeśli dane nie mogły zostać pobrane lub okazały się nieprawidłowe.

Klasa udostępnia metody chronione [checkData] i [check], które sprawdzają poprawność atrybutów (limites, coeffr, coeffn). Dzięki temu klasy pochodne nie muszą ich implementować. Wystarczy, że będą z nich korzystać.

Pierwszą klasą pochodną, z której skorzystamy, będzie następująca:


Imports System.Collections
Imports System

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

        ' konstruktor bez argumentów
        Public Sub New()
            ' inicjalizacja tablic za pomocą stałych
            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

        ' konstruktor z trzema tablicami jako argumentami wejściowymi
        Public Sub New(ByRef limites() As Decimal, ByRef coeffr() As Decimal, ByRef coeffn() As Decimal)
            ' zapisujemy dane
            Me.limites = limites
            Me.coeffr = coeffr
            Me.coeffn = coeffn
            checked = False
        End Sub

        Public Overrides Function getData() As Object()
            ' ewentualnie sprawdzamy dane
            Dim erreur As Integer
            If Not checked Then erreur = checkData() : checked = True
            ' jeśli dane są nieprawidłowe, to zgłaszamy wyjątek
            If Not valide Then Throw New Exception("Les données des tranches d'impôts sont invalides (" + erreur.ToString + ")")
            ' w przeciwnym razie zwracane są trzy tablice
            Return New Object() {limites, coeffr, coeffn}
        End Function
    End Class
End Namespace

Ta klasa o nazwie [impotsArray] posiada dwa konstruktory:

  • konstruktor bez argumentów, który inicjuje atrybuty (limity, coeffr, coeffn) klasy bazowej za pomocą „na stałe” zakodowanych tablic
  • konstruktor, który inicjuje atrybuty (limity, coeffr, coeffn) klasy bazowej za pomocą tablic przekazanych mu jako parametry

Metoda [getData], która umożliwi klasom zewnętrznym uzyskanie tablic (limites, coeffr, coeffn), ogranicza się do sprawdzenia poprawności tych trzech tablic za pomocą metody [checkData] klasy bazowej. W przypadku nieprawidłowych danych zgłasza wyjątek.

6.1.5. Testowanie klas biznesowych i klas dostępu do danych

Ważne jest, aby w aplikacji internetowej uwzględniać wyłącznie klasy biznesowe i klasy dostępu do danych, których poprawność została potwierdzona. Dzięki temu faza debugowania aplikacji internetowej będzie mogła skupić się na części dotyczącej kontrolera i widoków. Program testowy mógłby wyglądać następująco:


' opcje
Option Strict On
Option Explicit On 

' przestrzenie nazw
Imports System
Imports Microsoft.VisualBasic

Namespace st.istia.univangers.fr
    Module test
        Sub Main()
             ' interaktywny program do obliczania podatku
             ' użytkownik wprowadza trzy dane za pomocą klawiatury: żonaty nbEnfants wynagrodzenie
             ' następnie program wyświetla kwotę podatku do zapłaty
            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"

             ' utworzenie obiektu „podatek”
            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
            ' pętla nieskończona
            Dim marié As String
            Dim nbEnfants As Integer
            Dim salaire As Long
            While True
                ' wyświetlane jest zapytanie o parametry obliczenia podatku
                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()
                ' co należy zrobić?
                If paramètres Is Nothing OrElse paramètres = "" Then
                    Exit While
                End If
                ' sprawdzanie liczby argumentów w wprowadzonej linii
                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
                ' sprawdzanie poprawności parametrów
                If Not erreur Then
                    ' żonaty
                    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
                    ' wynagrodzenie
                    Try
                        salaire = Integer.Parse(args(2))
                        If salaire < 0 Then
                            Throw New Exception
                        End If
                    Catch
                        erreur = True
                    End Try
                End If
                 ' jeśli parametry są poprawne – obliczamy podatek
                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

Aplikacja prosi użytkownika o wpisanie z klawiatury trzech informacji potrzebnych do obliczenia podatku:

  • stan cywilny: o – żonaty/zamężna, n – niezamężna/nieżonaty
  • liczba dzieci
  • jego roczne wynagrodzenie

Obliczenie podatku odbywa się za pomocą obiektu typu [impot], tworzonego w momencie uruchomienia aplikacji:


             ' utworzenie obiektu podatku
            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

Jako źródło danych wykorzystuje się obiekt typu [impotsArray]. Wykorzystywany jest konstruktor tej klasy bez argumentów, który dostarcza trzy tablice (limites, coeffr, coeffn) z wartościami „stałymi”. Utworzenie obiektu typu [impot] może teoretycznie spowodować wyjątek, ponieważ w trakcie tworzenia obiekt ten będzie żądał danych (limity, coeffr, coeffn) od źródła danych przekazanego mu jako parametr, a proces pozyskiwania tych danych może wywołać wyjątek. Okazuje się jednak, że w tym przypadku metoda pozyskiwania danych (wartości stałe) nie może spowodować wyjątku. Pozostawiliśmy jednak obsługę wyjątku, aby zwrócić uwagę czytelnika na możliwość nieprawidłowego utworzenia obiektu [impot].

Oto przykład wykonania powyższego programu:

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

Kompilujemy wszystkie klasy [impot, impotsData, impotsArray] do zestawu [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

Kompilujemy program testowy:

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

Możemy przeprowadzić testy:

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. Widoki aplikacji internetowej

Aplikacja będzie miała dwa widoki: [formulaire.aspx] i [erreurs.aspx]. Zilustrujmy działanie aplikacji za pomocą zrzutów ekranu. Widok [formulaire.aspx] jest wyświetlany przy pierwszym wywołaniu adresu URL [main.aspx]:

Image

Użytkownik wypełnia formularz:

Image

i korzysta z przycisku [Calculer], aby uzyskać następującą odpowiedź:

Image

Może popełnić błąd przy wprowadzaniu danych:

Image

Kliknięcie przycisku [Calculer] powoduje wówczas wyświetlenie innej odpowiedzi [erreurs.aspx]:

Image

Może skorzystać z powyższego linku [Retour au formulaire], aby powrócić do widoku [formulaire.aspx] w stanie, w jakim został zatwierdzony przed wystąpieniem błędu:

Image

6.1.7. Widok [formulaire.aspx]

Strona [formulaire.aspx] będzie wyglądać następująco:


<%@ 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>

Pola dynamiczne na tej stronie to:

rdouichecked
„checked”, jeśli pole wyboru [oui] ma być zaznaczone, w przeciwnym razie „”
rdnonchecked
to samo dotyczy pola wyboru [non]
txtEnfants
wartość do umieszczenia w polu wprowadzania danych [txtEnfants]
txtSalaire
wartość do wpisania w polu wprowadzania danych [txtSalaire]
txtImpot
wartość do umieszczenia w polu wprowadzania danych [txtImpot]

Strona zawiera dwa formularze, z których każdy posiada przycisk [submit]. Przycisk [Calculer] jest przyciskiem [submit] z następującego formularza:


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

Widać, że parametry formularza zostaną przesłane do kontrolera za pomocą [action=calcul]. Przycisk [Effacer] to przycisk [submit] z następującego formularza:


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

Widać, że parametry formularza zostaną przesłane do kontrolera za pomocą [action=effacer]. W tym przypadku formularz nie ma żadnych parametrów. Liczy się tylko akcja.

Pola [formulaire.aspx] są obliczane przez [formulaire.aspx.vb]:


Imports System.Collections.Specialized

Public Class formulaire
    Inherits System.Web.UI.Page

    ' pola na stronie
    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
        ' pobieranie poprzedniego zapytania z kontekstu
        Dim form As NameValueCollection = Context.Items("formulaire")
        ' przygotowuje się stronę do wyświetlenia
        ' przyciski opcji
        rdouichecked = ""
        rdnonchecked = "checked"
        If form("rdMarie").ToString = "oui" Then
            rdouichecked = "checked"
            rdnonchecked = ""
        End If
        ' reszta
        txtEnfants = CType(form("txtEnfants"), String)
        txtSalaire = CType(form("txtSalaire"), String)
        txtImpot = CType(Context.Items("txtImpot"), String)
    End Sub
End Class

Obliczenia pól [main.aspx] opierają się na dwóch informacjach umieszczonych przez kontroler w kontekście strony:

  • Context.Items („formularz”): słownik typu NameValueCollection zawierający wartości pól HTML [rdmarie,txtEnfants,txtSalaire]
  • Context.Items („txtImpot”): wartość podatku

6.1.8. Widok [erreurs.aspx]

Widok [erreurs.aspx] wyświetla ewentualne błędy, które mogą wystąpić podczas działania aplikacji. Jego kod prezentacji jest następujący:


<%@ 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>

Strona zawiera trzy pola dynamiczne:

erreursHTML
kod HTML z listy błędów
href
adres URL linku
lien
tekst linku

Pola te są obliczane przez część sterującą strony w [erreurs.aspx.vb]:


Imports System.Collections
Imports Microsoft.VisualBasic

Public Class erreurs
    Inherits System.Web.UI.Page

    ' parametr strony
    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
        ' pobieramy elementy z kontekstu
        Dim erreurs As ArrayList = CType(context.Items("erreurs"), ArrayList)
        href = context.Items("href").ToString
        lien = context.Items("lien").ToString
        ' generowanie kodu HTML z listy
        Dim i As Integer
        For i = 0 To erreurs.Count - 1
            erreursHTML += "<li> " + erreurs(i).ToString + "</li>" + ControlChars.CrLf
        Next
    End Sub

End Class

Kontroler strony pobiera informacje umieszczone przez kontroler aplikacji w kontekście strony:

Context.Items("erreurs"
obiekt ArrayList zawierający listę komunikatów o błędach do wyświetlenia
Context.Items("href"
adres URL linku
Context.Items("lien"
tekst linku

Teraz, gdy wiemy już, co widzi użytkownik aplikacji, możemy przejść do pisania jej kontrolera.

6.1.9. Kontrolery [global.asax, main.aspx]

Przypomnijmy schemat MVC naszej aplikacji:

Image

Klient Logika aplikacji

Kontroler [main.aspx] musi obsłużyć trzy akcje:

  • init: odpowiada pierwszemu żądaniu klienta. Kontroler wyświetla widok [formulaire.aspx]
  • calcul: odpowiada żądaniu obliczenia podatku. Jeśli dane z formularza wprowadzania danych są poprawne, podatek jest obliczany za pomocą klasy biznesowej [impots]. Kontroler zwraca klientowi widok [formulaire.aspx] w postaci, w jakiej został zatwierdzony, wraz z obliczonym podatkiem. Jeśli dane w formularzu wprowadzania danych są nieprawidłowe, kontroler zwraca widok [erreurs.aspx] wraz z listą błędów i linkiem umożliwiającym powrót do formularza.
  • powrót: odpowiada powrotowi do formularza po wystąpieniu błędu. Kontroler wyświetla widok [formulaire.aspx] w stanie, w jakim został zatwierdzony przed wystąpieniem błędu.

Wiadomo ponadto, że każde żądanie kierowane do aplikacji przechodzi przez kontroler [global.asax], o ile istnieje. Mamy zatem na wejściu do aplikacji łańcuch dwóch kontrolerów:

  • [global.asax], który zgodnie z architekturą ASP.NET odbiera wszystkie żądania kierowane do aplikacji
  • [main.aspx], który zgodnie z decyzją programisty również odbiera wszystkie żądania kierowane do aplikacji

Konieczność zastosowania [main.aspx] wynika z faktu, że będziemy musieli zarządzać sesją. Widzieliśmy już, że [global.asax] nie nadaje się w tym przypadku na kontroler. W tym przypadku moglibyśmy całkowicie zrezygnować z [global.asax]. Wykorzystamy go jednak do wykonania kodu podczas uruchamiania aplikacji. Powyższy schemat MVC pokazuje, że będziemy musieli utworzyć obiekt [impot] w celu obliczenia podatku. Nie ma sensu tworzyć go wielokrotnie – wystarczy raz. Utworzymy go zatem podczas uruchamiania aplikacji w ramach zdarzenia [Application_Start] obsługiwanego przez kontroler [global.asax]. Jego kod wygląda następująco:

[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)
        ' tworzy się obiekt importu
        Dim objImpot As impot
        Try
            objImpot = New impot(New impotsArray)
            ' umieszczamy obiekt w aplikacji
            Application("objImpot") = objImpot
            ' brak błędu
            Application("erreur") = False
        Catch ex As Exception
            'wystąpił błąd, odnotowuje się go w aplikacji
            Application("erreur") = True
        End Try
    End Sub
End Class

Po utworzeniu obiekt typu [impot] jest umieszczany w aplikacji. To właśnie stamtąd będą go pobierać różne zapytania od różnych klientów. Ponieważ tworzenie obiektu typu [impot] może się nie powieść, obsługujemy ewentualny wyjątek i umieszczamy w aplikacji klucz [erreur], aby zasygnalizować, czy podczas tworzenia obiektu [impot] wystąpił błąd, czy nie.

Kod kontrolera [main.aspx, main.aspx.vb] będzie wyglądał następująco:

[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
        ' przede wszystkim sprawdzamy, czy aplikacja zainicjowała się poprawnie
        If CType(Application("erreur"), Boolean) Then
            ' przekierowujemy na stronę błędów
            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
        ' pobieramy akcję do wykonania
        Dim action As String
        If Request.QueryString("action") Is Nothing Then
            action = "init"
        Else
            action = Request.QueryString("action").ToString.ToLower
        End If
        ' wykonujemy działanie
        Select Case action
            Case "init"
                ' inicjowanie aplikacji
                initAppli()
            Case "calcul"
                ' obliczanie podatku
                calculImpot()
            Case "retour"
                ' powrót do formularza
                retourFormulaire()
            Case "effacer"
                ' inicjowanie aplikacji
                initAppli()
            Case Else
                ' nieznana akcja = inicjalizacja
                initAppli()
        End Select
    End Sub

    Private Sub initAppli()
        ' wyświetlanie wstępnie wypełnionego formularza
        Context.Items("formulaire") = initForm()
        Context.Items("txtImpot") = ""
        Server.Transfer("formulaire.aspx", True)
    End Sub

    Private Function initForm() As NameValueCollection
        ' formularz jest inicjowany
        Dim form As New NameValueCollection
        form.Set("rdMarie", "non")
        form.Set("txtEnfants", "")
        form.Set("txtSalaire", "")
        Return form
    End Function

    Private Sub calculImpot()
        ' sprawdzana jest poprawność wprowadzonych danych
        Dim erreurs As ArrayList = checkData()
        ' jeśli występują błędy, wyświetla się komunikat
        If erreurs.Count <> 0 Then
            ' zapisuje się wprowadzone dane
            Session.Item("formulaire") = Request.Form
            ' przygotowuje się stronę błędów
            context.Items("href") = "main.aspx?action=retour"
            context.Items("lien") = "Retour au formulaire"
            context.Items("erreurs") = erreurs
            Server.Transfer("erreurs.aspx")
        End If
        ' nie ma błędów – oblicza się podatek
        Dim impot As Long = CType(Application("objImpot"), impot).calculer( _
        Request.Form("rdMarie") = "oui", _
        CType(Request.Form("txtEnfants"), Integer), _
        CType(Request.Form("txtSalaire"), Long))
        ' wyświetla się strona z wynikami
        context.Items("txtImpot") = impot.ToString + " euro(s)"
        context.Items("formulaire") = Request.Form
        Server.Transfer("formulaire.aspx", True)
    End Sub

    Private Sub retourFormulaire()
        ' wyświetla się formularz z wartościami pobranymi z sesji
        Context.Items("formulaire") = Session.Item("formulaire")
        Context.Items("txtImpot") = ""
        Server.Transfer("formulaire.aspx", True)
    End Sub

    Private Function checkData() As ArrayList
        ' na początku brak błędów
        Dim erreurs As New ArrayList
        Dim erreur As Boolean = False
        ' przycisk opcji „żonaty”
        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
        ' liczba dzieci
        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
        ' wynagrodzenie
        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
        ' wyświetla się lista błędów
        Return erreurs
    End Function
End Class

Kontroler rozpoczyna od sprawdzenia, czy aplikacja została poprawnie zainicjowana:


         ' przede wszystkim sprawdzamy, czy aplikacja zainicjowała się poprawnie
        If CType(Application("erreur"), Boolean) Then
            ' przekierowujemy na stronę błędów
            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

Jeśli kontroler stwierdzi, że aplikacja nie mogła się poprawnie zainicjować (nie udało się utworzyć obiektu [impot] niezbędnego do obliczeń), wyświetla stronę błędów z odpowiednimi parametrami. W tym przypadku nie ma potrzeby umieszczania linku powrotnego do formularza, ponieważ cała aplikacja jest niedostępna. Ogólny komunikat o błędzie jest umieszczany w obiekcie [Context.Items("erreurs")] typu [ArrayList].

Jeśli kontroler stwierdzi, że aplikacja działa, analizuje następnie akcję, o której wykonanie został poproszony za pośrednictwem parametru [action]. Z tym sposobem działania mieliśmy już do czynienia wielokrotnie. Obsługa każdego rodzaju akcji jest powierzona funkcji.

6.1.9.1. Akcje init, effacer

Te dwie akcje powinny spowodować wyświetlenie pustego formularza wprowadzania danych. Przypominamy, że formularz ten (patrz widoki) ma dwa parametry:

  • Context.Items („formularz”): słownik typu [NameValueCollection] zawierający wartości pól HTML [rdmarie,txtEnfants,txtSalaire]
  • Context.Items("txtImpot”): wartość podatku

Funkcja [initAppli] inicjuje te dwa parametry w taki sposób, aby wyświetlić pusty formularz.

6.1.9.2. Akcja obliczeniowa

Ta akcja musi obliczyć podatek do zapłaty na podstawie danych wprowadzonych w formularzu i zwrócić go wstępnie wypełnionego wprowadzonymi wartościami oraz dodatkowo z obliczoną kwotą podatku. Funkcja [calculImpot], odpowiedzialna za to zadanie, rozpoczyna od sprawdzenia, czy dane w formularzu są poprawne:

  • pole [rdMarie] musi istnieć i mieć wartość [oui] lub [non]
  • pole [txtEnfants] musi istnieć i zawierać liczbę całkowitą >=0
  • pole [txtSalaire] musi być obecne i zawierać liczbę całkowitą >=0

Jeśli wprowadzone dane okażą się nieprawidłowe, kontroler wyświetla widok [erreurs.aspx], po uprzednim umieszczeniu w kontekście wartości oczekiwanych przez ten widok:

  • komunikaty o błędach są umieszczane w obiekcie [ArrayList], który następnie jest umieszczany w kontekście [Context.Items("erreurs")]
  • adres URL linku powrotnego oraz tekst tego linku są również umieszczane w kontekście.

Przed przekazaniem kontroli do strony [erreurs.aspx], która wyśle odpowiedź do klienta, wartości wprowadzone w formularzu (Request.Form) są umieszczane w sesji, powiązane z kluczem „formularz”. Umożliwi to ich odzyskanie w późniejszym żądaniu.

Można się tutaj zastanowić, czy warto sprawdzać, czy pola [rdMarie, txtEnfants, txtSalaire] znajdują się w żądaniu wysłanym przez klienta. Jest to zbędne, jeśli mamy pewność, że naszym klientem jest przeglądarka, która otrzymała widok [formulaire.aspx] zawierający te pola. Nigdy jednak nie można mieć co do tego pewności. Nieco później pokażemy przykład, w którym klientem jest aplikacja [curl], z którą już mieliśmy do czynienia. Wyślemy zapytanie do aplikacji bez przesyłania pól, których ona oczekuje, i zobaczymy, jak zareaguje. Jest to zasada wielokrotnie już podkreślana, o której przypominamy w tym miejscu: aplikacja nigdy nie powinna zakładać, jaki typ klienta wysyła do niej zapytanie. Ze względów bezpieczeństwa musi zakładać, że może zostać zapytana przez zaprogramowaną aplikację, która może wysłać jej nieoczekiwane ciągi parametrów. Musi zachowywać się poprawnie w każdym przypadku.

W naszym przypadku sprawdziliśmy, czy pola [rdMarie, txtEnfants, txtSalaire] występują w zapytaniu, ale nie sprawdziliśmy, czy może ono zawierać inne pola. W tej aplikacji zostałyby one zignorowane. Niemniej jednak, również ze względów bezpieczeństwa, warto byłoby zapisać tego typu żądania w pliku logów i zgłosić alert administratorowi aplikacji, aby ten wiedział, że aplikacja otrzymuje „dziwne” żądania. Analizując je w pliku logów, mógłby wykryć ewentualny atak na aplikację i podjąć niezbędne działania w celu jej ochrony.

Jeśli oczekiwane dane są poprawne, kontroler uruchamia obliczenie podatku przy użyciu obiektu [impot] przechowywanego w aplikacji. Następnie zapisuje w kontekście dwie informacje oczekiwane przez widok [formulaire.aspx]:

  • Context.Items („formularz”): słownik typu [NameValueCollection] zawierający wartości pól HTML, [rdmarie,txtEnfants,txtSalaire], w tym przypadku [Request.Form)] oraz c.a.d. wartości wprowadzone wcześniej w formularzu
  • Context.Items("txtImpot”): wartość podatku, która została właśnie obliczona

Uważny czytelnik mógł zadać sobie pytanie po przeczytaniu powyższego: skoro obiekt [impot] utworzony podczas uruchomienia aplikacji jest współdzielony przez wszystkie zapytania, czy nie mogą wystąpić konflikty dostępu prowadzące do uszkodzenia danych obiektu [impot]? Aby odpowiedzieć na to pytanie, musimy powrócić do kodu klasy [impot]. Zapytania wywołują metodę [impot].calculerImpot w celu uzyskania kwoty podatku do zapłaty. Musimy zatem przyjrzeć się właśnie temu kodowi:

        Public Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Long) As Long
             ' obliczanie liczby udziałów
            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
             ' obliczanie dochodu podlegającego opodatkowaniu i wskaźnika rodzinnego
            Dim revenu As Decimal = 0.72D * salaire
            Dim QF As Decimal = revenu / nbParts
             ' obliczanie podatku
            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

Załóżmy, że jakiś wątek wykonuje poprzednią metodę i zostaje przerwany. Wówczas metodę tę wykonuje inny wątek. Jakie są zagrożenia? Aby to sprawdzić, dodaliśmy następujący kod:


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

Wątek 1, po obliczeniu wartości [impot1] zmiennej lokalnej [impot], zostaje przerwany. Następnie uruchamia się wątek 2 i oblicza nową wartość [impot2] dla tej samej zmiennej [impot], po czym zostaje przerwany. Wątek 1 odzyskuje kontrolę. Co znajduje w zmiennej lokalnej [impot]? Ponieważ zmienna ta jest zmienną lokalną metody, jest przechowywana w strukturze pamięci zwanej stosem. Stos ten stanowi część kontekstu wątku, który jest zapisywany w momencie jego przerwania. Kiedy uruchamia się wątek 2, jego kontekst jest ustanawiany wraz z nowym stosem, a tym samym nową zmienną lokalną [impot]. Gdy z kolei wątek 2 zostanie przerwany, jego kontekst również zostanie zapisany. Po ponownym uruchomieniu wątku 1 jego kontekst zostaje przywrócony, w tym jego stos. Odzyskuje on wówczas swoją zmienną lokalną [impot], a nie tę z wątku 2. Mamy zatem do czynienia z sytuacją, w której nie występują konflikty dostępu między żądaniami. Testy przeprowadzone z 10-sekundową przerwą, o której mowa powyżej, potwierdziły, że równoczesne żądania rzeczywiście dawały oczekiwany wynik.

6.1.9.3. Akcja powrotu

Ta akcja odpowiada kliknięciu linku [Retour vers le formulaire] w widoku [erreurs.aspx] w celu powrotu do widoku [formulaire.aspx], wstępnie wypełnionego wartościami wprowadzonymi wcześniej i zapisanymi w sesji. Funkcja [retourFormulaire] pobiera te informacje. Dwa parametry wymagane przez widok [formulaire.aspx] są inicjowane:

  • Context.Items („formularz”) z wartościami wprowadzonymi wcześniej i zapisanymi w sesji
  • Context.Items("txtImpot”) z pustym ciągiem znaków

6.1.10. Testowanie aplikacji internetowej

Wszystkie powyższe pliki znajdują się w folderze <application-path>.

Image

W tym folderze tworzony jest podfolder [bin], w którym umieszczany jest pakiet [impot.dll] powstały w wyniku kompilacji plików klas biznesowych: [impots.vb, impotsData.vb, impotsArray.vb]. Poniżej przypominamy polecenie kompilacji:

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

Powyższy plik [impot.dll] należy umieścić w katalogu <application-path>\bin, aby aplikacja internetowa miała do niego dostęp. Serwer Cassini uruchamia się z parametrami (<application-path>,/impots1). W przeglądarce wpisujemy adres URL [http://localhost/impots1/main.aspx]:

Image

Wypełniamy formularz:

Image

Następnie uruchamiamy obliczenie podatku za pomocą przycisku [Calculer]. Otrzymujemy następującą odpowiedź:

Image

Następnie wprowadzamy błędne dane:

Image

Naciśnięcie przycisku [Calculer] powoduje wyświetlenie następującej odpowiedzi:

Image

Kliknięcie linku [Retour au formulaire] powoduje powrót do formularza w stanie, w jakim znajdował się w momencie zatwierdzenia:

Image

Wreszcie, kliknięcie przycisku [Effacer] powoduje zresetowanie strony:

Image

6.1.11. Korzystanie z klienta [curl]

Ważne jest, aby testować aplikacje internetowe przy użyciu innych klientów niż przeglądarki. Jeśli wyślemy do przeglądarki formularz z parametrami, które mają zostać wysłane po jego zatwierdzeniu, przeglądarka odeśle wartości tych parametrów do serwera. Inny klient może tego nie zrobić, a wtedy serwer otrzyma żądanie, w którym brakuje parametrów. Musi on wiedzieć, jak postąpić w takiej sytuacji. Innym przykładem są weryfikacje danych wprowadzanych przez użytkownika, przeprowadzane po stronie klienta. Jeśli formularz zawiera dane wymagające weryfikacji, weryfikacja ta może zostać przeprowadzona po stronie klienta za pomocą skryptów zawartych w dokumencie zawierającym formularz. Przeglądarka wyśle formularz tylko wtedy, gdy wszystkie dane zweryfikowane po stronie klienta są prawidłowe. Można by zatem ulec pokusie, aby po stronie serwera założyć, że otrzymamy już zweryfikowane dane i nie przeprowadzać tej weryfikacji po raz drugi. Byłoby to błędem. W rzeczywistości klient inny niż przeglądarka mógłby przesłać do serwera nieprawidłowe dane, co groziłoby nieoczekiwanym zachowaniem aplikacji internetowej. Zilustrujemy te kwestie na przykładzie klienta [curl].

Najpierw wysyłamy żądanie pod adresem 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>

Serwer przesłał nam kod formularza HTML. W nagłówkach HTTP znajduje się plik cookie sesji. Wykorzystamy go w kolejnych żądaniach w celu utrzymania sesji. Wywołajmy akcję [calcul] bez podawania parametrów:

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>

Widzimy, że aplikacja internetowa zwróciła widok [erreurs] wraz z trzema komunikatami o błędach dotyczącymi trzech brakujących parametrów. Wyślijmy teraz błędne parametry:

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>

Wszystkie trzy błędy zostały poprawnie wykryte. Teraz wyślijmy prawidłowe parametry:

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>

Udało nam się prawidłowo obliczyć należny podatek: 4300 euro. Z tego przykładu wynika, że nie należy dać się zwieść temu, że piszemy aplikację internetową przeznaczoną dla klientów korzystających z przeglądarek. Aplikacja internetowa jest usługą TCP/IP, a ten protokół sieciowy nie pozwala określić charakteru aplikacji klienckiej danej usługi. Nie można więc stwierdzić, czy klient aplikacji internetowej jest przeglądarką, czy nie. Stosujemy zatem dwie zasady:

  • po otrzymaniu żądania od klienta nie przyjmujemy żadnych założeń dotyczących klienta i sprawdzamy, czy parametry oczekiwane w żądaniu są obecne i prawidłowe
  • tworzymy odpowiedź przeznaczoną dla przeglądarek, a więc zazwyczaj dokumenty HTML

Aplikacja internetowa może być zaprojektowana tak, aby obsługiwać jednocześnie różnych klientów, na przykład przeglądarki i telefony komórkowe. W takim przypadku w każdym żądaniu można dołączyć nowy parametr wskazujący typ klienta. W ten sposób przeglądarka zażąda obliczenia podatku poprzez żądanie wysłane pod adresem http://machine/impots/main.aspx?client=przeglądarka&action=obliczenie, podczas gdy telefon komórkowy wyśle żądanie pod adresem http://machine/impots/main.aspx?client=mobile&action=calcul. Struktura MVC ułatwia napisanie takiej aplikacji. Wygląda ona następująco:

Image

Blok [Classes métier, Classes d'accès aux données] pozostaje bez zmian. Jest to bowiem część, która nie ma znaczenia dla klienta. Blok [Contrôleur] ulega niewielkiej zmianie, ale musi uwzględniać nowy parametr w zapytaniu – parametr [client] wskazujący, z jakim typem klienta ma do czynienia. Blok [vues] musi generować widoki dla każdego typu klienta. Warto byłoby uwzględnić obecność parametru [client] w zapytaniu już na etapie projektowania aplikacji, nawet jeśli w perspektywie krótko- lub średnioterminowej celem są wyłącznie przeglądarki. Jeśli aplikacja będzie musiała w przyszłości obsługiwać nowy typ klienta, wystarczy napisać tylko widoki dostosowane do tego typu.

6.2. Przykład 2

6.2.1. Problem

W tym miejscu zamierzamy zająć się tym samym problemem, co poprzednio, ale zmieniając źródło danych obiektu [impot] utworzonego przez aplikację internetową. W poprzedniej wersji wykorzystywano źródło danych dostarczające wartości z tablic „na stałe” zapisanych w kodzie. Tym razem nowe źródło danych będzie pobierać je ze źródła danych ODBC powiązanego z bazą MySQL.

6.2.2. Źródło danych ODBC

Dane będą znajdować się w tabeli o nazwie [IMPOTS] w bazie danych o nazwie MySQL, zwanej [dbimpots]. Zawartość tej tabeli będzie następująca:

Image

Właścicielem bazy danych jest użytkownik o nazwie [admimpots] z hasłem [mdpimpots]. Do tej bazy danych przypisujemy źródło danych o nazwie ODBC. Zanim to zrobimy, przypomnijmy najpierw różne sposoby uzyskiwania dostępu do bazy danych za pomocą platformy .NET.

Istnieje wiele baz danych przeznaczonych dla platform Windows. Aby uzyskać do nich dostęp, aplikacje korzystają z programów zwanych sterownikami (drivers).

Image

Na powyższym schemacie sterownik posiada dwa interfejsy:

  • interfejs I1 udostępniany aplikacji
  • interfejs I2 skierowany do bazy danych

Aby uniknąć konieczności przepisywania aplikacji napisanej dla bazy danych B1 w przypadku migracji do innej bazy danych B2, podjęto działania normalizacyjne dotyczące interfejsu I1. W przypadku korzystania z baz danych wykorzystujących „znormalizowane” sterowniki baza B1 zostanie dostarczona wraz ze sterownikiem P1, baza B2 wraz ze sterownikiem P2, a interfejs I1 dla obu tych sterowników będzie identyczny. Dzięki temu nie będzie konieczności przepisywania aplikacji. W ten sposób będzie można na przykład przenieść bazę danych ACCESS do bazy danych MySQL bez konieczności wprowadzania zmian w aplikacji.

Istnieją dwa rodzaje standardowych sterowników:

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

Sterowniki ODBC umożliwiają dostęp do baz danych. Źródła danych dla sterowników OLE i DB są bardziej zróżnicowane: bazy danych, systemy pocztowe, katalogi... Nie ma żadnych ograniczeń. Każde źródło danych może stać się przedmiotem sterownika Ole DB, jeśli zdecyduje się na to wydawca. Korzyści są oczywiście ogromne: uzyskujemy jednolity dostęp do szerokiej gamy danych.

Platforma .NET 1.1 zawiera trzy rodzaje klas dostępu do danych:

  1. klasy SQL i Server.NET, służące do uzyskiwania dostępu do baz danych Microsoft SQL Server
  2. klasy Ole Db.NET, umożliwiające dostęp do baz SGBD wyposażonych w sterownik OLE i DB
  3. klasy odbc.net, umożliwiające dostęp do baz SGBD, oferujące sterownik ODBC

Klasa SGBD MySQL od dawna posiada sterownik ODBC. To właśnie tego sterownika używamy obecnie. W systemie Windows wybieramy opcję [Menu Démarrer/Panneau de configuration/Outils d'administration/Sources ODBC 32 bits]. W zależności od wersji systemu Windows ścieżka ta może się nieznacznie różnić. Otrzymujemy następującą aplikację, która pozwoli nam utworzyć nasze źródło ODBC:

Image

Utworzymy źródło danych systemowe o nazwie c.a.d. Będzie to źródło danych, z którego będzie mógł korzystać każdy użytkownik komputera. W powyższym oknie wybieramy zakładkę [Source de données système]. Na wyświetlonej stronie znajduje się przycisk [Ajouter], którego użyjemy do utworzenia nowego źródła danych ODBC:

Image

Kreator prosi o wybranie sterownika ODBC, który ma być używany. System Windows zawiera szereg wstępnie zainstalowanych sterowników ODBC. Sterownik ODBC dla MySQL nie znajduje się w tym pakiecie. Należy go zatem wcześniej zainstalować. Można go znaleźć w Internecie, wpisując w wyszukiwarce frazę „MySQL ODBC” lub „MyODBC”. W tym przypadku zainstalowaliśmy sterownik [MySQL ODBC 3.51]. Wybieramy go i przechodzimy do [Terminer]:

Image

Należy podać pewne informacje:

Data Source Name
nazwa, która będzie określać źródło danych ODBC. Każda aplikacja systemu Windows będzie mogła uzyskać dostęp do źródła za pomocą tej nazwy
Description
dowolny tekst opisujący źródło danych
Host Name
nazwa komputera, na którym znajduje się SGBD MySQL. W tym przypadku jest to komputer lokalny. Może to być również komputer zdalny. Umożliwiłoby to aplikacji systemu Windows dostęp do zdalnej bazy danych bez konieczności stosowania specjalnego kodowania. Jest to jedna z głównych zalet źródła ODBC.
Database Name
SGBD MySQL może obsługiwać wiele baz danych. Tutaj określamy, którą z nich chcemy obsługiwać: dbimpots
User
nazwa użytkownika zarejestrowanego w ramach SGBD MySQL. Dostęp do źródła danych będzie odbywał się pod tą nazwą. W tym przypadku: admimpots
Password
hasło tego użytkownika. Tutaj: mdpimpots
Port
port roboczy SGBD MySQL. Domyślnie jest to port 3306. Nie zmieniliśmy go

Po wykonaniu tych czynności sprawdzamy poprawność naszych parametrów połączenia za pomocą przycisku [Test Data Source]:

Image

Po wykonaniu tej czynności mamy pewność co do naszego źródła danych ODBC. Teraz możemy z niego korzystać. Wykonujemy operację [OK] tyle razy, ile jest to konieczne, aby wyjść z kreatora ODBC.

Jeśli czytelnik nie posiada SGBD mySQL, może go bezpłatnie pobrać pod adresem URL [http://www.mysql.com]. Poniżej przedstawiamy procedurę tworzenia źródła danych ODBC w programie Access. Pierwsze kroki są identyczne jak te opisane wcześniej. Dodajemy nowe źródło danych systemowych:

Image

Wybranym sterownikiem będzie [Microsoft Access Driver]. Wybieramy [Terminer], aby przejść do definicji źródła ODBC:

Image

Należy podać następujące informacje:

Nom de la source de données
nazwa, która będzie określać źródło danych ODBC. Każda aplikacja systemu Windows będzie mogła uzyskać dostęp do źródła za pomocą tej nazwy
Description
dowolny tekst opisujący źródło danych
Base de données
pełna nazwa pliku ACCESS, który ma być przetwarzany

6.2.3. Nowa klasa dostępu do danych

Wróćmy do struktury MVC naszej aplikacji:

Image

Na powyższym schemacie klasa [impotsData] odpowiada za pobieranie danych. W tym przypadku będzie musiała pobrać je z bazy MySQL [dbimpots]. Z poprzedniej wersji tej aplikacji wiemy już, że [impotsData] jest klasą abstrakcyjną, którą należy derywować za każdym razem, gdy chcemy dostosować ją do nowego źródła danych. Przypomnijmy strukturę tej klasy abstrakcyjnej:


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

        ' metoda dostępu do danych
        Public MustOverride Function getData() As Object()

        ' metoda weryfikacji danych
        Protected Function checkData() As Integer
            ' weryfikuje pozyskane dane
...
        End Function

        ' sprawdza poprawność zawartości tablicy
        Protected Function check(ByRef tableau() As Decimal, ByVal n As Integer) As Boolean
        ...
        End Function
    End Class
End Namespace

Klasa dziedzicząca po [impotsData] musi zaimplementować dwie metody:

  • konstruktor, jeśli konstruktor bez argumentów klasy [impotsData] nie jest dla niej odpowiedni
  • metodę [getData], która zwraca trzy tablice (limites, coeffr, coeffn)

Tworzymy klasę [impotsODBC], która będzie pobierać dane (limity, coeffr, coeffn) ze źródła ODBC, któremu nadamy nazwę:


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

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

        ' zmienne instancji
        Protected DSNimpots As String

        ' konstruktor
        Public Sub New(ByVal DSNimpots As String)
            ' odnotowujemy trzy informacje
            Me.DSNimpots = DSNimpots
        End Sub

        Public Overrides Function getdata() As Object()
            ' inicjuje trzy tablice: limites, coeffr, coeffn na podstawie
            ' zawartości tabeli [impots] z bazy ODBC DSNimpots
            ' limity, coeffr, coeffn to trzy kolumny tej tabeli
            ' może wygenerować różne wyjątki

            Dim connectString As String = "DSN=" + DSNimpots + ";"         ' chaîne de connexion à la base
            Dim impotsConn As OdbcConnection = Nothing         ' la connexion
            Dim sqlCommand As OdbcCommand = Nothing         ' la commande SQL
            ' zapytanie SELECT
            Dim selectCommand As String = "select limites,coeffr,coeffn from impots"
            ' tabele służące do pobierania danych
            Dim aLimites As New ArrayList
            Dim aCoeffR As New ArrayList
            Dim aCoeffN As New ArrayList
            Try
                ' próbuje się uzyskać dostęp do bazy danych
                impotsConn = New OdbcConnection(connectString)
                impotsConn.Open()
                ' tworzy się obiekt polecenia
                sqlCommand = New OdbcCommand(selectCommand, impotsConn)
                ' wykonuje się zapytanie
                Dim myReader As OdbcDataReader = sqlCommand.ExecuteReader()
                ' Analiza pobranej tabeli
                While myReader.Read()
                    ' dane z bieżącego wiersza są umieszczane w tablicach
                    aLimites.Add(myReader("limites"))
                    aCoeffR.Add(myReader("coeffr"))
                    aCoeffN.Add(myReader("coeffn"))
                End While
                ' zwolnienie zasobów
                myReader.Close()
                impotsConn.Close()
            Catch e As Exception
                Throw New Exception("Erreur d'accès à la base de données (" + e.Message + ")")
            End Try
            ' tabele dynamiczne są umieszczane w tabelach statycznych
            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
            ' sprawdzanie pobranych danych
            Dim erreur As Integer = checkData()
            ' jeśli dane są nieprawidłowe, to zgłaszany jest wyjątek
            If Not valide Then Throw New Exception("Les données des tranches d'impôts sont invalides (" + erreur.ToString + ")")
            ' w przeciwnym razie zwracane są trzy tabele
            Return New Object() {limites, coeffr, coeffn}
        End Function
    End Class
End Namespace

Przyjrzyjmy się producentowi:


        ' konstruktor
        Public Sub New(ByVal DSNimpots As String)
            ' zapisuje się trzy informacje
            Me.DSNimpots = DSNimpots
        End Sub

Jako parametr otrzymuje nazwę źródła ODBC, w którym znajdują się dane do pobrania. Konstruktor ogranicza się do zapamiętania tej nazwy. Metoda [getData] odpowiada za odczytanie danych z tabeli [impots] i umieszczenie ich w trzech tablicach (limites, coeffr, coeffn). Omówmy jej kod:

  • parametry połączenia ze źródłem danych ODBC są zdefiniowane, ale połączenie nie zostało otwarte
             ' ciąg połączenia z bazą danych
            Dim connectString As String = "DSN=" + DSNimpots + ";"
            ' tworzymy obiekt połączenia z bazą danych – to połączenie nie jest otwarte
            Dim impotsConn As OdbcConnection = New OdbcConnection(connectString)
  • definiuje się trzy obiekty [ArrayList] w celu pobrania danych z tabeli [impots]:

             ' tabele służące do pobierania danych
            Dim aLimites As New ArrayList
            Dim aCoeffR As New ArrayList
            Dim aCoeffN As New ArrayList
  • Cały kod dostępu do bazy danych jest otoczony blokiem try/catch w celu obsługi ewentualnych błędów dostępu. Otwieramy połączenie z bazą danych:

                 ' próbuje się uzyskać dostęp do bazy danych
                impotsConn = New OdbcConnection(connectString)
                impotsConn.Open()
  • na otwartym połączeniu wykonujemy polecenie [select]. Otrzymujemy obiekt [OdbcDataReader], który pozwoli nam przeglądać wiersze tabeli wynikowej zapytania SELECT:

                 ' tworzy się obiekt polecenia
                Dim sqlCommand As OdbcCommand = New OdbcCommand(selectCommand, impotsConn)
                ' wykonuje się zapytanie
                Dim myReader As OdbcDataReader = sqlCommand.ExecuteReader()
  • Przeglądamy tabelę wyników wiersz po wierszu. W tym celu wykorzystujemy metodę [Read] obiektu [OdbcDataReader] uzyskanego wcześniej. Metoda ta wykonuje dwie czynności:
    • przechodzi o jeden wiersz dalej w tabeli. Na początku znajdujemy się przed pierwszym wierszem
    • zwraca wartość logiczną [true], jeśli udało się przejść do następnego wiersza, a [false] w przeciwnym razie; ten ostatni przypadek oznacza, że wszystkie wiersze zostały już przetworzone.

Kolumny bieżącego wiersza obiektu [OdbcDataReader] są uzyskiwane za pomocą funkcji OdbcDataReader. Otrzymujemy obiekt reprezentujący wartość kolumny. Przeglądamy całą tabelę, aby umieścić jej zawartość w trzech obiektach [ArrayList]:


                 ' Analiza pobranej tabeli
                While myReader.Read()
                    ' dane z bieżącego wiersza są umieszczane w tablicach
                    aLimites.Add(myReader("limites"))
                    aCoeffR.Add(myReader("coeffr"))
                    aCoeffN.Add(myReader("coeffn"))
  • Po wykonaniu tej czynności zwalniamy zasoby związane z połączeniem:
                 ' zwolnienie zasobów
                myReader.Close()
                impotsConn.Close()
  • Zawartość trzech obiektów [ArrayList] jest przenoszona do trzech standardowych tablic:

             ' tabele dynamiczne są umieszczane w tabelach statycznych
            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
  • po przeniesieniu danych z tabeli [impots] do trzech tablic pozostaje jedynie sprawdzić ich zawartość za pomocą metody [checkData] klasy bazowej [impotsData]:
             ' sprawdzanie pobranych danych
            Dim erreur As Integer = checkData()
             ' jeśli dane są nieprawidłowe, to zgłaszany jest wyjątek
            If Not valide Then Throw New Exception("Les données des tranches d'impôts sont invalides (" + erreur.ToString + ")")
             ' w przeciwnym razie zwracane są trzy tabele
            Return New Object() {limites, coeffr, coeffn}

6.2.4. Testy klasy dostępu do danych

Program testowy mógłby wyglądać następująco:

Option Explicit On 
Option Strict On

' przestrzenie nazw
Imports System
Imports Microsoft.VisualBasic

Namespace st.istia.univangers.fr

     ' strona testowa
    Module testimpots
        Sub Main(ByVal arguments() As String)
             ' interaktywny program do obliczania podatku
             ' użytkownik wprowadza trzy dane za pomocą klawiatury: żonaty nbEnfants wynagrodzenie
             ' następnie program wyświetla podatek do zapłaty
            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"

             ' sprawdzenie parametrów programu
            If arguments.Length <> 1 Then
                 ' komunikat o błędzie
                Console.Error.WriteLine(syntaxe1)
                 ' koniec
                Environment.Exit(1)
            End If
             ' pobieranie argumentów
            Dim DSNimpots As String = arguments(0)

             ' tworzenie obiektu podatku
            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

             ' pętla nieskończona
            While True
                 ' na początku brak błędów
                Dim erreur As Boolean = False

                 ' żądane są parametry obliczenia podatku
                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()

                 ' co należy zrobić?
                If paramètres Is Nothing Or paramètres = "" Then
                    Exit While
                End If

                 ' sprawdzanie liczby argumentów w wprowadzonej linii
                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
                     ' sprawdzanie poprawności parametrów
                     ' żonaty
                    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
                     ' wynagrodzenie
                    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
                     ' parametry są poprawne – obliczamy podatek
                    Console.Out.WriteLine(("impôt=" & objImpot.calculer(marié = "o", nbEnfants, salaire).ToString + " euro(s)"))
                End If
            End While
        End Sub
    End Module
End Namespace

Aplikacja jest uruchamiana z jednym parametrem:

  • DSNimpots: nazwa źródła danych ODBC, które ma zostać wykorzystane

Obliczenie podatku odbywa się za pomocą obiektu typu [impot], utworzonego w momencie uruchomienia aplikacji:


             ' tworzenie obiektu podatku
            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

Po uruchomieniu aplikacja wielokrotnie prosi użytkownika o wpisanie z klawiatury trzech informacji niezbędnych do obliczenia podatku:

  • stan cywilny: o – żonaty/zamężna, n – niezamężna/nieżonaty
  • liczba dzieci
  • jego roczne wynagrodzenie

Kompilacja wszystkich klas:

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

Następnie kompilowany jest program testowy:

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

Program testowy jest najpierw uruchamiany z wykorzystaniem źródła danych ODBC MySQL:

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)

Zmieniamy źródło na ODBC, aby użyć źródła 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. Widoki aplikacji internetowej

Są to te same widoki, co w poprzedniej aplikacji: formulaire.aspx i erreurs.aspx

6.2.6. Kontrolery aplikacji [global.asax, main.aspx]

Należy zmodyfikować jedynie kontroler [global.asax]. Jest on bowiem odpowiedzialny za utworzenie obiektu [impot] podczas uruchamiania aplikacji. Jedynym parametrem konstruktora tego obiektu jest obiekt typu [impotsData], którego zadaniem jest pobieranie danych. Parametr ten zmienia się zatem dla każdego nowego typu źródła danych. Kontroler [global.asax.vb] przyjmuje następujący kształt:


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)
        ' tworzy się obiekt podatku
        Dim objImpot As impot
        Try
            objImpot = New impot(New impotsODBC(ConfigurationSettings.AppSettings("DSNimpots")))
            ' obiekt jest umieszczany w aplikacji
            Application("objImpot") = objImpot
            ' brak błędu
            Application("erreur") = False
        Catch ex As Exception
            'wystąpił błąd, odnotowuje się go w aplikacji
            Application("erreur") = True
            Application("message") = ex.Message
        End Try
    End Sub
End Class

Źródłem danych obiektu [impot] jest teraz obiekt [impotODBC]. Ten ostatni ma jako parametr nazwę DSN źródła danych ODBC, które ma być wykorzystywane. Zamiast wpisywania tej nazwy na stałe w kodzie, umieszczamy ją w pliku konfiguracyjnym aplikacji o nazwie [web.config]:


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

Wiadomo, że wartość klucza C w sekcji <appSettings> pliku [web.config] jest uzyskiwana w kodzie aplikacji za pomocą [ConfigurationSettings.AppSettings(C)].

Aby ustalić przyczynę wyjątku, komunikat o nim jest rejestrowany w aplikacji, tak aby był dostępny na potrzeby zapytań. Kontroler [main.aspx.vb] uwzględni ten komunikat na swojej liście błędów:


    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' przede wszystkim sprawdzamy, czy aplikacja zainicjowała się poprawnie
        If CType(Application("erreur"), Boolean) Then
            ' przekierowujemy na stronę błędów
            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
        ' pobieramy akcję do wykonania
...

6.2.7. Podsumowanie zmian

Aplikacja jest gotowa do testów. Wymieńmy zmiany wprowadzone w stosunku do poprzedniej wersji:

  1. utworzono nową klasę dostępu do danych
  2. kontroler [global.asax.vb] został zmodyfikowany w dwóch miejscach: podczas tworzenia obiektu [impot] oraz podczas rejestrowania w aplikacji komunikatu związanego z ewentualnym wyjątkiem
  3. kontroler [main.aspx.vb] został zmodyfikowany w jednym miejscu w celu wyświetlenia poprzedniego komunikatu o wyjątku
  4. dodano plik [web.config]

Prace modyfikacyjne przeprowadzono głównie w pliku c.a.d, poza aplikacją internetową. Było to możliwe dzięki architekturze aplikacji MVC, która oddziela kontroler od klas biznesowych. Na tym właśnie polega zaleta tej architektury. Można by wykazać, że przy użyciu odpowiedniego pliku konfiguracyjnego [web.config] można było uniknąć jakichkolwiek zmian w kontrolerze aplikacji. W pliku [web.config] można podać nazwę klasy dostępu do danych, która ma zostać dynamicznie zainicjowana, a także różne parametry niezbędne do tego zainicjowania. Dzięki tym informacjom plik [global.asax] może utworzyć instancję obiektu dostępu do danych. Zmiana źródła danych sprowadza się wówczas do:

  • utworzenia klasy dostępu do tego źródła, jeśli jeszcze nie istnieje
  • zmodyfikowanie pliku [web.config], aby umożliwić dynamiczne utworzenie instancji tej klasy w pliku [global.asax]

6.2.8. Testowanie aplikacji internetowej

Wszystkie powyższe pliki umieszcza się w folderze <application-path>.

Image

W tym folderze tworzony jest podfolder [bin], w którym umieszczany jest zestaw [impot.dll] powstały w wyniku kompilacji plików klas biznesowych: [impots.vb, impotsData.vb, impotsArray.vb, impotsODBC.vb]. Poniżej przypominamy polecenie kompilacji niezbędne do tego celu:

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

Powyższy plik [impot.dll] należy umieścić w katalogu <application-path>\bin, aby aplikacja internetowa miała do niego dostęp. Serwer Cassini uruchamia się z parametrami (<application-path>,/impots2). Testy dają takie same wyniki jak w poprzedniej wersji, a obecność bazy danych jest dla użytkownika niezauważalna. Aby jednak zilustrować tę obecność, sprawiamy, że źródło ODBC staje się niedostępne poprzez zatrzymanie plików SGBD i MySQL, a następnie wysyłamy żądanie pod adresem URL [http://localhost/impots2/main.aspx]. Otrzymujemy następującą odpowiedź:

Image

6.3. Przykład 3

6.3.1. Problem

W tym przykładzie zamierzamy rozwiązać ten sam problem, ponownie zmieniając źródło danych obiektu [impot] utworzonego przez aplikację internetową. Tym razem nowym źródłem danych będzie baza danych ACCESS, do której uzyskamy dostęp za pośrednictwem sterownika OLEDB. Naszym celem jest pokazanie innego sposobu uzyskania dostępu do bazy danych.

6.3.2. Źródło danych OLEDB

Dane będą znajdować się w tabeli o nazwie [IMPOTS] w bazie ACCESS. Zawartość tej tabeli będzie następująca:

Image

6.3.3. Klasa dostępu do danych

Wróćmy do struktury MVC naszej aplikacji:

Image

  • Na powyższym schemacie klasa [impotsData] jest odpowiedzialna za pobieranie danych. Tym razem będzie musiała to zrobić ze źródła OLEDB.

Tworzymy klasę [impotsOLEDB], która będzie pobierać dane (limites, coeffr, coeffn) ze źródła ODBC, któremu nadamy nazwę:


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

        ' zmienne instancji
        Protected chaineConnexion As String

        ' konstruktor
        Public Sub New(ByVal chaineConnexion As String)
            ' zapisuje się trzy informacje
            Me.chaineConnexion = chaineConnexion
        End Sub

        Public Overrides Function getData() As Object()
            ' inicjuje trzy tablice: „limites”, „coeffr” i „coeffn” na podstawie
            ' zawartości tabeli [impots] z bazy OLEDB [chaineConnexion]
            ' limity, coeffr, coeffn to trzy kolumny tej tabeli
            ' może wywołać różne wyjątki

            ' tworzy się obiekt DataAdapter w celu odczytania danych ze źródła OLEDB
            Dim adaptateur As New OleDbDataAdapter("select limites,coeffr,coeffn from impots", chaineConnexion)
            ' tworzy się obraz w pamięci wyniku zapytania 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
            ' pobierana jest zawartość tabeli impots
            Dim lignesImpots As DataRowCollection = contenu.Rows
            ' określa się rozmiar tablic odbiorczych
            Me.limites = New Decimal(lignesImpots.Count - 1) {}
            Me.coeffr = New Decimal(lignesImpots.Count - 1) {}
            Me.coeffn = New Decimal(lignesImpots.Count - 1) {}
            ' przenosimy zawartość tabeli impots do tablic
            Dim i As Integer
            Dim ligne As DataRow
            Try
                For i = 0 To lignesImpots.Count - 1
                    ' wiersz i tabeli
                    ligne = lignesImpots.Item(i)
                    ' pobierana jest zawartość wiersza
                    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
            ' sprawdzamy pobrane dane
            Dim erreur As Integer = checkData()
            ' jeśli dane są nieprawidłowe, to zgłaszamy wyjątek
            If Not valide Then Throw New Exception("Les données des tranches d'impôts sont invalides (" + erreur.ToString + ")")
            ' w przeciwnym razie zwracane są trzy tablice
            Return New Object() {limites, coeffr, coeffn}
        End Function
    End Class
End Namespace

Przyjrzyjmy się producentowi:


         ' konstruktor
        Public Sub New(ByVal chaineConnexion As String)
             ' zapisuje się trzy informacje
            Me.chaineConnexion = chaineConnexion
        End Sub

Jako parametr otrzymuje ciąg połączenia ze źródłem OLEDB, w którym znajdują się dane do pobrania. Sterownik ogranicza się do jego zapamiętania. Ciąg połączenia zawiera wszystkie parametry niezbędne sterownikowi OLEDB do nawiązania połączenia ze źródłem OLEDB. Zazwyczaj jest on dość złożony. Aby poznać ciąg połączenia bazy danych ACCESS, można skorzystać z narzędzia [WebMatrix]. Uruchamiamy to narzędzie. Wyświetla się okno umożliwiające połączenie się ze źródłem danych:

Dzięki ikonie oznaczonej strzałką powyżej można nawiązać połączenie z dwoma typami baz danych Microsoft: SQL Server i ACCESS. Wybierzmy ACCESS:

Image

Użyliśmy przycisku [...], aby wskazać bazę ACCESS. Potwierdzamy działanie kreatora. W zakładce [Data] ikony symbolizują połączenie:

Image

Teraz utwórzmy nowy plik .aspx za pomocą [Files/New File]:

Image

Otrzymujemy pusty arkusz, na którym możemy zaprojektować nasz interfejs internetowy:

Image

Przeciągnijmy z zakładki [Data] tabelę [impots] na powyższy arkusz. Otrzymujemy następujący wynik:

Image

Kliknijmy prawym przyciskiem myszy na obiekt [AccessDataSourceControl] poniżej, aby uzyskać dostęp do jego właściwości:

Image

Ciąg połączenia OLEDB z bazą ACCESS jest określony przez powyższą właściwość [ConnectionString]:

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

Widać, że ciąg ten składa się z części stałej oraz części zmiennej, którą jest po prostu nazwa pliku ACCESS. Wykorzystamy ten fakt do wygenerowania ciągu połączenia z naszym źródłem danych OLEDB.

Wróćmy teraz do naszej klasy [impotsOLEDB]. Metoda [getData] odpowiada za odczytanie danych z tabeli [impots] i umieszczenie ich w trzech tablicach (limites, coeffr, coeffn). Omówmy jej kod:

  • definiujemy obiekt [DataAdapter], który pozwoli nam przenieść do pamięci wynik zapytania select o nazwie SQL. W tym celu definiujemy zapytanie [select], które ma zostać wykonane, i powiązujemy je z obiektem [DataAdapter]. Konstruktor tego obiektu wymaga również podania ciągu połączenia, którego będzie używał do połączenia się ze źródłem OLEDB

             ' tworzymy obiekt DataAdapter w celu odczytania danych ze źródła OLEDB
            Dim adaptateur As New OleDbDataAdapter("select limites,coeffr,coeffn from impots", chaineConnexion)
  • wykonujemy polecenie [select] za pomocą metody [Fill] obiektu [DataAdapter]. Wynik operacji [select] jest wprowadzany do obiektu [DataTable] utworzonego na tę okazję. Obiekt [DataTable] stanowi obraz w pamięci tabeli bazy danych c.a.d – zbioru wierszy i kolumn. Obsługujemy wyjątek, który może wystąpić, jeśli na przykład ciąg połączenia jest nieprawidłowy.

             ' tworzymy obraz w pamięci wyniku zapytania 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
  • W obiekcie [contenu] znajduje się tabela [impots], pobrana przez obiekt [select]. Obiekt [DataTable] jest tabelą, a więc zbiorem wierszy. Dostęp do nich można uzyskać poprzez właściwość [rows] obiektu [datatable]:

             ' pobieramy zawartość tabeli impots
            Dim lignesImpots As DataRowCollection = contenu.Rows
  • każdy element kolekcji [lignesImpots] jest obiektem typu [DataRow] reprezentującym wiersz tabeli. Kolumny tej tabeli są dostępne za pośrednictwem obiektu [DataRow] poprzez jego właściwość [Item]. [DataRow].[Item(i)] to kolumna nr i w wierszu [DataRow]. Przeglądając zbiór wierszy (zbiór DataRows z lignesImpots) oraz zbiory kolumn każdego wiersza, można uzyskać całą tabelę:
             ' określa się rozmiar tablic odbiorczych
            Me.limites = New Decimal(lignesImpots.Count - 1) {}
            Me.coeffr = New Decimal(lignesImpots.Count - 1) {}
            Me.coeffn = New Decimal(lignesImpots.Count - 1) {}
             ' przenosimy zawartość tabeli impots do tablic
            Dim i As Integer
            Dim ligne As DataRow
            Try
                For i = 0 To lignesImpots.Count - 1
                     ' wiersz i tabeli
                    ligne = lignesImpots.Item(i)
                     ' pobierana jest zawartość wiersza
                    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
  • po przeniesieniu danych z tabeli [impots] do trzech tablic pozostaje jedynie sprawdzić ich zawartość za pomocą metody [checkData] klasy bazowej [impotsData]:
            ' sprawdzamy pobrane dane
            Dim erreur As Integer = checkData()
             ' jeśli dane są nieprawidłowe, to zgłaszamy wyjątek
            If Not valide Then Throw New Exception("Les données des tranches d'impôts sont invalides (" + erreur.ToString + ")")
             ' w przeciwnym razie zwracane są trzy tablice
            Return New Object() {limites, coeffr, coeffn}

6.3.4. Testy klasy dostępu do danych

Program testowy mógłby wyglądać następująco:

Option Explicit On 
Option Strict On

' przestrzenie nazw
Imports System
Imports Microsoft.VisualBasic

Namespace st.istia.univangers.fr

     ' strona testowa
    Module testimpots
        Sub Main(ByVal arguments() As String)
             ' interaktywny program do obliczania podatku
             ' użytkownik wprowadza trzy dane za pomocą klawiatury: żonaty nbEnfants wynagrodzenie
             ' następnie program wyświetla podatek do zapłaty
            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"

             ' sprawdzenie parametrów programu
            If arguments.Length <> 1 Then
                 ' komunikat o błędzie
                Console.Error.WriteLine(syntaxe1)
                 ' koniec
                Environment.Exit(1)
            End If
             ' pobierane są argumenty
            Dim chemin As String = arguments(0)
             ' przygotowywanie ciągu połączenia
            Dim chaineConnexion As String = "Provider=Microsoft.Jet.OLEDB.4.0; Ole DB Services=-4; Data Source=" + chemin

             ' tworzenie obiektu podatku
            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

             ' pętla nieskończona
            While True
                 ' na początku brak błędów
                Dim erreur As Boolean = False

                 ' prosimy o podanie parametrów obliczania podatku
                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()

                 ' czy należy coś zrobić?
                If paramètres Is Nothing Or paramètres = "" Then
                    Exit While
                End If

                 ' sprawdzanie liczby argumentów w wprowadzonej linii
                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
                     ' sprawdzanie poprawności parametrów
                     ' żonaty
                    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
                     ' wynagrodzenie
                    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
                     ' parametry są poprawne – obliczamy podatek
                    Console.Out.WriteLine(("impôt=" & objImpot.calculer(marié = "o", nbEnfants, salaire).ToString + " euro(s)"))
                End If
            End While
        End Sub
    End Module
End Namespace

Aplikacja jest uruchamiana z jednym parametrem:

  • bdACCESS: nazwa pliku ACCESS, który ma zostać przetworzony

Obliczenie podatku odbywa się za pomocą obiektu typu [impot], utworzonego w momencie uruchomienia aplikacji:


             ' pobieramy argumenty
            Dim chemin As String = arguments(0)
            ' przygotowuje się ciąg połączenia
            Dim chaineConnexion As String = "Provider=Microsoft.Jet.OLEDB.4.0; Ole DB Services=-4; Data Source=" + chemin

             ' tworzenie obiektu podatku
            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

Łańcuch połączenia ze źródłem OLEDB został utworzony na podstawie informacji uzyskanych za pomocą [WebMatrix].

Po uruchomieniu aplikacja wielokrotnie prosi użytkownika o wpisanie z klawiatury trzech informacji niezbędnych do obliczenia podatku:

  • stan cywilny: o – żonaty/zamężna, n – niezamężna/nieżonaty
  • liczba dzieci
  • jego roczne wynagrodzenie

Kompilowany jest cały zestaw klas:

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

Plik [impots.mdb] jest umieszczany w folderze aplikacji testowej, a aplikacja jest uruchamiana w następujący sposób:

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)

Aplikację można uruchomić przy użyciu nieprawidłowego pliku 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. Widoki aplikacji internetowej

Są to te same, co w poprzedniej aplikacji: formulaire.aspx i erreurs.aspx

6.3.6. Kontrolery aplikacji [global.asax, main.aspx]

Należy zmodyfikować jedynie kontroler [global.asax]. Odpowiada on bowiem za utworzenie obiektu [impot] podczas uruchamiania aplikacji. Jedynym parametrem konstruktora tego obiektu jest obiekt typu [impotsData], odpowiedzialny za pobieranie danych. Parametr ten ulega zatem zmianie, ponieważ zmieniamy źródło danych. Kontroler [global.asax.vb] przyjmuje następujący kształt:


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)
        ' tworzy się obiekt podatku
        Dim objImpot As impot
        Try
            objImpot = New impot(New impotsOLEDB(ConfigurationSettings.AppSettings("chaineConnexion")))
            ' umieszczamy obiekt w aplikacji
            Application("objImpot") = objImpot
            ' brak błędu
            Application("erreur") = False
        Catch ex As Exception
            'wystąpił błąd, odnotowuje się go w aplikacji
            Application("erreur") = True
            Application("message") = ex.Message
        End Try
    End Sub
End Class

Źródłem danych obiektu [impot] jest teraz obiekt [impotOLEDB]. Ten ostatni ma jako parametr ciąg połączenia źródła danych OLEDB, które ma być wykorzystywane. Znajduje się on w pliku konfiguracyjnym aplikacji [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>

Kontroler [main.aspx] pozostaje bez zmian.

6.3.7. Podsumowanie zmian

Aplikacja jest gotowa do testów. Wymieńmy zmiany wprowadzone w stosunku do poprzedniej wersji:

  1. utworzono nową klasę dostępu do danych
  2. kontroler [global.asax.vb] został zmodyfikowany w jednym miejscu: utworzono obiekt [impot]
  3. dodano plik [web.config]

6.3.8. Testowanie aplikacji internetowej

Wszystkie powyższe pliki umieszczono w folderze <application-path>.

Image

W tym folderze tworzony jest podfolder [bin], w którym umieszczany jest zestaw [impot.dll] powstały w wyniku kompilacji plików klas biznesowych: [impots.vb, impotsData.vb, impotsArray.vb, impotsOLEDB.vb]. Poniżej przypominamy polecenie kompilacji:

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

Plik [impot.dll] wygenerowany przez to polecenie należy umieścić w katalogu <application-path>\bin, aby aplikacja internetowa miała do niego dostęp. Serwer Cassini uruchamia się z parametrami (<application-path>,/impots3). Testy dają takie same wyniki jak w poprzedniej wersji.

6.4. Przykład 4

6.4.1. Problem

Teraz zamierzamy przekształcić naszą aplikację w aplikację do symulacji obliczeń podatkowych. Użytkownik będzie mógł wykonywać kolejne obliczenia podatkowe, a ich wyniki będą mu prezentowane w nowym widoku wyglądającym mniej więcej tak:

Image

6.4.2. Struktura aplikacji MVC

Struktura aplikacji MVC przyjmuje następujący wygląd:

Image

Pojawia się nowy widok [simulations.aspx], którego zrzut ekranu właśnie przedstawiliśmy. Klasą dostępu do danych będzie klasa [impotsODBC] z przykładu 2.

6.4.3. Widoki aplikacji internetowej

Widok [erreurs.aspx] pozostaje bez zmian. Widok [formulaire.aspx] ulega niewielkiej zmianie. Kwota podatku nie pojawia się już bowiem w tym widoku. Znajduje się ona teraz w widoku [simulations.aspx]. W związku z tym po uruchomieniu aplikacji użytkownikowi wyświetla się następująca strona:

Image

Ponadto widok [formulaire] zawiera skrypt JavaScript, który sprawdza poprawność wprowadzonych danych przed wysłaniem ich na serwer, jak pokazuje poniższy przykład:

Image

Kod prezentacji wygląda następująco:


<%@ page src="formulaire.aspx.vb" inherits="formulaire" AutoEventWireup="false"%>
<html>
    <head>
        <title>Impôt</title>
        <script language="javascript">
        function calculer(){
          // sprawdzanie parametrów przed wysłaniem ich na serwer
        with(document.frmImpots){
          //liczba elementów podrzędnych
          champs=/^\s*(\d+)\s*$/.exec(txtEnfants.value);
          if(champs==null){
            // model nie został zweryfikowany
            alert("Le nombre d'enfants n'a pas été donné ou est incorrect");
            txtEnfants.focus();
            return;
          }//if
          //wynagrodzenie
          champs=/^\s*(\d+)\s*$/.exec(txtSalaire.value);
          if(champs==null){
            // szablon nie został zweryfikowany
            alert("Le salaire n'a pas été donné ou est incorrect");
            txtSalaire.focus();
            return;
          }//jeśli
          // w porządku – wysyłamy formularz na serwer
          submit();
        }//with
      }//obliczyć  
        </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>

Pola dynamiczne na stronie są takie same jak w poprzednich wersjach. Zniknęło dynamiczne pole kwoty podatku. Przycisk [Calculer] nie jest już przyciskiem typu [submit]. Jest to przycisk typu [button], a po jego kliknięciu uruchamiana jest funkcja JavaScript [calculer()]:


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

Formularzowi nadano nazwę [frmImpots], aby można było odwołać się do niego w skrypcie [calculer]:


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

Funkcja JavaScript [calculer] wykorzystuje wyrażenia regularne do sprawdzania poprawności pól formularzy [document.frmImpots.txtEnfants] i [document.frmImpots.txtSalaire]. Jeśli wprowadzone wartości są poprawne, są one wysyłane na serwer przez funkcję [document.frmImpots.submit()].

Strona prezentacyjna pobiera swoje pola dynamiczne z kontrolera [formulaire.aspx.vb] w następujący sposób:


Imports System.Collections.Specialized

Public Class formulaire
    Inherits System.Web.UI.Page

    ' pola strony
    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
        ' pobieramy poprzednie żądanie z kontekstu
        Dim form As NameValueCollection = Context.Items("formulaire")
        ' przygotowujemy stronę do wyświetlenia
        ' przyciski opcji
        rdouichecked = ""
        rdnonchecked = "checked"
        If form("rdMarie").ToString = "oui" Then
            rdouichecked = "checked"
            rdnonchecked = ""
        End If
        ' reszta
        txtEnfants = CType(form("txtEnfants"), String)
        txtSalaire = CType(form("txtSalaire"), String)
    End Sub
End Class

Kontroler [formulaire.aspx.vb] jest identyczny z poprzednimi wersjami, z tą różnicą, że nie musi już pobierać pola [txtImpot] z kontekstu, ponieważ pole to zniknęło ze strony.

Widok [simulations.aspx] wygląda następująco:

Image

i odpowiada następującemu kodowi prezentacji:


<%@ 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>

Ten kod zawiera trzy pola dynamiczne:

simulationsHTML
kod HTML z listy symulacji w postaci wierszy tabeli HTML
href
adres URL linku
lien
tekst linku

Są one generowane przez moduł sterujący [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
        'pobieramy symulacje z kontekstu
        Dim simulations As ArrayList = CType(context.Items("simulations"), ArrayList)
        ' każda symulacja to tablica zawierająca 4 elementy typu string
        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
        ' pobieramy pozostałe elementy z kontekstu
        href = context.Items("href").ToString
        lien = context.Items("lien").ToString
    End Sub
End Class

Kontroler strony pobiera informacje umieszczone przez kontroler aplikacji w kontekście strony:

Context.Items("simulations")
obiekt ArrayList zawierający listę symulacji do wyświetlenia. Każdy element jest tablicą składającą się z 4 ciągów znaków przedstawiających informacje (stan cywilny, dzieci, wynagrodzenie, podatek) dotyczące danej symulacji.
Context.Items("href")
adres URL linku
Context.Items("lien")
tekst linku

6.4.4. Kontrolery [global.asax, main.aspx]

Przypomnijmy schemat MVC naszej aplikacji:

Image

Kontroler [main.aspx] musi obsłużyć trzy akcje:

  • init: odpowiada pierwszemu żądaniu klienta. Kontroler wyświetla widok [formulaire.aspx]
  • calcul: odpowiada żądaniu obliczenia podatku. Jeśli dane z formularza wprowadzania danych są poprawne, podatek jest obliczany za pomocą klasy biznesowej [impotsODBC]. Kontroler zwraca klientowi widok [simulations.aspx] wraz z wynikiem bieżącej symulacji oraz wszystkich poprzednich. Jeśli dane z formularza wprowadzania danych są nieprawidłowe, kontroler zwraca widok [erreurs.aspx] wraz z listą błędów i linkiem umożliwiającym powrót do formularza.
  • powrót: oznacza powrót do formularza po wystąpieniu błędu. Kontroler wyświetla widok [formulaire.aspx] w stanie, w jakim został zatwierdzony przed wystąpieniem błędu.

W tej nowej wersji zmieniła się jedynie akcja [calcul]. Jeśli dane są prawidłowe, powinna ona bowiem prowadzić do widoku [simulations.aspx], podczas gdy wcześniej prowadziła do widoku [formulaire.aspx]. Kontroler [main.aspx.vb] przyjmuje następujący kształt:


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
        ' przede wszystkim sprawdzamy, czy aplikacja zainicjowała się poprawnie
...
        ' wykonujemy akcję
        Select Case action
            Case "init"
                ' inicjowanie aplikacji
                initAppli()
            Case "calcul"
                ' obliczanie podatku
                calculImpot()
            Case "retour"
                ' powrót do formularza
                retourFormulaire()
            Case "effacer"
                ' inicjowanie aplikacji
                initAppli()
            Case Else
                ' nieznana akcja = inicjalizacja
                initAppli()
        End Select
    End Sub

...
    Private Sub calculImpot()
        ' zapisywanie wprowadzonych danych
        Session.Item("formulaire") = Request.Form
        ' sprawdzanie poprawności wprowadzonych danych
        Dim erreurs As ArrayList = checkData()
        ' w przypadku błędów wyświetla się komunikat
        If erreurs.Count <> 0 Then
            ' przygotowuje się stronę błędów
            context.Items("href") = "main.aspx?action=retour"
            context.Items("lien") = "Retour au formulaire"
            context.Items("erreurs") = erreurs
            Server.Transfer("erreurs.aspx")
        End If
        ' nie ma błędów – oblicza się podatek
        Dim impot As Long = CType(Application("objImpot"), impot).calculer( _
        Request.Form("rdMarie") = "oui", _
        CType(Request.Form("txtEnfants"), Integer), _
        CType(Request.Form("txtSalaire"), Long))
        ' dodaje się wynik do istniejących symulacji
        Dim simulations As ArrayList
        If Not Session.Item("simulations") Is Nothing Then
            simulations = CType(Session.Item("simulations"), ArrayList)
        Else
            simulations = New ArrayList
        End If
        ' dodano bieżącą symulację
        Dim simulation() As String = New String() {Request.Form("rdMarie").ToString, _
        Request.Form("txtEnfants").ToString, Request.Form("txtSalaire").ToString, _
        impot.ToString}
        simulations.Add(simulation)
        ' umieszczamy symulacje w sesji i kontekście
        context.Items("simulations") = simulations
        Session.Item("simulations") = simulations
        ' wyświetla stronę wyników
        context.Items("href") = "main.aspx?action=retour"
        context.Items("lien") = "Retour au formulaire"
        Server.Transfer("simulations.aspx", True)
    End Sub
...
End Class

Powyżej zachowaliśmy jedynie te elementy, które są niezbędne do zrozumienia zmian występujących wyłącznie w funkcji [calculImpots]:

  • po pierwsze funkcja ta zapisuje formularz [Request.Form] w sesji, aby móc odtworzyć formularz w stanie, w jakim został zatwierdzony. Należy to zrobić w każdym przypadku, ponieważ niezależnie od tego, czy operacja zakończy się odpowiedzią [erreurs.aspx], czy odpowiedzią [simulations.aspx], powrót do formularza odbywa się poprzez link [Retour au formulaire]. Aby poprawnie przywrócić formularz, należy wcześniej zapisać jego wartości w sesji.
  • Jeśli wprowadzone dane okażą się poprawne, funkcja umieszcza bieżącą symulację (stan cywilny, dzieci, wynagrodzenie, podatek) na liście symulacji. Znajduje się ona w sesji powiązanej z kluczem „simulations”.
  • Lista symulacji jest ponownie umieszczana w sesji do wykorzystania w przyszłości. Jest ona również umieszczana w bieżącym kontekście, ponieważ właśnie tam oczekuje na nią widok [simulations.aspx]
  • Widok [simulations.aspx] jest wyświetlany po umieszczeniu w kontekście pozostałych informacji, których oczekuje

6.4.5. Podsumowanie zmian

Aplikacja jest gotowa do testowania. Wymieńmy zmiany wprowadzone w stosunku do poprzednich wersji:

  1. utworzono nowy widok
  2. kontroler [main.aspx.vb] został zmodyfikowany w jednym miejscu: obsługa akcji [calcul]

6.4.6. Testowanie aplikacji internetowej

Zachęcamy czytelnika do przeprowadzenia testów. Przypomnijmy procedurę. Wszystkie pliki aplikacji znajdują się w folderze <application-path>. W tym folderze utworzono podfolder [bin], w którym umieszczono zestaw [impot.dll] powstały w wyniku kompilacji plików klas biznesowych: [impots.vb, impotsData.vb, impotsArray.vb, impotsODBC.vb. Plik [impot.dll] wygenerowany przez to polecenie należy umieścić w katalogu <application-path>\bin, aby aplikacja internetowa miała do niego dostęp. Serwer Cassini uruchamia się z parametrami (<application-path>,/impots4).

6.5. Conclusion

Powyższe przykłady pokazały na konkretnym przypadku mechanizmy powszechnie stosowane w tworzeniu aplikacji internetowych. Systematycznie korzystaliśmy z architektury MVC ze względu na jej wartość edukacyjną. Te same przykłady można by było zrealizować inaczej, a być może prościej, bez tej architektury. Jednak zapewnia ona znaczne korzyści, gdy aplikacja staje się nieco bardziej złożona i zawiera wiele stron.

Moglibyśmy kontynuować nasze przykłady na różne sposoby. Oto kilka z nich:

  • użytkownik mógłby chcieć zachować swoje symulacje na przestrzeni czasu. Przeprowadzałby symulacje w dniu J i mógłby je odzyskać na przykład w dniu J+3. Możliwym rozwiązaniem tego problemu jest wykorzystanie plików cookie. Wiemy, że token sesji między serwerem a klientem jest przesyłany za pomocą tego mechanizmu. Można by go również wykorzystać do przesyłania symulacji między klientem a serwerem.
    • W tym samym czasie, gdy serwer wysyła stronę z wynikami symulacji, wysyła w swoich nagłówkach HTTP plik cookie zawierający ciąg znaków reprezentujący symulacje. Ponieważ znajdują się one w obiekcie [ArrayList], konieczne jest przekształcenie tego obiektu do postaci [String]. Serwer nadałby plikowi cookie okres ważności, na przykład 30 dni.
    • Przeglądarka klienta przechowuje otrzymane pliki cookie w pliku i odsyła je za każdym razem, gdy wysyła żądanie do serwera, który je wysłał, o ile są one nadal ważne (okres ważności nie upłynął). W ramach symulacji serwer otrzyma ciąg znaków [String], który będzie musiał przekształcić w obiekt [ArrayList].

Pliki cookie są zarządzane przez obiekt [Response.Cookies] podczas wysyłania ich do klienta oraz przez obiekt [Request.Cookies] podczas ich odbioru na serwerze.

  • Powyższy mechanizm może stać się dość obciążający, jeśli liczba symulacji jest znaczna. Ponadto często zdarza się, że użytkownik okresowo czyści swoje pliki cookie, usuwając je wszystkie, nawet jeśli jednocześnie zezwala przeglądarce na ich używanie. W związku z tym prędzej czy później utracimy plik cookie związany z symulacjami. Można więc rozważyć przechowywanie tych danych na serwerze zamiast na komputerze klienta, na przykład w bazie danych. Aby powiązać symulacje z konkretnym użytkownikiem, aplikacja mogłaby rozpoczynać się od etapu identyfikacji, wymagającego podania loginu i hasła, które same są przechowywane w bazie danych lub innym rodzaju magazynie danych.
  • Możemy również chcieć zabezpieczyć działanie naszej aplikacji. Obecnie opiera się ona na dwóch założeniach:
    • użytkownik zawsze przechodzi przez kontroler [main.aspx]
    • i w tym przypadku zawsze korzysta z akcji dostępnych na stronie, która została mu przesłana

Co się stanie na przykład, jeśli użytkownik bezpośrednio wywoła adres URL [http://localhost/impots4/formulaire.aspx]? Sytuacja ta jest mało prawdopodobna, ponieważ użytkownik nie wie o istnieniu tego adresu URL. Należy jednak ją przewidzieć. Można ją obsłużyć za pomocą kontrolera aplikacji [global.asax], który przetwarza wszystkie żądania kierowane do aplikacji. W ten sposób może on sprawdzić, czy żądany zasób to rzeczywiście [main.aspx].

Bardziej prawdopodobnym przypadkiem jest sytuacja, w której użytkownik nie korzysta z akcji dostępnych na stronie przesłanej mu przez serwer. Na przykład, co się stanie, jeśli użytkownik bezpośrednio wywoła adres URL [http://localhost/impots4/main.aspx?action=retour] bez uprzedniego wypełnienia formularza? Spróbujmy. Otrzymujemy następującą odpowiedź:

Image

Mamy awarię serwera. To normalne. W przypadku akcji [retour] kontroler oczekuje, że w sesji znajdzie obiekt [NameValueCollection] reprezentujący wartości formularza, które ma wyświetlić. Nie znajduje ich. Mechanizm kontrolera pozwala na eleganckie rozwiązanie tego problemu. Dla każdego żądania kontroler [main.aspx] może sprawdzić, czy żądana akcja jest rzeczywiście jedną z akcji strony wcześniej wysłanej do użytkownika. Można zastosować następujący mechanizm:

  • kontroler, przed wysłaniem odpowiedzi do klienta, zapisuje w jego sesji informację identyfikującą tę stronę
  • gdy otrzymuje nowe żądanie od klienta, sprawdza, czy żądana akcja rzeczywiście należy do ostatniej strony wysłanej do tego klienta
  • informacje łączące strony z akcjami dozwolonymi na tych stronach można wprowadzić do pliku konfiguracyjnego aplikacji o nazwie [web.config].
  • doświadczenie pokazuje, że kontrolery aplikacji mają szeroką wspólną podstawę i że możliwe jest stworzenie kontrolera generycznego, którego dostosowanie do konkretnej aplikacji odbywa się za pomocą pliku konfiguracyjnego. Taką ścieżkę obrało na przykład narzędzie [Struts] w dziedzinie programowania internetowego w języku Java.