Skip to content

9. ASP-Serverkomponenten – 3

9.1. Einführung

Wir setzen unsere Arbeit an der Benutzeroberfläche fort und untersuchen die Möglichkeiten der Komponenten [DataList] und [DataGrid], insbesondere im Bereich der Aktualisierung der angezeigten Daten

9.2. Behandlung von Ereignissen im Zusammenhang mit Daten in datengebundenen Komponenten

9.2.1. Das Beispiel

Betrachten Sie die folgende Seite:

Die Seite enthält drei Komponenten, die mit einer Datenliste verknüpft sind:

  • eine [DataList]-Komponente mit dem Namen [DataList1]
  • eine [DataGrid]-Komponente mit dem Namen [DataGrid1]
  • eine [Repeater]-Komponente mit dem Namen [Repeater1]

Die zugehörige Datenliste ist das Array {"zero", "one", "two", "three"}. Jeder dieser Datenpunkte ist mit einer Gruppe von zwei Schaltflächen mit den Bezeichnungen [Info1] und [Info2] verknüpft. Wenn der Benutzer auf eine der Schaltflächen klickt, wird der Name der angeklickten Schaltfläche angezeigt. Das Ziel hierbei ist es, zu demonstrieren, wie eine Liste von Schaltflächen oder Links verwaltet wird.

9.2.2. Komponentenkonfiguration

Die Komponente [DataList1] ist wie folgt konfiguriert:


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

Wir haben alles weggelassen, was das Erscheinungsbild der [DataList] betrifft, um uns ausschließlich auf ihren Inhalt zu konzentrieren:

  • Der Abschnitt <HeaderTemplate> definiert die Kopfzeile der [DataList], und der Abschnitt <FooterTemplate> definiert ihre Fußzeile.
  • Der Abschnitt <ItemTemplate> ist die Anzeigevorlage, die für jedes Element in der zugehörigen Datenliste verwendet wird. Er enthält die folgenden Elemente:
    • den Wert des aktuellen Datenelements in der mit der Komponente verknüpften Datenliste: <%# Container.DataItem %>
    • zwei Schaltflächen mit den Bezeichnungen [Info1] bzw. [Info2]. Die Klasse [Button] verfügt über ein [CommandName]-Attribut, das hier verwendet wird. Damit können wir feststellen, welche Schaltfläche ein Ereignis in der [DataList] ausgelöst hat. Um Schaltflächenklicks zu verarbeiten, verwenden wir einen einzigen Ereignis-Handler, der mit der [DataList] selbst verknüpft ist, nicht mit den Schaltflächen. Dieser Handler erhält Informationen darüber, in welcher Zeile der [DataList] der Klick stattgefunden hat. Das Attribut [CommandName] teilt uns mit, welche Schaltfläche in dieser Zeile den Klick ausgelöst hat.

Die [Repeater1]-Komponente wird auf sehr ähnliche Weise konfiguriert:


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

Wir haben einfach einen <SeparatorTemplate>-Abschnitt hinzugefügt, damit die von der Komponente angezeigten aufeinanderfolgenden Daten durch einen horizontalen Balken getrennt werden.

Schließlich wird die Komponente [DataGrid1] wie folgt konfiguriert:


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

Auch hier haben wir die Stilinformationen (Farben, Breiten usw.) weggelassen. Wir befinden uns im Modus zur automatischen Spaltenerstellung, dem Standardmodus für das [DataGrid]. Das bedeutet, dass es genauso viele Spalten gibt, wie in der Datenquelle vorhanden sind. Hier ist es eine. Wir haben zwei weitere Spalten hinzugefügt, die mit <asp:ButtonColumn> gekennzeichnet sind. Wir definieren ähnliche Informationen wie für die anderen beiden Komponenten sowie den Schaltflächentyp, der hier [PushButton] ist. Der Standardtyp ist [LinkButton], d. h. ein Link. Zusätzlich werden die Daten paginiert [AllowPaging=true] mit einer Seitengröße von zwei Elementen [PageSize=2].

9.2.3. Der Code für das Seitenlayout

Der Präsentationscode für unsere Beispielseite wurde in einer Datei namens [main.aspx] abgelegt:


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

Im obigen Code haben wir den Formatierungscode (Farben, Linien, Größen usw.) weggelassen

9.2.4. Der Code für das Seitensteuerelement

Der Anwendungssteuerungscode wurde in die Datei [main.aspx.vb] eingefügt:

Public Class main
    Inherits System.Web.UI.Page

    ' components page
    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

    ' the data source
    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
            'data source links
            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
        ' an event has occurred on one of the [datalist] lines
        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
        ' an event has occurred on one of the [repeater] lines
        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
        ' an event has occurred on one of the [datagrid] lines
        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
        ' change page
        With DataGrid1
            .CurrentPageIndex = e.NewPageIndex
            .DataSource = textes
            .DataBind()
        End With
    End Sub
End Class

Kommentare:

  • Die Datenquelle [textes] ist ein einfaches Array von Zeichenfolgen. Sie wird an die drei Komponenten auf der Seite gebunden
  • Diese Bindung wird in der Prozedur [Page_Load] bei der ersten Anfrage hergestellt. Bei nachfolgenden Anfragen rufen die drei Komponenten ihre Werte über den [VIEW_STATE]-Mechanismus ab.
  • Die drei Komponenten verfügen über einen Handler für das [ItemCommand]-Ereignis. Dieses Ereignis tritt ein, wenn in einer der Zeilen der Komponente auf eine Schaltfläche oder einen Link geklickt wird. Der Handler erhält zwei Informationen:
    • source: die Referenz auf das Objekt (Schaltfläche oder Link), das das Ereignis ausgelöst hat
    • a: Informationen zum Ereignis vom Typ [DataListCommandEventArgs], [RepeaterCommandEventArgs] oder [DataGridCommandEventArgs], je nach Fall. Das Argument a enthält verschiedene Informationen. Zwei davon sind für uns hier von Interesse:
      • a.Item: stellt die Zeile dar, in der das Ereignis aufgetreten ist, vom Typ [DataListItem], [DataGridItem] oder [RepeaterItem]. Unabhängig vom genauen Typ verfügt das [Item]-Element über ein [ItemIndex]-Attribut, das die Zeilennummer des [Item] in dem Container angibt, zu dem es gehört. Hier zeigen wir diese Zeilennummer an
      • a.CommandName: ist das [CommandName]-Attribut der Schaltfläche (Button, LinkButton, ImageButton), die das Ereignis ausgelöst hat. Diese Information ermöglicht es uns in Kombination mit der vorherigen festzustellen, welche Schaltfläche im Container das [ItemCommand]-Ereignis ausgelöst hat

9.3. Anwendung – Verwalten einer Abonnentenliste

Nachdem wir nun wissen, wie Ereignisse innerhalb eines Datencontainers abgefangen werden können, zeigen wir anhand eines Beispiels, wie man damit umgeht.

9.3.1. Einführung

Die hier vorgestellte Anwendung simuliert eine Anwendung zum Abonnieren einer Mailingliste. Diese werden durch ein dreispaltiges [DataTable]-Objekt definiert:

name
Typ
Rolle
ID
Zeichenkette
Primärschlüssel
Thema
Zeichenkette
Liste Themenname
Beschreibung
Zeichenkette
eine Beschreibung der Themen, die die Liste abdeckt

Um unser Beispiel einfach zu halten, wird das oben genannte [DataTable]-Objekt programmgesteuert auf beliebige Weise erstellt. In einer realen Anwendung würde es wahrscheinlich von einer Methode einer Datenzugriffsklasse bereitgestellt werden. Die Listentabelle wird in der Prozedur [Application_Start] erstellt, und die resultierende Tabelle wird in der Anwendung gespeichert. Wir nennen sie die Tabelle [dtThèmes]. Der Benutzer abonniert bestimmte Themen aus dieser Tabelle. Die Liste seiner Abonnements wird in einem [DataTable]-Objekt namens [dtAbonnements] gespeichert, das folgende Struktur aufweist:

name
Typ
Rolle
id
Zeichenkette
Primärschlüssel
Thema
Zeichenkette
Liste Themenname

Die Single-Page-Anwendung sieht wie folgt aus:

 
Nr.
Name
Typ
Eigenschaften
Rolle
1
dgThemes
DataGrid
 
Verfügbare Mailinglisten zum Abonnieren
2
dlAbonnements
DataList
 
Liste der Abonnements des Benutzers für die oben genannten Listen
3
panelInfo
Panel
 
Informationsbereich zum vom Benutzer ausgewählten Thema mit einem Link [Weitere Informationen]
4
lblTheme
Label
Teil von [panelInfos]
Themenname
5
lblBeschreibung
Beschriftung
Teil von [panelInfos]
Themenbeschreibung
6
lblInfo
Bezeichnung
 
Anwendungsinformationsmeldung

Unser Beispiel soll die Verwendung der Komponenten [DataGrid] und [DataList] veranschaulichen, insbesondere die Behandlung von Ereignissen, die auf Zeilenebene dieser Datencontainer auftreten. Daher verfügt die Anwendung nicht über eine Schaltfläche zum Absenden, mit der beispielsweise die Auswahl des Benutzers in einer Datenbank gespeichert würde. Dennoch ist sie realistisch. Wir befinden uns nahe an einer E-Commerce-Anwendung, in der ein Benutzer Produkte (Abonnements) in seinen Warenkorb legen würde.

9.3.2. Funktionalität

Die erste Ansicht, die der Benutzer sieht, sieht wie folgt aus:

Der Benutzer klickt auf die Links [Weitere Informationen], um Details zu einem Thema zu erhalten. Diese werden in [panelInfos] angezeigt:

Image

Er klickt auf die Links [Abonnieren], um ein Thema zu abonnieren. Die ausgewählten Themen werden der Komponente [dlSubscriptions] hinzugefügt:

Image

Möglicherweise möchte der Nutzer ein Thema abonnieren, das er bereits abonniert hat. Eine Meldung weist ihn darauf hin:

Image

Schließlich kann er sich über die Schaltflächen [Abmelden] oben von jedem der Themen abmelden. Eine Bestätigung ist nicht erforderlich. Dies ist hier nicht notwendig, da der Benutzer sich problemlos wieder anmelden kann. Hier ist die Ansicht nach der Abmeldung von [topic1]:

Image

9.3.3. Konfigurieren von Datencontainern

Die Komponente [dgThèmes] vom Typ [DataGrid] ist an eine Datenquelle vom Typ [DataTable] gebunden. Die Formatierung erfolgte über den Link [Auto Format] im Eigenschaftenfenster von [DataGrid]. Die Eigenschaften wurden über den Link [Property Generator] im selben Fenster definiert. Der generierte Code lautet wie folgt (der Formatierungscode wurde weggelassen):


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

Beachten Sie folgende Punkte:

AutoGenerateColumns=false
Wir definieren die anzuzeigenden Spalten selbst im Abschnitt <columns>...</columns>
AllowPaging=true
PageSize=5
für die Datenseitenaufteilung
<asp:BoundColumn>
definiert die [theme]-Spalte (HeaderText) des [DataGrid], die mit der [theme]-Spalte der Datenquelle (DataField) verknüpft wird
<asp:ButtonColumn>
definiert zwei Spalten mit Schaltflächen (oder Links). Um zwischen den beiden Links in derselben Zeile zu unterscheiden, verwenden wir ihre [CommandName]-Eigenschaft.

Die [DataGrid]-Komponente ist noch nicht vollständig konfiguriert. Sie wird im Controller-Code konfiguriert.

Die Komponente [dlAbonnements] vom Typ [DataList] ist mit einer Quelle vom Typ [DataTable] verknüpft. Die Formatierung erfolgte über den Link [AutoFormat] im Eigenschaftenfenster von [DataList]. Ihre Eigenschaften wurden direkt im Präsentationscode definiert. Dieser Code lautet wie folgt (der Formatierungscode wurde weggelassen):


                        <asp:DataList id="dlAbonnements" ... runat="server" >
                            <HeaderTemplate>
                                <div align="center">
                                    Vos abonnements</div>
                            </HeaderTemplate>
                            <ItemStyle ...></ItemStyle>
                            <ItemTemplate>
                                <TABLE>
                                    <TR>
                                        <TD><%#Container.DataItem("thème")%></TD>
                                        <TD>
                                            <asp:Button id="lnkRetirer" CommandName="retirer" runat="server" Text="Retirer" /></TD>
                                    </TR>
                                </TABLE>
                            </ItemTemplate>
                            <HeaderStyle ...></HeaderStyle>
                        </asp:DataList>
<HeaderTemplate>
definiert den Kopfzeilentext der [DataList]
<ItemTemplate>
definiert das aktuelle Element in der [DataList] – hier haben wir eine Tabelle mit zwei Spalten und einer Zeile platziert. Die erste Zelle enthält den Namen des Themas, das der Benutzer abonnieren möchte, und die andere enthält die Schaltfläche [Unsubscribe], über die der Benutzer seine Auswahl aufheben kann.

9.3.4. Die Präsentationsseite

Der Präsentationscode [main.aspx] lautet wie folgt:


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

Die Steuerungslogik ist auf die Dateien [global.asax] und [main.aspx] verteilt. Die Datei [global.asax] sieht wie folgt aus:

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

Die zugehörige Datei [global.asax.vb] enthält den folgenden Code:


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)
        ' initialize the data source
        Dim thèmes As New DataTable
        ' columns
        With thèmes.Columns
            .Add("id", GetType(System.Int32))
            .Add("thème", GetType(System.String))
            .Add("description", GetType(System.String))
        End With
        ' column id will be primary key
        thèmes.Constraints.Add("cléprimaire", thèmes.Columns("id"), True)
        ' lines
        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
        ' put the data source in the application
        Application("thèmes") = thèmes
    End Sub
 
    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' start of session - create an empty subscriptions table
        Dim dtAbonnements As New DataTable
        With dtAbonnements
            ' the columns
            .Columns.Add("id", GetType(String))
            .Columns.Add("thème", GetType(String))
            ' the primary key
            .PrimaryKey = New DataColumn() {.Columns("id")}
        End With
        ' the table is placed in the session
        Session.Item("abonnements") = dtAbonnements
    End Sub
 
End Class

Die Prozedur [Application_Start], die ausgeführt wird, wenn die Anwendung ihre allererste Anfrage erhält, erstellt die [DataTable] mit den Themen, die abonniert werden können. Sie wird willkürlich mithilfe von Code erstellt. Erinnern Sie sich an die Technik, die wir bereits kennengelernt haben. Wir erstellen in der folgenden Reihenfolge:

  • ein leeres [DataTable]-Objekt ohne Struktur und ohne Daten
  • die Tabellenstruktur durch Definition ihrer Spalten (Name und Datentyp)
  • die Tabellenzeilen, die die nützlichen Daten darstellen

Hier haben wir einen Primärschlüssel hinzugefügt. Die Spalte „id“ dient als Primärschlüssel. Es gibt mehrere Möglichkeiten, dies auszudrücken. Hier haben wir eine Einschränkung verwendet. In SQL ist eine Einschränkung eine Regel, der die Daten in einer Zeile folgen müssen, damit diese Zeile zu einer Tabelle hinzugefügt werden kann. Es gibt alle möglichen Arten von Einschränkungen. Die Einschränkung „Primärschlüssel“ erzwingt, dass die Spalte, auf die sie angewendet wird, eindeutige und nicht leere Werte enthält. Ein Primärschlüssel kann tatsächlich aus einem Ausdruck bestehen, der Werte aus mehreren Spalten einbezieht. [DataTable].Constraints ist die Sammlung der Constraints für eine bestimmte Tabelle. Um eine Constraint hinzuzufügen, verwenden wir die Methode [DataTable.Constraints.Add]. Diese Methode hat mehrere Signaturen. Hier haben wir die Methode [Add(Byval name as String, Byval column as DataColumn, Byval primaryKey as Boolean)] verwendet:

name
Name der Einschränkung – kann beliebig sein
column
Spalte, die der Primärschlüssel sein soll – vom Typ [DataColumn]
primaryKey
muss auf [true] gesetzt werden, damit [Spalte] zum Primärschlüssel wird. Wenn [primaryKey=false] ist, gilt für [Spalte] nur die Eindeutigkeitsbeschränkung

Um die Spalte mit dem Namen „id“ zum Primärschlüssel der Tabelle [dtAbonnements] zu machen, schreiben wir:

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

Die Prozedur [Session_Start] wird ausgeführt, wenn die Anwendung die erste Anfrage von einem Client erhält. Sie dient dazu, für jeden Client spezifische Objekte zu erstellen, die über die verschiedenen Anfragen des Clients hinweg bestehen bleiben müssen. Die Prozedur erstellt die [DataTable] mit den Abonnements des Clients. Es wird nur die Struktur erstellt, da diese Tabelle anfangs leer ist. Sie wird bei eingehenden Anfragen gefüllt. Auch hier dient die Spalte „id“ als Primärschlüssel. Wir haben eine andere Technik verwendet, um diese Einschränkung zu deklarieren:

[DataTable].PrimaryKey
ist das Array der Spalten, aus denen der Primärschlüssel besteht – hier haben wir ein Array mit einem Element deklariert: die Spalte mit dem Namen „id“

Wenn die Anfrage des Clients den Controller [main.aspx] erreicht, stehen beide [DataTable]-Objekte in der Anwendung für die Themen-Tabelle und in der Sitzung für die Abonnement-Tabelle zur Verfügung. Der Controller [main.aspx.vb] sieht wie folgt aus:


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

Die Hauptaufgabe der Prozedur [Page_Load] besteht darin:

  • die beiden Tabellen [dtThèmes] und [dtAbonnements], die sich jeweils in der Anwendung und der Sitzung befinden, abzurufen, um sie allen Methoden auf der Seite zur Verfügung zu stellen
  • die Daten aus diesen beiden Quellen an ihre jeweiligen Container zu binden. Dies geschieht nur bei der ersten Anfrage. Bei nachfolgenden Anfragen muss die Bindung nicht systematisch durchgeführt werden, und wenn sie erforderlich ist, kann es manchmal notwendig sein, auf ein Ereignis zu warten, das nach [Page_Load] eintritt, um sie durchzuführen.

Der Code für [Page_Load] lautet wie folgt:


    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' retrieve data sources
        dtThèmes = CType(Application("thèmes"), DataTable)
        dtAbonnements = CType(Session("abonnements"), DataTable)
        ' data link
        If Not IsPostBack Then
            liaisons()
        End If
        ' we hide certain information
        panelInfo.Visible = False
    End Sub
 
    Private Sub liaisons()
        'link the data source to the [datagrid] component
        With dgThèmes
            .DataSource = dtThèmes
            .DataKeyField = "id"
        End With
        ' link the data source to the [datalist] component
        With dlAbonnements
            .DataSource = dtAbonnements
            .DataKeyField = "id"
        End With
        ' assign data to components
        Page.DataBind()
    End Sub

In der Prozedur [Bindings] verwenden wir die Eigenschaft [DataKeyField] der Komponenten [DataList] und [DataGrid], um die Spalte in der Datenquelle zu definieren, die zur eindeutigen Identifizierung der Zeilen in den Containern verwendet wird. In der Regel ist diese Spalte der Primärschlüssel der Datenquelle, dies ist jedoch nicht zwingend erforderlich. Es reicht aus, wenn die Spalte keine Duplikate und keine leeren Werte enthält. Für den Container [dgThemes] dient die Spalte „id“ der Quelle [dtThemes] als Primärschlüssel, und für den Container [dlSubscriptions] ist es die Spalte „id“ der Quelle [dtSubscriptions]. Es ist nicht erforderlich, dass die Spalte, die als Primärschlüssel des Containers dient, vom Container selbst angezeigt wird. Hier zeigt keiner der Container die Primärschlüsselspalte an. Der Vorteil eines Containers mit Primärschlüssel besteht darin, dass Sie auf einfache Weise die Informationen aus der Datenquelle abrufen können, die mit der Containerzeile verknüpft sind, in der ein Ereignis aufgetreten ist. Tatsächlich ist es üblich, dass Sie ausgehend von einer Containerzeile, in der ein Ereignis aufgetreten ist, eine Aktion auf der entsprechenden Zeile in der verknüpften Datenquelle ausführen müssen. Der Primärschlüssel erleichtert diese Aufgabe.

Die Paginierung des [DataGrid] wird auf die übliche Weise gehandhabt:


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

Aktionen auf die Links [Weitere Informationen] und [Abonnieren] werden von der Prozedur [dgThemes_ItemCommand] verarbeitet:


    Private Sub dgThèmes_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles dgThèmes.ItemCommand
        ' evt on a [datagrid] line
        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
        ' link
        liaisons()
    End Sub

Wir nutzen die Tatsache, dass beide Links über ein [CommandName]-Attribut verfügen, um sie zu unterscheiden. Je nach Wert dieses Attributs rufen wir die Prozedur [info] oder [subscribe] auf und übergeben in beiden Fällen den Schlüssel „id“, der dem [DataGrid]-Element zugeordnet ist, bei dem das Ereignis aufgetreten ist. Mit diesen Informationen zeigt die Prozedur [info] die Details des vom Benutzer ausgewählten Themas an:

    Private Sub infos(ByVal id As String)
        ' information on the theme of key id
        ' we retrieve the line from the [datatable] corresponding to the key
        Dim ligne As DataRow
        ligne = dtThèmes.Rows.Find(id)
        If Not ligne Is Nothing Then
            ' display info
            lblThème.Text = CType(ligne("thème"), String)
            lblDescription.Text = CType(ligne("description"), String)
            panelInfo.Visible = True
        End If
    End Sub

Da die Tabelle [dtThèmes] über einen Primärschlüssel verfügt, können wir mit der Methode [dtThèmes.Rows.Find("P")] die Zeile mit dem Primärschlüssel P finden. Wird sie gefunden, erhalten wir ein [DataRow]-Objekt. Hier müssen wir die Zeile mit dem Primärschlüssel [id] finden, wobei [id] als Parameter übergeben wird. Wird die Zeile gefunden, fügen wir die Informationen [theme] und [description] aus dieser Zeile in das Informationsfeld ein, das wir anschließend sichtbar machen.

Die Prozedur [subscribe(id)] muss das Thema mit dem Schlüssel [id] zur Abonnementliste hinzufügen. Der Code lautet wie folgt:


    Private Sub abonner(ByVal id As String)
        ' id key theme subscription
        ' we retrieve the line from the [datatable] corresponding to the key
        Dim ligne As DataRow
        ligne = dtThèmes.Rows.Find(id)
        If Not ligne Is Nothing Then
            ' check if you are not already a subscriber
            Dim abonnement As DataRow
            abonnement = dtAbonnements.Rows.Find(id)
            If Not abonnement Is Nothing Then
                ' report the error
                lblInfo.Text = "Vous êtes déjà abonné au thème [" + ligne("thème") + "]"
            Else
                ' add the theme to the subscriptions
                abonnement = dtAbonnements.NewRow
                abonnement("id") = id
                abonnement("thème") = ligne("thème")
                dtAbonnements.Rows.Add(abonnement)
                ' we make the connections
                liaisons()
            End If
        End If
    End Sub

In der Themenliste [dtThemes] suchen wir zunächst nach der Zeile mit dem Schlüssel [id]. Wird diese gefunden, prüfen wir, ob dieses Thema nicht bereits in der Liste der Abonnements vorhanden ist, um ein doppeltes Hinzufügen zu vermeiden. Ist dies der Fall, zeigen wir eine Fehlermeldung an. Andernfalls fügen wir ein neues Abonnement zur Tabelle [dtSubscriptions] hinzu und verknüpfen die Datenlistenkomponenten mit ihren jeweiligen Quellen.

Wenn der Benutzer auf die Schaltfläche [Entfernen] klickt, muss ein Eintrag aus der Tabelle [dtAbonnements] gelöscht werden. Dies geschieht durch die folgende Prozedur:


    Private Sub dlAbonnements_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles dlAbonnements.ItemCommand
        ' withdraw a subscription
        Dim commande As String = e.CommandName
        If commande = "retirer" Then
            ' we remove the [datatable] subscription
            With dtAbonnements.Rows
                .Remove(.Find(dlAbonnements.DataKeys(e.Item.ItemIndex)))
            End With
            ' links
            liaisons()
        End If
    End Sub

Zunächst prüfen wir die [CommandName]-Eigenschaft des Elements, das das Ereignis ausgelöst hat. Dies ist eigentlich völlig unnötig, da die Schaltfläche [Remove] das einzige Steuerelement ist, das in der [DataList]-Komponente ein Ereignis auslösen kann. Es besteht daher keine Mehrdeutigkeit. Um eine Zeile aus einem [DataTable]-Objekt zu löschen, verwenden wir die Methode [DataList.Remove(DataRow)], die die als Parameter übergebene [DataRow]-Zeile aus der Tabelle entfernt. Diese Zeile wird mit der Methode [DataList.Find] gefunden, an die wir den Primärschlüssel der gesuchten Zeile übergeben. Sobald die Zeile gelöscht wurde, binden wir die Daten an die Komponenten

9.4. Verwalten einer paginierten [DataList]

Wir greifen das vorherige Beispiel wieder auf, um die [DataList]-Komponente, die die Abonnementliste des Benutzers darstellt, zu paginieren. Im Gegensatz zur [DataGrid]-Komponente bietet die [DataList]-Komponente keine integrierte Paginierungsfunktion. Wir werden sehen, dass die Implementierung der Paginierung komplex ist, was uns helfen wird, den Wert der automatischen Paginierung des [DataGrid] zu schätzen.

9.4.1. So funktioniert es

Der einzige Unterschied besteht in der Paginierung der [DataList]. Auf jeder Seite werden zwei Abonnements angezeigt. Wenn der Benutzer fünf Abonnements hat, gibt es drei Seiten. Die erste Seite sieht wie folgt aus:

Image

Der Zugriff auf die zweite Seite erfolgt über den Link [Weiter]:

Image

Die dritte Seite:

Image

Beachten Sie, dass die Links [Zurück] und [Weiter] nur sichtbar sind, wenn der aktuellen Seite jeweils eine vorhergehende bzw. eine nachfolgende Seite folgt.

9.4.2. Präsentationscode

Die Links [Zurück] und [Weiter] werden durch Hinzufügen eines <FooterTemplate>-Tags zur [DataList] erstellt:


                        <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. Steuerelement-Code

Die zugehörige Datei [global.asax.vb] ändert sich wie folgt:

...

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)
        ' start of session - create an empty subscriptions table
        Dim dtAbonnements As New DataTable
        With dtAbonnements
            ' the columns
            .Columns.Add("id", GetType(String))
            .Columns.Add("thème", GetType(String))
            ' the primary key
            .PrimaryKey = New DataColumn() {.Columns("id")}
        End With
        ' the table is placed in the session
        Session.Item("abonnements") = dtAbonnements
        ' the current page is page 0
        Session.Item("pAC") = 0
        ' the number of subscriptions on this page is 0
        Session.Item("nbAC") = 0
    End Sub

Zusätzlich zur Abonnementtabelle [dtAbonnements] werden zwei weitere Informationen in der Sitzung gespeichert:

pAC
vom Typ [Integer] – dies ist die Nummer der aktuellen Seite, die bei der letzten Anfrage angezeigt wurde
nbAC
vom Typ [Integer] – Anzahl der Zeilen, die auf der vorherigen aktuellen Seite angezeigt wurden

Zu Beginn einer Sitzung sind die aktuelle Seitenzahl und die Anzahl der Zeilen auf dieser Seite gleich Null.

Der Controller [main.aspx.vb] entwickelt sich wie folgt:

....

Public Class main
    Inherits System.Web.UI.Page

....

    ' application data
    Protected dtThèmes As DataTable
    Protected dtAbonnements As DataTable
    Protected dtPA As DataTable    ' subscription page displayed
    Protected Const nbAP As Integer = 2    ' subscriptions per page
    Protected pAC As Integer    ' current subscription page
    Protected nbAC As Integer    ' number of subscriptions in current page

    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

Hier definieren wir neue Daten im Zusammenhang mit der Paginierung von Abonnements:


    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

Einige dieser Informationen werden in der Sitzung gespeichert und bei jeder Anfrage in der Prozedur [Page_Load] abgerufen:


    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' retrieve data sources
        dtThèmes = CType(Application("thèmes"), DataTable)
        dtAbonnements = CType(Session("abonnements"), DataTable)
        ' and subscription display information
        pAC = CType(Session("pAC"), Integer)
        nbAC = CType(Session("nbAC"), Integer)
        ' data link
        If Not IsPostBack Then
            ' display an empty subscription list
            terminer()
        End If
        ' we hide certain information
        panelInfo.Visible = False
    End Sub

Die abgerufenen Informationen [pAC] und [nbAC] sind Details zur Abonnementseite, die bei der vorherigen Anfrage angezeigt wurde:

pAC
ist die Nummer der aktuellen Seite, die bei der vorherigen Anfrage angezeigt wurde
nbAC
ist die Anzahl der auf dieser aktuellen Seite angezeigten Zeilen

Die Methode [terminer] bindet die Komponenten an ihre Datenquellen, genau wie es die Methode [liaisons] in der vorherigen Anwendung getan hat. Neu ist hier die Bindung von [DataList] an die Tabelle [dtPA], bei der es sich um die anzuzeigende Abonnementseite handelt:


    Private Sub terminer()
        ' link the data source to the [datagrid] component
        With dgThèmes
            .DataSource = dtThèmes
            .DataKeyField = "id"
        End With
        ' link the subscriptions page to the [datalist] component, taking into account the current page pAC
        changePAC()
        ' page p is displayed
        With dlAbonnements
            .DataSource = dtPA
            .DataKeyField = "id"
        End With
        ' assign data to components
        Page.DataBind()
        ' management of [previous] and [next] links in the [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)
        ' save current page information in the session
        Session("pAC") = pAC
        Session("nbAC") = dtPA.Rows.Count
    End Sub

Folgende Punkte sind zu beachten:

  • Die Quelle [dtPA] hängt von der aktuell anzuzeigenden Seitenzahl [pAC] ab. Die Variable [pAC] ist eine globale Variable der Klasse, die von Methoden manipuliert wird, die diese aktuelle Seitenzahl ändern müssen. Die Methode [changePAC] ist für den Aufbau der Tabelle [dtPA] zuständig, die mit der Komponente [dlAbonnements] verknüpft wird.
  • Die Methode [setLiens] ist dafür zuständig, die Links [Previous] und [Next] anzuzeigen oder auszublenden, je nachdem, ob der aktuell angezeigten Seite [pAC] eine vorherige und eine nachfolgende Seite vorangeht bzw. folgt. Sie hat vier Parameter:
    • [dlAbonnements]: das [DataList]-Steuerelement, dessen Steuerelementbaum wir durchsuchen werden, um die beiden Links zu finden. Obwohl sich diese beiden Links an einer bestimmten Stelle befinden – der Fußzeile des [DataList] – scheint es keine einfache Möglichkeit zu geben, direkt auf sie zu verweisen. Jedenfalls wurde hier keine gefunden.
    • [blPrecedent]: Ein Boolescher Wert, der der Eigenschaft [visible] des Links [Precedent] zugewiesen wird – ist wahr, wenn die aktuelle Seite nicht 0 ist
    • [blNext]: Ein Boolescher Wert, der der Eigenschaft [visible] des Links [Next] zugewiesen wird – ist wahr, wenn die aktuelle Seite nicht die letzte Seite in der Abonnementliste ist
    • [nbLiensTrouvés]: ein Ausgabeparameter, der die Anzahl der gefundenen Links zählt. Sobald diese Zahl zwei erreicht, wird die Methode beendet.
  • Die Informationen [pAC] und [nbAC] werden für die nächste Anfrage in der Sitzung gespeichert.

