Skip to content

9. Componenti server ASP - 3

9.1. Introduction

Continuiamo il nostro lavoro sull’interfaccia utente approfondendo le funzionalità dei componenti [DataList] e [DataGrid], in particolare per quanto riguarda l’aggiornamento dei dati che visualizzano

9.2. Gestione degli eventi associati ai dati dei componenti con collegamento dati

9.2.1. L'esempio

Consideriamo la seguente pagina:

La pagina comprende tre componenti associati a un elenco di dati:

  • un componente [DataList] denominato [DataList1]
  • un componente [DataGrid] denominato [DataGrid1]
  • un componente [Repeater] denominato [Repeater1]

L'elenco di dati associato è la tabella {"zero", "uno", "due", "tre"}. A ciascuno di questi dati è associato un gruppo di due pulsanti denominati [Infos1] e [Infos2]. L'utente fa clic su uno dei pulsanti e viene visualizzato un testo che riporta il nome del pulsante su cui ha fatto clic. In questa sede si intende illustrare come gestire un elenco di pulsanti o di link.

9.2.2. Configurazione dei componenti

Il componente [DataList1] è configurato come segue:


                        <asp:datalist id="DataList1" ... runat="server">
                            <SelectedItemStyle ...</SelectedItemStyle>
                            <HeaderTemplate>
                                [début]
                            </HeaderTemplate>
                            <FooterTemplate>
                                [fin]
                            </FooterTemplate>
                            <ItemStyle ...></ItemStyle>
                            <ItemTemplate>
                                <P><%# Container.DataItem %>
                                    <asp:Button runat="server" Text="Infos1" CommandName="infos1"></asp:Button>
                                    <asp:Button runat="server" Text="Infos2" CommandName="infos2"></asp:Button></P>
                            </ItemTemplate>
                            <FooterStyle ...></FooterStyle>
                            <HeaderStyle ...></HeaderStyle>
                        </asp:datalist>

Abbiamo tralasciato tutto ciò che riguardava l'aspetto del [DataList] per concentrarci esclusivamente sul suo contenuto:

  • la sezione <HeaderTemplate> definisce l'intestazione del [DataList] e la sezione <FooterTemplate> il suo piè di pagina.
  • la sezione <ItemTemplate> è il modello di visualizzazione utilizzato per ciascuno dei dati dell’elenco associato. In essa si trovano i seguenti elementi:
    • il valore del dato corrente dell’elenco dei dati associato al componente: <%# Container.DataItem %>
    • due pulsanti denominati rispettivamente [Infos1] e [Infos2]. La classe [Button] ha un attributo [CommandName] che viene utilizzato in questo contesto. Ci consentirà di determinare quale pulsante sia all’origine di un evento nel [DataList]. Per gestire il clic sui pulsanti, avremo un unico gestore di eventi che sarà collegato al [DataList] stesso e non ai pulsanti. Questo gestore riceverà un'informazione che gli indicherà su quale riga del [DataList] si è verificato il clic. L'attributo [CommandName] ci permetterà di sapere su quale pulsante della riga si è verificato.

Il componente [Repeater1] è configurato in modo molto simile:


                        <asp:repeater id="Repeater1" runat="server">
                            <HeaderTemplate>
                                [début]<hr />
                            </HeaderTemplate>
                            <FooterTemplate>
                                <hr />
                                [fin]
                            </FooterTemplate>
                            <SeparatorTemplate>
                                <hr />
                            </SeparatorTemplate>
                            <ItemTemplate>
                                <%# Container.DataItem %>
                                <asp:Button runat="server" Text="Infos1" CommandName="infos1"></asp:Button>
                                <asp:Button runat="server" Text="Infos2" CommandName="infos2"></asp:Button></P>
                            </ItemTemplate>
                        </asp:repeater>

È stata semplicemente aggiunta una sezione <SeparatorTemplate> affinché i dati successivi visualizzati dal componente siano separati da una barra orizzontale.

Infine, il componente [DataGrid1] è configurato come segue:


                        <asp:datagrid id="DataGrid1" ... runat="server" PageSize="2" AllowPaging="True">
                            <SelectedItemStyle ...></SelectedItemStyle>
                            <AlternatingItemStyle ...></AlternatingItemStyle>
                            <ItemStyle ...></ItemStyle>
                            <HeaderStyle ...></HeaderStyle>
                            <FooterStyle ....></FooterStyle>
                            <Columns>
                                <asp:ButtonColumn Text="Infos1" ButtonType="PushButton" CommandName="Infos1">
                              </asp:ButtonColumn>
                                <asp:ButtonColumn Text="Infos2" ButtonType="PushButton" CommandName="Infos2">
                              </asp:ButtonColumn>
                            </Columns>
                            <PagerStyle .... Mode="NumericPages"></PagerStyle>
                        </asp:datagrid>

Anche in questo caso abbiamo omesso le informazioni relative allo stile (colori, larghezze, ecc.). Ci troviamo nella modalità di generazione automatica delle colonne, che è la modalità predefinita di [DataGrid]. Ciò significa che ci saranno tante colonne quante ce ne sono nel source di dati. In questo caso, ce n’è una. Abbiamo aggiunto altre due colonne contrassegnate da <asp:ButtonColumn>. In esse definiamo informazioni analoghe a quelle definite per gli altri due componenti, oltre al tipo di pulsante, in questo caso [PushButton]. Il tipo predefinito è [LinkButton], c.a.d. un link. Inoltre, i dati saranno impaginati [AllowPaging=true] con una dimensione di pagina pari a due elementi [PageSize=2].

9.2.3. Il codice di presentazione della pagina

Il codice di presentazione della nostra pagina di esempio è stato inserito in un file [main.aspx]:


<%@ page codebehind="main.aspx.vb" inherits="vs.main" autoeventwireup="false" %>
<HTML>
    <HEAD>
    </HEAD>
    <body>
        <form runat="server">
            <P>Gestion d'événements de composants associés à des listes de données</P>
            <HR width="100%" SIZE="1">
            <table cellSpacing="1" cellPadding="1" bgColor="#ffcc00" border="1">
                <tr>
                    <td ...>DataList</td>
                    <td ...>DataGrid</td>
                    <td ...>Repeater</td>
                </tr>
                <tr>
                    <td ...>

                      <asp:datalist id="DataList1" ... runat="server">
                            <HeaderTemplate>
                                [début]
                            </HeaderTemplate>
                            <FooterTemplate>
                                [fin]
                            </FooterTemplate>
                            <ItemStyle ...></ItemStyle>
                            <ItemTemplate>
                                <P><%# Container.DataItem %>
                                    <asp:Button runat="server" Text="Infos1" CommandName="infos1"></asp:Button>
                                    <asp:Button runat="server" Text="Infos2" CommandName="infos2"></asp:Button></P>
                            </ItemTemplate>
                            <FooterStyle ...></FooterStyle>
                            <HeaderStyle ....></HeaderStyle>
                        </asp:datalist>

                        <P></P>
                    </td>
                    <td ...>

                       <asp:datagrid id="DataGrid1" ... runat="server" PageSize="2" AllowPaging="True">
                            <AlternatingItemStyle ...></AlternatingItemStyle>
                            <ItemStyle ...></ItemStyle>
                            <HeaderStyle ...></HeaderStyle>
                            <FooterStyle ...></FooterStyle>
                            <Columns>
                                <asp:ButtonColumn Text="Infos1" ButtonType="PushButton" CommandName="Infos1"> 
                              </asp:ButtonColumn>
                                <asp:ButtonColumn Text="Infos2" ButtonType="PushButton" CommandName="Infos2">
                               </asp:ButtonColumn>
                            </Columns>
                            <PagerStyle ... Mode="NumericPages"></PagerStyle>
                        </asp:datagrid>

                  </td>
                    <td ...>

                       <asp:repeater id="Repeater1" runat="server">
                            <HeaderTemplate>
                                [début]<hr />
                            </HeaderTemplate>
                            <FooterTemplate>
                                <hr />
                                [fin]
                            </FooterTemplate>
                            <SeparatorTemplate>
                                <hr />
                            </SeparatorTemplate>
                            <ItemTemplate>
                                <%# Container.DataItem %>
                                <asp:Button runat="server" Text="Infos1" CommandName="infos1"></asp:Button>
                                <asp:Button runat="server" Text="Infos2" CommandName="infos2"></asp:Button></P>
                            </ItemTemplate>
                        </asp:repeater>

                  </td>
                </tr>
            </table>
            <P><asp:label id="lblInfo" runat="server"></asp:label></P>
            <P></P>
        </form>
    </body>
</HTML>

Nel codice sopra riportato abbiamo omesso il codice di formattazione (colori, linee, dimensioni, ecc.)

9.2.4. Il codice di controllo della pagina

Il codice di controllo dell'applicazione è stato inserito nel file [main.aspx.vb]:

Public Class main
    Inherits System.Web.UI.Page

     ' componenti della pagina
    Protected WithEvents DataList1 As System.Web.UI.WebControls.DataList
    Protected WithEvents lblInfo As System.Web.UI.WebControls.Label
    Protected WithEvents DataGrid1 As System.Web.UI.WebControls.DataGrid
    Protected WithEvents Repeater1 As System.Web.UI.WebControls.Repeater

     ' la fonte dati
    Protected textes() As String = {"zéro", "un", "deux", "trois"}

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        If Not IsPostBack Then
             'collegamenti con la fonte dati
            DataList1.DataSource = textes
            DataGrid1.DataSource = textes
            Repeater1.DataSource = textes
            Page.DataBind()
        End If
    End Sub

    Private Sub DataList1_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles DataList1.ItemCommand
         ' si è verificato un evento su una delle righe di [datalist]
        lblInfo.Text = "Vous avez cliqué sur le bouton [" + e.CommandName + "] de l'élément [" + e.Item.ItemIndex.ToString + "] du composant [DataList]"
    End Sub

    Private Sub Repeater1_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.RepeaterCommandEventArgs) Handles Repeater1.ItemCommand
         ' si è verificato un evento su una delle righe del [repeater]
        lblInfo.Text = "Vous avez cliqué sur le bouton [" + e.CommandName + "] de l'élément [" + e.Item.ItemIndex.ToString + "] du composant [Repeater]"
    End Sub

    Private Sub DataGrid1_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.ItemCommand
         ' Si è verificato un evento su una delle righe del [datagrid]
        lblInfo.Text = "Vous avez cliqué sur le bouton [" + e.CommandName + "] de l'élément [" + e.Item.ItemIndex.ToString + "] de la page [" + DataGrid1.CurrentPageIndex.ToString() + "] du composant [DataGrid]"
    End Sub

    Private Sub DataGrid1_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles DataGrid1.PageIndexChanged
         ' cambio di pagina
        With DataGrid1
            .CurrentPageIndex = e.NewPageIndex
            .DataSource = textes
            .DataBind()
        End With
    End Sub
End Class

Commenti:

  • la fonte dati [textes] è una semplice tabella di stringhe di caratteri. Verrà collegata ai tre componenti presenti nella pagina
  • tale collegamento viene effettuato nella procedura [Page_Load] al momento della prima richiesta. Per le richieste successive, i tre componenti recupereranno i propri valori tramite il meccanismo di [VIEW_STATE].
  • I tre componenti dispongono di un gestore per l’evento [ItemCommand]. Questo evento si verifica quando si fa clic su un pulsante o su un link in una delle righe del componente. Il gestore riceve due informazioni:
    • fonte: il riferimento all’oggetto (pulsante o link) all’origine dell’evento
    • a: informazioni sull’evento di tipo [DataListCommandEventArgs], [RepeaterCommandEventArgs], [DataGridCommandEventArgs] a seconda dei casi. L’argomento a porta con sé diverse informazioni. In questo caso, due di esse ci interessano:
      • a.Item: rappresenta la riga in cui si è verificato l’evento, di tipo [DataListItem], [DataGridItem] o [RepeaterItem]. Indipendentemente dal tipo esatto, l’elemento [Item] possiede un attributo [ItemIndex] che indica il numero della riga [Item] del contenitore a cui appartiene. Qui visualizziamo questo numero di riga
      • a.CommandName: è l’attributo [CommandName] del pulsante (Button, LinkButton, ImageButton) all’origine dell’evento. Questa informazione, unita alla precedente, ci permette di sapere quale pulsante del contenitore è all’origine dell’evento [ItemCommand]

9.3. Applicazione - gestione di un elenco di abbonamenti

Ora che sappiamo come intercettare gli eventi che si verificano all’interno di un contenitore di dati, presentiamo un esempio che mostra come gestirli.

9.3.1. Introduzione

L’applicazione presentata simula un’applicazione per l’iscrizione a mailing list. Queste sono definite da un oggetto [DataTable] a tre colonne:

nome
tipo
ruolo
id
stringa
chiave primaria
thème
stringa
nome del tema dell'elenco
description
stringa
una descrizione degli argomenti trattati dalla lista

Per non appesantire il nostro esempio, l'oggetto [DataTable] sopra riportato verrà generato in modo arbitrario tramite codice. In un'applicazione reale, verrebbe probabilmente fornito da un metodo di una classe di accesso ai dati. La creazione della tabella delle liste avverrà nella procedura [Application_Start] e la tabella risultante verrà inserita nell'applicazione. La chiameremo tabella [dtThèmes]. L’utente si abbonerà ad alcuni dei temi di questa tabella. L’elenco dei suoi abbonamenti sarà conservato, anche in questo caso, in un oggetto [DataTable] denominato [dtAbonnements], la cui struttura sarà la seguente:

nome
tipo
ruolo
id
stringa
chiave primaria
thème
stringa
nome del tema dell'elenco

L'applicazione a pagina singola è la seguente:

n.
nome
tipo
proprietà
ruolo
1
dgThèmes
DataGrid
 
liste di distribuzione disponibili per l'iscrizione
2
dlAbonnements
DataList
 
elenco delle iscrizioni dell'utente alle liste precedenti
3
panelInfos
pannello
 
