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 |
Zeichenkette | Primärschlüssel | |
Zeichenkette | Liste Themenname | |
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 |
Zeichenkette | Primärschlüssel | |
Zeichenkette | Liste Themenname |
Die Single-Page-Anwendung sieht wie folgt aus:
![]() | |||||
Nr. | Name | Typ | Eigenschaften | Rolle | |
DataGrid | Verfügbare Mailinglisten zum Abonnieren | ||||
DataList | Liste der Abonnements des Benutzers für die oben genannten Listen | ||||
Panel | Informationsbereich zum vom Benutzer ausgewählten Thema mit einem Link [Weitere Informationen] | ||||
Label | Teil von [panelInfos] | Themenname | |||
Beschriftung | Teil von [panelInfos] | Themenbeschreibung | |||
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:

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

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

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

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ème" HeaderText="Thè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:
Wir definieren die anzuzeigenden Spalten selbst im Abschnitt <columns>...</columns> | |
für die Datenseitenaufteilung | |
definiert die [theme]-Spalte (HeaderText) des [DataGrid], die mit der [theme]-Spalte der Datenquelle (DataField) verknüpft wird | |
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>
definiert den Kopfzeilentext der [DataList] | |
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:
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 der Einschränkung – kann beliebig sein | |
Spalte, die der Primärschlüssel sein soll – vom Typ [DataColumn] | |
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:
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:
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:

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

Die dritte Seite:

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:
vom Typ [Integer] – dies ist die Nummer der aktuellen Seite, die bei der letzten Anfrage angezeigt wurde | |
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:
ist die Nummer der aktuellen Seite, die bei der vorherigen Anfrage angezeigt wurde | |
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
Die Datenbankverbindung wird geöffnet, um einen SQL-Befehl auszuführen, und unmittelbar danach wieder geschlossen | |
SQL-Abfrage [select] zum Abrufen der gesamten Tabelle [list] | |
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. | |
Abfrage zur Aktualisierung der Felder (name, price) der Zeile in der Tabelle [list] mit dem Schlüssel [id] | |
Abfrage, die die Zeile mit dem Schlüssel [id] aus der Tabelle [list] löscht | |
[OleDbCommand]-Objekt, das die Abfrage [selectText] auf der Verbindung [connection] ausführt | |
[OleDbCommand]-Objekt, das die Abfrage [updateText] über die Verbindung [connection] ausführt | |
[OleDbCommand]-Objekt, das die Abfrage [insertText] über die Verbindung [connection] ausführt | |
[OleDbCommand]-Objekt, das die Abfrage [deleteText] über die Verbindung [connection] ausführt | |
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:

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 |
LinkButton | zeigt die Ansicht [Form] an, die zum Festlegen der Filterbedingung verwendet wird | ||
LinkButton | zeigt die Ansicht [Produkte] an, die zum Anzeigen und Aktualisieren der Produkttabelle (Bearbeiten und Löschen) verwendet wird | ||
LinkButton | zeigt die Ansicht [Hinzufügen] an, die zum Hinzufügen eines Produkts dient | ||
FormView | die Ansicht [Formular] | ||
Textfeld | die Filterbedingung | ||
Textfeld | Anzahl der Produkte pro Seite | ||
RequiredFieldValidator | prüft auf einen Wert in [txtPages] | ||
RangeValidator | prüft, ob txtPages im Bereich [3,10] liegt | ||
[Senden]-Schaltfläche, die die nach Bedingung (5) gefilterte Ansicht [products] anzeigt | |||
Label | Informationstext bei Fehlern |
Wenn beispielsweise die Ansicht [Formular] wie folgt ausgefüllt wird:

erhält man folgendes Ergebnis:
![]() |
Nr. | Name | Typ | Rolle |
Panel | |||
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]. | ||
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 | ||
DataGrid | zeigt die gesamte Produkttabelle an – ermöglicht die Verfolgung von Aktualisierungen | ||
Label | Informationstext, insbesondere bei Fehlern | ||
DataGrid | zeigt gelöschte Produkte in der Produkttabelle an | ||
DataGrid | zeigt die geänderten Produkte in der Produkttabelle an | ||
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:
kein Filter für den Zeilenstatus | ||
DataViewRowState.CurrentRows | zeigt den aktuellen Status der Produkttabelle an | |
DataViewRowState.Deleted | zeigt die gelöschten Zeilen der Produkttabelle an | |
DataViewRowState.ModifiedOriginal | zeigt geänderte Zeilen aus der Produkttabelle zusammen mit ihren ursprünglichen Werten an | |
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 |
Panel | |||
TextBox | Produktname | ||
RequiredFieldValidator | prüft auf einen Wert in [txtName] | ||
Textfeld | Produktpreis | ||
RequiredFieldValidator | prüft auf einen Wert in [txtPrice] | ||
CompareValidator | prüft, ob der Preis >= 0 ist | ||
Schaltfläche | [Absenden]-Schaltfläche zum Hinzufügen des Produkts | ||
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 |
Panel | |||
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]):

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

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

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:

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:

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:

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 à jour" CancelText="Annuler" EditText="Modifier"></asp:EditCommandColumn>
<asp:ButtonColumn Text="Supprimer" CommandName="Delete"></asp:ButtonColumn>
</Columns>
<PagerStyle NextPageText="Suivant" PrevPageText="Précé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 la table LISTE. Exemple : prix<100 and
prix>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 à jour"
CancelText="Annuler" EditText="Modifier">
</asp:EditCommandColumn>
<asp:ButtonColumn Text="Supprimer" CommandName="Delete">
</asp:ButtonColumn>
</Columns>
<PagerStyle NextPageText="Suivant" PrevPageText="Précé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:
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
die [DataTable]-Tabelle für Produkte – wird von allen Clients gemeinsam genutzt | |
die [DataView] für Produkte – spezifisch für jeden Kunden | |
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:

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

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

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 |
RequiredFieldValidator | prüft auf einen Wert in [txtPages] | ||
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:

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:

Wir haben 5 [DataGrid]-Komponenten, von denen jede eine bestimmte Ansicht der Produkttabelle [dtProduits] anzeigt. Von links beginnend:
Name | Rolle |
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 | |
Zeigt die gesamte Produkttabelle an – ermöglicht die Verfolgung von Aktualisierungen | |
zeigt die gelöschten Produkte in der Produkttabelle an | |
zeigt die geänderten Produkte in der Produkttabelle an | |
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 |
RowStateFilter= DataRowViewState.CurrentRows | |
RowStateFilter = DataRowViewState.Deleted | |
RowStateFilter = DataRowViewState.ModifiedOriginal | |
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:
Es kann mehrere kritische Abschnitte im Anwendungscode geben:
Der Mechanismus funktioniert wie folgt:
- 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.
- 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.
- 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:

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:

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:

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:

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:
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:
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"> ' 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("Produit [{0}] inexistant", 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"> ' 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):

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

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

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

Die erhaltene Antwort lautet wie folgt:

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:


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 |
RequiredFieldValidator | prüft auf einen Wert in [txtName] | |
RequiredFieldValidator | prüft auf einen Wert in [txtPrice] | |
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:

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

Dadurch wird in den Bearbeitungsmodus gewechselt:

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.

Das Ergebnis sieht wie folgt aus:

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

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

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

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

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

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:

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

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

Ein Web-Client wird gestartet:

Wir fügen ein Produkt hinzu:

Wir nehmen [product1] aus dem Sortiment:

Wir ändern den Preis von [Produkt2]:

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

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:

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

Er erhält folgende Antwort:

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:

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:

Ein Client [Internet Explorer] macht dasselbe:

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

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

Er erhält die folgende Antwort:

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:

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.