Die Methode [changePAC] erstellt die Tabelle [dtPA], die mit der Komponente [dlAbonnements] verknüpft wird. Dies geschieht auf der Grundlage der [pAC]-Nummer der aktuell anzuzeigenden Seite. Die Tabelle [dtPA] muss bestimmte Zeilen aus der Abonnementtabelle [dtAbonnements] anzeigen. Zur Erinnerung: Diese Tabelle wird in der Sitzung gespeichert und bei eingehenden Anfragen aktualisiert (erhöht oder verringert). Wir beginnen damit, den Bereich [first,last] der Zeilennummern in der Tabelle [dtAbonnements] festzulegen, die die Tabelle [dtPA] anzeigen muss:


    Private Sub changePAC()
        ' makes the pAC page the current subscription page
        ' datalist] page management
        Dim nbAbonnements = dtAbonnements.Rows.Count
        Dim dernièrePage = (nbAbonnements - 1) \ nbAP
        ' first and last subscription
        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

Sobald dies erledigt ist, können wir die Tabelle [dtPA] erstellen. Zunächst definieren wir ihre Struktur [id, theme], dann füllen wir sie, indem wir die Zeilen aus [dtSubscriptions] kopieren, deren Nummern in den zuvor berechneten Bereich [first, last] fallen.


        ' creation of the datatable dtpa
        dtPA = New DataTable
        With dtPA
            ' the columns
            .Columns.Add("id", GetType(String))
            .Columns.Add("thème", GetType(String))
            ' the primary key
            .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

Am Ende der Methode [changePAC] ist die Tabelle [dtPA] erstellt und kann an die Komponente [DataList] gebunden werden. Dies geschieht in der Methode [terminer]. In derselben Methode wird die Prozedur [setLiens] verwendet, um den Status der Links [Previous] und [Next] in der [DataList] festzulegen. Der Code für diese Prozedur lautet wie folgt:


    Private Sub setLiens(ByVal ctl As Control, ByVal blPrec As Boolean, ByVal blSuivant As Boolean, ByRef nbLiensTrouvés As Integer)
        ' search for [previous] and [next] links
        ' in the [datalist] control tree
        ' have all the links been found?
        If nbLiensTrouvés = 2 Then Exit Sub
        ' review of child controls
        Dim c As Control
        For Each c In ctl.Controls
            ' we work deep down first - the links are at the bottom of the tree
            setLiens(c, blPrec, blSuivant, nbLiensTrouvés)
            ' link [Previous] ?
            If c.ID = "lnkPrecedent" Then
                CType(c, LinkButton).Visible = blPrec
                nbLiensTrouvés += 1
            End If
            ' next] link ?
            If c.ID = "lnkSuivant" Then
                CType(c, LinkButton).Visible = blSuivant
                nbLiensTrouvés += 1
            End If
        Next
    End Sub

Die Prozedur ist rekursiv. Sie sucht zunächst unter den untergeordneten Steuerelementen der Komponente [dlAbonnements] nach Komponenten mit den Namen [lnkPrecedent] und [lnkSuivant], bei denen es sich um die IDs der beiden Paginierungslinks handelt. Die Suche beginnt am unteren Ende des Steuerelementbaums, da sich die Links dort befinden. Sobald ein Link gefunden wird, wird der Zähler [nbLiensTrouvés] erhöht und die Eigenschaft [visible] des Links auf einen Wert gesetzt, der als Parameter an die Prozedur übergeben wurde. Sobald beide Links gefunden wurden, wird der Steuerelementbaum nicht mehr durchlaufen und die rekursive Prozedur endet.

Wir haben erwähnt, dass die Methode [changePAC], die die Datenquelle [dtPA] für die Komponente [dlAbonnements] festlegt, mit der [pAC]-Nummer der aktuell anzuzeigenden Seite arbeitet. Mehrere Prozeduren ändern diese Nummer:

    Private Sub abonner(ByVal id As String)
        ' id key theme subscription
..
            ' add the theme to the subscriptions
..
            ' update current page number - it's now the last page
            pAC = (dtAbonnements.Rows.Count - 1) \ nbAP
            ' data links
            terminer()
        End If
    End Sub

Nach dem Hinzufügen eines Abonnements erscheint dieses am Ende der Abonnementliste. Daher wird die Ansicht auf die letzte Seite der Abonnements gesetzt, damit der Benutzer die vorgenommene Ergänzung sehen kann.


    Private Sub dlAbonnements_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles dlAbonnements.ItemCommand
        ' withdraw a subscription
        Dim commande As String = e.CommandName
        Select Case commande
            Case "retirer"
                ' we remove the [datatable] subscription
                With dtAbonnements.Rows
                    .Remove(.Find(dlAbonnements.DataKeys(e.Item.ItemIndex)))
                End With
                ' is it necessary to change the current page?
                nbAC -= 1
                If nbAC = 0 Then pAC -= 1
            Case "precedent"
                ' change current page
                pAC -= 1
            Case "suivant"
                ' change current page
                pAC += 1
        End Select
        ' data links
        terminer()
    End Sub
  • [nbAC] ist die Anzahl der Zeilen, die auf der aktuellen Seite angezeigt werden, bevor ein Abonnement gekündigt wird. Wenn die neue Zeilenzahl auf der Seite gleich 0 ist, wird die aktuelle Seitenzahl [pAC] um eins verringert.
  • Wenn auf den Link [Zurück] geklickt wird, wird die aktuelle Seitenzahl [pAC] um eins verringert.
  • Wenn auf den Link [Weiter] geklickt wird, wird die aktuelle Seitenzahl [pAC] um eins erhöht.

Die übrigen Abläufe bleiben unverändert.

9.4.4. Fazit

In diesem Beispiel haben wir gezeigt, dass wir eine [DataList]-Komponente paginieren können. Diese Paginierung ist komplex, und es ist vorzuziehen, sich nach Möglichkeit auf die automatische Paginierung der [DataGrid]-Komponente zu verlassen. Dieses Beispiel hat uns auch gezeigt, wie man auf die Komponenten zugreift, die sich in der Fußzeile der [DataList]-Komponente befinden.

9.5. Klasse für den Zugriff auf eine Produktdatenbank

Wir konzentrieren uns erneut auf die ACCESS-Datenbank [products], die wir bereits verwendet haben. Erinnern Sie sich daran, dass sie eine einzige Tabelle namens [list] mit der folgenden Struktur enthält:

Wir werden eine Zugriffsklasse für die Tabelle [list] erstellen, die es uns ermöglicht, Daten aus dieser Tabelle zu lesen und sie zu aktualisieren. Außerdem erstellen wir einen Konsolen-Client, der die zuvor erstellte Klasse nutzt, um die Tabelle zu aktualisieren. Als Nächstes erstellen wir einen Web-Client, der dieselbe Aufgabe ausführt.

9.5.1. Die Klasse „ProductException“

Die Klasse [Exception] verfügt über einen Konstruktor, der eine Fehlermeldung als Parameter entgegennimmt. Hier benötigen wir eine Ausnahmeklasse mit einem Konstruktor, der eine Liste von Fehlermeldungen anstelle einer einzelnen Fehlermeldung akzeptiert. Dies wird die unten stehende Klasse [ExceptionProduits] sein:


    Public Class ExceptionProduits
        Inherits Exception
 
        ' exception error msg
        Private _erreurs As ArrayList
 
        ' manufacturer
        Public Sub New(ByVal erreurs As ArrayList)
            Me._erreurs = erreurs
        End Sub
 
        ' property
        Public ReadOnly Property erreurs() As ArrayList
            Get
                Return _erreurs
            End Get
        End Property
    End Class

9.5.2. Die Struktur [sProduct]

Die Struktur [product] repräsentiert ein Produkt [id, name, price]:


    ' structure sProduit
    Public Structure sProduit
        ' the fields
        Private _id As Integer
        Private _nom As String
        Private _prix As Double
 
        ' property id
        Public Property id() As Integer
            Get
                Return _id
            End Get
            Set(ByVal Value As Integer)
                _id = Value
            End Set
        End Property
 
        ' property name
        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
 
        ' property price
        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

Die Struktur akzeptiert nur gültige Daten für die Felder [name] und [price].

9.5.3. Die Klasse „Products“

Die Klasse [Products] ist die Klasse, mit der wir die Tabelle [list] in der Produktdatenbank aktualisieren können. Ihre Struktur sieht wie folgt aus:

    Public Class produits
        ' instance data

        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

Instanzdaten

Die von den verschiedenen Methoden der Klasse gemeinsam genutzten Daten lauten wie folgt:


        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
Verbindung
Die Datenbankverbindung wird geöffnet, um einen SQL-Befehl auszuführen, und unmittelbar danach wieder geschlossen
selectText
SQL-Abfrage [select] zum Abrufen der gesamten Tabelle [list]
insertText
Abfrage, die das Einfügen einer Zeile (name, price) in die Tabelle [list] ermöglicht. Beachten Sie, dass das Feld [id] nicht angegeben ist. Dies liegt daran, dass das DBMS dieses Feld automatisch inkrementiert, sodass wir es nicht angeben müssen.
updateText
Abfrage zur Aktualisierung der Felder (name, price) der Zeile in der Tabelle [list] mit dem Schlüssel [id]
deleteText
Abfrage, die die Zeile mit dem Schlüssel [id] aus der Tabelle [list] löscht
selectCommand
[OleDbCommand]-Objekt, das die Abfrage [selectText] auf der Verbindung [connection] ausführt
updateCommand
[OleDbCommand]-Objekt, das die Abfrage [updateText] über die Verbindung [connection] ausführt
insertCommand
[OleDbCommand]-Objekt, das die Abfrage [insertText] über die Verbindung [connection] ausführt
deleteCommand
[OleDbCommand]-Objekt, das die Abfrage [deleteText] über die Verbindung [connection] ausführt
adapter
Objekt, das verwendet wird, um das Ergebnis der Ausführung von [selectCommand] in einem [DataSet]-Objekt abzurufen

Der Konstruktor

Der Konstruktor nimmt einen einzigen Parameter [OLEDBConnectionString] entgegen, bei dem es sich um die Verbindungszeichenfolge handelt, die die zu verwendende Datenbank angibt. Daraus werden die vier Befehle zum Abfragen und Aktualisieren der Tabelle sowie der Adapter vorbereitet. Dies ist lediglich ein Vorbereitungsschritt, es wird keine Verbindung hergestellt.


        Public Sub New(ByVal chaineConnexionOLEDB As String)
            ' preparing the connection
            connexion = New OleDbConnection(chaineConnexionOLEDB)
            ' prepare query orders
            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
            ' prepare the data access adapter
            adaptateur.SelectCommand = selectCommand
        End Sub

Die Methode getProducts

Diese Methode ruft den Inhalt der Tabelle [List] in ein [DataTable]-Objekt ab. Der Code lautet wie folgt:


        Public Function getProduits() As DataTable
            ' we put the [list] table in a [dataset]
            Dim contenu As New DataSet
            ' create a DataAdapter object to read data from source 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
            ' we return the result
            Return contenu.Tables(0)
        End Function

Die Arbeit wird durch die folgenden beiden Anweisungen erledigt:


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

Die Methode [FillSchema] legt die Struktur (Spalten, Einschränkungen, Beziehungen) des enthaltenen [DataSet] auf der Grundlage der Struktur der Datenbank fest, auf die [adapter.Connection] verweist. Auf diese Weise können wir die Struktur der Tabelle [list] einschließlich ihres Primärschlüssels abrufen. Der darauf folgende Vorgang [Fill] füllt das enthaltene [DataSet] mit den Zeilen aus der Tabelle [list]. Mit diesem einzigen Vorgang hätten wir die Daten und die Struktur erhalten, jedoch nicht den Primärschlüssel. Dies ist jedoch nützlich, um die Tabelle [list] im Arbeitsspeicher zu aktualisieren. Hier behandeln wir, wie auch in den anderen Methoden, etwaige Fehler mithilfe der Klasse [ProductExceptions], um eine Liste (ArrayList) von Fehlern anstelle eines einzelnen Fehlers zu erhalten. Die Methode [getProducts] gibt die Tabelle [list] als [DataTable]-Objekt zurück.

Die Methode addProducts

Mit dieser Methode können Sie eine Zeile (id, name, price) zur Tabelle [list] hinzufügen. Diese Informationen werden ihr in Form einer [sProduct]-Struktur mit den Feldern [id, name, price] übergeben. Der Code der Methode lautet wie folgt:


        Public Sub ajouterProduit(ByVal produit As sProduit)
            ' add a product [name,price]
            ' we prepare the parameters for the addition
            With insertCommand.Parameters
                .Clear()
                .Add(New OleDbParameter("nom", produit.nom))
                .Add(New OleDbParameter("prix", produit.prix))
            End With
            ' we add
            Try
                ' opening connection
                connexion.Open()
                ' order execution
                insertCommand.ExecuteNonQuery()
            Catch ex As Exception
                ' pb
                Dim erreursCommande As New ArrayList
                erreursCommande.Add(String.Format("Erreur lors de l'ajout : {0}", ex.Message))
                Throw New ExceptionProduits(erreursCommande)
            Finally
                ' locking connection
                connexion.Close()
            End Try
        End Sub

Die Felder der Struktur [product] werden als Parameter an den Befehl [insertCommand] übergeben. Sehen wir uns die aktuelle Konfiguration dieses Befehls an (siehe Konstruktor):


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

Der Text des SQL-Befehls [insert] enthält formale Parameter ?, die durch tatsächliche Parameter ersetzt werden müssen. Dies geschieht mithilfe der Sammlung [Parameters] der Klasse [OleDbCommand]. Diese Sammlung enthält Elemente vom Typ [OleDbParameter], die die tatsächlichen Parameter definieren, welche die formalen Parameter ? ersetzen müssen. Da diese nicht benannt sind, wird der Index der tatsächlichen Parameter verwendet, um zu bestimmen, welcher formale Parameter einem bestimmten tatsächlichen Parameter entspricht. Hier ersetzt der tatsächliche Parameter #i in der [Parameters]-Sammlung den formalen Parameter ? #i. Um einen tatsächlichen Parameter vom Typ [OleDbParameter] zu erstellen, verwenden wir den Konstruktor [OleDbParameter (Byval name as String, Byval value as Object)], der den Namen und den Wert des tatsächlichen Parameters definiert. Der Name kann beliebig sein. Außerdem wird er hier nicht verwendet. Die beiden Parameter der SQL-Anweisung [insert] erhalten als Werte die Werte der Felder [name, price] der Struktur [product]. Sobald dies geschehen ist, wird das Einfügen durch die Anweisung [insertCommand.ExecuteNonQuery] ausgeführt.

Die Methode modifyProducts

Mit dieser Methode können Sie eine Zeile in der Tabelle [list] ändern. Die dafür erforderlichen Informationen werden in der Struktur [sProduct] bereitgestellt, die die Felder [id, name, price] enthält.


        Public Sub modifierProduit(ByVal produit As sProduit)
            ' modify a product [id,name,price]
            ' prepare update parameters
            With updateCommand.Parameters
                .Clear()
                .Add(New OleDbParameter("nom", produit.nom))
                .Add(New OleDbParameter("prix", produit.prix))
                .Add(New OleDbParameter("id", produit.id))
            End With
            ' make the change
            Try
                ' opening connection
                connexion.Open()
                ' order execution
                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
                ' pb
                Dim erreursCommande As New ArrayList
                erreursCommande.Add(String.Format("Erreur lors de la modification : {0}", ex.Message))
                Throw New ExceptionProduits(erreursCommande)
            Finally
                ' locking connection
                connexion.Close()
            End Try
        End Sub