pannello informativo sull'argomento selezionato dall'utente con un link [Plus d'informations]
4
lblThème
Etichetta
fa parte di [panelInfos]
nome del tema
5
lblDescription
Etichetta
fa parte di [panelInfos]
Descrizione del tema
6
lblInfo
Etichetta
 
Messaggio informativo dell'applicazione

Il nostro esempio mira a illustrare l'utilizzo dei componenti [DataGrid] e [DataList], in particolare la gestione degli eventi che si verificano a livello delle righe di questi contenitori di dati. Pertanto, l’applicazione non dispone di un pulsante di conferma che memorizzi, ad esempio, in un database le scelte effettuate dall’utente. Tuttavia, è realistica. Siamo vicini a un’applicazione di e-commerce in cui un utente aggiungerebbe prodotti (abbonamenti) al proprio carrello.

9.3.2. Funzionamento

La prima schermata che vede l’utente è la seguente:

L’utente clicca sui link [Plus d'informations] per ottenere informazioni su un argomento. Queste vengono visualizzate in [panelInfos]:

Image

Clicca sui link [S'abonner] per iscriversi a un argomento. Gli argomenti scelti vengono inseriti nel componente [dlAbonnements]:

Image

L'utente potrebbe voler sottoscrivere un elenco a cui è già iscritto. Un messaggio lo avvisa:

Image

Infine, può annullare l’iscrizione a uno dei temi utilizzando i pulsanti [Retirer] sopra indicati. Non viene richiesta alcuna conferma. In questo caso non è necessaria, poiché l’utente può riiscriversi facilmente. Ecco la schermata dopo l’annullamento dell’iscrizione a [thème1]:

Image

9.3.3. Configurazione dei contenitori di dati

Il componente [dgThèmes] di tipo [DataGrid] è collegato a una fonte di tipo [DataTable]. La sua formattazione è stata effettuata tramite il collegamento [Mise en forme automatique] nel pannello delle proprietà del componente [DataGrid]. La definizione delle sue proprietà è stata invece effettuata tramite il collegamento [Générateur de proprités] dello stesso pannello. Il codice generato è il seguente (il codice di formattazione è omesso):


                        <asp:datagrid id="dgThèmes" AutoGenerateColumns="False" AllowPaging="True" PageSize="5" 
                            runat="server">
                            <ItemStyle ...></ItemStyle>
                            <HeaderStyle ...></HeaderStyle>
                            <FooterStyle ...></FooterStyle>
                            <Columns>
                                <asp:BoundColumn DataField="th&#232;me" HeaderText="Th&#232;me"></asp:BoundColumn>
                                <asp:ButtonColumn Text="Plus d'informations" CommandName="infos"></asp:ButtonColumn>
                                <asp:ButtonColumn Text="S'abonner" CommandName="abonner"></asp:ButtonColumn>
                            </Columns>
                            <PagerStyle HorizontalAlign="Center" ... Mode="NumericPages"></PagerStyle>
                        </asp:datagrid>

Si notino i seguenti punti:

AutoGenerateColumns=false
definiamo noi stessi le colonne da visualizzare nella sezione <columns>...</columns>
AllowPaging=true
PageSize=5
per l'impaginazione dei dati
<asp:BoundColumn>
definisce la colonna [thème] (HeaderText) di [DataGrid], che sarà collegata alla colonna [thème] della fonte dati (DataField)
<asp:ButtonColumn>
definisce due colonne di pulsanti (o link). Per distinguere i due link della stessa riga, si utilizzerà la loro proprietà [CommandName].

Il componente [DataGrid] non è completamente configurato. Lo sarà nel codice del controller.

Il componente [dlAbonnements] di tipo [DataList] è collegato a una fonte di tipo [DataTable]. La sua formattazione è stata effettuata tramite il collegamento [Mise en forme automatique] dal pannello delle proprietà del [DataList]. La definizione delle sue proprietà è stata invece effettuata direttamente nel codice di presentazione. Il codice è il seguente (il codice di formattazione è omesso):


                        <asp:DataList id="dlAbonnements" ... runat="server" >
                            <HeaderTemplate>
                                <div align="center">
                                    Vos abonnements</div>
                            </HeaderTemplate>
                            <ItemStyle ...></ItemStyle>
                            <ItemTemplate>
                                <TABLE>
                                    <TR>
                                        <TD><%#Container.DataItem("tema")%></TD>
                                        <TD>
                                            <asp:Button id="lnkRetirer" CommandName="retirer" runat="server" Text="Retirer" /></TD>
                                    </TR>
                                </TABLE>
                            </ItemTemplate>
                            <HeaderStyle ...></HeaderStyle>
                        </asp:DataList>
<HeaderTemplate>
definisce il testo dell'intestazione di [DataList]
<ItemTemplate>
definisce l'elemento corrente del [DataList] - qui è stata inserita una tabella a due colonne e una riga. La prima cella conterrà il nome del tema a cui l'utente desidera iscriversi, l'altra il pulsante [Retirer] che gli consente di annullare la propria scelta.

9.3.4. La pagina di presentazione

Il codice di presentazione [main.aspx] è il seguente:


<%@ page src="main.aspx.vb" inherits="main" autoeventwireup="false" %>
<HTML>
    <HEAD>
        <title></title>
    </HEAD>
    <body>
        <P>Indiquez les thèmes auxquels vous voulez vous abonner :</P>
        <HR width="100%" SIZE="1">
        <form runat="server">
            <table>
                <tr>
                    <td vAlign="top">
                        <asp:datagrid id="dgThèmes" ... runat="server">
...
                        </asp:datagrid>
                  </td>
                    <td vAlign="top">
                        <asp:DataList id="dlAbonnements" ... runat="server" GridLines="Horizontal" ShowFooter="False">
....
                        </asp:DataList>
                   </td>
                    <td vAlign="top">
                        <asp:Panel ID="panelInfo" Runat="server" EnableViewState="False">
                            <TABLE>
                                <TR>
                                    <TD bgColor="#99cccc">
                                        <asp:Label id="lblThème" runat="server"></asp:Label></TD>
                                </TR>
                                <TR>
                                    <TD bgColor="#ffff99">
                                        <asp:Label id="lblDescription" runat="server"></asp:Label></TD>
                                </TR>
                            </TABLE>
                        </asp:Panel>
                    </td>
                </tr>
            </table>
            <P>
                <asp:Label id="lblInfo" runat="server" EnableViewState="False"></asp:Label></P>
        </form>
    </body>
</HTML>

9.3.5. I file di controllo

Il controllo è suddiviso tra i file [global.asax] e [main.aspx]. Il file [global.asax] è il seguente:

<%@ Application src="global.asax.vb" inherits="global" %>

Il file associato [global.asax.vb] contiene il seguente codice:


Imports System.Web
Imports System.Web.SessionState
Imports System.Data
Imports System

Public Class global
    Inherits System.Web.HttpApplication

    Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' si inizializza la fonte dati
        Dim thèmes As New DataTable
        ' colonne
        With thèmes.Columns
            .Add("id", GetType(System.Int32))
            .Add("thème", GetType(System.String))
            .Add("description", GetType(System.String))
        End With
        ' la colonna id sarà la chiave primaria
        thèmes.Constraints.Add("cléprimaire", thèmes.Columns("id"), True)
        ' righe
        Dim ligne As DataRow
        For i As Integer = 0 To 10
            ligne = thèmes.NewRow
            ligne.Item("id") = i.ToString
            ligne.Item("thème") = "thème" + i.ToString
            ligne.Item("description") = "description du thème " + i.ToString
            thèmes.Rows.Add(ligne)
        Next
        ' si inserisce la fonte dati nell'applicazione
        Application("thèmes") = thèmes
    End Sub

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' inizio sessione - si crea una tabella degli abbonamenti vuota
        Dim dtAbonnements As New DataTable
        With dtAbonnements
            ' le colonne
            .Columns.Add("id", GetType(String))
            .Columns.Add("thème", GetType(String))
            ' la chiave primaria
            .PrimaryKey = New DataColumn() {.Columns("id")}
        End With
        ' la tabella viene inserita nella sessione
        Session.Item("abonnements") = dtAbonnements
    End Sub

End Class

La procedura [Application_Start], eseguita quando l'applicazione riceve la sua primissima richiesta, crea il [DataTable] dei temi a cui è possibile iscriversi. Viene creato in modo arbitrario con del codice. Ricordiamo la tecnica che abbiamo già incontrato. Si creano nell'ordine:

  • un oggetto [DataTable] vuoto, senza struttura e senza dati
  • la struttura della tabella definendone le colonne (nome e tipo di dati che contengono)
  • le righe della tabella che rappresentano i dati utili

Qui abbiamo aggiunto una chiave primaria. È la colonna "id" a fungere da chiave primaria. Ci sono diversi modi per dirlo. In questo caso abbiamo utilizzato un vincolo. In SQL, un vincolo è una regola che i dati di una riga devono rispettare affinché questa possa essere aggiunta a una tabella. Esistono vincoli di ogni tipo. Il vincolo “Primary Key” impone alla colonna a cui è applicato di contenere valori univoci e non vuoti. Una chiave primaria può infatti essere costituita da un’espressione che coinvolge i valori di più colonne. [DataTable].Constraints è l’insieme dei vincoli di una determinata tabella. Per aggiungere un vincolo, si utilizza il metodo [DataTable.Constraints.Add]. Questo metodo presenta diverse firme. In questo caso, è stato utilizzato il metodo [Add(Byval nom as String, Byval colonne as DataColumn, Byval cléPrimaire as Boolean)]:

nom
nome del vincolo - può essere qualsiasi
colonne
colonna che fungerà da chiave primaria - di tipo [DataColumn]
cléPrimaire
deve essere impostato su [vrai] per rendere [colonne] una chiave primaria. Se è impostato su [cléPrimaire=false], si applica solo il vincolo di valori univoci su [colonne]

Per rendere la colonna denominata "id" la chiave primaria della tabella [dtAbonnements], si scrive quindi:

dtAbonnements.Constraints.Add("xxx",dtAbonnements.Columns("id"),true)

La procedura [Session_Start], eseguita quando l’applicazione riceve la prima richiesta da un cliente. Serve a creare oggetti specifici per ciascun cliente che devono persistere attraverso le sue diverse richieste. La procedura costruisce la tabella [DataTable] degli abbonamenti del cliente. Viene creata solo la sua struttura, poiché inizialmente questa tabella è vuota. Verrà popolata man mano che vengono effettuate le richieste. Anche in questo caso, la colonna "id" funge da chiave primaria. Per dichiarare questo vincolo è stata utilizzata una tecnica diversa:

[DataTable].PrimaryKey
è l’array delle colonne che formano la chiave primaria – in questo caso è stato dichiarato un array a elemento singolo: la colonna denominata “id”

Quando la richiesta del cliente raggiunge il controller [main.aspx], i due oggetti [DataTable] sono disponibili nell’applicazione per la tabella dei temi e nella sessione per la tabella degli abbonamenti. Il controller [main.aspx.vb] è il seguente:


Imports System.Data

Public Class main
    Inherits System.Web.UI.Page

    Protected WithEvents dgThèmes As System.Web.UI.WebControls.DataGrid
    Protected WithEvents lblThème As System.Web.UI.WebControls.Label
    Protected WithEvents lblDescription As System.Web.UI.WebControls.Label
    Protected WithEvents dlAbonnements As System.Web.UI.WebControls.DataList
    Protected WithEvents lblInfo As System.Web.UI.WebControls.Label
    Protected WithEvents panelInfo As System.Web.UI.WebControls.Panel

    Protected dtThèmes As DataTable
    Protected dtAbonnements As DataTable

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
    End Sub

    Private Sub liaisons()
...
    End Sub

    Private Sub dgThèmes_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles dgThèmes.PageIndexChanged
...
    End Sub

    Private Sub dgThèmes_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles dgThèmes.ItemCommand
...
    End Sub

    Private Sub infos(ByVal id As String)
...
    End Sub

    Private Sub abonner(ByVal id As String)
...
    End Sub

    Private Sub dlAbonnements_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles dlAbonnements.ItemCommand
...
    End Sub
End Class

La procedura [Page_Load] ha come funzione principale quella di:

  • recuperare le due tabelle [dtThèmes] e [dtAbonnements], che si trovano rispettivamente nell’applicazione e nella sessione, al fine di renderle disponibili a tutti i metodi della pagina
  • collegare i dati di queste due fonti ai rispettivi contenitori. Ciò avviene solo durante la prima richiesta. Nelle richieste successive, il collegamento non deve essere effettuato sistematicamente e, quando è necessario, a volte è necessario attendere un evento successivo a [Page_Load] per eseguirlo.

Il codice di [Page_Load] è il seguente:


    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' si recuperano le fonti dei dati
        dtThèmes = CType(Application("thèmes"), DataTable)
        dtAbonnements = CType(Session("abonnements"), DataTable)
        ' collegamento dati
        If Not IsPostBack Then
            liaisons()
        End If
        ' alcune informazioni vengono nascoste
        panelInfo.Visible = False
    End Sub

    Private Sub liaisons()
        'si collega la fonte dati al componente [datagrid]
        With dgThèmes
            .DataSource = dtThèmes
            .DataKeyField = "id"
        End With
        ' si collega la fonte dati al componente [datalist]
        With dlAbonnements
            .DataSource = dtAbonnements
            .DataKeyField = "id"
        End With
        ' si assegnano i dati ai componenti
        Page.DataBind()
    End Sub

Nella procedura [liaisons], utilizziamo la proprietà [DataKeyField] dei componenti [DataList] e [DataGrid] per definire la colonna della fonte dati che servirà a identificare in modo univoco le righe dei contenitori. Di norma, questa colonna è la chiave primaria della fonte dati, ma non è obbligatorio. È sufficiente che la colonna sia priva di duplicati e di valori vuoti. Per il contenitore [dgThèmes], sarà la colonna "id" della fonte [dtThèmes] a fungere da chiave primaria, mentre per il contenitore [dlAbonnements] sarà la colonna "id" della fonte [dtAbonnements]. Non è necessario che la colonna che funge da chiave primaria per il contenitore venga visualizzata da quest’ultimo. In questo caso, nessuno dei due contenitori visualizza la colonna della chiave primaria. Il vantaggio di un contenitore dotato di chiave primaria è che questa consente di individuare facilmente nella fonte di dati le informazioni relative alla riga del contenitore in cui si è verificato un evento. Infatti, capita spesso che, partendo dalla riga di un contenitore in cui si è verificato un evento, sia necessario intervenire sulla riga corrispondente della fonte dati ad essa collegata. La chiave primaria facilita questo lavoro.

L’impaginazione del [DataGrid] è gestita in modo classico:


    Private Sub dgThèmes_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles dgThèmes.PageIndexChanged
        ' cambio di pagina
        dgThèmes.CurrentPageIndex = e.NewPageIndex
        ' collegamento
        liaisons()
    End Sub

Le operazioni relative ai collegamenti [Plus d'informations] e [S'abonner] sono gestite dalla procedura [dgThèmes_ItemCommand]:


    Private Sub dgThèmes_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles dgThèmes.ItemCommand
        ' evento su una riga del [datagrid]
        Dim commande As String = e.CommandName
        Select Case commande
            Case "infos"
                infos(dgThèmes.DataKeys(e.Item.ItemIndex))
            Case "abonner"
                abonner(dgThèmes.DataKeys(e.Item.ItemIndex))
        End Select
        ' collegamento
        liaisons()
    End Sub

Per distinguerli, si utilizza il fatto che entrambi i link hanno un attributo [CommandName]. A seconda del valore di questo attributo, si chiama la procedura [infos] o [abonner], passando in entrambi i casi la chiave "id" associata all'elemento di [DataGrid] in cui si è verificato l'evento. Grazie a queste informazioni, la procedura [info] visualizzerà le informazioni relative al tema scelto dall’utente:


    Private Sub infos(ByVal id As String)
        ' informazioni relative alla chiave id
        ' si recupera la riga di [datatable] corrispondente alla chiave
        Dim ligne As DataRow
        ligne = dtThèmes.Rows.Find(id)
        If Not ligne Is Nothing Then
            ' vengono visualizzate le informazioni
            lblThème.Text = CType(ligne("thème"), String)
            lblDescription.Text = CType(ligne("description"), String)
            panelInfo.Visible = True
        End If
    End Sub

Poiché la tabella [dtThèmes] ha una chiave primaria, il metodo [dtThèmes.Rows.Find("P")] consente di trovare la riga con chiave primaria P. Se viene trovata, si ottiene un oggetto [DataRow]. In questo caso, dobbiamo cercare la riga con chiave primaria [id], dove [id] viene passato come parametro. Se la riga viene trovata, si inseriscono le informazioni [thème] e [description] relative a tale riga nel pannello informativo, che viene poi reso visibile.

La procedura [abonner(id)] deve aggiungere il tema chiave [id] all’elenco degli abbonamenti. Il suo codice è il seguente:


    Private Sub abonner(ByVal id As String)
         ' Abbonamento al tema con ID chiave
         ' si recupera la riga di [datatable] corrispondente alla chiave
        Dim ligne As DataRow
        ligne = dtThèmes.Rows.Find(id)
        If Not ligne Is Nothing Then
             ' si verifica se non si è già abbonati
            Dim abonnement As DataRow
            abonnement = dtAbonnements.Rows.Find(id)
            If Not abonnement Is Nothing Then
                 ' si segnala l'errore
                lblInfo.Text = "Vous êtes déjà abonné au thème [" + ligne("thème") + "]"
            Else
                 ' si aggiunge il tema agli abbonamenti
                abonnement = dtAbonnements.NewRow
                abonnement("id") = id
                abonnement("thème") = ligne("thème")
                dtAbonnements.Rows.Add(abonnement)
                 ' si creano i collegamenti
                liaisons()
            End If
        End If
    End Sub

Nell'elenco dei temi [dtThèmes], si cerca innanzitutto la riga con la chiave [id]. Se la si trova, si verifica che quel tema non sia già presente nell’elenco degli abbonamenti per evitare di inserirlo due volte. In tal caso, viene visualizzato un messaggio di errore. Altrimenti, si aggiunge un nuovo abbonamento alla tabella [dtAbonnements] e si effettuano i collegamenti dei componenti dell’elenco dati alle rispettive fonti.

Quando l’utente fa clic su un pulsante [Retirer], è necessario eliminare un elemento dalla tabella [dtAbonnements]. Ciò avviene tramite la seguente procedura:


    Private Sub dlAbonnements_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles dlAbonnements.ItemCommand
        ' si rimuove un abbonamento
        Dim commande As String = e.CommandName
        If commande = "retirer" Then
            ' si rimuove l'abbonamento da [datatable]
            With dtAbonnements.Rows
                .Remove(.Find(dlAbonnements.DataKeys(e.Item.ItemIndex)))
            End With
            ' collegamenti
            liaisons()
        End If
    End Sub

Si verifica innanzitutto l'attributo [CommandName] dell'elemento all'origine dell'evento. In realtà è piuttosto superfluo, poiché il pulsante [Retirer] è l'unico controllo in grado di generare un evento nel componente [DataList]. Non vi è quindi alcuna ambiguità. Per eliminare una riga da un oggetto [DataTable], si utilizza il metodo [DataList.Remove(DataRow)] che rimuove dalla tabella la riga di tipo [DataRow] passata come parametro. Tale riga viene individuata dal metodo [DataList.Find], al quale è stata passata la chiave primaria della riga ricercata. Una volta eliminata la riga, si esegue il collegamento dei dati ai componenti

9.4. Gestire un [DataList] impaginato

Riprendiamo l’esempio precedente per impaginare il componente [DataList] che rappresenta l’elenco degli abbonamenti dell’utente. A differenza del componente [DataGrid], il componente [DataList] non offre alcuna funzionalità per l’impaginazione. Vedremo che la sua implementazione è complessa, il che ci farà apprezzare appieno la paginazione automatica del componente [DataGrid].

9.4.1. Funzionamento

L’unica differenza è l’impaginazione del componente [DataList]. Le pagine conterranno due abbonamenti. Se l’utente ha cinque abbonamenti, si avranno tre pagine. La prima sarà la seguente:

Image

La seconda pagina si ottiene tramite il link [Suivant]:

Image

La terza pagina:

Image

Si noti che i link [Précédent] e [Suivant] sono visibili solo se, rispettivamente, c'è una pagina precedente e una pagina successiva alla pagina corrente.

9.4.2. Codice di presentazione

I link [Précédent] e [Suivant] si ottengono aggiungendo un tag <FooterTemplate> al [DataList]:


                        <asp:datalist id="dlAbonnements" runat="server" ...>
....
                            <FooterTemplate>
                                <asp:LinkButton id="lnkPrecedent" runat="server" CommandName="precedent">Précédent</asp:LinkButton>
                                <asp:LinkButton id="lnkSuivant" runat="server" CommandName="suivant">Suivant</asp:LinkButton>
                            </FooterTemplate>
....
                        </asp:datalist>

9.4.3. Codice di controllo

Il file associato [global.asax.vb] subisce le seguenti modifiche:

...

Public Class global
    Inherits System.Web.HttpApplication

    Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
...
    End Sub

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
         ' inizio sessione - si crea una tabella degli abbonamenti vuota
        Dim dtAbonnements As New DataTable
        With dtAbonnements
             ' le colonne
            .Columns.Add("id", GetType(String))
            .Columns.Add("thème", GetType(String))
             ' la chiave primaria
            .PrimaryKey = New DataColumn() {.Columns("id")}
        End With
         ' la tabella viene inserita nella sessione
        Session.Item("abonnements") = dtAbonnements
         ' la pagina corrente è la pagina 0
        Session.Item("pAC") = 0
         ' il numero di abbonamenti in questa pagina è 0
        Session.Item("nbAC") = 0
    End Sub

Oltre alla tabella degli abbonamenti [dtAbonnements], nella sessione vengono inserite altre due informazioni:

pAC
di tipo [Integer]: si tratta del numero della pagina corrente visualizzata durante l'ultima richiesta
nbAC
di tipo [Integer] - numero di righe visualizzate nella pagina corrente precedente

All'inizio di una sessione, il numero della pagina corrente e il numero di righe di tale pagina sono pari a zero.

Il controller [main.aspx.vb] si evolve come segue:

....

Public Class main
    Inherits System.Web.UI.Page

....

     ' dati dell'applicazione
    Protected dtThèmes As DataTable
    Protected dtAbonnements As DataTable
    Protected dtPA As DataTable    ' la page d'abonnements affichée
    Protected Const nbAP As Integer = 2    ' nbre abonnements par page
    Protected pAC As Integer    ' page abonnement courant
    Protected nbAC As Integer    ' nombre d'abonnements dans page courante

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
    End Sub

    Private Sub terminer()
...
    End Sub

    Private Sub dgThèmes_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles dgThèmes.PageIndexChanged
...
    End Sub

    Private Sub dgThèmes_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles dgThèmes.ItemCommand
...
    End Sub

    Private Sub infos(ByVal id As String)
...
    End Sub

    Private Sub abonner(ByVal id As String)
...
    End Sub

    Private Sub dlAbonnements_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles dlAbonnements.ItemCommand
...
    End Sub

    Private Sub changePAC()
...
    End Sub

    Private Sub setLiens(ByVal ctl As Control, ByVal blPrec As Boolean, ByVal blSuivant As Boolean)
...
    End Sub
End Class

Qui vengono definiti nuovi dati relativi all'impaginazione degli abbonamenti:


    Protected dtPA As DataTable    ' la page d'abonnements affichée
    Protected Const nbAP As Integer = 2    ' nbre abonnements par page
    Protected pAC As Integer    ' page abonnement courant
    Protected nbAC As Integer    ' nombre d'abonnements dans page courante

Alcune di queste informazioni vengono memorizzate nella sessione e recuperate ad ogni richiesta nella procedura [Page_Load]:


    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' vengono recuperate le fonti dei dati
        dtThèmes = CType(Application("thèmes"), DataTable)
        dtAbonnements = CType(Session("abonnements"), DataTable)
        ' e le informazioni di visualizzazione degli abbonamenti
        pAC = CType(Session("pAC"), Integer)
        nbAC = CType(Session("nbAC"), Integer)
        ' collegamento dati
        If Not IsPostBack Then
            ' visualizzazione di un elenco di abbonamenti vuoto
            terminer()
        End If
        ' si nascondono alcune informazioni
        panelInfo.Visible = False
    End Sub

Le informazioni recuperate [pAC] e [nbAC] riguardano la pagina degli abbonamenti visualizzata durante la richiesta precedente:

pAC
è il numero della pagina corrente visualizzata durante la richiesta precedente
nbAC
numero di righe visualizzate nella pagina corrente

Il metodo [terminer] associa i componenti alle rispettive fonti di dati, proprio come faceva il metodo [liaisons] nell'applicazione precedente. La novità, in questo caso, è il collegamento del [DataList] con la tabella [dtPA], che rappresenta la pagina degli abbonamenti da visualizzare:


    Private Sub terminer()
        ' collegamento della fonte dati al componente [datagrid]
        With dgThèmes
            .DataSource = dtThèmes
            .DataKeyField = "id"
        End With
        ' si collega la pagina degli abbonamenti al componente [datalist] tenendo conto della pagina corrente pAC
        changePAC()
        ' si visualizza la pagina p
        With dlAbonnements
            .DataSource = dtPA
            .DataKeyField = "id"
        End With
        ' si assegnano i dati ai componenti
        Page.DataBind()
        ' gestione dei collegamenti [précédent] e [suivant] del [datalist]
        Dim blprec As Boolean = pAC <> 0
        Dim blsuivant As Boolean = pAC <> (dtAbonnements.Rows.Count - 1) \ nbAP
        Dim nbLiensTrouvés As Integer = 0
        setLiens(dlAbonnements, blprec, blsuivant, nbLiensTrouvés)
        ' si salvano le informazioni della pagina corrente nella sessione
        Session("pAC") = pAC
        Session("nbAC") = dtPA.Rows.Count
    End Sub

Si prega di notare quanto segue:

  • la sorgente [dtPA] dipende dal numero di pagina corrente [pAC] da visualizzare. La variabile [pAC] è una variabile globale della classe, gestita dai metodi che devono modificare tale numero di pagina corrente. È il metodo [changePAC] che si occupa di costruire la tabella [dtPA], che sarà collegata al componente [dlAbonnements].
  • Il metodo [setLiens] ha il compito di visualizzare o nascondere i collegamenti [Précédent] e [Suivant] a seconda che la pagina corrente [pAC] visualizzata sia preceduta e seguita da una pagina. Ha quattro parametri:
    • [dlAbonnements]: il controllo [DataList], di cui esploreremo l’albero dei controlli per individuare i due link. Infatti, sebbene questi due link si trovino in una posizione specifica, ovvero il piè di pagina del [DataList], non sembra esserci un modo semplice per farvi riferimento direttamente. In ogni caso, qui non è stato trovato.
    • [blPrecedent]: valore booleano da assegnare alla proprietà [visible] del collegamento [Precedent] – è vero se la pagina corrente è diversa da 0
    • [blSuivant]: valore booleano da assegnare alla proprietà [visible] del collegamento [Suivant] - è vero se la pagina corrente non è l'ultima pagina dell'elenco degli abbonamenti
    • [nbLiensTrouvés]: un parametro di output che conta il numero di link trovati. Non appena tale numero è pari a due, il metodo termina.
  • Le informazioni [pAC] e [nbAC] vengono salvate nella sessione per la richiesta successiva.

Il metodo [changePAC] crea la tabella [dtPA] che verrà collegata al componente [dlAbonnements]. Lo fa in base al numero [pAC] della pagina corrente da visualizzare. La tabella [dtPA] deve visualizzare alcune righe della tabella degli abbonamenti [dtAbonnements]. Si ricorda che questa tabella è memorizzata nella sessione e viene aggiornata (aumentata o diminuita) man mano che vengono effettuate le richieste. Si inizia impostando l’intervallo [premier,dernier] dei numeri di riga della tabella [dtAbonnements] che la tabella [dtPA] deve visualizzare:


    Private Sub changePAC()
        ' imposta la pagina pAC come pagina corrente degli abbonamenti
        ' gestione delle pagine di [datalist]
        Dim nbAbonnements = dtAbonnements.Rows.Count
        Dim dernièrePage = (nbAbonnements - 1) \ nbAP
        ' primo e ultimo abbonamento
        If pAC < 0 Then pAC = 0
        If pAC > dernièrePage Then pAC = dernièrePage
        Dim premier As Integer = pAC * nbAP
        Dim dernier As Integer = (pAC + 1) * nbAP - 1
        If dernier > nbAbonnements - 1 Then dernier = nbAbonnements - 1

Fatto ciò, è possibile creare la tabella [dtPA]. Si definisce innanzitutto la sua struttura [id,thème], quindi la si compila copiando le righe di [dtAbonnements] il cui numero rientra nell'intervallo [premier,dernier] calcolato in precedenza.


         ' creazione del datatable dtpa
        dtPA = New DataTable
        With dtPA
            ' le colonne
            .Columns.Add("id", GetType(String))
            .Columns.Add("thème", GetType(String))
            ' la chiave primaria
            .PrimaryKey = New DataColumn() {.Columns("id")}
        End With
        Dim abonnement As DataRow
        For i As Integer = premier To dernier
            abonnement = dtPA.NewRow
            With abonnement
                .Item("id") = dtAbonnements.Rows(i).Item("id")
                .Item("thème") = dtAbonnements.Rows(i).Item("thème")
            End With
            dtPA.Rows.Add(abonnement)
        Next
    End Sub

Al termine del metodo [changePAC], la tabella [dtPA] è stata creata e può essere collegata al componente [DataList]. Ciò avviene nel metodo [terminer]. Nello stesso metodo, si utilizza la procedura [setLiens] per impostare lo stato dei collegamenti [Précédent] e [Suivant] del [DataList]. Il codice di questa procedura è il seguente:


    Private Sub setLiens(ByVal ctl As Control, ByVal blPrec As Boolean, ByVal blSuivant As Boolean, ByRef nbLiensTrouvés As Integer)
        ' si cercano i collegamenti [précédent] e [suivant]
        ' nella struttura gerarchica dei controlli di [datalist]
        ' Sono stati trovati tutti i collegamenti?
        If nbLiensTrouvés = 2 Then Exit Sub
        ' analisi dei controlli secondari
        Dim c As Control
        For Each c In ctl.Controls
            ' si lavora innanzitutto in profondità: i collegamenti si trovano in fondo all'albero
            setLiens(c, blPrec, blSuivant, nbLiensTrouvés)
            ' collegamento [Précédent]?
            If c.ID = "lnkPrecedent" Then
                CType(c, LinkButton).Visible = blPrec
                nbLiensTrouvés += 1
            End If
            ' link [Suivant]?
            If c.ID = "lnkSuivant" Then
                CType(c, LinkButton).Visible = blSuivant
                nbLiensTrouvés += 1
            End If
        Next
    End Sub

La procedura è ricorsiva. Innanzitutto, cerca, tra i controlli figli del componente [dlAbonnements] i componenti denominati [lnkPrecedent] e [lnkSuivant], che rappresentano le identità (attributo ID) dei due link di paginazione. Li cerca innanzitutto partendo dalla base dell’albero dei controlli, poiché è lì che si trovano. Non appena viene individuato un link, il contatore [nbLiensTrouvés] viene incrementato e la proprietà [visible] del link viene popolata con un valore passato come parametro alla procedura. Non appena vengono trovati entrambi i collegamenti, l’albero dei controlli non viene più esplorato e la procedura ricorsiva termina.

Abbiamo detto che il metodo [changePAC], che imposta la fonte dati [dtPA] per il componente [dlAbonnements], operava con il numero [pAC] della pagina corrente da visualizzare. Diverse procedure modificano questo numero:


    Private Sub abonner(ByVal id As String)
        ' abbonamento al tema con ID chiave
..
            ' si aggiunge il tema agli abbonamenti
..
             ' aggiornamento del numero della pagina corrente - ora è l'ultima pagina
            pAC = (dtAbonnements.Rows.Count - 1) \ nbAP
             ' collegamenti dati
            terminer()
        End If
    End Sub

Dopo aver aggiunto un abbonamento, questo viene inserito alla fine dell'elenco degli abbonamenti. Pertanto, ci si posiziona sull'ultima pagina degli abbonamenti in modo che l'utente possa vedere l'aggiunta effettuata.


    Private Sub dlAbonnements_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles dlAbonnements.ItemCommand
        ' si rimuove un abbonamento
        Dim commande As String = e.CommandName
        Select Case commande
            Case "retirer"
                ' si rimuove l'abbonamento da [datatable]
                With dtAbonnements.Rows
                    .Remove(.Find(dlAbonnements.DataKeys(e.Item.ItemIndex)))
                End With
                ' è necessario cambiare la pagina corrente?
                nbAC -= 1
                If nbAC = 0 Then pAC -= 1
            Case "precedent"
                ' cambio della pagina corrente
                pAC -= 1
            Case "suivant"
                ' cambio della pagina corrente
                pAC += 1
        End Select
        ' collegamenti dati
        terminer()
    End Sub
  • [nbAC] è il numero di righe visualizzate nella pagina corrente prima della cancellazione di un abbonamento. Se il nuovo numero di righe della pagina è pari a 0, il numero della pagina corrente [pAC] viene decrementato di un'unità.
  • Se si clicca sul link [Precedent], il numero della pagina corrente [pAC] viene decrementato di un'unità.
  • Se si clicca sul link [Suivant], il numero della pagina corrente [pAC] viene incrementato di un'unità.

Le altre procedure rimangono identiche a quelle precedenti.

9.4.4. Conclusione

Con questo esempio abbiamo dimostrato che è possibile impaginare un componente [DataList]. Questa impaginazione è complessa ed è preferibile affidarsi all’impaginazione automatica del componente [DataGrid], quando possibile. Questo esempio ci ha inoltre mostrato come accedere ai componenti presenti nel piè di pagina del componente [DataList].

9.5. Classe di accesso a un database di prodotti

Ci concentriamo nuovamente sul database ACCESS [produits] già utilizzato. Ricordiamo che esso contiene un'unica tabella denominata [liste], la cui struttura è la seguente:

Creeremo una classe di accesso alla tabella [liste] che consentirà di leggerla e aggiornarla. Creeremo inoltre un client da console che utilizzerà la classe precedente per aggiornare la tabella. In una seconda fase, creeremo un client web per svolgere lo stesso compito.

9.5.1. La classe ExceptionProduits

La classe [Exception] dispone di un costruttore che accetta un messaggio di errore come parametro. In questo caso desideriamo disporre di una classe di eccezioni dotata di un costruttore che accetti un elenco di messaggi di errore anziché un unico messaggio di errore. Sarà la classe [ExceptionProduits] riportata di seguito:


    Public Class ExceptionProduits
        Inherits Exception

        ' messaggi di errore relativi all'eccezione
        Private _erreurs As ArrayList

        ' costruttore
        Public Sub New(ByVal erreurs As ArrayList)
            Me._erreurs = erreurs
        End Sub

        ' proprietà
        Public ReadOnly Property erreurs() As ArrayList
            Get
                Return _erreurs
            End Get
        End Property
    End Class

9.5.2. La struttura [sProduit]

La struttura [produit] rappresenterà un prodotto [id, nom, prix]:


     ' struttura sProduit
    Public Structure sProduit
         ' i campi
        Private _id As Integer
        Private _nom As String
        Private _prix As Double

         ' ID proprietà
        Public Property id() As Integer
            Get
                Return _id
            End Get
            Set(ByVal Value As Integer)
                _id = Value
            End Set
        End Property

        ' proprietà nome
        Public Property nom() As String
            Get
                Return _nom
            End Get
            Set(ByVal Value As String)
                If IsNothing(Value) OrElse Value.Trim = String.Empty Then Throw New Exception
                _nom = Value
            End Set
        End Property

        ' proprietà prezzo
        Public Property prix() As Double
            Get
                Return _prix
            End Get
            Set(ByVal Value As Double)
                If IsNothing(Value) OrElse Value < 0 Then Throw New Exception
                _prix = Value
            End Set
        End Property
    End Structure

La struttura ammette solo dati validi per i campi [nom] e [prix].

9.5.3. La classe Prodotti

La classe [Produits] è quella che ci consentirà di aggiornare la tabella [liste] del database dei prodotti. La sua struttura è la seguente:

    Public Class produits
         ' dati dell'istanza

        Public Sub New(ByVal chaineConnexionOLEDB As String)
....
        End Sub

        Public Function getProduits() As DataTable
....
        End Function

        Public Sub ajouterProduit(ByVal produit As sProduit)
...
        End Sub

        Public Sub modifierProduit(ByVal produit As sProduit)
...
        End Sub

        Public Sub supprimerProduit(ByVal id As Integer)
...
        End Sub

    End Class

I dati di istanza

I dati condivisi dai diversi metodi della classe sono i seguenti:


        Private connexion As OleDbConnection
        Private Const selectText As String = "select id,nom,prix from liste"
        Private Const insertText As String = "insert into liste(nom,prix) values(?,?)"
        Private Const updateText As String = "update liste set nom=?,prix=? where id=?"
        Private Const deleteText As String = "delete from liste where id=?"
        Private selectCommand As New OleDbCommand
        Dim insertCommand As New OleDbCommand
        Dim updateCommand As New OleDbCommand
        Dim deleteCommand As New OleDbCommand
        Dim adaptateur As New OleDbDataAdapter
connexion
la connessione al database verrà aperta per l'esecuzione di un comando SQL e poi chiusa immediatamente dopo
selectText
Richiesta SQL [select] per ottenere l'intera tabella [liste]
insertText
query che consente l'inserimento di una riga (nome, prezzo) nella tabella [liste]. Si noti che non viene specificato il campo [id]. Infatti, questo campo viene autoincrementato dal SGBD e quindi non è necessario specificarlo.
updateText
query che consente l'aggiornamento dei campi (nome, prezzo) della riga della tabella [liste] con chiave [id]
deleteText
query che consente l'eliminazione della riga della tabella [liste] con chiave [id]
selectCommand
oggetto [OleDbCommand] che esegue la query [selectText] sulla connessione [connexion]
updateCommand
oggetto [OleDbCommand] che esegue la richiesta [updateText] sulla connessione [connexion]
insertCommand
oggetto [OleDbCommand] che esegue la richiesta [insertText] sulla connessione [connexion]
deleteCommand
oggetto [OleDbCommand] che esegue la richiesta [deleteText] sulla connessione [connexion]
adaptateur
oggetto che consente di recuperare il risultato dell'esecuzione di [selectCommand] in un oggetto [DataSet]

Il costruttore

Il costruttore riceve un unico parametro, [chaineConnexionOLEDB], che è la stringa di connessione che identifica il database da utilizzare. A partire da questa, vengono preparati i quattro comandi di interrogazione e aggiornamento della tabella, nonché l’adattatore. Si tratta solo di una preparazione e non viene effettuata alcuna connessione.


        Public Sub New(ByVal chaineConnexionOLEDB As String)
            ' si prepara la connessione
            connexion = New OleDbConnection(chaineConnexionOLEDB)
            ' si stanno preparando i comandi di query
            Dim commandes() As OleDbCommand = {selectCommand, insertCommand, updateCommand, deleteCommand}
            Dim textes() As String = {selectText, insertText, updateText, deleteText}
            For i As Integer = 0 To commandes.Length - 1
                With commandes(i)
                    .CommandText = textes(i)
                    .Connection = connexion
                End With
            Next
            ' si sta preparando l'adattatore di accesso ai dati
            adaptateur.SelectCommand = selectCommand
        End Sub

Il metodo getProduits

Questo metodo consente di ottenere il contenuto della tabella [Liste] in un oggetto [DataTable]. Il codice è il seguente:


        Public Function getProduits() As DataTable
            ' la tabella [liste] viene inserita in una [dataset]
            Dim contenu As New DataSet
            ' si crea un oggetto DataAdapter per leggere i dati dalla fonte OLEDB
            Try
                With adaptateur
                    .FillSchema(contenu, SchemaType.Source)
                    .Fill(contenu)
                End With
            Catch e As Exception
                ' pb
                Dim erreursCommande As New ArrayList
                erreursCommande.Add(String.Format("Erreur d'accès à la base de données : {0}", e.Message))
                Throw New ExceptionProduits(erreursCommande)
            End Try
            ' si restituisce il risultato
            Return contenu.Tables(0)
        End Function

L'operazione viene eseguita tramite le due istruzioni seguenti:


                With adaptateur
                    .FillSchema(contenu, SchemaType.Source)
                    .Fill(contenu)
                End With

Il metodo [FillSchema] definisce la struttura (colonne, vincoli, relazioni) di [DataSet] e contenu sulla base della struttura del database a cui fa riferimento [adaptateur.Connexion]. Ciò ci consente di recuperare la struttura della tabella [liste] e, in particolare, la sua chiave primaria. L’operazione [Fill] che segue popola le tabelle [Dataset] e contenu con le righe della tabella [liste]. Con questa sola operazione avremmo ottenuto i dati e la struttura, ma non la chiave primaria. Quest’ultima ci sarà però utile per aggiornare la tabella [liste] in memoria. Qui, come negli altri metodi, gestiamo gli eventuali errori utilizzando la classe [ExceptionProduits] per ottenere un elenco (ArrayList) di errori anziché un singolo errore. Il metodo [getProduits] restituisce la tabella [liste] sotto forma di un oggetto [DataTable].

Il metodo ajouterProduits

Questo metodo consente di aggiungere una riga (id, nome, prezzo) alla tabella [liste]. Queste informazioni gli vengono fornite sotto forma di una struttura [sProduit] composta da campi [id, nom, prix]. Il codice del metodo è il seguente:


        Public Sub ajouterProduit(ByVal produit As sProduit)
            ' si aggiunge un prodotto [nom,prix]
            ' si preparano i parametri dell'aggiunta
            With insertCommand.Parameters
                .Clear()
                .Add(New OleDbParameter("nom", produit.nom))
                .Add(New OleDbParameter("prix", produit.prix))
            End With
            ' si effettua l'aggiunta
            Try
                ' apertura connessione
                connexion.Open()
                ' esecuzione comando
                insertCommand.ExecuteNonQuery()
            Catch ex As Exception
                ' problema
                Dim erreursCommande As New ArrayList
                erreursCommande.Add(String.Format("Erreur lors de l'ajout : {0}", ex.Message))
                Throw New ExceptionProduits(erreursCommande)
            Finally
                ' chiusura connessione
                connexion.Close()
            End Try
        End Sub

I campi della struttura [produit] vengono inseriti nei parametri del comando [insertCommand]. Ricordiamo la configurazione attuale di questo comando (cfr. produttore):


 Private Const insertText As String = "insert into liste(nom,prix) values(?,?)"
 insertCommand.Connexion=connexion

Il testo del comando SQL [insert] contiene parametri formali ? che devono essere sostituiti con parametri effettivi. Ciò avviene tramite la collezione [Parameters] della classe [OleDbCommand]. Questa contiene elementi di tipo [OleDbParameter] che definiscono i parametri effettivi destinati a sostituire i parametri formali ?. Poiché questi ultimi non sono denominati, si utilizza l’indice dei parametri effettivi per determinare a quale parametro formale corrisponda un determinato parametro effettivo. In questo caso, il parametro effettivo n. i nella collezione [Parameters] sostituirà il parametro formale ? n. i. Per creare un parametro effettivo di tipo [OleDbParameter], qui si utilizza il costruttore [OleDbParameter (Byval nom as String, Byval valeur as Object)] che definisce il nome e il valore del parametro effettivo. Il nome può essere qualsiasi. Inoltre, in questo caso non verrà utilizzato. I due parametri dell’istruzione SQL [insert] ricevono come valori quelli dei campi [nom, prix] della struttura [produit]. Fatto ciò, l’inserimento viene effettuato dall’istruzione [insertCommand.ExecuteNonQuery].

Il metodo modifierProduits

Questo metodo consente di modificare la riga della tabella [liste]. Le informazioni necessarie gli vengono fornite nella struttura [sProduit] tramite i campi [id, nom, prix].


        Public Sub modifierProduit(ByVal produit As sProduit)
            ' si modifica un prodotto [id,nom,prix]
            ' si preparano i parametri dell'aggiornamento
            With updateCommand.Parameters
                .Clear()
                .Add(New OleDbParameter("nom", produit.nom))
                .Add(New OleDbParameter("prix", produit.prix))
                .Add(New OleDbParameter("id", produit.id))
            End With
            ' si effettua la modifica
            Try
                ' apertura della connessione
                connexion.Open()
                ' Esecuzione del comando
                Dim nbLignes As Integer = updateCommand.ExecuteNonQuery()
                If nbLignes = 0 Then Throw New Exception(String.Format("Le produit de clé [{0}] n'existe pas dans la table des données", produit.id))
            Catch ex As Exception
                ' problema
                Dim erreursCommande As New ArrayList
                erreursCommande.Add(String.Format("Erreur lors de la modification : {0}", ex.Message))
                Throw New ExceptionProduits(erreursCommande)
            Finally
                ' chiusura della connessione
                connexion.Close()
            End Try
        End Sub

Il codice è quasi identico a quello del metodo [ajouterProduits], tranne per il fatto che il comando [OleDbCommand] in questione è [updateCommand] anziché [insertCommand].

Il metodo supprimerProduits

Questo metodo consente di eliminare la riga della tabella [liste] con chiave [id] passata come parametro. Il codice è il seguente:


        Public Sub supprimerProduit(ByVal id As Integer)
            ' elimina il prodotto della chiave [id]
            ' si preparano i parametri per l'eliminazione
            With deleteCommand.Parameters
                .Clear()
                .Add(New OleDbParameter("id", id))
            End With
            ' si esegue l'eliminazione
            Try
                ' apertura connessione
                connexion.Open()
                ' esecuzione del comando
                Dim nbLignes As Integer = deleteCommand.ExecuteNonQuery()
                If nbLignes = 0 Then Throw New Exception(String.Format("Le produit de clé [{0}] n'existe pas dans la table des données", id))
            Catch ex As Exception
                ' problema
                Dim erreursCommande As New ArrayList
                erreursCommande.Add(String.Format("Erreur lors de la suppression : {0}", ex.Message))
                Throw New ExceptionProduits(erreursCommande)
            Finally
                ' chiusura della connessione
                connexion.Close()
            End Try
        End Sub

Si segue la stessa procedura dei metodi precedenti.

9.5.4. Test della classe [produits]

Un programma di test [testproduits.vb] in modalità console potrebbe essere il seguente:

Option Explicit On 
Option Strict On

' spazi dei nomi
Imports System
Imports System.Data
Imports Microsoft.VisualBasic
Imports System.Collections

Namespace st.istia.univangers.fr

     ' pagina di prova
    Module testproduits
        Dim contenu As DataTable

        Sub Main(ByVal arguments() As String)
             ' visualizza il contenuto di una tabella dei prodotti
             ' la tabella si trova in un database ACCESS il cui file di pagina riceve il nome del file
            Const syntaxe1 As String = "pg bdACCESS"

             ' verifica dei parametri del programma
            If arguments.Length <> 1 Then
                 ' messaggio di errore
                Console.Error.WriteLine(syntaxe1)
                 ' fine
                Environment.Exit(1)
            End If
             ' si prepara la stringa di connessione
            Dim chaineConnexion As String = "Provider=Microsoft.Jet.OLEDB.4.0; Ole DB Services=-4; Data Source=" + arguments(0)
             ' creazione di un oggetto prodotti
            Dim objProduits As produits = New produits(chaineConnexion)
             ' visualizzazione di tutti i prodotti
            afficheProduits(objProduits)
             ' si inserisce un prodotto
            Dim produit As New sProduit
            With produit
                .nom = "xxx"
                .prix = 1
            End With
            Try
                objProduits.ajouterProduit(produit)
            Catch ex As ExceptionProduits
                afficheErreurs(ex.erreurs)
            End Try
             ' visualizzazione di tutti i prodotti
            afficheProduits(objProduits)
             ' si recupera l'ID del prodotto aggiunto
            produit.id = CType(contenu.Rows(contenu.Rows.Count - 1)("id"), Integer)
             ' modifica il prodotto aggiunto
            produit.prix = 200
            Try
                objProduits.modifierProduit(produit)
            Catch ex As ExceptionProduits
                afficheErreurs(ex.erreurs)
            End Try
             ' visualizza tutti i prodotti
            afficheProduits(objProduits)
             ' si elimina il prodotto aggiunto
            Try
                objProduits.supprimerProduit(produit.id)
            Catch ex As ExceptionProduits
                afficheErreurs(ex.erreurs)
            End Try
             ' si visualizzano tutti i prodotti
            afficheProduits(objProduits)
        End Sub

        Sub afficheProduits(ByRef objProduits As produits)
             ' si recupera la tabella dei prodotti in un datatable
            Try
                contenu = objProduits.getProduits()
            Catch ex As ExceptionProduits
                afficheErreurs(ex.erreurs)
                Environment.Exit(2)
            End Try
            Dim lignes As DataRowCollection = contenu.Rows
            For i As Integer = 0 To lignes.Count - 1
                 ' riga i della tabella
                Console.Out.WriteLine(lignes(i).Item("id").ToString + "," + lignes(i).Item("nom").ToString + _
                "," + lignes(i).Item("prix").ToString)
            Next
             ' interrompe il flusso della console
            Console.WriteLine("...")
            Console.ReadLine()
        End Sub

        Sub afficheErreurs(ByRef erreurs As ArrayList)
             ' visualizza gli errori sulla console
            If erreurs.Count <> 0 Then
                Console.WriteLine("Les erreurs suivantes se sont produites :")
                For i As Integer = 0 To erreurs.Count - 1
                    Console.WriteLine(String.Format("-- {0}", CType(erreurs(i), String)))
                Next
            End If
             ' interrompe il flusso della console
            Console.WriteLine("...")
            Console.ReadLine()
        End Sub
    End Module
End Namespace

Si compilano i due file sorgente:

dos>vbc /t:library /r:system.dll /r:system.data.dll /r:system.xml.dll produits.vb

dos >vbc /r:produits.dll /r:system.dll /r:system.data.dll /r:system.xml.dll testproduits.vb

dos>dir
07/04/2004  08:40             7 168 produits.dll
04/04/2004  16:38           118 784 produits.mdb
07/04/2004  08:31             6 209 produits.vb
07/04/2004  08:40             5 120 testproduits.exe
03/04/2004  19:02             3 312 testproduits.vb

poi si esegue il test:

dos>testproduits produits.mdb
1,produit1,10
2,produit2,20
3,produit3,30
...

1,produit1,10
2,produit2,20
3,produit3,30
8,xxx,1
...

1,produit1,10
2,produit2,20
3,produit3,30
8,xxx,200
...

1,produit1,10
2,produit2,20
3,produit3,30
...

Si invita il lettore a confrontare l'output dello schermo sopra riportato con il codice del programma di test.

9.6. Applicazione web per l'aggiornamento della tabella dei prodotti in cache

9.6.1. Introduzione

Stiamo ora sviluppando un'applicazione web per l'aggiornamento della tabella dei prodotti (aggiunta, eliminazione, modifica). La tabella aggiornata rimarrà in memoria in un oggetto [DataTable] e sarà condivisa da tutti gli utenti. Desideriamo mettere in evidenza due punti:

  • la gestione di un oggetto [DataTable]
  • i problemi legati all’aggiornamento simultaneo di una tabella da parte di più utenti.

L’architettura MVC dell’applicazione sarà la seguente:

9.6.2. Funzionamento e viste

La schermata iniziale dell’applicazione è la seguente:

Image

 

Questa schermata, denominata [formulaire], consente all’utente di applicare un filtro ai prodotti e di impostare il numero di prodotti per pagina che desidera visualizzare.

n.
nome
tipo
ruolo
1
lnkFiltre
LinkButton
visualizza la vista [Formulaire], utilizzata per impostare la condizione di filtro
2
lnkMisaJour
LinkButton
visualizza la vista [Produits], utilizzata per visualizzare e aggiornare la tabella dei prodotti (modifica ed eliminazione)
3
lnkAjout
LinkButton
visualizza la vista [Ajout], utilizzata per aggiungere un prodotto
4
panel
vueFormulaire
la vista [Formulaire]
5
txtFiltre
TextBox
la condizione di filtro
6
txtPages
TextBox
numero di prodotti per pagina
7
rfvLignes
RequiredFieldValidator
verifica la presenza di un valore in [txtPages]
8
rvLignes
RangeValidator
verifica che txtPages rientri nell'intervallo [3,10]
9
btnExécuter
 
pulsante [submit] che visualizza la vista [produits] filtrata in base alla condizione (5)
10
lblInfo1
Etichetta
Testo informativo in caso di errori

Ad esempio, se la vista [formulaire] è compilata come segue:

Image

si ottiene il seguente risultato:

n.
nome
tipo
ruolo
1
vueProduits
pannello
 
2
rdCroissant
rdDécroissant
RadioButton
consente all'utente di impostare l'ordine di ordinamento desiderato quando fa clic sul titolo di una delle colonne [nom], [prix]. I due pulsanti fanno parte del gruppo [rdTri].
3
DataGrid1
DataGrid
griglia di visualizzazione di una vista filtrata della tabella dei prodotti. Il filtro è quello impostato dalla vista [formulaire]. Si ha inoltre .AllowPaging=true, .AllowSorting=true
4
DataGrid2
DataGrid
visualizza l'intera tabella dei prodotti - consente di tenere traccia degli aggiornamenti
5
LblInfo2
Etichetta
testo informativo, in particolare in caso di errori
6
DataGrid3
DataGrid
visualizzerà i prodotti eliminati nella tabella dei prodotti
7
DataGrid4
DataGrid
visualizzerà i prodotti modificati nella tabella dei prodotti
8
DataGrid5
DataGrid
visualizzerà i prodotti aggiunti nella tabella dei prodotti

In questa pagina sono presenti cinque contenitori di dati. Tutti visualizzano la stessa tabella [dtProduits] attraverso una vista [DataView] diversa. Una vista rappresenta un sottoinsieme delle righe della tabella di origine della vista. Questo sottoinsieme viene creato a partire dalle proprietà [RowFilter] e [RowStateFilter] della classe [DataView]:

  • [RowFilter] consente di applicare un filtro alle righe, come ad esempio [prix>30] sopra riportato. Questo tipo di filtraggio verrà utilizzato da [DataGrid1].
  • [RowStateFilter] consente di impostare un filtro in base allo stato della riga della tabella. Questo indica lo stato della riga rispetto allo stato originale che aveva quando è stata creata la vista sulla tabella. In questo caso, la tabella [dtProduits] proviene da un database. Inizialmente tutte le sue righe avranno uno stato pari a [Original] per indicare che si tratta delle righe originali della tabella. Questo stato può poi evolversi e assumere diversi valori, di cui riportiamo alcuni:
    • [Added]: la riga è stata aggiunta – non faceva parte della tabella originale
    • [Deleted]: la riga è stata eliminata – è ancora presente nella tabella ma «contrassegnata» come «da eliminare»
    • [Modified]: la riga è stata modificata

[RowStateFilter] consente di visualizzare le righe della tabella con un determinato stato:

  • (continua)
    • [DataViewRowState.Added]: vengono visualizzate solo le righe aggiunte. Queste vengono visualizzate con i loro valori attuali.
    • [DataViewRowState.ModifiedOriginal]: vengono visualizzate solo le righe modificate. Queste vengono visualizzate con i loro valori originali.
    • [DataViewRowState.ModifiedCurrent]: vengono visualizzate solo le righe modificate. Queste vengono visualizzate con i loro valori attuali.
    • [DataViewRowState.Deleted]: vengono visualizzate solo le righe eliminate. Vengono visualizzate con i loro valori originali.
    • [DataViewRowState.CurrentRows]: vengono visualizzate le righe non eliminate. Vengono visualizzate con i loro valori attuali.

Pertanto, il filtro [RowStateFilter] avrà i seguenti valori:

DataGrid1
nessun filtro sullo stato delle linee
 
DataGrid2
DataViewRowState.CurrentRows
visualizza lo stato attuale della tabella dei prodotti
DataGrid3
DataViewRowState.Deleted
visualizza le righe eliminate dalla tabella dei prodotti
DataGrid4
DataViewRowState.ModifiedOriginal
visualizza le righe modificate della tabella dei prodotti con i loro valori iniziali
DataGrid5
DataViewRowState.Added
visualizza le righe aggiunte alla tabella dei prodotti iniziale

I contenitori [DataGrid1-5] ci consentiranno di monitorare l'aggiornamento della tabella [dtProduits]. Il componente [DataGrid1] consente la modifica e l'eliminazione di un prodotto. Vedremo come la configurazione del componente consenta tale aggiornamento. È inoltre impaginato e ordinato. Questi due aspetti sono già stati esaminati in un esempio precedente.

Il link [Ajout] dà accesso a un modulo per l’aggiunta di un prodotto:

n.
nome
tipo
ruolo
1
vueAjout
pannello
 
2
txtNom
TextBox
nome del prodotto
3
rfvNom
RequiredFieldValidator
verifica la presenza di un valore in [txtNom]
4
txtPrix
TextBox
prezzo del prodotto
5
rfvPrix
RequiredFieldValidator
verifica la presenza di un valore in [txtPrix]
6
cvPrix
CompareValidator
verifica che il prezzo sia >= 0
7
btnAjouter
Pulsante
pulsante [submit] per l'aggiunta del prodotto
8
lblInfo3
Etichetta
Testo informativo sul risultato dell'operazione di aggiunta

Il database [produits] potrebbe non essere disponibile all'avvio dell'applicazione. In tal caso, all'utente viene presentata la vista [erreurs]:

n.
nome
tipo
ruolo
1
vueErreurs
pannello
 
2
rptErreurs
Ripetitore
elenco degli errori

9.6.3. Configurazione dei contenitori di dati

I cinque contenitori [DataGrid] sono stati configurati in [WebMatrix]. Sono stati formattati (colori e bordi) tramite il collegamento [Configuration automatique] nel loro pannello delle proprietà. Il contenitore [DataGrid1] è stato configurato utilizzando il collegamento [Générateur de propriétés] presente nello stesso pannello. È stato abilitato l'ordinamento (scheda [Général]):

Image

Le colonne non vengono generate automaticamente, a differenza degli altri quattro contenitori. Sono state definite manualmente tramite la procedura guidata:

Image

In primo luogo sono state create due colonne di tipo [Colonne connexe] denominate [nom] e [prix]. Sono state associate rispettivamente ai campi [nom] e [prix] della fonte dati che i contenitori visualizzeranno. Ecco, ad esempio, la configurazione della colonna [nom]:

Image

L’espressione di ordinamento è quella che dovrà essere inserita dopo la clausola [order by] dell’istruzione SQL [select], che verrà eseguita quando l’utente cliccherà sul nome della colonna di intestazione [nom] associata al campo [nom] del [DataGrid]. Qui abbiamo inserito [nom] in modo che la clausola di ordinamento sia [order by nom]. Vedremo che la modificheremo in modo che diventi [order by nom asc] o [order by nom desc] a seconda dell’ordine di ordinamento scelto dall’utente.

Abbiamo inoltre creato due colonne di pulsanti:

Image

La colonna [Modifier, Mettre à jour, Annuler] ci consentirà di modificare un prodotto, mentre la colonna [Supprimer] di eliminarlo. Ciascuna di queste colonne può essere configurata. La colonna [Modifier, Mettre à jour, Annuler] offre la seguente configurazione:

Image

Si nota che è possibile modificare i testi dei pulsanti. Per quanto riguarda i pulsanti, è possibile scegliere tra link e pulsanti (elenco a discesa in alto). In questo caso sono stati scelti i link. La configurazione della colonna [Supprimer] è analoga. Oltre a questa procedura guidata di configurazione, abbiamo utilizzato direttamente la finestra delle proprietà di [DataGrid] per impostare la proprietà [DataKeyField], che indica con quale campo della fonte dati vengono indicizzate le righe di [DataGrid]. In questo caso viene utilizzata la chiave primaria della tabella dei prodotti:

Image

Alla fine, questa configurazione genera il seguente codice di presentazione:


<asp:DataGrid id="DataGrid1" runat="server" AllowSorting="True" PageSize="4" AllowPaging="True" AutoGenerateColumns="False" DataKeyField="id">
<SelectedItemStyle ...></SelectedItemStyle>
<HeaderStyle ...></HeaderStyle>
<FooterStyle ...></FooterStyle>
<Columns>
         <asp:BoundColumn DataField="nom" SortExpression="nom" HeaderText="nom"></asp:BoundColumn>
          <asp:BoundColumn DataField="prix" SortExpression="prix" HeaderText="prix"></asp:BoundColumn>
           <asp:EditCommandColumn ButtonType="LinkButton" UpdateText="Mettre &#224; jour" CancelText="Annuler" EditText="Modifier"></asp:EditCommandColumn>
          <asp:ButtonColumn Text="Supprimer" CommandName="Delete"></asp:ButtonColumn>
  </Columns>
<PagerStyle NextPageText="Suivant" PrevPageText="Pr&#233;c&#233;dent" ...></PagerStyle>
</asp:DataGrid>

Come sempre, una volta acquisita una certa esperienza, è possibile scrivere direttamente tutto o parte del codice sopra riportato.

Gli altri contenitori [DataGrid] presentano la configurazione predefinita ottenuta tramite la generazione automatica delle colonne del [DataGrid] a partire da quelle della fonte dati a cui è associato.

Il contenitore [Repeater] serve a visualizzare un elenco di errori. La sua configurazione viene effettuata direttamente nel codice di presentazione:


                            <asp:Repeater id="rptErreurs" runat="server" EnableViewState="False">
                                <HeaderTemplate>
                                    Les erreurs suivantes se sont produites :
                                    <ul>
                                </HeaderTemplate>
                                <ItemTemplate>
                                    <li>
                                        <%# Container.DataItem %>
                                    </li>
                                </ItemTemplate>
                                <FooterTemplate>
                                    </ul>
                                </FooterTemplate>
                            </asp:Repeater>

Ogni riga del componente visualizza il valore [Container.DataItem], c.a.d, corrispondente all'elenco dei dati. Quest'ultimo sarà di tipo [ArrayList] e rappresenterà un elenco di errori.

9.6.4. Il codice di presentazione dell'applicazione

Questo è contenuto nel file [main.aspx]:


<%@ Page src="main.aspx.vb" inherits="main" autoeventwireup="false" Language="vb" %>
<HTML>
    <HEAD>
    </HEAD>
    <body>
        <form id="Form1" runat="server">
            <P>
                <table>
                    <tr>
                        <td><FONT size="6">Options :</FONT></td>
                        <td>
                          <asp:linkbutton id="lnkFiltre" runat="server" CausesValidation="False">
                                  Filtrage
                          </asp:linkbutton>
                    </td>
                        <td>
                        <asp:linkbutton id="lnkMisajour" runat="server" CausesValidation="False">
                            Mise à jour
                           </asp:linkbutton>
                    </td>
                        <td>
                          <asp:linkbutton id="lnkAjout" runat="server" CausesValidation="False">
                               Ajout
                           </asp:linkbutton>
                       </td>
                    </tr>
                </table>
            </P>
            <HR width="100%" SIZE="1">
            <table>
                <tr>
                    <td>

                  <asp:panel id="vueFormulaire" runat="server">
                            <P>Condition de filtrage sur&nbsp;la table LISTE.&nbsp;Exemple : prix&lt;100 and 
                                prix&gt;50</P>
                            <P>
                                <asp:TextBox id="txtFiltre" runat="server" Columns="60"></asp:TextBox></P>
                            <P>Nombre de lignes par page :
                                <asp:TextBox id="txtPages" runat="server" Columns="3">5</asp:TextBox>
                                <asp:RequiredFieldValidator id="rfvLignes" runat="server" Display="Dynamic"
                            ControlToValidate="txtPages" ErrorMessage="Indiquez le nombre de lignes par page"
                                    EnableClientScript="False">
                     </asp:RequiredFieldValidator></P>
                            <P>
                                <asp:RangeValidator id="rvLignes" runat="server" Display="Dynamic"
                        ControlToValidate="txtPages" ErrorMessage="Vous devez indiquer un nombre entre 3 et 10"
                                    EnableClientScript="False" MaximumValue="10" MinimumValue="3" Type="Integer"
                             EnableViewState="False">
                             </asp:RangeValidator></P>
                            <P>
                                <asp:Label id="lblinfo1" runat="server"></asp:Label></P>
                            <P>
                                <asp:Button id="btnExécuter" runat="server" CausesValidation="False"
                              EnableViewState="False" Text="Exécuter">
                             </asp:Button></P>
                        </asp:panel>

                      <asp:panel id="vueProduits" runat="server">
                            <TABLE>
                                <TR>
                                    <TD align="center" bgColor="#ff9966">
                                        <P>Tri
                                            <asp:RadioButton id="rdCroissant" runat="server" Text="croissant"
                                     GroupName="rdTri" Checked="True">
                                 </asp:RadioButton>
                                            <asp:RadioButton id="rdDécroissant" runat="server" Text="décroissant"
                                                 GroupName="rdTri">
                                    </asp:RadioButton></P>
                                    </TD>
                                    <TD align="center" bgColor="#ff9966">Tous les produits
                                    </TD>
                                    <TD align="center" bgColor="#ff9966">Suppressions
                                    </TD>
                                    <TD align="center" bgColor="#ff9966">Modifications
                                    </TD>
                                    <TD align="center" bgColor="#ff9966">Ajouts
                                    </TD>
                                <TR>
                                    <TD vAlign="top">
                                        <P>
                                            <asp:DataGrid id="DataGrid1" runat="server" AllowSorting="True" PageSize="4"
                                                 AllowPaging="True" ....     AutoGenerateColumns="False" DataKeyField="id">
                                                <ItemStyle ...></ItemStyle>
                                                <HeaderStyle ...></HeaderStyle>
                                                <FooterStyle ...></FooterStyle>
                                                <Columns>
                                                    <asp:BoundColumn DataField="nom" SortExpression="nom" HeaderText="nom">
                                               </asp:BoundColumn>
                                                    <asp:BoundColumn DataField="prix" SortExpression="prix" HeaderText="prix">
                                          </asp:BoundColumn>
                                                    <asp:EditCommandColumn ButtonType="LinkButton" UpdateText="Mettre &#224; jour"
                                                         CancelText="Annuler" EditText="Modifier">
                                       </asp:EditCommandColumn>
                                                    <asp:ButtonColumn Text="Supprimer" CommandName="Delete">
                                     </asp:ButtonColumn>
                                                </Columns>
                                                <PagerStyle NextPageText="Suivant" PrevPageText="Pr&#233;c&#233;dent" ....>
                                           </PagerStyle>
                                            </asp:DataGrid>
                                       </P>
                                    </TD>
                                    <TD vAlign="top">
                                        <asp:DataGrid id="DataGrid2" runat="server" ...>
                                            <AlternatingItemStyle ...></AlternatingItemStyle>
                                            <ItemStyle ...></ItemStyle>
                                            <HeaderStyle ....></HeaderStyle>
                                            <FooterStyle ...></FooterStyle>
                                            <PagerStyle ... Mode="NumericPages"></PagerStyle>
                                        </asp:DataGrid>
                            </TD>
                                    <TD vAlign="top">
                                        <asp:DataGrid id="DataGrid3" runat="server" ...>
                                            <ItemStyle ...></ItemStyle>
                                            <HeaderStyle ...></HeaderStyle>
                                            <FooterStyle ...></FooterStyle>
                                            <PagerStyle ...></PagerStyle>
                                        </asp:DataGrid>
                                   </TD>
                                    <TD vAlign="top">
                                        <asp:DataGrid id="DataGrid4" runat="server" ...>
                                            <ItemStyle ....></ItemStyle>
                                            <HeaderStyle ....></HeaderStyle>
                                            <FooterStyle ...></FooterStyle>
                                            <PagerStyle .... Mode="NumericPages"></PagerStyle>
                                        </asp:DataGrid>
                       </TD>
                                    <TD vAlign="top">
                                        <asp:DataGrid id="DataGrid5" runat="server....>
                                            <AlternatingItemStyle ...></AlternatingItemStyle>
                                            <HeaderStyle ..></HeaderStyle>
                                            <FooterStyle ...></FooterStyle>
                                            <PagerStyle ....></PagerStyle>
                                        </asp:DataGrid>
                         </TD>
                                </TR>
                            </TABLE>
                            <P></P>
                            <P>
                                <asp:Label id="lblInfo2" runat="server"></asp:Label></P>
                    </asp:panel>

                   <asp:panel id="vueErreurs" runat="server">
                            <asp:Repeater id="rptErreurs" runat="server" EnableViewState="False">
                                <HeaderTemplate>
                                    Les erreurs suivantes se sont produites :
                                    <ul>
                                </HeaderTemplate>
                                <ItemTemplate>
                                    <li>
                                        <%# Container.DataItem %>
                                    </li>
                                </ItemTemplate>
                                <FooterTemplate>
                                    </ul>
                                </FooterTemplate>
                            </asp:Repeater>
                        </asp:panel>

                  <asp:panel id="vueAjout" EnableViewState="False" Runat="server">
                            <P>Ajout d'un produit</P>
                            <P>
                                <TABLE ... border="1">
                                    <TR>
                                        <TD>nom</TD>
                                        <TD>
                                            <asp:TextBox id="txtNom" runat="server" Columns="30"></asp:TextBox>
                                            <asp:RequiredFieldValidator id="rfvNom" runat="server" ControlToValidate="txtNom"
                                                     ErrorMessage="Vous devez indiquer un nom">
                               </asp:RequiredFieldValidator>
                                      </TD>
                                    </TR>
                                    <TR>
                                        <TD>prix</TD>
                                        <TD>
                                            <asp:TextBox id="txtPrix" runat="server" Columns="10"></asp:TextBox>
                                            <asp:RequiredFieldValidator id="rfvPrix" runat="server" ControlToValidate="txtPrix"
                                                 ErrorMessage="Vous devez indiquer un prix">
                                           </asp:RequiredFieldValidator>
                                            <asp:CompareValidator id="cvPrix" runat="server" ControlToValidate="txtPrix"
                                                     ErrorMessage="CompareValidator" Type="Double" Operator="GreaterThanEqual"
                                                     ValueToCompare="0">
                             </asp:CompareValidator>
                                   </TD>
                                    </TR>
                                </TABLE>
                            </P>
                            <P>
                                <asp:Button id="btnAjouter" runat="server" CausesValidation="False"
                                     Text="Ajouter">
                               </asp:Button></P>
                            <P>
                                <asp:Label id="lblInfo3" runat="server"></asp:Label></P>
                        </asp:panel>

                </td>
                </tr>
            </table>
        </form>
    </body>
</HTML>

Si notino i seguenti punti:

  • la pagina è composta da quattro contenitori (panel) [vueFormulaire, vueProduits, vueAjout, vueErreurs] che costituiranno le quattro viste dell'applicazione.
  • i pulsanti o i link di tipo [submit] hanno la proprietà [CausesValidation=false]. La proprietà [causesValidation=true] fa sì che vengano eseguiti tutti i controlli di validità della pagina. Tuttavia, in questo caso, non tutti i controlli di validità devono essere effettuati contemporaneamente. Quando si effettua un'aggiunta, ad esempio, non si desidera che vengano eseguiti i controlli relativi al numero di righe per pagina. Specificheremo quindi noi stessi quali controlli di validità devono essere effettuati.

9.6.5. Il codice di controllo [global.asax]

Il controller [global.asax] è il seguente:

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

Il codice associato [global.asax.vb]:


Imports System
Imports System.Web
Imports System.Web.SessionState
Imports st.istia.univangers.fr
Imports System.Configuration
Imports System.Data
Imports Microsoft.VisualBasic
Imports System.Collections

Public Class Global
    Inherits System.Web.HttpApplication

    Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' si recuperano le informazioni di configurazione
        Dim chaînedeConnexion As String = ConfigurationSettings.AppSettings("OLEDBStringConnection")
        Dim defaultProduitsPage As String = ConfigurationSettings.AppSettings("defaultProduitsPage")
        Dim erreurs As New ArrayList
        If IsNothing(chaînedeConnexion) Then erreurs.Add("Le paramètre [OLEDBStringConnection] n'a pas été initialisé")
        If IsNothing(defaultProduitsPage) Then
            erreurs.Add("Le paramètre [defaultProduitsPage] n'a pas été initialisé")
        Else
            Try
                Dim defProduitsPage As Integer = CType(defaultProduitsPage, Integer)
                If defProduitsPage <= 0 Then Throw New Exception
            Catch ex As Exception
                erreurs.Add("Le paramètre [defaultProduitsPage] a une valeur incorrecte")
            End Try
        End If
        ' Ci sono errori di configurazione?
        If erreurs.Count <> 0 Then
            ' si annotano gli errori
            Application("erreurs") = erreurs
            ' si esce
            Exit Sub
        End If
        ' qui non ci sono errori di configurazione
        ' si crea un oggetto prodotti
        Dim dtProduits As DataTable
        Try
            dtProduits = New produits(chaînedeConnexion).getProduits
        Catch ex As ExceptionProduits
            'si è verificato un errore di accesso ai prodotti, lo si registra nell'applicazione
            Application("erreurs") = ex.erreurs
            Exit Sub
        Catch ex As Exception
            ' errore non gestito
            erreurs.Add(ex.Message)
            Application("erreurs") = erreurs
            ' uscita dalla subroutine
        End Try
        ' qui non ci sono errori di inizializzazione
        ' si memorizza il numero di prodotti per pagina
        Application("defaultProduitsPage") = defaultProduitsPage
        ' si memorizza la tabella dei prodotti
        Application("dtProduits") = dtProduits
    End Sub

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' inizializzazione delle variabili di sessione
        If IsNothing(Application("erreurs")) Then
            ' visualizzazione della tabella dei prodotti
            Session("dvProduits") = CType(Application("dtProduits"), DataTable).DefaultView
            ' numero di prodotti per pagina
            Session("nbProduitsPage") = Application("defaultProduitsPage")
            ' pagina corrente visualizzata
            Session("pageCourante") = 0
        End If
    End Sub
End Class

In [Application_Start], si inizia recuperando due informazioni dal file di configurazione [web.config] dell'applicazione:

  • OLEDBStringConnection: la stringa OLEDB per la connessione al database dei prodotti
  • defaultProduitsPage: il numero predefinito di prodotti per pagina visualizzata

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <appSettings>
        <add key="OLEDBStringConnection" value="Provider=Microsoft.Jet.OLEDB.4.0; Ole DB Services=-4; Data Source=D:\data\devel\aspnet\poly\webforms3\vs\majproduits1\produits.mdb" />
        <add key="defaultProduitsPage" value="5" />
    </appSettings>
</configuration>

Se una di queste due informazioni è assente, viene generato un elenco di errori e inserito nell'applicazione. Lo stesso vale se il parametro [defaultProduitsPage] esiste ma non è corretto. Se entrambi i parametri previsti sono presenti e corretti, viene creata una tabella [dtProduits] e inserita nell’applicazione. È questa tabella che verrà utilizzata e aggiornata dai vari clienti. Il database, invece, rimarrà invariato. Ci occuperemo del suo aggiornamento in una prossima applicazione. Questa tabella viene creata a partire da un'istanza della classe [produits], già esaminata in precedenza, e dal relativo metodo [getProduits]. L'ottenimento della tabella [dtProduits] potrebbe non andare a buon fine. In tal caso, è noto che la classe [produits] genera un'eccezione di tipo [ExceptionProduits]. Qui viene intercettata e l'elenco degli errori associato viene inserito nell'applicazione associata alla chiave [erreurs]. La presenza di questa chiave nelle informazioni registrate nell’applicazione verrà verificata ad ogni elaborazione della richiesta. Se viene rilevata, la vista [erreurs] verrà inviata al cliente.

Sebbene la tabella [dtProduits] sia condivisa da tutti i client web, ciascuno di essi disporrà comunque di una propria vista [dvProduits] su di essa. Infatti, ogni client web ha la possibilità di impostare un filtro sulla tabella [dtProduits] e un ordine di ordinamento. Queste informazioni specifiche di ciascun client web vengono memorizzate nella vista del client. Pertanto, essa viene creata in [Session_Start] per essere inserita nella sessione specifica di ciascun client. Si utilizza l’attributo [DefaultView] della tabella [dtProduits] per ottenere una vista predefinita della tabella. Inizialmente non sono presenti né filtri né ordini di ordinamento. Inoltre, nella sessione vengono inserite anche due informazioni:

  • il numero di prodotti per pagina con chiave [nbProduitsPage]. All'avvio della sessione, questo numero è pari al valore predefinito definito nel file di configurazione.
  • il numero della pagina corrente dei prodotti. Inizialmente, si tratta della prima pagina.

9.6.6. Il codice del controller [main.aspx.vb]

Lo scheletro del controller è il seguente:


Imports System.Collections
Imports Microsoft.VisualBasic
Imports System.Data
Imports st.istia.univangers.fr
Imports System
Imports System.Xml
Imports System.Web.UI.WebControls

Public Class main
    Inherits System.Web.UI.Page

    ' componenti della pagina
    Protected WithEvents txtPages As System.Web.UI.WebControls.TextBox
    Protected WithEvents btnExécuter As System.Web.UI.WebControls.Button
    Protected WithEvents vueFormulaire As System.Web.UI.WebControls.Panel
    Protected WithEvents DataGrid1 As System.Web.UI.WebControls.DataGrid
    Protected WithEvents lnkErreurs As System.Web.UI.WebControls.LinkButton
    Protected WithEvents vueErreurs As System.Web.UI.WebControls.Panel
    Protected WithEvents rdCroissant As System.Web.UI.WebControls.RadioButton
    Protected WithEvents rdDécroissant As System.Web.UI.WebControls.RadioButton
    Protected WithEvents txtFiltre As System.Web.UI.WebControls.TextBox
    Protected WithEvents lblInfo2 As System.Web.UI.WebControls.Label
    Protected WithEvents lblinfo1 As System.Web.UI.WebControls.Label
    Protected WithEvents lblErreurs As System.Web.UI.WebControls.Label
    Protected WithEvents DataGrid2 As System.Web.UI.WebControls.DataGrid
    Protected WithEvents vueProduits As System.Web.UI.WebControls.Panel
    Protected WithEvents vueAjout As System.Web.UI.WebControls.Panel
    Protected WithEvents lnkMisajour As System.Web.UI.WebControls.LinkButton
    Protected WithEvents lnkFiltre As System.Web.UI.WebControls.LinkButton
    Protected WithEvents txtNom As System.Web.UI.WebControls.TextBox
    Protected WithEvents txtPrix As System.Web.UI.WebControls.TextBox
    Protected WithEvents btnAjouter As System.Web.UI.WebControls.Button
    Protected WithEvents lblInfo3 As System.Web.UI.WebControls.Label
    Protected WithEvents rfvLignes As System.Web.UI.WebControls.RequiredFieldValidator
    Protected WithEvents rvLignes As System.Web.UI.WebControls.RangeValidator
    Protected WithEvents rfvNom As System.Web.UI.WebControls.RequiredFieldValidator
    Protected WithEvents rfvPrix As System.Web.UI.WebControls.RequiredFieldValidator
    Protected WithEvents cvPrix As System.Web.UI.WebControls.CompareValidator
    Protected WithEvents lnkAjout As System.Web.UI.WebControls.LinkButton
    Protected WithEvents DataGrid3 As System.Web.UI.WebControls.DataGrid
    Protected WithEvents DataGrid4 As System.Web.UI.WebControls.DataGrid
    Protected WithEvents DataGrid5 As System.Web.UI.WebControls.DataGrid

    ' dati della pagina
    Protected dtProduits As DataTable
    Protected dvProduits As DataView
    Protected defaultProduitsPage As Integer
    Protected erreur As Boolean = False

    ' caricamento della pagina
    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
    End Sub

    Private Sub afficheErreurs()
...
    End Sub

    Private Sub afficheFormulaire()
...
    End Sub

    Private Sub btnExécuter_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnExécuter.Click
....
    End Sub

    Private Sub afficheProduits(ByVal page As Integer, ByVal taillePage As Integer)
...
    End Sub

    Private Sub DataGrid1_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles DataGrid1.PageIndexChanged
...
    End Sub

    Private Sub DataGrid1_SortCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridSortCommandEventArgs) Handles DataGrid1.SortCommand
....
    End Sub

    Private Sub DataGrid1_DeleteCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.DeleteCommand
....
    End Sub

    Private Sub DataGrid1_EditCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.EditCommand
...
    End Sub

    Private Sub DataGrid1_CancelCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.CancelCommand
...
    End Sub

    Private Sub DataGrid1_UpdateCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.UpdateCommand
...
    End Sub

    Private Sub supprimerProduit(ByVal idProduit As Integer)
...
    End Sub

    Private Sub modifierProduit(ByVal idProduit As Integer, ByVal item As DataGridItem)
....
    End Sub

    Private Sub lnkMisajour_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkMisajour.Click
...
    End Sub

    Private Sub lnkAjout_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkAjout.Click
...
    End Sub

    Private Sub afficheAjout()
...
    End Sub

    Private Sub lnkFiltre_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkFiltre.Click
....
    End Sub

    Private Sub btnAjouter_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnAjouter.Click
....
    End Sub
End Class

9.6.7. Dati di istanza

La classe [main] utilizza i seguenti dati di istanza:

Public Class main
    Inherits System.Web.UI.Page

     ' componenti della pagina
    Protected WithEvents txtPages As System.Web.UI.WebControls.TextBox
...

     ' dati della pagina
    Protected dtProduits As DataTable
    Protected dvProduits As DataView
    Protected defaultProduitsPage As Integer
dtProduits
la tabella [DataTable] dei prodotti - comune a tutti i clienti
dvProduits
la vista [DataView] dei prodotti - specifica per ogni cliente
defaultProduitsPage
numero di prodotti per pagina proposto di default

9.6.8. La procedura [Page_Load] per il caricamento della pagina

Il codice della procedura [page_Load] è il seguente:


    ' caricamento pagina
    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' si verifica se l'applicazione presenta un errore
        If Not IsNothing(Application("erreurs")) Then
            ' l'applicazione non si è inizializzata correttamente
            afficheErreurs(CType(Application("erreurs"), ArrayList))
            Exit Sub
        End If
        ' si recupera un riferimento dalla tabella dei prodotti
        dtProduits = CType(Application("dtProduits"), DataTable)
         ' si recupera la vista dei prodotti
        dvProduits = CType(Session("dvProduits"), DataView)
         ' si recupera il numero di prodotti per pagina
        defaultProduitsPage = CType(Application("defaultProduitsPage"), Integer)
         ' si annulla un eventuale aggiornamento in corso
        DataGrid1.EditItemIndex = -1
        'prima richiesta
        If Not IsPostBack Then
            ' viene visualizzato il modulo iniziale
            txtPages.Text = defaultProduitsPage.ToString
            afficheFormulaire()
        End If
    End Sub
  • Si inizia innanzitutto verificando se la tabella dei prodotti è stata caricata all'avvio dell'applicazione. In caso contrario, viene visualizzata la vista [erreurs] con i messaggi di errore appropriati, quindi si esce dalla procedura dopo aver impostato il valore booleano [erreur] su vero. Questo indicatore verrà verificato da alcune procedure.
  • Si recupera la tabella dei prodotti [dtProduits] dall’applicazione e la si assegna alla variabile di istanza [dtProduits] affinché sia condivisa da tutti i metodi della pagina.
  • Si procede allo stesso modo con la vista [dvProduits], recuperata dalla sessione, e con il numero predefinito di prodotti per pagina, recuperato dall’applicazione.
  • Se si tratta della prima richiesta del cliente, gli viene presentato il modulo per la definizione delle condizioni di filtro e di paginazione con una paginazione predefinita di [defaultProduitsPage] prodotti per pagina.
  • Si annulla la modalità di modifica del componente [dataGrid1]. Questo componente ha un attributo [EditItemIndex] che rappresenta l’indice dell’elemento attualmente in fase di modifica. Se [EditItemIndex]=-1, allora non vi è alcun elemento in fase di modifica. Se [EditItemIndex]=i, allora l’elemento n. i del componente [DataGrid] è in fase di modifica. Viene quindi visualizzato in modo diverso dagli altri elementi del [dataGrid]:

Image

Nell’esempio sopra riportato, l’elemento [Produit8, 80] si trova in modalità [édition]. Come si vede sopra, l’utente può confermare l’aggiornamento tramite il link [Mettre à jour] e annullarlo tramite il link [Annuler]. Può inoltre utilizzare altri link che non hanno nulla a che vedere con l’elemento in fase di modifica. Inserendo [DataGrid1.EditItemIndex=-1] ad ogni caricamento della pagina, ci si assicura di annullare sistematicamente la modalità di modifica di [DataGrid1]. Gli verrà assegnato un valore diverso solo se è stato cliccato un link [Modifier]. Ciò avverrà nella procedura che gestisce tale evento. Si verificheranno le seguenti situazioni:

  1. è stato cliccato il link [Modifier] dell’elemento n. 8 di [DataGrid1]. [Page_Load] imposta innanzitutto -1 in [DataGrid1.EditItemIndex]. Successivamente, la procedura che gestisce l’evento [Modifier] inserirà 8 in [DataGrid1.EditItemIndex]. Alla fine, quando la pagina verrà rinviata al cliente, l’elemento n. 8 sarà effettivamente in modalità [édition] e apparirà come mostrato sopra.
  2. L'utente modifica il prodotto che si trova in modalità [édition] e conferma la modifica tramite il link [Mettre à jour]. [Page_Load] inserisce -1 in [DataGrid1.EditItemIndex]. Successivamente verrà eseguita la procedura che gestisce l’evento [Mettre à jour] e l’elemento n. 8 di [dtProduits] verrà aggiornato. Quando la pagina verrà rinviata al cliente, nessun elemento di [DataGrid1] sarà in modalità di modifica (DataGrid.EditItemIndex=-1).
  3. Se un utente ha avviato l'aggiornamento di un prodotto, sarebbe logico che interrompesse tale aggiornamento tramite [Annuler]. Tuttavia, nulla gli impedisce di cliccare su un altro link. Dobbiamo quindi prevedere questo caso. In questo caso, come in quelli precedenti, [Page_Load] inizia inserendo -1 in [DataGrid1.EditItemIndex], dopodiché verrà eseguita la procedura che gestisce l’evento verificatosi. Non modificherà la proprietà [DataGrid1.EditItemIndex], che rimarrà quindi con il valore -1. Quando la pagina verrà rinviata al cliente, nessun elemento di [DataGrid1] sarà in modalità di modifica.

Si nota che, impostando [EditItemIndex] a -1 al momento del caricamento della pagina, si evita di doversi preoccupare se l’utente fosse o meno in modalità di aggiornamento quando ha cliccato su un link.

9.6.9. Visualizzazione delle viste [erreurs], [formulaire] e [ajout]

La visualizzazione della vista [erreurs] viene richiesta al caricamento della pagina [Page_Load] se si rileva che l'applicazione non si è inizializzata correttamente. La visualizzazione avviene associando il componente dati [rptErreurs] all’elenco degli errori passato come parametro e rendendo visibile il contenitore appropriato. Infine, i tre link [Filtre, Mise à jour, Ajout] vengono nascosti poiché, in caso di errore, l’applicazione non può essere utilizzata.


    Private Sub afficheErreurs(ByVal erreurs As ArrayList)
        ' si associa l'elenco degli errori al ripetitore rptErreurs
        With rptErreurs
            .DataSource = erreurs
            .DataBind()
        End With
        'si disabilitano le opzioni
        lnkAjout.Visible = False
        lnkMisajour.Visible = False
        lnkFiltre.Visible = False
        ' viene visualizzata la vista [erreurs]
        vueErreurs.Visible = True
        vueFormulaire.Visible = False
        vueProduits.Visible = False
        vueAjout.Visible = False
    End Sub

Le altre visualizzazioni vengono richieste in base alle opzioni proposte all'utente:

Image

La visualizzazione della vista [Ajout] viene richiesta quando l'utente clicca sul link [Ajout] nella pagina:


    Private Sub lnkAjout_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkAjout.Click
        ' si visualizza la vista [ajout]
        afficheAjout()
    End Sub

    Private Sub afficheAjout()
        ' visualizza la vista "Aggiungi"
        vueAjout.Visible = True
        vueFormulaire.Visible = False
        vueProduits.Visible = False
        vueErreurs.Visible = False
    End Sub

La visualizzazione della vista [Formulaire] viene richiesta quando l'utente clicca sul link [Filtrage] nella pagina. A quel punto deve essere visualizzata la vista che consente di definire

  • il filtro sulla tabella dei prodotti
  • il numero di prodotti visualizzati in ogni pagina web

Viene visualizzato un modulo che riprende i valori memorizzati nella sessione:


    Private Sub lnkFiltre_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkFiltre.Click
        ' definire le condizioni di filtro
        txtFiltre.Text = dvProduits.RowFilter
        ' definire l'impaginazione
        txtPages.Text = CType(Session("nbProduitsPage"), String)
        ' viene visualizzato il modulo di filtro
        afficheFormulaire()
    End Sub

La condizione di filtro di una vista è definita nel suo attributo [RowFilter]. Ricordiamo che la vista filtrata e impaginata si chiama [dvProduits] ed è stata recuperata nella sessione al momento del caricamento della pagina [Page_Load]. La condizione di filtro viene quindi recuperata in [dvProduits.RowFilter]. Anche il numero di prodotti per pagina viene recuperato dalla sessione. Se l’utente non ha mai definito questa informazione, tale numero è pari al numero predefinito di prodotti per pagina. Fatto ciò, viene visualizzato il modulo che consente di definire queste due informazioni:


    Private Sub afficheFormulaire()
        ' viene visualizzata la vista [formulaire]
        vueFormulaire.Visible = True
        vueErreurs.Visible = False
        vueProduits.Visible = False
        vueAjout.Visible = False
    End Sub

Image

9.6.10. Convalida della vista [formulaire]

Il clic sul pulsante [Exécuter] qui sopra viene gestito dalla seguente procedura:


    Private Sub btnExécuter_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnExécuter.Click
        ' pagina valida?
        rfvLignes.Validate()
        rvLignes.Validate()
        If Not rfvLignes.IsValid Or Not rvLignes.IsValid Then
            afficheFormulaire()
            Exit Sub
        End If
        ' allegato dei dati filtrati alla griglia
        Try
            dvProduits.RowFilter = txtFiltre.Text.Trim
        Catch ex As Exception
            lblinfo1.Text = "Erreur de filtrage (" + ex.Message + ")"
            afficheFormulaire()
            Exit Sub
        End Try
        ' si memorizza il numero di prodotti per pagina
        Session("nbProduitsPage") = txtPages.Text
        'e la pagina corrente
        Session("pageCourante") = 0
        ' tutto a posto - si visualizzano i dati
        afficheProduits(0, CType(txtPages.Text, Integer))
    End Sub

Ricordiamo innanzitutto che la pagina presenta due componenti di convalida:

n.
nome
tipo
ruolo
7
rfvLignes
RequiredFieldValidator
verifica la presenza di un valore in [txtPages]
8
rvLignes
RangeValidator
verifica che txtPages rientri nell'intervallo [3,10]

La procedura inizia eseguendo il codice di controllo dei due componenti sopra indicati tramite il loro metodo [Validate], quindi verifica il valore del loro attributo [IsValid]. Quest'ultimo assume il valore [true] solo se il dato verificato è risultato valido. Se i test di validità di uno dei due componenti hanno dato esito negativo, viene nuovamente visualizzata la vista [formulaire] affinché l'utente possa correggere il proprio errore o i propri errori. La condizione di filtro viene applicata all’attributo [dvProduits.RowFilter]. In questo caso può verificarsi un’eccezione se l’utente ha inserito un criterio di filtro errato, come mostrato di seguito:

Image

In questo caso, viene nuovamente visualizzata la vista [formulaire]. Se entrambe le informazioni inserite sono corrette, nella sessione vengono inseriti due dati:

  • il numero di prodotti per pagina scelto dall’utente
  • il numero della pagina che verrà visualizzata – inizialmente la pagina 0

Queste due informazioni vengono utilizzate ogni volta che viene visualizzata la vista [produits].

9.6.11. Visualizzazione della vista [produits]

La vista [produits] è la seguente:

Image

Abbiamo 5 componenti [DataGrid], ciascuno dei quali espone una vista specifica della tabella dei prodotti [dtProduits]. Partendo da sinistra:

nome
ruolo
DataGrid1
griglia di visualizzazione di una vista filtrata della tabella dei prodotti. Il filtro è quello impostato dalla vista [formulaire]. Abbiamo inoltre .AllowPaging=true, .AllowSorting=true
DataGrid2
visualizza l'intera tabella dei prodotti - consente di tenere traccia degli aggiornamenti
DataGrid3
visualizzerà i prodotti eliminati nella tabella dei prodotti
DataGrid4
visualizzerà i prodotti modificati nella tabella dei prodotti
DataGrid5
visualizzerà i prodotti aggiunti nella tabella dei prodotti

La procedura [afficheProduits] è incaricata di visualizzare la vista precedente:


    Private Sub afficheProduits(ByVal page As Integer, ByVal taillePage As Integer)
        ' si associano i dati ai due componenti [DataGrid]
        Application.Lock()
        With DataGrid1
            .DataSource = dvProduits
            .PageSize = taillePage
            .CurrentPageIndex = page
            .DataBind()
        End With
        Dim dvCurrent As New DataView(dtProduits)
        dvCurrent.RowStateFilter = DataViewRowState.CurrentRows
        With DataGrid2
            .DataSource = dvCurrent
            .DataBind()
        End With
        Dim dvSupp As DataView = New DataView(dtProduits)
        dvSupp.RowStateFilter = DataViewRowState.Deleted
        With DataGrid3
            .DataSource = dvSupp
            .DataBind()
        End With
        Dim dvModif As DataView = New DataView(dtProduits)
        dvModif.RowStateFilter = DataViewRowState.ModifiedOriginal
        With DataGrid4
            .DataSource = dvModif
            .DataBind()
        End With
        Dim dvAjout As DataView = New DataView(dtProduits)
        dvAjout.RowStateFilter = DataViewRowState.Added
        With DataGrid5
            .DataSource = dvAjout
            .DataBind()
        End With
        Application.UnLock()
        ' viene visualizzata la vista [produits]
        vueProduits.Visible = True
        vueFormulaire.Visible = False
        vueErreurs.Visible = False
        vueAjout.Visible = False
        ' si salva la pagina corrente
        Session("pageCourante") = page
    End Sub

La procedura accetta due parametri:

  • il numero di pagina [page] da visualizzare in [DataGrid1]
  • il numero di prodotti [taillePage] per pagina

Il collegamento di [DataGrid] alla tabella dei prodotti avviene tramite 5 diverse viste.

  • [DataGrid1] è collegata alla vista paginata e ordinata [dvProduits].

        With DataGrid1
            .DataSource = dvProduits
            .PageSize = taillePage
            .CurrentPageIndex = page
            .DataBind()
        End With

È proprio durante questo collegamento di [DataGrid1] alla sua fonte dati che servono le due informazioni passate come parametri alla procedura.

  • [DataGrid2] è collegato a una vista che mostra tutti gli elementi attuali della tabella [dtProduits]. Come [DataGrid1], presenta una vista aggiornata della tabella [dtProduits], ma senza impaginazione né ordinamento.

        Dim dvCurrent As New DataView(dtProduits)
        dvCurrent.RowStateFilter = DataViewRowState.CurrentRows
        With DataGrid2
            .DataSource = dvCurrent
            .DataBind()
        End With

Qui utilizziamo l'attributo [RowStateFilter] della classe [DataView]. Una riga di una tabella o, in questo caso, di una vista possiede un attributo [RowState] che indica lo stato della riga. Eccone alcuni:

  • (continua)
    • [Added]: la riga è stata aggiunta
    • [Modified]: la riga è stata modificata
    • [Deleted]: la riga è stata eliminata
    • [Unchanged]: la riga non è cambiata

Quando la tabella [dtProduits] è stata creata inizialmente in [Application_Start], tutte le sue righe si trovavano nello stato [Unchanged]. Questo stato cambierà man mano che i client web effettueranno gli aggiornamenti:

  • (continua)
    • quando un cliente crea un nuovo prodotto, viene aggiunta una riga alla tabella [dtProduits]. Essa si troverà nello stato [Added].
    • quando un prodotto viene modificato, il suo stato passa da [Unchanged] a [Modified].
    • Quando un prodotto viene eliminato, la riga non viene eliminata fisicamente. Viene invece contrassegnata come da eliminare e il suo stato passa a [Deleted]. È possibile annullare tale eliminazione.

L’attributo [RowStateFilter] della classe [DataView] consente di filtrare una vista in base allo stato [RowState] delle sue righe. I suoi valori possibili sono quelli dell’enumerazione [DataRowViewState]:

  • (continua)
    • DataRowViewState.CurrentRows: le righe non eliminate vengono visualizzate con i loro valori attuali
    • DataRowViewState.Added: le righe aggiunte vengono visualizzate con i loro valori attuali
    • DataRowViewState.Deleted: le righe eliminate vengono visualizzate con i loro valori originali
    • DataRowViewState.ModifiedOriginal: le righe modificate vengono visualizzate con i loro valori originali
    • DataRowViewState.ModifiedCurrent: le righe modificate vengono visualizzate con i loro valori attuali

Una riga modificata presenta due valori: il valore originale, ovvero quello che la riga aveva prima della prima modifica, e il valore attuale, ottenuto dopo una o più modifiche. Questi due valori vengono conservati contemporaneamente.

Le righe da 2 a 5 di DataGrid mostrano una vista della tabella [dtProduits] filtrata in base all’attributo [RowStateFilter]

DataGrid
Filtro
DataGrid2
RowStateFilter= DataRowViewState.CurrentRows
DataGrid3
RowStateFilter= DataRowViewState.Deleted
DataGrid4
RowStateFilter= DataRowViewState.ModifiedOriginal
DataGrid5
RowStateFilter= DataRowViewState.Added

La tabella [dtPoduits] viene aggiornata contemporaneamente da diversi client web, il che può causare conflitti di accesso alla tabella. Quando un client visualizza le proprie viste della tabella [dtProduits], si vuole evitare che lo faccia mentre la tabella si trova in uno stato instabile perché è in fase di modifica da parte di un altro client web. Pertanto, ogni volta che un client necessita della tabella [dtProduits] in lettura come sopra descritto o in scrittura per aggiungere, modificare ed eliminare prodotti, si sincronizzerà con gli altri client tramite la seguente sequenza:

Application.Lock
... section critique 1
Application.Unlock

Nel codice dell’applicazione possono esserci diverse sezioni critiche:

Application.Lock
... section critique 2
Application.Unlock

Il meccanismo è il seguente:

  1. uno o più client raggiungono l'istruzione [Application.Lock] della sezione critica 1. Si tratta di un distributore di token di ingresso unico. Un solo client ottiene questo token. Chiamiamolo C1.
  2. Finché il client C1 non ha restituito il token tramite [Application.Unlock], nessun altro client è autorizzato ad accedere a una sezione critica controllata da [Application.Lock]. Nell’esempio sopra riportato, nessun client può quindi accedere alle sezioni critiche 1 e 2.
  3. Il client C1 esegue [Application.Unlock] e restituisce quindi il token di accesso. Quest’ultimo può quindi essere assegnato a un altro client. I passaggi da 1 a 3 vengono ripetuti.

Con questo meccanismo, garantiremo che un solo client abbia accesso alla tabella [dtProduits], sia in lettura che in scrittura.

Il collegamento [Mise à jour] visualizza anche la vista [Produits]:

Image

La procedura associata è la seguente:


    Private Sub lnkMisajour_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkMisajour.Click
        ' visualizzazione della vista prodotti
        afficheProduits(CType(Session("pageCourante"), Integer), CType(Session("nbProduitsPage"), Integer))
    End Sub

È necessario visualizzare la vista [produits]. Ciò avviene tramite la procedura [afficheProduits], che accetta due parametri: il numero della pagina corrente da visualizzare e il numero di prodotti per pagina da visualizzare. Queste due informazioni sono presenti nella sessione, eventualmente con i loro valori iniziali se l'utente non li ha mai definiti autonomamente.

9.6.12. Impaginazione e ordinamento di [DataGrid1]

Abbiamo già visto le procedure che gestiscono l’impaginazione e l’ordinamento di una [DataGrid] in un altro esempio. In caso di cambio di pagina, si visualizza la vista [produits] passando il numero della nuova pagina come parametro alla procedura [afficheProduits].


    Private Sub DataGrid1_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles DataGrid1.PageIndexChanged
        ' cambio di pagina
        afficheProduits(e.NewPageIndex, DataGrid1.PageSize)
    End Sub

La procedura [DataGrid1_SortCommand] viene eseguita quando l’utente fa clic sull’intestazione di una delle colonne di [DataGrid1]. È quindi necessario assegnare l'espressione di ordinamento all'attributo [Sort] della vista [dvProduits] visualizzata da [DataGrid1]. Quest’ultima è sintatticamente equivalente all’espressione di ordinamento che segue la clausola [order by] dell’istruzione SQL SELECT. L'argomento [e] della procedura possiede un attributo [SortExpression] che fornisce l'espressione di ordinamento associata alla colonna la cui intestazione è stata selezionata. Al momento della creazione del componente [DataGrid1], tale espressione di ordinamento era già stata definita. Di seguito è riportata quella definita per la colonna [nom] di [DataGrid1]:

Image

Se l’espressione di ordinamento non è stata definita durante la progettazione di [DataGrid], viene utilizzato il nome del campo dati associato alla colonna di [DataGrid]. L'ordine di ordinamento (crescente, decrescente) viene qui impostato dall'utente tramite i pulsanti di opzione [rdCroissant, rd Décroissant]:

Image

La procedura di ordinamento è la seguente:


    Private Sub DataGrid1_SortCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridSortCommandEventArgs) Handles DataGrid1.SortCommand
        ' si ordina la vista dati
        With dvProduits
            .Sort = e.SortExpression + " " + CType(IIf(rdCroissant.Checked, "asc", "desc"), String)
        End With
        ' visualizzazione
        afficheProduits(0, DataGrid1.PageSize)
    End Sub

9.6.13. Eliminazione di un prodotto

Per eliminare un prodotto, l'utente utilizza il link [Supprimer] presente nella riga del prodotto:

Image

Viene quindi eseguita la procedura [DataGrid1_DeleteCommand]:


    Private Sub DataGrid1_DeleteCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.DeleteCommand
        ' eliminazione di un prodotto
        ' chiave del prodotto da eliminare
        Dim idProduit As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)
        ' eliminazione del prodotto
        Dim erreur As Boolean = False
        Try
            supprimerProduit(idProduit)
        Catch ex As Exception
            ' problema
            lblInfo2.Text = ex.Message
            erreur = True
        End Try
        ' cambio di pagina?
        Dim page As Integer = DataGrid1.CurrentPageIndex
        If Not erreur AndAlso DataGrid1.Items.Count = 1 Then
            page = DataGrid1.CurrentPageIndex - 1
            If page < 0 Then page = 0
        End If
        ' visualizzazione prodotti
        afficheProduits(page, DataGrid1.PageSize)
    End Sub