Der Code ist fast identisch mit dem der Methode [addProducts], außer dass der entsprechende [OleDbCommand] [updateCommand] statt [insertCommand] ist.

Die Methode [deleteProducts]

Diese Methode löscht die Zeile aus der Tabelle [list] mit dem als Parameter übergebenen Schlüssel [id]. Der Code lautet wie folgt:


        Public Sub supprimerProduit(ByVal id As Integer)
            ' deletes key product [id]
            ' prepare the parameters for the deletion
            With deleteCommand.Parameters
                .Clear()
                .Add(New OleDbParameter("id", id))
            End With
            ' we delete
            Try
                ' opening connection
                connexion.Open()
                ' order execution
                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
                ' pb
                Dim erreursCommande As New ArrayList
                erreursCommande.Add(String.Format("Erreur lors de la suppression : {0}", ex.Message))
                Throw New ExceptionProduits(erreursCommande)
            Finally
                ' locking connection
                connexion.Close()
            End Try
        End Sub

Der Ansatz ist derselbe wie bei den vorherigen Methoden.

9.5.4. Testen der Klasse [products]

Ein konsolenbasiertes Testprogramm [testproducts.vb] könnte wie folgt aussehen:

Option Explicit On 
Option Strict On

' namespaces
Imports System
Imports System.Data
Imports Microsoft.VisualBasic
Imports System.Collections

Namespace st.istia.univangers.fr

    ' test pg
    Module testproduits
        Dim contenu As DataTable

        Sub Main(ByVal arguments() As String)
            ' displays the contents of a product table
            ' the table is in a ACCESS database whose pg receives the file name
            Const syntaxe1 As String = "pg bdACCESS"

            ' checking program parameters
            If arguments.Length <> 1 Then
                ' error msg
                Console.Error.WriteLine(syntaxe1)
                ' end
                Environment.Exit(1)
            End If
            ' prepare the connection chain
            Dim chaineConnexion As String = "Provider=Microsoft.Jet.OLEDB.4.0; Ole DB Services=-4; Data Source=" + arguments(0)
            ' creation of a product object
            Dim objProduits As produits = New produits(chaineConnexion)
            ' display all products
            afficheProduits(objProduits)
            ' insert a product
            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
            ' display all products
            afficheProduits(objProduits)
            ' retrieve the id of the ahjouté product
            produit.id = CType(contenu.Rows(contenu.Rows.Count - 1)("id"), Integer)
            ' modify the added product
            produit.prix = 200
            Try
                objProduits.modifierProduit(produit)
            Catch ex As ExceptionProduits
                afficheErreurs(ex.erreurs)
            End Try
            ' display all products
            afficheProduits(objProduits)
            ' remove the added product
            Try
                objProduits.supprimerProduit(produit.id)
            Catch ex As ExceptionProduits
                afficheErreurs(ex.erreurs)
            End Try
            ' display all products
            afficheProduits(objProduits)
        End Sub

        Sub afficheProduits(ByRef objProduits As produits)
            ' retrieve the product table from a 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
                ' table line i
                Console.Out.WriteLine(lignes(i).Item("id").ToString + "," + lignes(i).Item("nom").ToString + _
                "," + lignes(i).Item("prix").ToString)
            Next
            ' stops console flow
            Console.WriteLine("...")
            Console.ReadLine()
        End Sub

        Sub afficheErreurs(ByRef erreurs As ArrayList)
            ' displays errors on the 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
            ' stops console flow
            Console.WriteLine("...")
            Console.ReadLine()
        End Sub
    End Module
End Namespace

Kompilieren Sie die beiden Quelldateien:

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

Dann testen wir:

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
...

Der Leser ist eingeladen, die obige Bildschirmausgabe mit dem Code des Testprogramms zu vergleichen.

9.6. Webanwendung zur Aktualisierung der zwischengespeicherten Produkttabelle

9.6.1. Einleitung

Wir schreiben nun eine Webanwendung zur Aktualisierung der Produkttabelle (Hinzufügen, Löschen, Ändern). Die aktualisierte Tabelle verbleibt im Speicher in einem [DataTable]-Objekt und wird von allen Benutzern gemeinsam genutzt. Wir möchten zwei Punkte hervorheben:

  • die Verwaltung eines [DataTable]-Objekts
  • die Herausforderungen bei der gleichzeitigen Aktualisierung der Tabelle durch mehrere Benutzer.

Die MVC-Architektur der Anwendung sieht wie folgt aus:

9.6.2. Funktionalität und Ansichten

Die Startansicht der Anwendung sieht wie folgt aus:

Image

 

Diese Ansicht, die als [form] bezeichnet wird, ermöglicht es dem Benutzer, eine Filterbedingung auf die Produkte anzuwenden und die Anzahl der Produkte pro Seite festzulegen, die er sehen möchte.

Nr.
Name
Typ
Rolle
1
lnkFilter
LinkButton
zeigt die Ansicht [Form] an, die zum Festlegen der Filterbedingung verwendet wird
2
lnkUpdate
LinkButton
zeigt die Ansicht [Produkte] an, die zum Anzeigen und Aktualisieren der Produkttabelle (Bearbeiten und Löschen) verwendet wird
3
lnkAdd
LinkButton
zeigt die Ansicht [Hinzufügen] an, die zum Hinzufügen eines Produkts dient
4
Panel
FormView
die Ansicht [Formular]
5
txtFilter
Textfeld
die Filterbedingung
6
txtPages
Textfeld
Anzahl der Produkte pro Seite
7
rfvLines
RequiredFieldValidator
prüft auf einen Wert in [txtPages]
8
rvLines
RangeValidator
prüft, ob txtPages im Bereich [3,10] liegt
9
btnExecute
 
[Senden]-Schaltfläche, die die nach Bedingung (5) gefilterte Ansicht [products] anzeigt
10
lblInfo1
Label
Informationstext bei Fehlern

Wenn beispielsweise die Ansicht [Formular] wie folgt ausgefüllt wird:

Image

erhält man folgendes Ergebnis:

Nr.
Name
Typ
Rolle
1
ProductView
Panel
 
2
rdAscending
rdDescending
RadioButton
ermöglicht es dem Benutzer, die gewünschte Sortierreihenfolge festzulegen, wenn er auf den Titel einer der Spalten [name], [price] klickt. Die beiden Schaltflächen sind Teil der Gruppe [rdSort].
3
DataGrid1
DataGrid
Ein Raster, das eine gefilterte Ansicht der Produkttabelle anzeigt. Der Filter ist derjenige, der von der Ansicht [form] festgelegt wurde. Wir haben außerdem .AllowPaging=true, .AllowSorting=true
4
DataGrid2
DataGrid
zeigt die gesamte Produkttabelle an – ermöglicht die Verfolgung von Aktualisierungen
5
LblInfo2
Label
Informationstext, insbesondere bei Fehlern
6
DataGrid3
DataGrid
zeigt gelöschte Produkte in der Produkttabelle an
7
DataGrid4
DataGrid
zeigt die geänderten Produkte in der Produkttabelle an
8
DataGrid5
DataGrid
zeigt die zur Produkttabelle hinzugefügten Produkte an

Auf dieser Seite befinden sich fünf Datencontainer. Sie alle zeigen dieselbe Tabelle [dtProduits] über eine andere [DataView] an. Eine Ansicht stellt eine Teilmenge der Zeilen in der Quelltabelle der Ansicht dar. Diese Teilmenge wird mithilfe der Eigenschaften [RowFilter] und [RowStateFilter] der Klasse [DataView] erstellt:

  • Mit [RowFilter] können Sie einen Filter für die Zeilen festlegen, wie beispielsweise [price>30] oben. Diese Art der Filterung wird von [DataGrid1] verwendet.
  • Mit [RowStateFilter] können Sie einen Filter basierend auf dem Status der Tabellenzeile festlegen. Dieser gibt den Status der Zeile im Verhältnis zu ihrem ursprünglichen Status an, als die Ansicht der Tabelle erstellt wurde. Hier stammt die Tabelle [dtProduits] aus einer Datenbank. Zu Beginn haben alle ihre Zeilen den Status [Original], um anzuzeigen, dass es sich um die ursprünglichen Zeilen der Tabelle handelt. Dieser Status kann sich dann ändern und verschiedene Werte annehmen, von denen einige unten aufgeführt sind:
    • [Added]: Die Zeile wurde hinzugefügt – sie war nicht Teil der ursprünglichen Tabelle
    • [Deleted]: Die Zeile wurde gelöscht – sie befindet sich noch in der Tabelle, ist jedoch als „zu löschen“ markiert
    • [Modified]: Die Zeile wurde geändert

Mit [RowStateFilter] können Sie die Zeilen in der Tabelle anzeigen, die einen bestimmten Status haben:

  • (Fortsetzung)
    • [DataViewRowState.Added]: Es werden nur hinzugefügte Zeilen angezeigt. Sie werden mit ihren aktuellen Werten angezeigt.
    • [DataViewRowState.ModifiedOriginal]: Es werden nur geänderte Zeilen angezeigt. Sie werden mit ihren ursprünglichen Werten angezeigt.
    • [DataViewRowState.ModifiedCurrent]: Es werden nur geänderte Zeilen angezeigt. Sie werden mit ihren aktuellen Werten angezeigt.
    • [DataViewRowState.Deleted]: Es werden nur gelöschte Zeilen angezeigt. Sie werden mit ihren ursprünglichen Werten angezeigt.
    • [DataViewRowState.CurrentRows]: Nicht gelöschte Zeilen werden angezeigt. Sie werden mit ihren aktuellen Werten angezeigt.

Somit nimmt der [RowStateFilter] folgende Werte an:

DataGrid1
kein Filter für den Zeilenstatus
 
DataGrid2
DataViewRowState.CurrentRows
zeigt den aktuellen Status der Produkttabelle an
DataGrid3
DataViewRowState.Deleted
zeigt die gelöschten Zeilen der Produkttabelle an
DataGrid4
DataViewRowState.ModifiedOriginal
zeigt geänderte Zeilen aus der Produkttabelle zusammen mit ihren ursprünglichen Werten an
DataGrid5
DataViewRowState.Added
zeigt die Zeilen an, die zur ursprünglichen Produkttabelle hinzugefügt wurden

Die Container [DataGrid1-5] ermöglichen es uns, Aktualisierungen der Tabelle [dtProduits] zu verfolgen. Die Komponente [DataGrid1] ermöglicht das Ändern und Löschen eines Produkts. Wir werden sehen, wie die Konfiguration der Komponente diese Aktualisierung ermöglicht. Sie ist zudem paginiert und sortiert. Diese beiden Funktionen wurden bereits in einem früheren Beispiel behandelt.

Der Link [Hinzufügen] bietet Zugriff auf ein Formular zum Hinzufügen eines Produkts:

Nr.
Name
Typ
Rolle
1
AddView
Panel
 
2
txtName
TextBox
Produktname
3
rfvName
RequiredFieldValidator
prüft auf einen Wert in [txtName]
4
txtPrice
Textfeld
Produktpreis
5
rfvPrice
RequiredFieldValidator
prüft auf einen Wert in [txtPrice]
6
cvPrice
CompareValidator
prüft, ob der Preis >= 0 ist
7
btnAdd
Schaltfläche
[Absenden]-Schaltfläche zum Hinzufügen des Produkts
8
lblInfo3
Beschriftung
Informationstext zum Ergebnis des Vorgangs „Hinzufügen“

Die Datenbank [products] ist möglicherweise beim Start der Anwendung nicht verfügbar. In diesem Fall wird dem Benutzer die Ansicht [errors] angezeigt:

Nr.
Name
Typ
Rolle
1
errorView
Panel
 
2
rptErrors
Repeater
Liste der Fehler

9.6.3. Konfiguration der Datencontainer

Die fünf [DataGrid]-Container wurden in [WebMatrix] konfiguriert. Sie wurden über den Link [Auto Configuration] im Eigenschaftenfenster formatiert (Farben und Rahmen). Der [DataGrid1]-Container wurde über den Link [Property Generator] im selben Fenster konfiguriert. Die Sortierung wurde aktiviert (Registerkarte [General]):

Image

Die Spalten werden nicht automatisch generiert, anders als in den anderen vier Containern. Sie wurden manuell mithilfe des Assistenten definiert:

Image

Zunächst wurden zwei [Related Column]-Spalten mit den Namen [name] und [price] erstellt. Sie wurden jeweils den Feldern [name] und [price] der Datenquelle zugeordnet, die die Container anzeigen werden. Hier ist beispielsweise die Konfiguration der Spalte [name]:

Image

Der Sortierausdruck ist der Ausdruck, der nach der [order by]-Klausel in der SQL-[select]-Anweisung stehen muss, die ausgeführt wird, wenn der Benutzer auf die Kopfzeile [name] klickt, die mit dem Feld [name] des [DataGrid] verknüpft ist. Hier haben wir [name] eingegeben, sodass die Sortierklausel [order by name] lautet. Wir werden sehen, dass wir dies je nach der vom Benutzer gewählten Sortierreihenfolge in [order by name asc] oder [order by name desc] ändern werden.

Wir haben außerdem zwei Spalten mit Schaltflächen erstellt:

Image

Die Spalte [Bearbeiten, Aktualisieren, Abbrechen] ermöglicht es uns, ein Produkt zu bearbeiten, und die Spalte [Löschen] dient zum Löschen. Jede dieser Spalten kann konfiguriert werden. Die Spalte [Bearbeiten, Aktualisieren, Abbrechen] bietet folgende Konfigurationsmöglichkeiten:

Image

Wir sehen, dass die Schaltflächentexte geändert werden können. Bei den Schaltflächen haben wir die Wahl zwischen Links und Schaltflächen (Dropdown-Liste oben). Hier wurden Links gewählt. Die Konfiguration der Spalte [Löschen] ist ähnlich. Zusätzlich zu diesem Konfigurationsassistenten haben wir das Eigenschaftenfenster [DataGrid] direkt verwendet, um die Eigenschaft [DataKeyField] festzulegen, die angibt, welches Feld in der Datenquelle zur Indizierung der Zeilen des [DataGrid] verwendet wird. Hier wird der Primärschlüssel der Produkttabelle verwendet:

Image

Letztendlich erzeugt diese Konfiguration den folgenden Präsentationscode:


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

Wie immer gilt: Sobald Sie etwas Erfahrung gesammelt haben, können Sie den oben stehenden Code ganz oder teilweise direkt schreiben.

Die anderen [DataGrid]-Container verfügen über die Standardkonfiguration, die durch die automatische Generierung der [DataGrid]-Spalten aus denen der zugehörigen Datenquelle entsteht.

Der [Repeater]-Container dient zur Anzeige einer Fehlerliste. Seine Konfiguration erfolgt direkt im Präsentationscode:


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

Jede Zeile der Komponente zeigt den Wert [Container.DataItem] an, d. h. den entsprechenden Wert aus der Datenliste. Dieser ist vom Typ [ArrayList] und stellt eine Liste von Fehlern dar.

9.6.4. Der Präsentationscode der Anwendung

Dieser befindet sich in der Datei [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>

Beachten Sie folgende Punkte:

  • Die Seite besteht aus vier Containern (Panels) [vueFormulaire, vueProduits, vueAjout, vueErreurs], die die vier Ansichten der Anwendung bilden.
  • Schaltflächen oder Links vom Typ [submit] haben die Eigenschaft [CausesValidation=false]. Die Eigenschaft [CausesValidation=true] löst die Ausführung aller Validierungsprüfungen auf der Seite aus. In diesem Fall müssen jedoch nicht alle Validierungsprüfungen gleichzeitig durchgeführt werden. Wenn wir beispielsweise einen Eintrag hinzufügen, möchten wir nicht, dass die Prüfungen bezüglich der Anzahl der Zeilen pro Seite ausgeführt werden. Wir legen daher selbst fest, welche Validierungsprüfungen durchgeführt werden sollen.

9.6.5. Der [global.asax]-Steuerungscode

Der [global.asax]-Controller lautet wie folgt:

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

Der zugehörige Code [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)
        ' retrieve configuration information
        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
        ' configuration errors?
        If erreurs.Count <> 0 Then
            ' errors are noted
            Application("erreurs") = erreurs
            ' we leave
            Exit Sub
        End If
        ' no configuration errors here
        ' create a product object
        Dim dtProduits As DataTable
        Try
            dtProduits = New produits(chaînedeConnexion).getProduits
        Catch ex As ExceptionProduits
            'there has been an error accessing the products, this is noted in the application
            Application("erreurs") = ex.erreurs
            Exit Sub
        Catch ex As Exception
            ' unhandled error
            erreurs.Add(ex.Message)
            Application("erreurs") = erreurs
            ' exit sub
        End Try
        ' no initialization errors here
        ' store the number of products per page
        Application("defaultProduitsPage") = defaultProduitsPage
        ' memorize the product table
        Application("dtProduits") = dtProduits
    End Sub
 
    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' init session variables
        If IsNothing(Application("erreurs")) Then
            ' view of the product table
            Session("dvProduits") = CType(Application("dtProduits"), DataTable).DefaultView
            ' products per page
            Session("nbProduitsPage") = Application("defaultProduitsPage")
            ' current page displayed
            Session("pageCourante") = 0
        End If
    End Sub
End Class

In [Application_Start] rufen wir zunächst zwei Informationen aus der Konfigurationsdatei [web.config] der Anwendung ab:

  • OLEDBStringConnection: die OLEDB-Verbindungszeichenfolge zur Produktdatenbank
  • defaultProductsPage: die Standardanzahl der pro Seite angezeigten Produkte

<?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>

Fehlt eine dieser beiden Angaben, wird eine Fehlerliste generiert und der Anwendung hinzugefügt. Dasselbe gilt, wenn der Parameter [defaultProduitsPage] zwar vorhanden, aber falsch ist. Sind beide erwarteten Parameter vorhanden und korrekt, wird eine Tabelle [dtProduits] erstellt und der Anwendung hinzugefügt. Diese Tabelle wird von den verschiedenen Clients verwendet und aktualisiert. Die Datenbank selbst bleibt unverändert. Die Aktualisierung der Datenbank werden wir in einer zukünftigen Anwendung behandeln. Diese Tabelle wird aus einer Instanz der zuvor besprochenen Klasse [products] und deren Methode [getProducts] erstellt. Das Abrufen der Tabelle [dtProducts] kann fehlschlagen. In diesem Fall wissen wir, dass die Klasse [products] eine Ausnahme vom Typ [ProductException] auslöst. Diese wird hier abgefangen, und die zugehörige Fehlerliste wird in der Anwendung unter dem Schlüssel [errors] gespeichert. Das Vorhandensein dieses Schlüssels in den in der Anwendung gespeicherten Informationen wird bei jeder Anfrage überprüft. Wird er gefunden, wird die Ansicht [errors] an den Client gesendet.

Auch wenn die Tabelle [dtProducts] von allen Web-Clients gemeinsam genutzt wird, verfügt jeder von ihnen dennoch über eine eigene Ansicht [dvProducts] davon. Dies liegt daran, dass jeder Web-Client einen Filter für die Tabelle [dtProducts] sowie eine Sortierreihenfolge festlegen kann. Diese für jeden Web-Client spezifischen Informationen werden in der Ansicht des jeweiligen Clients gespeichert. Daher wird diese Ansicht in [Session_Start] erstellt, damit sie in die für jeden Client spezifische Sitzung eingefügt werden kann. Wir verwenden das Attribut [DefaultView] der Tabelle [dtProduits], um eine Standardansicht der Tabelle zu erhalten. Zu Beginn gibt es weder einen Filter noch eine Sortierreihenfolge. Zusätzlich werden zwei Informationen in die Sitzung aufgenommen:

  • die Anzahl der Produkte pro Seite, gekennzeichnet durch den Schlüssel [nbProduitsPage]. Zu Beginn der Sitzung entspricht diese Zahl dem in der Konfigurationsdatei definierten Standardwert.
  • die Nummer der aktuellen Produktseite. Zu Beginn ist dies die erste Seite.

9.6.6. Der Controller-Code [main.aspx.vb]

Das Controller-Gerüst sieht wie folgt aus:


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
 
    ' components page
    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
 
    ' data page
    Protected dtProduits As DataTable
    Protected dvProduits As DataView
    Protected defaultProduitsPage As Integer
    Protected erreur As Boolean = False
 
    ' loading page
    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. Instanzdaten

Die Klasse [main] verwendet die folgenden Instanzdaten:

Public Class main
    Inherits System.Web.UI.Page

    ' components page
    Protected WithEvents txtPages As System.Web.UI.WebControls.TextBox
...

    ' data page
    Protected dtProduits As DataTable
    Protected dvProduits As DataView
    Protected defaultProduitsPage As Integer
dtProducts
die [DataTable]-Tabelle für Produkte – wird von allen Clients gemeinsam genutzt
dvProducts
die [DataView] für Produkte – spezifisch für jeden Kunden
defaultProductsPage
Standardanzahl der Produkte pro Seite

9.6.8. Die [Page_Load]-Prozedur zum Laden der Seite

Der Code für die [Page_Load]-Prozedur lautet wie folgt:


    ' loading page
    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' check for application errors
        If Not IsNothing(Application("erreurs")) Then
            ' the application has not initialized correctly
            afficheErreurs(CType(Application("erreurs"), ArrayList))
            Exit Sub
        End If
        ' retrieve a reference from the product table
        dtProduits = CType(Application("dtProduits"), DataTable)
        ' product view
        dvProduits = CType(Session("dvProduits"), DataView)
        ' retrieve the number of products per page
        defaultProduitsPage = CType(Application("defaultProduitsPage"), Integer)
        ' cancels any update in progress
        DataGrid1.EditItemIndex = -1
        '1st request
        If Not IsPostBack Then
            ' the initial form is displayed
            txtPages.Text = defaultProduitsPage.ToString
            afficheFormulaire()
        End If
    End Sub
  • Zunächst prüfen wir, ob die Produkttabelle beim Start der Anwendung geladen wurde. Ist dies nicht der Fall, zeigen wir die Ansicht [errors] mit den entsprechenden Fehlermeldungen an und beenden die Prozedur, nachdem wir den booleschen Wert [error] auf true gesetzt haben. Dieser Indikator wird von bestimmten Prozeduren überprüft.
  • Wir rufen die Produkttabelle [dtProduits] aus der Anwendung ab und speichern sie in der Instanzvariablen [dtProduits], damit sie von allen Methoden auf der Seite gemeinsam genutzt werden kann.
  • Das Gleiche tun wir mit der aus der Sitzung abgerufenen Ansicht [dvProducts] und der aus der Anwendung abgerufenen Standardanzahl von Produkten pro Seite.
  • Wenn dies die erste Anfrage des Kunden ist, zeigen wir das Formular zur Definition von Filter- und Paginierungsbedingungen mit einer Standardpaginierung von [defaultProduitsPage] Produkten pro Seite an.
  • Der Bearbeitungsmodus der Komponente [dataGrid1] wird abgebrochen. Diese Komponente verfügt über ein Attribut [EditItemIndex], das den Index des aktuell bearbeiteten Elements angibt. Wenn [EditItemIndex]=-1 ist, wird derzeit kein Element bearbeitet. Wenn [EditItemIndex]=i ist, wird derzeit das Element Nummer i der [DataGrid]-Komponente bearbeitet. Es wird dann anders als die anderen Elemente im [dataGrid] angezeigt:

Image

Oben befindet sich das Element [Product8, 80] im [edit]-Modus. Wie oben gezeigt, kann der Benutzer die Aktualisierung über den Link [Update] bestätigen oder über den Link [Cancel] abbrechen. Er kann auch andere Links verwenden, die nichts mit dem aktuell bearbeiteten Element zu tun haben. Indem wir bei jedem Laden der Seite [DataGrid1.EditItemIndex=-1] setzen, stellen wir sicher, dass der Bearbeitungsmodus von [DataGrid1] systematisch aufgehoben wird. Wir weisen ihm nur dann einen anderen Wert zu, wenn ein [Edit]-Link angeklickt wurde. Dies erfolgt in der Prozedur, die dieses Ereignis verarbeitet. Es ergeben sich folgende Situationen:

  1. Der [Edit]-Link für Element Nr. 8 in [DataGrid1] wurde angeklickt. [Page_Load] setzt zunächst [DataGrid1.EditItemIndex] auf -1. Anschließend setzt die Prozedur, die das [Edit]-Ereignis verarbeitet, [DataGrid1.EditItemIndex] auf 8. Wenn die Seite schließlich an den Client zurückgesendet wird, befindet sich Element Nr. 8 tatsächlich im [Edit]-Modus und wird wie oben gezeigt angezeigt.
  2. Der Benutzer bearbeitet das Produkt, das sich im [Edit]-Modus befindet, und bestätigt die Änderung über den Link [Update]. [Page_Load] setzt [DataGrid1.EditItemIndex] auf -1. Anschließend wird die Prozedur ausgeführt, die das [Update]-Ereignis verarbeitet, und Element Nr. 8 in [dtProduits] wird aktualisiert. Wenn die Seite an den Client zurückgesendet wird, befindet sich kein Element in [DataGrid1] im Bearbeitungsmodus (DataGrid.EditItemIndex=-1).
  3. Wenn ein Benutzer mit der Aktualisierung eines Produkts begonnen hat, wäre es sinnvoll, dass er diese Aktualisierung durch Klicken auf [Abbrechen] abbricht. Nichts hindert ihn jedoch daran, auf einen anderen Link zu klicken. Wir müssen daher dieses Szenario berücksichtigen. In diesem Fall beginnt [Page_Load] wie in den vorherigen Fällen damit, [DataGrid1.EditItemIndex] auf -1 zu setzen, dann wird die Prozedur ausgeführt, die das aufgetretene Ereignis behandelt. Sie ändert die Eigenschaft [DataGrid1.EditItemIndex] nicht, die daher bei -1 bleibt. Wenn die Seite an den Client zurückgesendet wird, befindet sich kein Element in [DataGrid1] im Bearbeitungsmodus.

Wir sehen also, dass wir durch das Setzen von [EditItemIndex] auf -1 beim Laden der Seite vermeiden, uns Gedanken darüber machen zu müssen, ob sich der Benutzer beim Klicken auf einen Link im Bearbeitungsmodus befand oder nicht.

9.6.9. Anzeigen der Ansichten [errors], [form] und [add]

Die Ansicht [errors] wird beim Laden der Seite [Page_Load] angezeigt, wenn festgestellt wird, dass die Anwendung nicht ordnungsgemäß initialisiert wurde. Dies geschieht, indem die Datenkomponente [rptErrors] an die als Parameter übergebene Fehlerliste gebunden und der entsprechende Container sichtbar gemacht wird. Schließlich werden die drei Links [Filter, Update, Add] ausgeblendet, da die Anwendung im Falle eines Fehlers nicht verwendet werden kann.


    Private Sub afficheErreurs(ByVal erreurs As ArrayList)
        ' associate the error list with repeater rptErreurs
        With rptErreurs
            .DataSource = erreurs
            .DataBind()
        End With
        'inhibit options
        lnkAjout.Visible = False
        lnkMisajour.Visible = False
        lnkFiltre.Visible = False
        ' the [errors] view is displayed
        vueErreurs.Visible = True
        vueFormulaire.Visible = False
        vueProduits.Visible = False
        vueAjout.Visible = False
    End Sub

Die anderen Ansichten werden basierend auf den dem Benutzer angezeigten Optionen aufgerufen:

Image

Die Ansicht [Add] wird angezeigt, wenn der Benutzer auf den Link [Add] auf der Seite klickt:


    Private Sub lnkAjout_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkAjout.Click
        ' the [add] view is displayed
        afficheAjout()
    End Sub

    Private Sub afficheAjout()
        ' displays the add view
        vueAjout.Visible = True
        vueFormulaire.Visible = False
        vueProduits.Visible = False
        vueErreurs.Visible = False
    End Sub

Die Ansicht [Form] wird angezeigt, wenn der Benutzer auf den Link [Filter] auf der Seite klickt. Anschließend müssen wir die Ansicht anzeigen, in der der Benutzer

  • den Filter für die Produkttabelle
  • die Anzahl der auf jeder Webseite angezeigten Produkte

Es wird ein Formular angezeigt, das die in der Sitzung gespeicherten Werte abruft:


    Private Sub lnkFiltre_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkFiltre.Click
        ' define filtering conditions
        txtFiltre.Text = dvProduits.RowFilter
        ' set pagination
        txtPages.Text = CType(Session("nbProduitsPage"), String)
        ' the filter form is displayed
        afficheFormulaire()
    End Sub

Die Filterbedingung einer Ansicht wird in ihrer [RowFilter]-Eigenschaft definiert. Zur Erinnerung: Die gefilterte und paginierte Ansicht heißt [dvProducts] und wurde beim Laden der Seite [Page_Load] aus der Sitzung abgerufen. Die Filterbedingung wird daher aus [dvProduits.RowFilter] abgerufen. Die Anzahl der Produkte pro Seite wird ebenfalls aus der Sitzung abgerufen. Wenn der Benutzer diese Informationen nie definiert hat, entspricht diese Zahl der Standardanzahl von Produkten pro Seite. Sobald dies erledigt ist, zeigen wir das Formular an, in dem der Benutzer diese beiden Informationen festlegen kann:


    Private Sub afficheFormulaire()
        ' the [form] view is displayed
        vueFormulaire.Visible = True
        vueErreurs.Visible = False
        vueProduits.Visible = False
        vueAjout.Visible = False
    End Sub

Image

9.6.10. Validierung der [Formular]-Ansicht

Das Klicken auf die Schaltfläche [Execute] oben wird durch die folgende Prozedur verarbeitet:


    Private Sub btnExécuter_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnExécuter.Click
        ' valid page?
        rfvLignes.Validate()
        rvLignes.Validate()
        If Not rfvLignes.IsValid Or Not rvLignes.IsValid Then
            afficheFormulaire()
            Exit Sub
        End If
        ' attaching filtered data to the grid
        Try
            dvProduits.RowFilter = txtFiltre.Text.Trim
        Catch ex As Exception
            lblinfo1.Text = "Erreur de filtrage (" + ex.Message + ")"
            afficheFormulaire()
            Exit Sub
        End Try
        ' store the number of products per page
        Session("nbProduitsPage") = txtPages.Text
        'and the current page
        Session("pageCourante") = 0
        ' all's well - data displayed
        afficheProduits(0, CType(txtPages.Text, Integer))
    End Sub

Zunächst sei daran erinnert, dass die Seite zwei Validierungskomponenten enthält:

Nr.
name
Typ
Rolle
7
rfvLines
RequiredFieldValidator
prüft auf einen Wert in [txtPages]
8
rvLines
RangeValidator
prüft, ob txtPages im Bereich [3,10] liegt

Der Vorgang beginnt mit der Ausführung des Validierungscodes für die beiden oben genannten Komponenten mithilfe ihrer [Validate]-Methode; anschließend wird der Wert ihres [IsValid]-Attributs überprüft. Dieses Attribut wird nur dann auf [true] gesetzt, wenn die validierten Daten als gültig befunden wurden. Wenn die Gültigkeitsprüfungen für eine der beiden Komponenten fehlschlagen, wird die Ansicht [form] erneut angezeigt, damit der Benutzer den oder die Fehler korrigieren kann. Die Filterbedingung wird auf das Attribut [dvProduits.RowFilter] angewendet. Hier kann eine Ausnahme auftreten, wenn der Benutzer ein falsches Filterkriterium eingegeben hat, wie unten gezeigt:

Image

In diesem Fall wird die Ansicht [form] erneut angezeigt. Sind beide eingegebenen Informationen korrekt, werden zwei Daten in die Sitzung geschrieben:

  • die vom Benutzer ausgewählte Anzahl von Produkten pro Seite
  • die anzuzeigende Seitenzahl – zunächst Seite 0

Diese beiden Informationen werden bei jeder Anzeige der Ansicht [products] verwendet.

9.6.11. Anzeigen der Ansicht [products]

Die Ansicht [products] sieht wie folgt aus:

Image

Wir haben 5 [DataGrid]-Komponenten, von denen jede eine bestimmte Ansicht der Produkttabelle [dtProduits] anzeigt. Von links beginnend:

Name
Rolle
DataGrid1
Raster, das eine gefilterte Ansicht der Produkttabelle anzeigt. Der Filter ist derjenige, der in der Ansicht [form] festgelegt wurde. Wir haben außerdem .AllowPaging=true, .AllowSorting=true
DataGrid2
Zeigt die gesamte Produkttabelle an – ermöglicht die Verfolgung von Aktualisierungen
DataGrid3
zeigt die gelöschten Produkte in der Produkttabelle an
DataGrid4
zeigt die geänderten Produkte in der Produkttabelle an
DataGrid5
zeigt die hinzugefügten Produkte in der Produkttabelle an

Die Prozedur [displayProducts] ist für die Anzeige der vorherigen Ansicht zuständig:


    Private Sub afficheProduits(ByVal page As Integer, ByVal taillePage As Integer)
        ' attach the data to the two components [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()
        ' the [products] view is displayed
        vueProduits.Visible = True
        vueFormulaire.Visible = False
        vueErreurs.Visible = False
        vueAjout.Visible = False
        ' the current page is saved
        Session("pageCourante") = page
    End Sub

Die Prozedur akzeptiert zwei Parameter:

  • die Seitenzahl [page], die in [DataGrid1] angezeigt werden soll
  • die Anzahl der Produkte [pageSize] pro Seite

Das [DataGrid] ist über 5 verschiedene Ansichten mit der Produkttabelle verknüpft.

  • [DataGrid1] ist mit der paginierten und sortierten Ansicht [dvProduits] verknüpft.

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

Bei dieser Bindung von [DataGrid1] an seine Datenquelle benötigen wir die beiden Informationen, die als Parameter an die Prozedur übergeben werden.

  • [DataGrid2] ist an eine Ansicht gebunden, die alle aktuellen Elemente der Tabelle [dtProduits] anzeigt. Wie [DataGrid1] bietet es eine aktuelle Ansicht der Tabelle [dtProduits], jedoch ohne Paginierung oder Sortierung.

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

Hier verwenden wir die Eigenschaft [RowStateFilter] der Klasse [DataView]. Eine Zeile in einer Tabelle – oder in diesem Fall in einer Ansicht – verfügt über eine Eigenschaft [RowState], die den Status der Zeile angibt. Hier sind einige Beispiele:

  • (Fortsetzung)
    • [Added]: Die Zeile wurde hinzugefügt
    • [Modified]: Die Zeile wurde geändert
    • [Deleted]: Die Zeile wurde gelöscht
    • [Unchanged]: Die Zeile hat sich nicht geändert

Als die Tabelle [dtProduits] ursprünglich in [Application_Start] erstellt wurde, befanden sich alle ihre Zeilen im Status [Unverändert]. Dieser Status ändert sich, wenn Web-Clients die Daten aktualisieren:

  • (Fortsetzung)
    • Wenn ein Kunde ein neues Produkt anlegt, wird der Tabelle [dtProduits] eine Zeile hinzugefügt. Diese befindet sich im Status [Added].
    • Wenn ein Produkt geändert wird, ändert sich sein Status von [Unchanged] zu [Modified].
    • Wenn ein Produkt gelöscht wird, wird die Zeile nicht physisch gelöscht. Stattdessen wird sie zum Löschen markiert und ihr Status ändert sich in [Deleted]. Es ist möglich, diese Löschung rückgängig zu machen.

Das Attribut [RowStateFilter] der Klasse [DataView] ermöglicht es Ihnen, eine Ansicht basierend auf dem [RowState] ihrer Zeilen zu filtern. Die möglichen Werte entsprechen denen der Aufzählung [DataRowViewState]:

  • (Fortsetzung)
    • DataRowViewState.CurrentRows: Zeilen, die nicht gelöscht wurden, werden mit ihren aktuellen Werten angezeigt
    • DataRowViewState.Added: Hinzugefügte Zeilen werden mit ihren aktuellen Werten angezeigt
    • DataRowViewState.Deleted: Gelöschte Zeilen werden mit ihren ursprünglichen Werten angezeigt
    • DataRowViewState.ModifiedOriginal: Geänderte Zeilen werden mit ihren ursprünglichen Werten angezeigt
    • DataRowViewState.ModifiedCurrent: Geänderte Zeilen werden mit ihren aktuellen Werten angezeigt

Eine geänderte Zeile hat zwei Werte: den ursprünglichen Wert, also den Wert, den die Zeile vor der ersten Änderung hatte, und den aktuellen Wert, also den Wert, der nach einer oder mehreren Änderungen erhalten wurde. Beide Werte werden gleichzeitig beibehalten.

Die DataGrids 2 bis 5 zeigen eine Ansicht der Tabelle [dtProduits] an, die nach dem Attribut [RowStateFilter] gefiltert ist

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

Die Tabelle [dtProducts] wird gleichzeitig von verschiedenen Web-Clients aktualisiert, was beim Zugriff auf die Tabelle zu Konflikten führen kann. Wenn ein Client seine Ansichten der Tabelle [dtProducts] anzeigt, möchten wir verhindern, dass dies geschieht, während sich die Tabelle in einem instabilen Zustand befindet, weil sie gerade von einem anderen Web-Client geändert wird. Daher synchronisiert sich ein Client, wann immer er die Tabelle [dtProducts] zum Lesen (wie oben) oder zum Schreiben beim Hinzufügen, Ändern oder Löschen von Produkten benötigt, mit den anderen Clients anhand der folgenden Abfolge:

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

Es kann mehrere kritische Abschnitte im Anwendungscode geben:

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

Der Mechanismus funktioniert wie folgt:

  1. Ein oder mehrere Clients erreichen die Anweisung [Application.Lock] im kritischen Abschnitt 1. Dies ist ein Token-Spender mit einmaligem Zugriff. Ein einzelner Client erhält dieses Token. Nennen wir ihn C1.
  2. Solange Client C1 das Token nicht über [Application.Unlock] zurückgibt, darf kein anderer Client einen durch [Application.Lock] kontrollierten kritischen Abschnitt betreten. Im obigen Beispiel kann daher kein Client die kritischen Abschnitte 1 und 2 betreten.
  3. Client C1 führt [Application.Unlock] aus und gibt damit das Zugangstoken zurück. Dieses Token kann dann an einen anderen Client vergeben werden. Die Schritte 1 bis 3 werden wiederholt.

Mit diesem Mechanismus stellen wir sicher, dass nur ein Client Zugriff auf die Tabelle [dtProduits] hat, sei es zum Lesen oder zum Schreiben.

Der Link [Update] zeigt ebenfalls die Ansicht [Products] an:

Image

Die zugehörige Prozedur lautet wie folgt:


    Private Sub lnkMisajour_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkMisajour.Click
        ' product view display
        afficheProduits(CType(Session("pageCourante"), Integer), CType(Session("nbProduitsPage"), Integer))
    End Sub

Wir müssen die Ansicht [products] anzeigen. Dies geschieht mithilfe der Prozedur [displayProducts], die zwei Parameter benötigt: die Nummer der aktuell anzuzeigenden Seite und die Anzahl der pro Seite anzuzeigenden Produkte. Beide Informationen befinden sich in der Sitzung, möglicherweise mit ihren Standardwerten, falls der Benutzer sie nie selbst definiert hat.

9.6.12. Paginierung und Sortierung von [DataGrid1]

Wir haben die Prozeduren, die die Paginierung und Sortierung eines [DataGrid] übernehmen, bereits in einem anderen Beispiel kennengelernt. Wenn sich die Seite ändert, zeigen wir die Ansicht [products] an, indem wir die neue Seitenzahl als Parameter an die Prozedur [displayProducts] übergeben.


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

Die Prozedur [DataGrid1_SortCommand] wird ausgeführt, wenn der Benutzer auf die Kopfzeile einer der Spalten in [DataGrid1] klickt. Der Sortierausdruck muss dann dem Attribut [Sort] der Ansicht [dvProduits] zugewiesen werden, die von [DataGrid1] angezeigt wird. Dieser Ausdruck entspricht syntaktisch dem Sortierausdruck, der in der SQL-SELECT-Anweisung hinter der [order by]-Klausel steht. Das Argument [e] der Prozedur verfügt über ein Attribut [SortExpression], das den Sortierausdruck bereitstellt, der der Spalte zugeordnet ist, auf deren Überschrift geklickt wurde. Dieser Sortierausdruck wurde beim Erstellen der Komponente [DataGrid1] definiert. Nachfolgend ist der für die Spalte [name] von [DataGrid1] definierte Ausdruck aufgeführt:

Image

Wurde der Sortierausdruck beim Entwerfen des [DataGrid] nicht definiert, wird der Name des Datenfelds verwendet, das der [DataGrid]-Spalte zugeordnet ist. Die Sortierreihenfolge (aufsteigend, absteigend) wird hier vom Benutzer über die Optionsfelder [rdAscending, rdDescending] festgelegt:

Image

Der Sortiervorgang läuft wie folgt ab:


    Private Sub DataGrid1_SortCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridSortCommandEventArgs) Handles DataGrid1.SortCommand
        ' sort the dataview
        With dvProduits
            .Sort = e.SortExpression + " " + CType(IIf(rdCroissant.Checked, "asc", "desc"), String)
        End With
        ' we display it
        afficheProduits(0, DataGrid1.PageSize)
    End Sub