L'aggiornamento dei prodotti avverrà tramite la chiave [id] del prodotto. Infatti, la colonna [id] della tabella [dtProduits] è la chiave primaria e, grazie ad essa, possiamo individuare nella tabella [dtProduits] la riga del prodotto che viene aggiornata nel componente [DataGrid1]. La procedura inizia quindi recuperando la chiave del prodotto il cui link [Supprimer] è stato cliccato:


         ' codice del prodotto da eliminare
        Dim idProduit As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)

Sappiamo che [e.Item] è l’elemento di [DataGrid1] all’origine dell’evento. In sostanza, questo elemento è una riga di [DataGrid]. [e.Item.ItemIndex] è il numero della riga all'origine dell'evento. Questo indice è relativo alla pagina attualmente visualizzata. Pertanto, la prima riga della pagina visualizzata ha la proprietà [ItemIndex=0], anche se nella tabella dei prodotti ha il numero 17. [DataGrid1.DataKeys] è l’elenco delle chiavi di [DataGrid]. Poiché in fase di progettazione abbiamo inserito [DataGrid1.DataKey=id], le chiavi di [DataGrid1] sono costituite dai valori della colonna [id] della tabella [dtProduits], che è al contempo la chiave primaria. Pertanto, [DataGrid1.DataKeys(e.Item.ItemIndex)] è la chiave [id] del prodotto da eliminare. Una volta ottenuto questo dato, se ne richiede l’eliminazione tramite la procedura [supprimerProduit]:


         ' eliminazione del prodotto
        Dim erreur As Boolean = False
        Try
            supprimerProduit(idProduit)
        Catch ex As Exception
            ' pb
            lblInfo2.Text = ex.Message
            erreur = True
        End Try