9.6.13. Ein Produkt löschen

Um ein Produkt zu löschen, klickt der Benutzer auf den Link [Löschen] in der Produktzeile:

Image

Daraufhin wird die Prozedur [DataGrid1_DeleteCommand] ausgeführt:


    Private Sub DataGrid1_DeleteCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.DeleteCommand
        ' product deletion
        ' product key to be deleted
        Dim idProduit As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)
        ' product deletion
        Dim erreur As Boolean = False
        Try
            supprimerProduit(idProduit)
        Catch ex As Exception
            ' pb
            lblInfo2.Text = ex.Message
            erreur = True
        End Try
        ' change page?
        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
        ' product display
        afficheProduits(page, DataGrid1.PageSize)
    End Sub

Die Produkte werden anhand des [id]-Schlüssels des Produkts aktualisiert. Die Spalte [id] in der Tabelle [dtProduits] ist der Primärschlüssel, und mithilfe dieses Schlüssels können wir die Produktzeile in der Tabelle [dtProduits] finden, die in der Komponente [DataGrid1] aktualisiert wird. Die Prozedur beginnt daher damit, den Schlüssel des Produkts abzurufen, dessen [Delete]-Link angeklickt wurde:


        ' product key to delete
        Dim idProduit As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)

Wir wissen, dass [e.Item] das Element in [DataGrid1] ist, das das Ereignis ausgelöst hat. Grob gesagt ist dieses Element eine Zeile im [DataGrid]. [e.Item.ItemIndex] ist die Nummer der Zeile, die das Ereignis ausgelöst hat. Dieser Index bezieht sich auf die aktuell angezeigte Seite. Somit hat die erste Zeile der angezeigten Seite die Eigenschaft [ItemIndex=0], auch wenn sie in der Produkttabelle die Nummer 17 hat. [DataGrid1.DataKeys] ist die Liste der Schlüssel für das [DataGrid]. Da wir während der Entwurfsphase [DataGrid1.DataKey=id] festgelegt haben, bestehen die Schlüssel für [DataGrid1] aus den Werten der Spalte [id] der Tabelle [dtProduits], die gleichzeitig der Primärschlüssel ist. Somit ist [DataGrid1.DataKeys(e.Item.ItemIndex)] der [id]-Schlüssel des zu löschenden Produkts. Nachdem wir dies ermittelt haben, fordern wir dessen Löschung mithilfe der Prozedur [deleteProduct] an:


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

Diese Löschung kann in einigen Fällen fehlschlagen. Wir werden sehen, warum. In diesem Fall wird eine Ausnahme ausgelöst. Diese wird hier abgefangen. Wenn ein Fehler auftritt, muss sich [DataGrid1] nicht ändern. Es wird lediglich eine Fehlermeldung zur Ansicht [products] hinzugefügt. Wenn kein Fehler vorliegt und der Benutzer gerade das einzige Produkt auf der aktuellen Seite gelöscht hat, wird die vorherige Seite angezeigt.


        ' change page?
        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 jedem Fall wird die Ansicht [products] mithilfe der Prozedur [displayProducts] neu gezeichnet:

        ' affichage produits
        afficheProduits(page, DataGrid1.PageSize)

Die Prozedur [deleteProduct] lautet wie folgt:


    Private Sub supprimerProduit(ByVal idProduit As Integer)
        Dim erreur As String
        Try
            ' synchronization
            Application.Lock()
            ' search for the line to be deleted
            Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
            If ligne Is Nothing Then
                erreur = String.Format("Produit [{0}] inexistant", idProduit)
            Else
                ' delete line
                ligne.Delete()
            End If
        Catch ex As Exception
            erreur = String.Format("Erreur de suppression : {0}", ex.Message)
        Finally
            ' end of synchronization
            Application.UnLock()
        End Try
        ' throw an exception if error
        If erreur <> String.Empty Then Throw New Exception(erreur)
    End Sub

Die Prozedur erhält den Schlüssel des zu löschenden Produkts als Parameter. Wenn nur ein Client die Aktualisierungen durchführen würde, könnte diese Löschung wie folgt erfolgen:

            ' suppression de la ligne [idProduit]
            dtProduits.Rows.Find(idProduit).Delete()

Wenn mehrere Clients gleichzeitig Aktualisierungen vornehmen, ist dies komplizierter. Betrachten Sie dazu die folgende Abfolge von Ereignissen:

Zeit
Aktion
T1
Client A liest die Tabelle [dtProducts] – es gibt ein Produkt mit dem Schlüssel 20
T2
Client B liest die Tabelle [dtProducts] – es gibt ein Produkt mit dem Schlüssel 20
T3
Kunde A löscht das Produkt mit dem Schlüssel 20
T4
Client B löscht das Produkt mit dem Schlüssel 20

Wenn die Clients A und B die Produkttabelle lesen und deren Inhalt auf einer Webseite anzeigen, enthält die Tabelle das Produkt mit dem Schlüssel 20. Sie möchten daher möglicherweise eine Aktion für dieses Produkt ausführen, beispielsweise es löschen. Einer von ihnen wird dies zwangsläufig zuerst tun. Derjenige, der als Nächstes kommt, wird dann versuchen, ein Produkt zu löschen, das nicht mehr existiert. Dieses Szenario wird von der Prozedur [deleteProducts] auf verschiedene Weise behandelt:

  • Zunächst erfolgt eine Synchronisation mithilfe von [Application.Lock]. Das bedeutet, dass, sobald ein Client diese Barriere überschreitet, kein anderer Client die Produkttabelle ändern oder lesen kann. Tatsächlich werden alle derartigen Operationen auf diese Weise synchronisiert. Wir haben dies bereits beim Lesen gesehen.
  • Als Nächstes prüfen wir, ob das Produkt, das wir löschen möchten, existiert. Ist dies der Fall, wird es gelöscht; andernfalls wird eine Fehlermeldung ausgegeben.