In alcuni casi questa operazione di eliminazione potrebbe non andare a buon fine. Vedremo perché. In tal caso viene generata un'eccezione, che viene gestita in questa sede. In caso di errore, il codice [DataGrid1] non deve essere modificato. Viene aggiunto solo un messaggio di errore alla vista [produits]. Se non si verifica alcun errore e l’utente ha appena eliminato l’unico prodotto dalla pagina corrente, viene visualizzata la pagina precedente.


         ' cambio di pagina?
        Dim page As Integer = DataGrid1.CurrentPageIndex
        If Not erreur AndAlso DataGrid1.Items.Count = 1 Then
            page = DataGrid1.CurrentPageIndex - 1
            If page < 0 Then page = 0
        End If

In ogni caso, la vista [produits] viene visualizzata nuovamente con la procedura [afficheProduits]:

         ' visualizzazione prodotti
        afficheProduits(page, DataGrid1.PageSize)

La procedura [supprimerProduit] è la seguente:


    Private Sub supprimerProduit(ByVal idProduit As Integer)
        Dim erreur As String
        Try
            ' sincronizzazione
            Application.Lock()
            ' ricerca della riga da eliminare
            Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
            If ligne Is Nothing Then
                erreur = String.Format("Produit [{0}] inexistant", idProduit)
            Else
                ' eliminazione della riga
                ligne.Delete()
            End If
        Catch ex As Exception
            erreur = String.Format("Erreur de suppression : {0}", ex.Message)
        Finally
            ' fine sincronizzazione
            Application.UnLock()
        End Try
        ' in caso di errore viene generata un'eccezione
        If erreur <> String.Empty Then Throw New Exception(erreur)
    End Sub