<div class="odt-code-rich" data-linenums="false" style="counter-reset: odtline 0;"><pre><code class="language-csharp">
<span class="odt-code-line"><span class="odt-code-line-content">            &#x27; recherche de la ligne à supprimer</span></span>
<span class="odt-code-line"><span class="odt-code-line-content">            <span style="color:#0000ff">Dim</span> ligne <span style="color:#0000ff">As</span> DataRow = dtProduits.Rows.Find(idProduit)</span></span>
<span class="odt-code-line"><span class="odt-code-line-content">            <span style="color:#0000ff">If</span> ligne <span style="color:#0000ff">Is</span> <span style="color:#0000ff">Nothing</span> <span style="color:#0000ff">Then</span></span></span>
<span class="odt-code-line"><span class="odt-code-line-content">                erreur = <span style="color:#0000ff">String</span>.Format(&quot;Produit [{0}] inexistant&quot;, idProduit)</span></span>
<span class="odt-code-line"><span class="odt-code-line-content">            <span style="color:#0000ff">Else</span><span style="color:#0000ff"></span></span></span>
<span class="odt-code-line"><span class="odt-code-line-content">                &#x27; suppression de la ligne</span></span>
<span class="odt-code-line"><span class="odt-code-line-content">                ligne.Delete()</span></span>
<span class="odt-code-line"><span class="odt-code-line-content">            <span style="color:#0000ff">End</span> <span style="color:#0000ff">If</span></span></span>
</code></pre></div>
  • Wir verlassen den kritischen Abschnitt mit [Application.Unlock], damit andere Clients ihre Aktualisierungen durchführen können.
  • Wenn die Zeile nicht gelöscht werden konnte, löst die Prozedur eine Ausnahme mit einer Fehlermeldung aus.

Sehen wir uns ein Beispiel an. Wir starten einen [Mozilla]-Client und wählen sofort die Option [Update] (Teilansicht):

Image

Wir machen dasselbe mit einem [Internet Explorer]-Client (Teilansicht):

Image

Mit dem [Mozilla]-Client löschen wir das Produkt [product2]. Wir erhalten die folgende neue Seite:

Image

Der Löschvorgang war erfolgreich. Das gelöschte Produkt erscheint im [DataGrid] der gelöschten Produkte und taucht nicht mehr in den Produktlisten der Komponenten [DataGrid1] und [DataGrid2] auf, die die aktuell in der Tabelle vorhandenen Produkte anzeigen. Machen wir dasselbe mit [Internet Explorer]. Wir löschen den Eintrag [product2]:

Image

Die erhaltene Antwort lautet wie folgt:

Image

Eine Fehlermeldung informiert den Benutzer darüber, dass das Produkt mit dem Schlüssel [2] nicht existiert. Die Antwort gibt eine neue Ansicht an den Client zurück, die den aktuellen Zustand der Tabelle widerspiegelt. Wir können sehen, dass das Produkt mit dem Schlüssel [2] tatsächlich in der Liste der gelöschten Produkte enthalten ist. Die Ansicht [products] spiegelt daher Aktualisierungen von allen Clients wider, nicht nur von einem.

9.6.14. Ein Produkt hinzufügen

Um ein Produkt hinzuzufügen, klickt der Benutzer auf den Link [Hinzufügen] in den Optionen. Dadurch wird einfach die Ansicht [Hinzufügen] angezeigt:

Image

Image

Die beiden mit dieser Aktion verbundenen Prozeduren lauten wie folgt:


    Private Sub lnkAjout_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkAjout.Click
        ' the [add] view is displayed
        afficheAjout()
    End Sub
 
    Private Sub afficheAjout()
        ' displays the add view
        vueAjout.Visible = True
        vueFormulaire.Visible = False
        vueProduits.Visible = False
        vueErreurs.Visible = False
    End Sub

Beachten Sie, dass die Ansicht [Add] Validierungskomponenten enthält:

name
Typ
Rolle
rfvName
RequiredFieldValidator
prüft auf einen Wert in [txtName]
rfvPrice
RequiredFieldValidator
prüft auf einen Wert in [txtPrice]
cvPrice
CompareValidator
prüft, ob der Preis >= 0 ist

Die Prozedur zur Verarbeitung des Klicks auf die Schaltfläche [Add] lautet wie folgt:


    Private Sub btnAjouter_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnAjouter.Click
        ' add a new item to the product table
        ' first of all, the data must be valid
        rfvNom.Validate()
        rfvPrix.Validate()
        cvPrix.Validate()
        If Not rfvNom.IsValid Or Not rfvPrix.IsValid Or Not cvPrix.IsValid Then
            ' the input form again
            afficheAjout()
            Exit Sub
        End If
        ' create a line
        Dim produit As DataRow = dtProduits.NewRow
        produit("nom") = txtNom.Text.Trim
        produit("prix") = txtPrix.Text.Trim
        ' add the row to the
        Application.Lock()
        Try
            dtProduits.Rows.Add(produit)
            lblInfo3.Text = "Ajout réussi"
            ' cleaning
            txtNom.Text = ""
            txtPrix.Text = ""
        Catch ex As Exception
            lblInfo3.Text = String.Format("Erreur : {0}", ex.Message)
        End Try
        Application.UnLock()
    End Sub

Zunächst führen wir Gültigkeitsprüfungen für die drei Komponenten [rfvNom, rfvPrix, cvPrix] durch. Wenn eine der Prüfungen fehlschlägt, wird die Ansicht [Add] mit Fehlermeldungen für die Validierungskomponenten erneut angezeigt. Hier ein Beispiel:

Image

Sind die Daten gültig, bereiten wir die Zeile für den Einfügevorgang in die Tabelle [dtProduits] vor.


        ' create a line
        Dim produit As DataRow = dtProduits.NewRow
        produit("nom") = txtNom.Text.Trim
        produit("prix") = txtPrix.Text.Trim

Zunächst erstellen wir mit der Methode [DataTable.NewRow] eine neue Zeile in der Tabelle [dtProducts]. Diese Zeile enthält die drei Spalten [id, name, price] aus der Tabelle [dtProducts]. Die Spalten [name, price] werden mit den im Hinzufügungsformular eingegebenen Werten gefüllt. Die Spalte [id] wird nicht gefüllt. Diese Spalte ist vom Typ [AutoIncrement], was bedeutet, dass das DBMS den maximal vorhandenen Schlüssel plus 1 als Schlüssel für ein neues Produkt zuweist. Die hier erstellte Zeile [product] ist von der Tabelle [dtProducts] getrennt. Wir müssen sie nun in diese Tabelle einfügen. Da wir die Tabelle [dtProducts] aktualisieren werden, aktivieren wir die clientübergreifende Synchronisation:

        Application.Lock()

Sobald dies erledigt ist, versuchen wir, die Zeile zur Tabelle [dtProduits] hinzuzufügen. Bei Erfolg zeigen wir eine Erfolgsmeldung an, andernfalls eine Fehlermeldung.

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

Normalerweise sollten keine Ausnahmen auftreten. Die Ausnahmebehandlung wurde jedoch vorsorglich implementiert. Sobald die Addition abgeschlossen ist, verlassen wir den kritischen Abschnitt mit [Application.Unlock].

9.6.15. Ein Produkt bearbeiten

Um ein Produkt zu bearbeiten, klickt der Benutzer auf den Link [Bearbeiten] in der Produktzeile:

Image

Dadurch wird in den Bearbeitungsmodus gewechselt:

Image

Dank der [DataGrid]-Komponente lässt sich dieses Ergebnis mit sehr wenig Code erreichen:


    Private Sub DataGrid1_EditCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.EditCommand
        ' puts current element in edit mode
        DataGrid1.EditItemIndex = e.Item.ItemIndex
        ' products are re-displayed
        afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
    End Sub

Ein Klick auf den Link [Bearbeiten] löst auf der Serverseite die Ausführung der Prozedur [DataGrid1_EditCommand] aus. Die Komponente [DataGrid1] verfügt über ein Feld [EditItemIndex]. Die Zeile mit dem Indexwert von [EditItemIndex] wird in den Bearbeitungsmodus versetzt. Jeder Wert in der so aktualisierten Zeile kann in einem Eingabefeld geändert werden, wie im obigen Screenshot gezeigt. Wir rufen daher den Index des Produkts ab, auf dessen [Bearbeiten]-Link geklickt wurde, und weisen ihn der Eigenschaft [EditItemIndex] der Komponente [DataGrid1] zu. Nun muss nur noch die Ansicht [products] neu geladen werden. Sie sieht identisch wie zuvor aus, jedoch mit einer Zeile im Bearbeitungsmodus.

Über den Link [Cancel] für das zu bearbeitende Produkt kann der Benutzer die Aktualisierung abbrechen. Die mit diesem Link verknüpfte Prozedur lautet wie folgt:


    Private Sub DataGrid1_CancelCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.CancelCommand
        ' products are re-displayed
        afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
    End Sub

Hiermit wird lediglich die Ansicht [products] erneut angezeigt. Man könnte versucht sein, [DataGrid1.EditItemIndex] auf -1 zu setzen, um den Aktualisierungsmodus abzubrechen. Tatsächlich wissen wir, dass die Prozedur [Page_Load] dies automatisch erledigt. Es besteht daher keine Notwendigkeit, dies erneut zu tun.

Die Änderung wird über den [Update]-Link in der zu bearbeitenden Zeile bestätigt. Anschließend wird die folgende Prozedur ausgeführt:


    Private Sub DataGrid1_UpdateCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.UpdateCommand
        ' product key to be modified
        Dim idProduit As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)
        ' modified items
        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
        ' valid modifications?
        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
        '  if error, redisplay update page
        If lblInfo2.Text <> String.Empty Then
            ' return the line to update mode
            DataGrid1.EditItemIndex = e.Item.ItemIndex
            ' product display
            afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
            ' end
            Exit Sub
        End If
        ' if no error - we modify the table
        Try
            modifierProduit(idProduit, e.Item)
        Catch ex As Exception
            ' pb
            lblInfo2.Text = ex.Message
        End Try
        ' product display
        afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
    End Sub

Wie beim Löschen müssen wir den Schlüssel des zu ändernden Produkts abrufen, um dessen Zeile in der Produkttabelle [dtProduits] zu finden:


        ' product key to be modified
        Dim idProduit As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)

Als Nächstes müssen wir die neuen Werte abrufen, die der Zeile zugewiesen werden sollen. Diese befinden sich in der Komponente [DataGrid1]. Das Argument [e] der Prozedur hilft uns dabei. [e.Item] steht für die Zeile in [DataGrid1], die das Ereignis ausgelöst hat. Es handelt sich also um die Zeile, die gerade aktualisiert wird, da der Link [Update] nur in dieser Zeile vorhanden ist. Diese Zeile enthält Spalten, die durch die [Cells]-Sammlung der Zeile bezeichnet werden. Somit steht [e.Item.Cells(0)] für Spalte 0 der zu aktualisierenden Zeile. Wir wissen, dass sich die neuen Werte in Textfeldern befinden. Die Sammlung [e.Item.Cells(i).Controls] repräsentiert die Sammlung der Steuerelemente in Spalte i der Zeile [e.Item]. Die folgenden beiden Anweisungen rufen die Werte aus den Textfeldern der Zeile ab, die in [DataGrid1] aktualisiert wird:


        ' modified items
        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

Wir haben nun die neuen Werte für die geänderte Zeile als Zeichenfolgen. Anschließend prüfen wir, ob diese Daten gültig sind. Der Name darf nicht leer sein, und der Preis muss eine positive Zahl oder Null sein:


        ' valid modifications?
        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

Wenn ein Fehler auftritt, zeigt das Label [lblInfo2] eine Fehlermeldung an, und die Seite wird einfach neu geladen:


        '  if error, redisplay update page
        If lblInfo2.Text <> String.Empty Then
            ' return the line to update mode
            DataGrid1.EditItemIndex = e.Item.ItemIndex
            ' product display
            afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
            ' end
            Exit Sub
        End If

Was der obige Code nicht zeigt, ist, dass die eingegebenen Werte verloren gehen. Das liegt daran, dass [DataGrid1] mit den Daten in der Tabelle [dtProduits] verknüpft ist, die die ursprünglichen Werte der geänderten Zeile enthält. Hier ist ein Beispiel.

Image

Das Ergebnis sieht wie folgt aus:

Image

Wir sehen, dass die ursprünglich eingegebenen Werte verloren gegangen sind. In einer professionellen Anwendung wäre dies wahrscheinlich inakzeptabel. Hier stoßen wir auf gewisse Einschränkungen des Standard-Aktualisierungsmodus für die [DataGrid]-Komponente. Es wäre vorzuziehen, eine [Edit]-Ansicht zu haben, die der [Add]-Ansicht entspricht.

Wenn die Daten gültig sind, werden sie zur Aktualisierung der Tabelle [dtProducts] verwendet:


        ' cas où pas d'erreur - on modifie la table
        Try
            modifierProduit(idProduit, e.Item)
        Catch ex As Exception
            ' pb
            lblInfo2.Text = ex.Message
        End Try
        ' affichage produits
        afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)

Aus dem gleichen Grund, der beim Löschen eines Produkts genannt wurde, kann die Änderung fehlschlagen. Tatsächlich kann es vorkommen, dass zwischen dem Zeitpunkt, zu dem der Client die Tabelle [dtProduits] liest, und dem Zeitpunkt, zu dem er versucht, eines seiner Produkte zu ändern, dieses Produkt von einem anderen Client gelöscht wurde. Die Prozedur [modifyProduct] behandelt diesen Fall, indem sie eine Ausnahme auslöst. Diese Ausnahme wird hier abgefangen. Nachdem die Aktualisierung erfolgreich war oder fehlgeschlagen ist, gibt die Anwendung die Ansicht [products] an den Client zurück. Wir müssen uns noch ansehen, wie die Prozedur [modifyProduct] die Aktualisierung durchführt:


    Private Sub modifierProduit(ByVal idProduit As Integer, ByVal item As DataGridItem)
        Dim erreur As String
        Try
            ' synchronization
            Application.Lock()
            ' search for the line to be modified
            Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
            If ligne Is Nothing Then
                erreur = String.Format("Produit [{0}] inexistant", idProduit)
            Else
                ' modify the line
                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
            ' end of synchronization
            Application.UnLock()
        End Try
        ' throw an exception if error
        If erreur <> String.Empty Then Throw New Exception(erreur)
    End Sub

Wir werden nicht näher auf diese Prozedur eingehen, deren Code dem der Prozedur [deleteProduct] ähnelt, die bereits ausführlich erläutert wurde. Sehen wir uns einfach zwei Beispiele an. Zunächst ändern wir den Preis des Produkts [product1]:

Image

Ein Klick auf den Link [Update] oben führt zu folgender Reaktion:

Image

Die Änderung ist in den [DataGrid]-Komponenten 1 und 2 deutlich sichtbar, die den aktuellen Zustand der Tabelle [dtProducts] widerspiegeln. Sie ist auch in der [DataGrid4]-Komponente sichtbar, die eine Ansicht der geänderten Zeilen anzeigt, wobei diese mit ihren ursprünglichen Werten dargestellt werden. Betrachten wir nun einen Fall eines Parallelitätskonflikts. Wie beim Löschbeispiel verwenden wir zwei verschiedene Web-Clients. Ein Client [Mozilla] liest die Tabelle [dtProduits] und beginnt mit der Bearbeitung des Produkts [product1]:

Image

Ein [Internet Explorer]-Client ist dabei, das Produkt [product1] zu löschen:

Image

Der [Internet Explorer]-Client löscht [product1]:

Image

Beachten Sie, dass [product1] nicht mehr in der Tabelle der geänderten Zeilen, sondern in der Tabelle der gelöschten Zeilen steht. Der [Mozilla]-Client validiert seine Aktualisierung:

Image

Der Client [Mozilla] erhält folgende Antwort auf seine Aktualisierung:

Image

Er kann sehen, dass [product1] gelöscht wurde, da es in der Liste der gelöschten Produkte steht.

9.7. Webanwendung zur Aktualisierung der physischen Produkttabelle

9.7.1. Vorgeschlagene Lösungen

Die vorherige Anwendung war eher ein Lehrbuchbeispiel, das die Verwaltung eines zwischengespeicherten [DataTable]-Objekts demonstrieren sollte, als ein reales Szenario. Tatsächlich muss die eigentliche Datenquelle irgendwann aktualisiert werden. Dabei können zwei verschiedene Strategien gewählt werden:

  1. Wir verwenden den [dtProduits]-Cache im Speicher, um die Datenquelle zu aktualisieren. Innerhalb der Webstruktur der vorherigen Anwendung kann eine Seite erstellt werden, die Zugriff auf den [dtProduits]-Cache bietet. Diese Seite würde es einem Administrator ermöglichen, Änderungen am [dtProduits]-Cache mit der physischen Datenquelle zu synchronisieren. Dazu könnten wir der Zugriffsklasse [products] eine neue Methode hinzufügen, die den [dtProduits]-Cache als Parameter übernimmt und diesen Cache zur Aktualisierung der physischen Datenquelle verwendet.
  2. Die physische Datenquelle wird gleichzeitig mit dem Cache aktualisiert.

Strategie Nr. 1 erlaubt das Öffnen nur einer Verbindung zur physischen Datenquelle. Strategie Nr. 2 erfordert eine Verbindung für jede Aktualisierung. Je nach Verfügbarkeit der Verbindung kann eine Strategie der anderen vorzuziehen sein. Da wir über die Werkzeuge zur Umsetzung verfügen (die [products]-Klasse), wählen wir Strategie Nr. 2.

9.7.2. Lösung 1

Um die Kontinuität zur vorherigen Anwendung zu wahren, wählen wir die folgende Strategie:

  • Die physische Quelle wird gleichzeitig mit dem Cache aktualisiert
  • Der Cache wird nur einmal während der Anwendungsinitialisierung in [global.asax.vb] erstellt. Das bedeutet, dass Web-Clients diese Änderungen nicht sehen, wenn die physische Datenquelle von anderen Clients als Web-Clients aktualisiert wird. Sie sehen nur die Änderungen, die sie selbst an der zwischengespeicherten Tabelle vornehmen.

Um die physische Datenquelle zu aktualisieren, benötigen wir eine Instanz der Produktzugriffsklasse. Jeder Client könnte eine eigene haben. Wir können auch eine einzige Instanz gemeinsam nutzen, die von der Anwendung beim Start erstellt würde. Dies ist die Lösung, für die wir uns hier entscheiden. Der Steuerungscode [global.asax.vb] wird wie folgt geändert:


Imports System
Imports System.Web
...
 
Public Class Global
    Inherits System.Web.HttpApplication
 
    Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' retrieve configuration information
        Dim chaînedeConnexion As String = ConfigurationSettings.AppSettings("OLEDBStringConnection")
        Dim defaultProduitsPage As String = ConfigurationSettings.AppSettings("defaultProduitsPage")
        Dim erreurs As New ArrayList
...
        ' no configuration errors here
        ' create a product object
        Dim objProduits As New produits(chaînedeConnexion)
        Dim dtProduits As DataTable
        Try
            dtProduits = objProduits.getProduits
        Catch ex As ExceptionProduits
            'there has been an error accessing the products, this is noted in the application
            Application("erreurs") = ex.erreurs
            Exit Sub
        Catch ex As Exception
            ' unhandled error
            erreurs.Add(ex.Message)
            Application("erreurs") = erreurs
            ' exit sub
        End Try
        ' no initialization errors here
...
        ' the data access instance is stored
        Application("objProduits") = objProduits
    End Sub
 
    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
...
    End Sub
End Class

Eine Instanz der Datenzugriffsklasse wurde in der Anwendung gespeichert und dem Schlüssel [objProducts] zugeordnet. Jeder Client verwendet diese Instanz, um auf die physische Datenquelle zuzugreifen. Sie wird in der Prozedur [Page_Load] von [main.aspx.vb] abgerufen:


    Protected objProduits As produits
....
    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
        ' retrieve the product access instance
        objProduits = CType(Application("objProduits"), produits)
...
    End Sub

Die Datenzugriffsinstanz [objProducts] ist für alle Methoden auf der Seite zugänglich. Sie wird für die drei Aktualisierungsvorgänge verwendet: Hinzufügen, Löschen und Ändern.

Die Add-Prozedur wird wie folgt geändert:


    Private Sub btnAjouter_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnAjouter.Click
...
        ' we add the line
        Application.Lock()
        Try
            ' add the line to the cached table
            dtProduits.Rows.Add(produit)
            ' add the line to the physical source
            Dim nouveauProduit As sProduit
            With nouveauProduit
                .nom = CType(produit("nom"), String)
                .prix = CType(produit("prix"), Double)
            End With
            objProduits.ajouterProduit(nouveauProduit)
            ' follow-up
            lblInfo3.Text = "Ajout réussi"
            ' cleaning
            txtNom.Text = ""
            txtPrix.Text = ""
        Catch ex As Exception
            ' error
            lblInfo3.Text = String.Format("Erreur : {0}", ex.Message)
        End Try
        Application.UnLock()
    End Sub

Die Änderungsprozedur wird wie folgt geändert:


    Private Sub modifierProduit(ByVal idProduit As Integer, ByVal item As DataGridItem)
        Dim erreur As String
        Try
            ' synchronization
            Application.Lock()
            ' search for the line to be modified
            Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
            If ligne Is Nothing Then
                erreur = String.Format("Produit [{0}] inexistant", idProduit)
            Else
                ' modify the line in the 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
                ' modify the line in the physical source
                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
            ' end of synchronization
            Application.UnLock()
        End Try
        ' throw an exception if error
        If erreur <> String.Empty Then Throw New Exception(erreur)
    End Sub

Die Löschprozedur wird wie folgt geändert:


    Private Sub supprimerProduit(ByVal idProduit As Integer)
        Dim erreur As String
        Try
            ' synchronization
            Application.Lock()
            ' search for the line to be deleted
            Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
            If ligne Is Nothing Then
                erreur = String.Format("Produit [{0}] inexistant", idProduit)
            Else
                ' delete line from cache
                ligne.Delete()
                ' delete the line in the physical source
                objProduits.supprimerProduit(idProduit)
            End If
        Catch ex As Exception
            erreur = String.Format("Erreur de suppression : {0}", ex.Message)
        Finally
            ' end of synchronization
            Application.UnLock()
        End Try
        ' throw an exception if error
        If erreur <> String.Empty Then Throw New Exception(erreur)
    End Sub

9.7.3. Tests

Wir beginnen mit der folgenden Datentabelle in einer ACCESS-Datei:

Image

Ein Web-Client wird gestartet:

Image

Wir fügen ein Produkt hinzu:

Image

Wir nehmen [product1] aus dem Sortiment:

Image

Wir ändern den Preis von [Produkt2]:

Image

Nach diesen Änderungen sehen wir uns den Inhalt der Tabelle [list] in der ACCESS-Datenbank an:

Image

Die drei Änderungen wurden korrekt in der physischen Tabelle übernommen. Betrachten wir nun ein Konfliktszenario. Wir löschen die Zeile für [product2] direkt in Access:

Image

Wir kehren zu unserem Web-Client zurück. Der Client sieht die vorgenommene Löschung nicht und möchte [product2] ebenfalls löschen:

Image

Er erhält folgende Antwort:

Image

Die Zeile [product2] wurde tatsächlich aus dem Cache entfernt, wie in der Liste der gelöschten Produkte zu sehen ist. Die Löschung von [product2] in der physischen Quelle ist jedoch fehlgeschlagen, wie die Fehlermeldung anzeigt.

9.7.4. Lösung 2

In der vorherigen Lösung aktualisieren Web-Clients gleichzeitig die physische Datenquelle, sehen jedoch nicht die von anderen Clients vorgenommenen Änderungen. Sie sehen nur ihre eigenen. Wir möchten nun, dass ein Client die physische Datenquelle so sehen kann, wie sie aktuell ist, und nicht so, wie sie beim Start der Anwendung war. Dazu bieten wir dem Client eine neue Option an:

Image

Mit der Option [Refresh] erzwingt der Client ein erneutes Einlesen der physischen Datenquelle. Um sicherzustellen, dass dies keine Auswirkungen auf andere Clients hat, muss die aus diesem Lesevorgang resultierende Tabelle dem Client gehören, der die Aktualisierung durchführt, und darf nicht mit anderen Clients geteilt werden. Dies ist der erste Unterschied zur vorherigen Anwendung. Der Cache [dtProduits] der Datenquelle wird von jedem Client und nicht von der Anwendung selbst erstellt. Die Änderung wird in [global.asax.vb] vorgenommen:


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)
        ' retrieve configuration information
...
        ' no configuration errors here
        ' create a product object
        Dim objProduits As New produits(chaînedeConnexion)
        ' store the number of products per page
        Application("defaultProduitsPage") = defaultProduitsPage
        ' the data access instance is stored
        Application("objProduits") = objProduits
    End Sub
 
    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' init session variables
        If IsNothing(Application("erreurs")) Then
            ' cache the data source
            Dim dtProduits As DataTable
            Try
                Application.Lock()
                dtProduits = CType(Application("objProduits"), produits).getProduits
            Catch ex As ExceptionProduits
                'there has been an error accessing the products, this is noted in the session
                Session("erreurs") = ex.erreurs
                Exit Sub
            Finally
                Application.UnLock()
            End Try
            ' cache [dtProduits] in the session
            Session("dtProduits") = dtProduits
            ' view of the product table
            Session("dvProduits") = dtProduits.DefaultView
            ' products per page
            Session("nbProduitsPage") = Application("defaultProduitsPage")
            ' current page displayed
            Session("pageCourante") = 0
        End If
    End Sub
 
End Class

Die in der Sitzung gespeicherten Informationen werden bei jeder Anfrage in der [Page_Load]-Prozedur abgerufen:


    ' data page
    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
        ' check for application errors
        If Not IsNothing(Application("erreurs")) Then
            ' the application has not initialized correctly
            afficheErreurs(CType(Application("erreurs"), ArrayList))
            Exit Sub
        End If
        ' check for session errors
        If Not IsNothing(Session("erreurs")) Then
            ' the session has not initialized correctly
            afficheErreurs(CType(Session("erreurs"), ArrayList))
            Exit Sub
        End If
        ' retrieve a reference from the product table
        dtProduits = CType(Session("dtProduits"), DataTable)
        ' product view
        dvProduits = CType(Session("dvProduits"), DataView)
        ' retrieve the number of products per page
        nbProduitsPage = CType(Session("nbProduitsPage"), Integer)
        ' retrieve the current page
        pageCourante = CType(Session("pageCourante"), Integer)
        ' retrieve the product access instance
        objProduits = CType(Application("objProduits"), produits)
        ' cancels any update in progress
        DataGrid1.EditItemIndex = -1
        '1st request
        If Not IsPostBack Then
            ' the initial form is displayed
            txtPages.Text = nbProduitsPage.ToString
            afficheFormulaire()
        End If
    End Sub

Die während der Sitzung abgerufenen Informationen werden am Ende der Sitzung nach jeder Anfrage gespeichert:


    Private Sub Page_PreRender(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.PreRender
        ' certain information is stored in the session
        Session("dtProduits") = dtProduits
        Session("dvProduits") = dvProduits
        Session("nbProduitsPage") = nbProduitsPage
        Session("pageCourante") = pageCourante
    End Sub

Das [PreRender]-Ereignis signalisiert, dass die Antwort kurz davor ist, an den Client gesendet zu werden. Wir nutzen diese Gelegenheit, um alle Daten zu speichern, die in der Sitzung beibehalten werden müssen. Dies ist übertrieben, da sich oft nur ein Teil der Daten geändert hat. Dieses systematische Speichern hat den Vorteil, dass wir in den anderen Methoden der Seite von der Sitzungsverwaltung entlastet werden.

Die Aktualisierung des Caches wird durch die folgende Prozedur abgewickelt:


    Private Sub lnkRefresh_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkRefresh.Click
        ' refresh the [dtProduits] cache with the physical data source
        ' synchro start
        Application.Lock()
        Try
            ' product table
            dtProduits = CType(Application("objProduits"), produits).getProduits
            ' the current filter is saved
            Dim filtre As String = dvProduits.RowFilter
            ' create the new filtered view
            dvProduits = New DataView(dtProduits)
            ' put the filter back on
            dvProduits.RowFilter = filtre
        Catch ex As ExceptionProduits
            'there has been an error accessing the products, this is noted in the session
            Session("erreurs") = ex.erreurs
            ' display the [errors] view
            afficheErreurs(ex.erreurs)
            ' finish
            Exit Sub
        Finally
            ' end synchro
            Application.UnLock()
        End Try
        ' it went well - the products are displayed from the 1st page
        afficheProduits(0, nbProduitsPage)
    End Sub

Die Prozedur generiert neue Werte für den Cache [dtProduits] und die Ansicht [dvProduits]. Diese werden durch die oben beschriebene Prozedur [Page_PreRender] in die Sitzung geschrieben. Sobald der Cache [dtProduits] neu aufgebaut wurde, zeigen wir die Produkte ab der ersten Seite an.

Hier ist ein Ausführungsbeispiel. Ein Client [Mozilla] wird gestartet und zeigt die Produkte an:

Image

Ein Client [Internet Explorer] macht dasselbe:

Image

Der [Mozilla]-Client löscht [product1], ändert [product2] und fügt ein neues Produkt hinzu. Er erhält die folgende neue Seite:

Image

Der [Internet Explorer]-Client möchte [product1] löschen.

Image

Er erhält die folgende Antwort:

Image

Ihm wurde mitgeteilt, dass [product1] nicht mehr existiert. Der Benutzer beschließt daraufhin, seinen Cache über den obigen Link [Refresh] zu aktualisieren. Er erhält die folgende Antwort:

Image

Er verfügt nun über dieselbe Datenquelle wie der [Mozilla]-Client.

9.8. Fazit

In diesem Kapitel haben wir uns ausführlich mit Datencontainern und deren Verbindungen zu Datenquellen befasst. Zum Abschluss haben wir gezeigt, wie man eine Datenquelle mithilfe einer [DataGrid]-Komponente aktualisiert. Als Datenquelle haben wir eine Datenbanktabelle verwendet. Auch wenn die [DataGrid]-Komponente die Darstellung der Daten etwas vereinfacht, liegt die eigentliche Herausforderung nicht in der Präsentationsschicht, sondern in der Verwaltung der Aktualisierungen der Datenquelle durch verschiedene Clients. Es können Zugriffskonflikte auftreten, die bewältigt werden müssen. Hier haben wir sie im Controller mithilfe von [Application.Lock] behandelt. Es wäre wahrscheinlich sinnvoller, den Zugriff auf die Datenquelle innerhalb der Datenzugriffsklasse zu synchronisieren, damit sich der Controller nicht um solche Details kümmern muss, die außerhalb seines Zuständigkeitsbereichs liegen.

In der Praxis sind Tabellen in einer Datenbank durch Beziehungen miteinander verknüpft, und ihre Aktualisierungen müssen diese berücksichtigen. Dies betrifft in erster Linie die Datenzugriffsklasse, die dadurch komplexer wird, als es für eine eigenständige Tabelle erforderlich wäre. Es wirkt sich in der Regel auch auf die Präsentationsschicht der Webanwendung aus, da oft nicht nur eine einzelne Tabelle, sondern durch Beziehungen miteinander verknüpfte Tabellen angezeigt werden müssen.

In diesem Kapitel wurden außerdem verschiedene Datenstrukturen wie [DataTable, DataView] vorgestellt.