La procedura riceve come parametro la chiave del prodotto da eliminare. Se ci fosse un solo cliente da aggiornare, tale eliminazione potrebbe avvenire come segue:

             ' eliminazione della riga [idProduit]
            dtProduits.Rows.Find(idProduit).Delete()

Quando più clienti effettuano gli aggiornamenti contemporaneamente, la situazione si complica. Consideriamo infatti la seguente sequenza temporale:

Tempo
Azione
T1
il client A legge la tabella [dtProduits] – è presente un prodotto con chiave 20
T2
il cliente B legge la tabella [dtProduits] - c'è un prodotto con chiave 20
T3
il cliente A elimina il prodotto con chiave 20
T4
il cliente B elimina il prodotto con chiave 20

Quando i clienti A e B leggono la tabella dei prodotti e ne visualizzano il contenuto in una pagina web, la tabella contiene il prodotto con chiave 20. Potrebbero quindi voler eseguire un'azione su questo prodotto, ad esempio eliminarlo. Inevitabilmente uno dei due lo farà per primo. Chi arriverà dopo vorrà quindi eliminare un prodotto che non esiste più. Questo caso viene gestito dalla procedura [supprimerProduits] in diversi modi:

  • innanzitutto avviene la sincronizzazione con [Application.Lock]. Ciò significa che quando un cliente supera questa barriera, nessun altro può modificare o leggere la tabella dei prodotti. Infatti, tutte queste operazioni sono sincronizzate in questo modo. Lo abbiamo già visto per la lettura.
  • Si verifica quindi se il prodotto che si desidera eliminare esiste. In caso affermativo, viene eliminato; in caso contrario, viene generato un messaggio di errore.

             ' ricerca della riga da eliminare
            Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
            If ligne Is Nothing Then
                erreur = String.Format("Produit [{0}] inexistant", idProduit)
            Else
                 ' eliminazione della riga
                ligne.Delete()
            End If
  • Si esce dalla sequenza critica tramite [Application.Unlock] per consentire agli altri client di eseguire i propri aggiornamenti.
  • Se la riga non è stata eliminata, la procedura genera un'eccezione associata a un messaggio di errore.

Vediamo un esempio. Avviamo un client [Mozilla] e selezioniamo immediatamente l’opzione [Mise à jour] (vista parziale):

Image

Facciamo lo stesso con un client [Internet Explorer] (vista parziale):

Image

Con il cliente [Mozilla] eliminiamo il prodotto [produit2]. Otteniamo la seguente nuova pagina:

Image

L'operazione di eliminazione è andata a buon fine. Il prodotto eliminato compare correttamente in [DataGrid] tra i prodotti eliminati e non compare più nell'elenco dei prodotti dei componenti [DataGrid1] e [DataGrid2] che mostrano i prodotti attualmente presenti nella tabella. Procediamo allo stesso modo con [Interbet Explorer]. Eliminiamo l'elemento [produit2]:

Image

La risposta ottenuta è la seguente:

Image

Un messaggio di errore segnala all’utente che il prodotto con chiave [2] non esiste. La risposta restituisce al client una nuova vista che riflette lo stato attuale della tabella. Si nota che, effettivamente, il prodotto con chiave [2] è presente nell’elenco dei prodotti eliminati. La vista [produits] riflette quindi gli aggiornamenti di tutti i clienti e non di uno solo.

9.6.14. Aggiunta di un prodotto

Per aggiungere un prodotto, l’utente utilizza il link [Ajout] presente tra le opzioni. Questo si limita a visualizzare la vista [Ajout]:

Image

Image

Le due procedure coinvolte in questa azione sono le seguenti:


    Private Sub lnkAjout_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkAjout.Click
        ' viene visualizzata la vista [ajout]
        afficheAjout()
    End Sub

    Private Sub afficheAjout()
        ' visualizza la vista "Aggiungi"
        vueAjout.Visible = True
        vueFormulaire.Visible = False
        vueProduits.Visible = False
        vueErreurs.Visible = False
    End Sub

Si ricorda che la vista [Ajout] presenta i seguenti componenti di convalida:

nome
tipo
ruolo
rfvNom
RequiredFieldValidator
verifica la presenza di un valore in [txtNom]
rfvPrix
RequiredFieldValidator
verifica la presenza di un valore in [txtPrix]
cvPrix
CompareValidator
verifica che il prezzo sia maggiore o uguale a 0

La procedura di gestione del clic sul pulsante [Ajouter] è la seguente:


    Private Sub btnAjouter_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnAjouter.Click
        ' aggiunta di un nuovo elemento alla tabella dei prodotti
        ' innanzitutto i dati devono essere validi
        rfvNom.Validate()
        rfvPrix.Validate()
        cvPrix.Validate()
        If Not rfvNom.IsValid Or Not rfvPrix.IsValid Or Not cvPrix.IsValid Then
            ' di nuovo il modulo di inserimento
            afficheAjout()
            Exit Sub
        End If
        ' si crea una riga
        Dim produit As DataRow = dtProduits.NewRow
        produit("nom") = txtNom.Text.Trim
        produit("prix") = txtPrix.Text.Trim
        ' si aggiunge la riga alla tabella
        Application.Lock()
        Try
            dtProduits.Rows.Add(produit)
            lblInfo3.Text = "Ajout réussi"
            ' pulizia
            txtNom.Text = ""
            txtPrix.Text = ""
        Catch ex As Exception
            lblInfo3.Text = String.Format("Erreur : {0}", ex.Message)
        End Try
        Application.UnLock()
    End Sub

Innanzitutto, vengono eseguiti i controlli di validità dei tre componenti [rfvNom, rfvPrix, cvPrix]. Se uno dei controlli fallisce, viene nuovamente visualizzata la vista [Ajout] con i messaggi di errore relativi ai componenti di validazione. Ecco un esempio:

Image

Se i dati sono validi, si prepara la riga da inserire nella tabella [dtProduits].


         ' si crea una riga
        Dim produit As DataRow = dtProduits.NewRow
        produit("nom") = txtNom.Text.Trim
        produit("prix") = txtPrix.Text.Trim

Per prima cosa si crea una nuova riga della tabella [dtProduits] utilizzando il metodo [DataTable.NewRow]. Questa riga conterrà le tre colonne [id, nom, prix] della tabella [dtProduits]. Le colonne [nom, prix] vengono compilate con i valori inseriti nel modulo di aggiunta. La colonna [id] non viene invece compilata. Questa colonna è di tipo [AutoIncrement], c.a.d. Il valore SGBD fungerà da chiave per un nuovo prodotto, ovvero la chiave massima esistente aumentata di 1. La riga [produit] qui creata è separata dalla tabella [dtProduits]. Ora dobbiamo inserirla in questa tabella. Poiché aggiorneremo la tabella [dtProduits], attiviamo la sincronizzazione tra clienti:

        Application.Lock()

Fatto ciò, proviamo ad aggiungere la riga alla tabella [dtProduits]. Se l’operazione va a buon fine, visualizziamo un messaggio di conferma, altrimenti un messaggio di errore.

        Try
            dtProduits.Rows.Add(produit)
            lblInfo3.Text = "Ajout réussi"
             ' pulizia
            txtNom.Text = ""
            txtPrix.Text = ""
        Catch ex As Exception
            lblInfo3.Text = String.Format("Erreur : {0}", ex.Message)
        End Try

Normalmente non sono previste eccezioni. Tuttavia, per precauzione, è stata implementata una gestione delle eccezioni. Una volta effettuata l'aggiunta, si esce dalla sezione critica tramite [Application.Unlock].

9.6.15. Modifica di un prodotto

Per modificare un prodotto, l’utente utilizza il link [Modifier] presente nella riga del prodotto:

Image

Si passa quindi alla modalità di modifica:

Image

Grazie al componente [DataGrid], questo risultato si ottiene con pochissimo codice:


    Private Sub DataGrid1_EditCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.EditCommand
        ' si imposta l'elemento corrente in modalità di modifica
        DataGrid1.EditItemIndex = e.Item.ItemIndex
        ' si visualizzano nuovamente i prodotti
        afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
    End Sub

Facendo clic sul link [Modifier] si avvia l'esecuzione, lato server, della procedura [DataGrid1_EditCommand]. Il componente [DataGrid1] presenta un campo denominato [EditItemIndex]. La riga con indice pari al valore di [EditItemIndex] viene messa in modalità di modifica. Ogni valore della riga così aggiornata può essere modificato in una casella di immissione, come mostrato nella schermata sopra. Recuperiamo quindi l’indice del prodotto su cui è stato effettuato il clic sul link [Modifier] e lo assegniamo all’attributo [EditItemIndex] del componente [DataGrid1]. Non resta che visualizzare nuovamente la vista [produits]. Apparirà identica a prima, ma con una riga in modalità di modifica.

Il link [Annuler] del prodotto in fase di modifica consente all’utente di annullare l’aggiornamento. La procedura associata a questo link è la seguente:


    Private Sub DataGrid1_CancelCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.CancelCommand
        ' si visualizzano nuovamente i prodotti
        afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
    End Sub

Si limita a visualizzare nuovamente la vista [produits]. Si potrebbe essere tentati di inserire -1 in [DataGrid1.EditItemIndex] per annullare la modalità di aggiornamento. In realtà, sappiamo che la procedura [Page_Load] lo fa sistematicamente. Non è quindi necessario ripeterlo.

La modifica viene confermata tramite il link [Mettre à jour] della riga in fase di modifica. Viene quindi eseguita la procedura seguente:


    Private Sub DataGrid1_UpdateCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.UpdateCommand
        ' chiave del prodotto da modificare
        Dim idProduit As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)
        ' elementi modificati
        Dim nom As String = CType(e.Item.Cells(0).Controls(0), TextBox).Text.Trim
        Dim prix As String = CType(e.Item.Cells(1).Controls(0), TextBox).Text.Trim
        ' Modifiche valide?
        lblInfo2.Text = ""
        If nom = String.Empty Then lblInfo2.Text += "[Indiquez un nom]"
        If prix = String.Empty Then
            lblInfo2.Text += "[Indiquez un prix]"
        Else
            Dim nouveauPrix As Double
            Try
                nouveauPrix = CType(prix, Double)
                If nouveauPrix < 0 Then Throw New Exception
            Catch ex As Exception
                lblInfo2.Text += "[prix invalide]"
            End Try
        End If
        '  in caso di errore, si visualizza nuovamente la pagina di aggiornamento
        If lblInfo2.Text <> String.Empty Then
            ' si riporta la riga in modalità aggiornamento
            DataGrid1.EditItemIndex = e.Item.ItemIndex
            ' visualizzazione prodotti
            afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
            ' fine
            Exit Sub
        End If
        ' se non ci sono errori, si modifica la tabella
        Try
            modifierProduit(idProduit, e.Item)
        Catch ex As Exception
            ' problema
            lblInfo2.Text = ex.Message
        End Try
        ' visualizzazione prodotti
        afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
    End Sub

Come per l'eliminazione, dobbiamo recuperare la chiave del prodotto da modificare per individuare la relativa riga nella tabella dei prodotti [dtProduits]:


         ' chiave del prodotto da modificare
        Dim idProduit As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)

Successivamente dobbiamo recuperare i nuovi valori da assegnare alla riga. Questi si trovano nel componente [DataGrid1]. L'argomento [e] della procedura ci aiuta in questo. [e.Item] rappresenta la riga di [DataGrid1] da cui proviene l’evento. Si tratta quindi della riga in fase di aggiornamento, poiché il collegamento [Mettre à jour] esiste solo su questa riga. Questa riga contiene colonne designate dalla collezione [Cells] della riga. Pertanto, [e.Item.Cells(0)] rappresenta la colonna 0 della riga aggiornata. Sappiamo che i nuovi valori si trovano nei campi di immissione. La collezione [e.Item.Cells(i).Controls] rappresenta la collezione dei controlli della colonna i della riga [e.Item]. Le due istruzioni seguenti recuperano i valori delle caselle di immissione della riga aggiornata in [DataGrid1]:


         ' elementi modificati
        Dim nom As String = CType(e.Item.Cells(0).Controls(0), TextBox).Text.Trim
        Dim prix As String = CType(e.Item.Cells(1).Controls(0), TextBox).Text.Trim

Abbiamo quindi i nuovi valori della riga modificata sotto forma di stringhe di caratteri. Verifichiamo quindi se questi dati sono validi. Il nome non deve essere vuoto e il prezzo deve essere un numero positivo o zero:


         ' modifiche valide?
        lblInfo2.Text = ""
        If nom = String.Empty Then lblInfo2.Text += "[Indiquez un nom]"
        If prix = String.Empty Then
            lblInfo2.Text += "[Indiquez un prix]"
        Else
            Dim nouveauPrix As Double
            Try
                nouveauPrix = CType(prix, Double)
                If nouveauPrix < 0 Then Throw New Exception
            Catch ex As Exception
                lblInfo2.Text += "[prix invalide]"
            End Try
        End If

In caso di errore, l'etichetta [lblInfo2] visualizzerà un messaggio di errore e si limiterà a visualizzare nuovamente la stessa pagina:


          ' in caso di errore, si visualizza nuovamente la pagina di aggiornamento
        If lblInfo2.Text <> String.Empty Then
            ' si riporta la riga in modalità aggiornamento
            DataGrid1.EditItemIndex = e.Item.ItemIndex
            ' visualizzazione prodotti
            afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
             ' fine
            Exit Sub
        End If

Ciò che il codice sopra riportato non mostra è che i valori inseriti vanno persi. Infatti, [DataGrid1] è collegato ai dati della tabella [dtProduits], che contiene i valori originali della riga modificata. Ecco un esempio.

Image

La risposta ottenuta è la seguente:

Image

Si nota che i valori inseriti inizialmente sono andati persi. In un’applicazione professionale, ciò probabilmente non sarebbe accettabile. Qui si raggiungono alcuni limiti della modalità standard di aggiornamento del componente [DataGrid]. Sarebbe preferibile disporre di una vista [Modification] analoga alla vista [Ajout].

Se i dati sono validi, vengono utilizzati per aggiornare la tabella [dtProduits]:


         ' se non ci sono errori, si modifica la tabella
        Try
            modifierProduit(idProduit, e.Item)
        Catch ex As Exception
            ' problema
            lblInfo2.Text = ex.Message
        End Try
        ' visualizzazione prodotti
        afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)

Per lo stesso motivo indicato in caso di eliminazione di un prodotto, la modifica potrebbe non andare a buon fine. Infatti, tra il momento in cui il cliente ha letto la tabella [dtProduits] e quello in cui intende modificare uno dei propri prodotti, quest'ultimo potrebbe essere stato eliminato da un altro cliente. La procedura [modifierProduit] gestisce questo caso generando un'eccezione. Quest'ultima viene gestita in questa sede. A seguito del successo o del fallimento dell'aggiornamento, l'applicazione restituisce al cliente la vista [produits]. Resta da vedere come la procedura [modifierProduit] esegua l’aggiornamento:


    Private Sub modifierProduit(ByVal idProduit As Integer, ByVal item As DataGridItem)
        Dim erreur As String
        Try
            ' sincronizzazione
            Application.Lock()
            ' ricerca della riga da modificare
            Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
            If ligne Is Nothing Then
                erreur = String.Format("Produit [{0}] inexistant", idProduit)
            Else
                ' si modifica la riga
                With ligne
                    .Item("nom") = CType(item.Cells(0).Controls(0), TextBox).Text.Trim
                    .Item("prix") = CType(item.Cells(1).Controls(0), TextBox).Text.Trim
                End With
            End If
        Catch ex As Exception
            erreur = String.Format("Erreur lors de la modification : {0}", ex.Message)
        Finally
            ' fine della sincronizzazione
            Application.UnLock()
        End Try
        ' in caso di errore viene generata un'eccezione
        If erreur <> String.Empty Then Throw New Exception(erreur)
    End Sub

Non entreremo nei dettagli di questa procedura, il cui codice è analogo a quello della procedura [supprimerProduit], che è stata spiegata ampiamente. Vediamo semplicemente due esempi. Innanzitutto modifichiamo il prezzo del prodotto [produit1]:

Image

L'utilizzo del link [Mettre à jour] sopra riportato porta alla seguente risposta:

Image

La modifica è effettivamente presente nei componenti [DataGrid] 1 e 2, che riflettono lo stato attuale della tabella [dtProduits]. È presente anche nel componente [DataGrid4], che è una vista delle righe modificate, visualizzate con i loro valori originali. Ora esaminiamo un caso di conflitto di accesso. Come nell’esempio di cancellazione, utilizzeremo due client web diversi. Un client [Mozilla] legge la tabella [dtProduits] e passa alla modalità di modifica del prodotto [produit1]:

Image

Un cliente [Internet Explorer] sta per eliminare il prodotto [produit1]:

Image

Il cliente [Internet Explorer] elimina [produit1]:

Image

Si noti che [produit1] non figura più nella tabella delle righe modificate, ma in quella delle righe eliminate. Il client [Mozilla], dal canto suo, convalida il proprio aggiornamento:

Image

Il client [Mozilla] riceve la seguente risposta al proprio aggiornamento:

Image

Può constatare che [produit1] è stato eliminato poiché figura nell’elenco dei prodotti eliminati.

9.7. Applicazione web per l’aggiornamento della tabella fisica dei prodotti

9.7.1. Soluzioni proposte

L'applicazione precedente era più che altro un caso di studio volto a illustrare la gestione di un oggetto [DataTable] nella cache piuttosto che un caso reale. Infatti, prima o poi è necessario aggiornare la fonte effettiva dei dati. Si possono adottare due strategie diverse:

  1. si utilizza la cache [dtProduits] in memoria per aggiornare la fonte dei dati. Si può prevedere una pagina situata nella struttura ad albero web dell’applicazione precedente per poter accedere alla cache [dtProduits] di quest’ultima. Tale pagina offrirebbe a un amministratore la possibilità di replicare sulla fonte fisica dei dati le modifiche presenti nella cache in [dtProduits]. A tal fine, si potrebbe aggiungere un nuovo metodo alla classe di accesso [produits] che accetti come parametro la cache [dtProduits] e che, utilizzando tale cache, aggiorni la fonte fisica dei dati.
  2. La fonte fisica dei dati viene aggiornata contemporaneamente alla cache.

La strategia n. 1 consente di aprire una sola connessione con la fonte fisica dei dati. La strategia n. 2 richiede una connessione ad ogni aggiornamento. A seconda della disponibilità delle connessioni, si potrà preferire l’una o l’altra strategia. Poiché disponiamo degli strumenti per implementarla (la classe [produits]), scegliamo la strategia n. 2.

9.7.2. Soluzione 1

Per motivi di continuità con l’applicazione precedente, scegliamo la seguente strategia:

  • la fonte fisica viene aggiornata contemporaneamente alla cache
  • la cache viene costruita una sola volta all’avvio dell’applicazione in [global.asax.vb]. Ciò significa che se la fonte fisica dei dati viene aggiornata da clienti diversi da quelli web, questi ultimi non vedono tali modifiche. Vedono solo quelle che essi stessi apportano alla tabella nella cache.

Per aggiornare la fonte fisica dei dati, abbiamo bisogno di un'istanza della classe di accesso ai prodotti. Ogni client potrebbe averne una propria. È anche possibile condividere un'unica istanza che verrebbe creata dall'applicazione al suo avvio. È questa la soluzione che scegliamo in questo caso. Il codice di controllo [global.asax.vb] viene modificato come segue:


Imports System
Imports System.Web
...

Public Class Global
    Inherits System.Web.HttpApplication

    Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' si recuperano le informazioni di configurazione
        Dim chaînedeConnexion As String = ConfigurationSettings.AppSettings("OLEDBStringConnection")
        Dim defaultProduitsPage As String = ConfigurationSettings.AppSettings("defaultProduitsPage")
        Dim erreurs As New ArrayList
...
        ' qui non ci sono errori di configurazione
        ' si crea un oggetto prodotti
        Dim objProduits As New produits(chaînedeConnexion)
        Dim dtProduits As DataTable
        Try
            dtProduits = objProduits.getProduits
        Catch ex As ExceptionProduits
            'si è verificato un errore di accesso ai prodotti, lo si registra nell'applicazione
            Application("erreurs") = ex.erreurs
            Exit Sub
        Catch ex As Exception
            ' errore non gestito
            erreurs.Add(ex.Message)
            Application("erreurs") = erreurs
            ' uscita dalla subroutine
        End Try
        ' qui non si verificano errori di inizializzazione
...
        ' si memorizza l'istanza di accesso ai dati
        Application("objProduits") = objProduits
    End Sub

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
...
    End Sub
End Class

Nell'applicazione è stata memorizzata un'istanza della classe di accesso ai dati, associata alla chiave [objProduits]. Ogni cliente utilizzerà questa istanza per accedere alla fonte fisica dei dati. Essa verrà recuperata nella procedura [Page_Load] di [main.aspx.vb]:


    Protected objProduits As produits
....
    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
        ' si recupera l'istanza di accesso ai prodotti
        objProduits = CType(Application("objProduits"), produits)
...
    End Sub

L'istanza di accesso ai dati [objProduits] è accessibile a tutti i metodi della pagina. Verrà utilizzata per le tre operazioni di aggiornamento: aggiunta, eliminazione e modifica.

La procedura di inserimento viene modificata come segue:


    Private Sub btnAjouter_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnAjouter.Click
...
        ' si aggiunge la riga
        Application.Lock()
        Try
            ' si aggiunge la riga alla tabella nella cache
            dtProduits.Rows.Add(produit)
             ' si aggiunge la riga alla fonte fisica
            Dim nouveauProduit As sProduit
            With nouveauProduit
                .nom = CType(produit("nom"), String)
                .prix = CType(produit("prix"), Double)
            End With
            objProduits.ajouterProduit(nouveauProduit)
            ' monitoraggio
            lblInfo3.Text = "Ajout réussi"
            ' pulizia
            txtNom.Text = ""
            txtPrix.Text = ""
        Catch ex As Exception
            ' errore
            lblInfo3.Text = String.Format("Erreur : {0}", ex.Message)
        End Try
        Application.UnLock()
    End Sub

La procedura di modifica viene modificata come segue:


    Private Sub modifierProduit(ByVal idProduit As Integer, ByVal item As DataGridItem)
        Dim erreur As String
        Try
            ' sincronizzazione
            Application.Lock()
            ' ricerca della riga da modificare
            Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
            If ligne Is Nothing Then
                erreur = String.Format("Produit [{0}] inexistant", idProduit)
            Else
                ' si modifica la riga nella cache
                With ligne
                    .Item("nom") = CType(item.Cells(0).Controls(0), TextBox).Text.Trim
                    .Item("prix") = CType(item.Cells(1).Controls(0), TextBox).Text.Trim
                End With
                 ' modifica della riga nella fonte fisica
                Dim nouveauProduit As sProduit
                With nouveauProduit
                    .id = idProduit
                    .nom = CType(ligne.Item("nom"), String)
                    .prix = CType(ligne.Item("prix"), Double)
                End With
                objProduits.modifierProduit(nouveauProduit)
            End If
        Catch ex As Exception
            erreur = String.Format("Erreur lors de la modification : {0}", ex.Message)
        Finally
            ' fine della sincronizzazione
            Application.UnLock()
        End Try
        ' in caso di errore viene generata un'eccezione
        If erreur <> String.Empty Then Throw New Exception(erreur)
    End Sub

La procedura di cancellazione viene modificata come segue:


    Private Sub supprimerProduit(ByVal idProduit As Integer)
        Dim erreur As String
        Try
            ' sincronizzazione
            Application.Lock()
            ' ricerca della riga da eliminare
            Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
            If ligne Is Nothing Then
                erreur = String.Format("Produit [{0}] inexistant", idProduit)
            Else
                ' eliminazione della riga dalla cache
                ligne.Delete()
                 ' eliminazione della riga nella fonte fisica
                objProduits.supprimerProduit(idProduit)
            End If
        Catch ex As Exception
            erreur = String.Format("Erreur de suppression : {0}", ex.Message)
        Finally
            ' fine della sincronizzazione
            Application.UnLock()
        End Try
        ' in caso di errore viene generata un'eccezione
        If erreur <> String.Empty Then Throw New Exception(erreur)
    End Sub

9.7.3. Test

Partiamo dalla seguente tabella di dati contenuta nel file ACCESS:

Image

Viene avviato un client web:

Image

Aggiungiamo un prodotto:

Image

Eliminiamo [produit1]:

Image

Modifichiamo il prezzo di [produit2]:

Image

Dopo queste modifiche, esaminiamo il contenuto della tabella [liste] nel database ACCESS:

Image

Le tre modifiche sono state correttamente applicate alla tabella fisica. Esaminiamo ora un caso di conflitto. Eliminiamo, direttamente sotto ACCESS, la riga relativa a [produit2]:

Image

Torniamo al nostro client web. Quest’ultimo non vede l’eliminazione che è stata effettuata e vuole a sua volta eliminare [produit2]:

Image

Riceve la seguente risposta:

Image

La riga [produit2] è stata effettivamente rimossa dalla cache, come indicato dall’elenco dei prodotti eliminati. L’eliminazione di [produit2] nella fonte fisica, invece, non è andata a buon fine, come indicato dal messaggio di errore.

9.7.4. Soluzione 2

Nella soluzione precedente, i client web aggiornano simultaneamente la fonte dati fisica, ma non vedono le modifiche apportate dagli altri client. Vedono solo le proprie. Ora vorremmo che un client potesse vedere la fonte dati fisica così com’è attualmente e non com’era all’avvio dell’applicazione. A tal fine, offriremo al client una nuova opzione:

Image

Con l’opzione [Rafraîchir], il client forza una nuova lettura della fonte dati fisica. Affinché ciò non influisca sugli altri client, è necessario che la tabella risultante da questa lettura appartenga al client che esegue l’aggiornamento e non sia condivisa con gli altri client. Questa è la prima differenza rispetto all’applicazione precedente. La cache [dtProduits] della fonte dati verrà creata da ciascun client e non dall’applicazione stessa. La modifica viene apportata in [global.asax.vb]:


Imports System
Imports System.Web
Imports System.Web.SessionState
Imports st.istia.univangers.fr
Imports System.Configuration
Imports System.Data
Imports Microsoft.VisualBasic
Imports System.Collections

Public Class Global
    Inherits System.Web.HttpApplication

    Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' si recuperano le informazioni di configurazione
...
        ' qui non ci sono errori di configurazione
        ' si crea un oggetto prodotti
        Dim objProduits As New produits(chaînedeConnexion)
        ' si memorizza il numero di prodotti per pagina
        Application("defaultProduitsPage") = defaultProduitsPage
        ' si memorizza l'istanza di accesso ai dati
        Application("objProduits") = objProduits
    End Sub

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' inizializzazione delle variabili di sessione
        If IsNothing(Application("erreurs")) Then
            ' si memorizza nella cache la fonte dei dati
            Dim dtProduits As DataTable
            Try
                Application.Lock()
                dtProduits = CType(Application("objProduits"), produits).getProduits
            Catch ex As ExceptionProduits
                'si è verificato un errore nell'accesso ai prodotti, lo si registra nella sessione
                Session("erreurs") = ex.erreurs
                Exit Sub
            Finally
                Application.UnLock()
            End Try
            ' si inserisce la cache [dtProduits] nella sessione
            Session("dtProduits") = dtProduits
            ' visualizzazione della tabella dei prodotti
            Session("dvProduits") = dtProduits.DefaultView
            ' numero di prodotti per pagina
            Session("nbProduitsPage") = Application("defaultProduitsPage")
            ' pagina corrente visualizzata
            Session("pageCourante") = 0
        End If
    End Sub

End Class

Le informazioni relative alla sessione vengono recuperate ad ogni richiesta nella procedura [Page_Load]:


     ' dati della pagina
    Protected dtProduits As DataTable
    Protected dvProduits As DataView
    Protected objProduits As produits
    Protected nbProduitsPage As Integer
    Protected pageCourante As Integer

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' si verifica se l'applicazione presenta un errore
        If Not IsNothing(Application("erreurs")) Then
            ' l'applicazione non si è inizializzata correttamente
            afficheErreurs(CType(Application("erreurs"), ArrayList))
            Exit Sub
        End If
        ' si verifica se la sessione presenta un errore
        If Not IsNothing(Session("erreurs")) Then
            ' la sessione non si è inizializzata correttamente
            afficheErreurs(CType(Session("erreurs"), ArrayList))
            Exit Sub
        End If
        ' si recupera un riferimento dalla tabella dei prodotti
        dtProduits = CType(Session("dtProduits"), DataTable)
        ' si recupera la vista dei prodotti
        dvProduits = CType(Session("dvProduits"), DataView)
        ' si recupera il numero di prodotti per pagina
        nbProduitsPage = CType(Session("nbProduitsPage"), Integer)
        ' si recupera la pagina corrente
        pageCourante = CType(Session("pageCourante"), Integer)
        ' si recupera l'istanza di accesso ai prodotti
        objProduits = CType(Application("objProduits"), produits)
        ' si annulla un eventuale aggiornamento in corso
        DataGrid1.EditItemIndex = -1
        'prima richiesta
        If Not IsPostBack Then
            ' viene visualizzato il modulo iniziale
            txtPages.Text = nbProduitsPage.ToString
            afficheFormulaire()
        End If
    End Sub

Le informazioni recuperate durante la sessione verranno salvate alla fine della stessa sessione al termine di ogni richiesta:


    Private Sub Page_PreRender(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.PreRender
        ' si memorizzano alcune informazioni nella sessione
        Session("dtProduits") = dtProduits
        Session("dvProduits") = dvProduits
        Session("nbProduitsPage") = nbProduitsPage
        Session("pageCourante") = pageCourante
    End Sub

L'evento [PreRender] segnala che la risposta sta per essere inviata al cliente. Ne approfittiamo per salvare nella sessione tutti i dati da conservare. Si tratta di un'operazione eccessiva, poiché molto spesso solo alcuni di essi hanno cambiato valore. Questo salvataggio sistematico ha il vantaggio di sollevarci dalla gestione della sessione negli altri metodi della pagina.

L'operazione di aggiornamento della cache è gestita dalla seguente procedura:


    Private Sub lnkRefresh_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkRefresh.Click
        ' è necessario aggiornare la cache [dtProduits] con la fonte dati fisica
        ' inizio sincronizzazione
        Application.Lock()
        Try
            ' la tabella dei prodotti
            dtProduits = CType(Application("objProduits"), produits).getProduits
            ' si memorizza il filtro attuale
            Dim filtre As String = dvProduits.RowFilter
            ' si crea la nuova vista filtrata
            dvProduits = New DataView(dtProduits)
            ' si ripristina il filtro
            dvProduits.RowFilter = filtre
        Catch ex As ExceptionProduits
            'si è verificato un errore di accesso ai prodotti, lo si registra nella sessione
            Session("erreurs") = ex.erreurs
            ' si visualizza la vista [erreurs]
            afficheErreurs(ex.erreurs)
            ' finito
            Exit Sub
        Finally
            ' sincronizzazione completata
            Application.UnLock()
        End Try
        ' tutto andato bene - si visualizzano i prodotti a partire dalla prima pagina
        afficheProduits(0, nbProduitsPage)
    End Sub

La procedura rigenera nuovi valori per la cache [dtProduits] e la vista [dvProduits]. Questi valori saranno inseriti nella sessione dalla procedura [Page_PreRender] descritta in precedenza. Una volta ricostruita la cache [dtProduits], vengono visualizzati i prodotti a partire dalla prima pagina.

Ecco un esempio di esecuzione. Viene avviato un client [mozilla] che visualizza i prodotti:

Image

Anche un client [Internet Explorer] fa lo stesso:

Image

Il client [Mozilla] elimina [produit1], modifica [produit2] e aggiunge un nuovo prodotto. Si ottiene la seguente nuova pagina:

Image

Il cliente [Internet Explorer] desidera eliminare [produit1].

Image

Ottiene la seguente risposta:

Image

Gli è stato segnalato che [produit1] non esiste più. L'utente decide quindi di aggiornare la cache utilizzando il link [Rafraîchir] sopra indicato. Ottiene la seguente risposta:

Image

Ora dispone della stessa fonte di dati del client [Mozilla].

9.8. Conclusion

In questo capitolo abbiamo dedicato molto tempo ai contenitori di dati e al loro collegamento con le fonti di dati. Abbiamo concluso mostrando come aggiornare una fonte di dati utilizzando un componente [DataGrid]. Abbiamo utilizzato come fonte di dati una tabella di un database. Anche se il componente [DataGrid] semplifica leggermente la rappresentazione dei dati, la vera difficoltà non risiede nella parte di presentazione, ma nella gestione degli aggiornamenti della fonte di dati effettuati dai diversi clienti. Possono verificarsi conflitti di accesso che devono essere gestiti. In questo caso, li abbiamo gestiti nel controller utilizzando il componente [Application.Lock]. Probabilmente sarebbe più opportuno sincronizzare gli accessi alla fonte di dati nella classe di accesso, in modo che il controller non debba occuparsi di tali dettagli che non rientrano nelle sue competenze.

In pratica, le tabelle di un database sono collegate tra loro da relazioni e il loro aggiornamento deve tenerne conto. Ciò ha conseguenze innanzitutto sulla classe di accesso ai dati, che diventa più complessa di quella necessaria per una tabella indipendente. In genere ha anche conseguenze sulla parte di presentazione dell’applicazione web, poiché spesso è necessario visualizzare non una singola tabella, ma tabelle collegate tra loro da relazioni.

Questo capitolo ha inoltre permesso di presentare diverse strutture di dati, come ad esempio [DataTable, DataView].