9. Komponenty serwerowe ASP – 3
9.1. Introduction
Kontynuujemy prace nad interfejsem użytkownika, rozbudowując możliwości komponentów [DataList] i [DataGrid], zwłaszcza w zakresie aktualizacji wyświetlanych przez nie danych
9.2. Zarządzanie zdarzeniami związanymi z danymi komponentów z powiązaniem danych
9.2.1. Przykład
Rozważmy następującą stronę:
![]() |
Strona zawiera trzy komponenty powiązane z listą danych:
- komponent [DataList] o nazwie [DataList1]
- komponent [DataGrid] o nazwie [DataGrid1]
- komponent [Repeater] o nazwie [Repeater1]
Powiązaną listą danych jest tablica {"zero", "jeden", "dwa", "trzy"}. Każdej z tych wartości przypisana jest grupa dwóch przycisków o nazwach [Infos1] i [Infos2]. Użytkownik klika jeden z przycisków, a na ekranie pojawia się tekst z nazwą klikniętego przycisku. Chcemy tutaj pokazać, jak zarządzać listą przycisków lub linków.
9.2.2. Konfiguracja komponentów
Komponent [DataList1] jest skonfigurowany w następujący sposób:
<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>
Pominęliśmy wszystkie elementy związane z wyglądem pliku [DataList], skupiając się wyłącznie na jego treści:
- sekcja <HeaderTemplate> definiuje nagłówek pliku [DataList], a sekcja <FooterTemplate> – jego stopkę.
- sekcja <ItemTemplate> stanowi szablon wyświetlania stosowany dla każdego z elementów z powiązanej listy danych. Zawiera ona następujące elementy:
- wartość bieżącej pozycji z listy danych powiązanej z komponentem: <%# Container.DataItem %>
- dwa przyciski oznaczone odpowiednio jako [Infos1] i [Infos2]. Klasa [Button] posiada atrybut [CommandName], który jest tutaj wykorzystywany. Pozwoli nam to ustalić, który przycisk wywołał zdarzenie w elemencie [DataList]. Aby obsłużyć kliknięcia na przyciskach, będziemy mieli tylko jedną procedurę obsługi zdarzeń, która będzie powiązana z samym elementem [DataList], a nie z przyciskami. Ten handler otrzyma informację wskazującą, w którym wierszu komponentu [DataList] nastąpiło kliknięcie. Atrybut [CommandName] pozwoli nam ustalić, na którym przycisku w tym wierszu nastąpiło kliknięcie.
Komponent [Repeater1] jest skonfigurowany w bardzo podobny sposób:
<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>
Wystarczyło dodać sekcję <SeparatorTemplate>, aby kolejne dane wyświetlane przez komponent były oddzielone poziomą kreską.
Wreszcie komponent [DataGrid1] jest skonfigurowany w następujący sposób:
<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>
Również w tym przypadku pominięto informacje dotyczące stylu (kolory, szerokości itp.). Działamy w trybie automatycznego generowania kolumn, który jest trybem domyślnym dla [DataGrid]. Oznacza to, że liczba kolumn będzie taka sama jak w źródle danych. W tym przypadku jest jedna kolumna. Dodaliśmy dwie kolejne kolumny oznaczone tagiem <asp:ButtonColumn>. Definiujemy w nich informacje analogiczne do tych zdefiniowanych dla pozostałych dwóch komponentów, a także typ przycisku, w tym przypadku [PushButton]. Domyślnym typem jest [LinkButton], c.a.d. link. Ponadto dane będą paginowane [AllowPaging=true], a rozmiar strony wyniesie dwa elementy [PageSize=2].
9.2.3. Kod prezentacji strony
Kod wyświetlania naszej przykładowej strony został umieszczony w pliku [main.aspx]:
<%@ page codebehind="main.aspx.vb" inherits="vs.main" autoeventwireup="false" %>
<HTML>
<HEAD>
</HEAD>
<body>
<form runat="server">
<P>Gestion d'événements de composants associés à des listes de données</P>
<HR width="100%" SIZE="1">
<table cellSpacing="1" cellPadding="1" bgColor="#ffcc00" border="1">
<tr>
<td ...>DataList</td>
<td ...>DataGrid</td>
<td ...>Repeater</td>
</tr>
<tr>
<td ...>
<asp:datalist id="DataList1" ... runat="server">
<HeaderTemplate>
[début]
</HeaderTemplate>
<FooterTemplate>
[fin]
</FooterTemplate>
<ItemStyle ...></ItemStyle>
<ItemTemplate>
<P><%# Container.DataItem %>
<asp:Button runat="server" Text="Infos1" CommandName="infos1"></asp:Button>
<asp:Button runat="server" Text="Infos2" CommandName="infos2"></asp:Button></P>
</ItemTemplate>
<FooterStyle ...></FooterStyle>
<HeaderStyle ....></HeaderStyle>
</asp:datalist>
<P></P>
</td>
<td ...>
<asp:datagrid id="DataGrid1" ... runat="server" PageSize="2" AllowPaging="True">
<AlternatingItemStyle ...></AlternatingItemStyle>
<ItemStyle ...></ItemStyle>
<HeaderStyle ...></HeaderStyle>
<FooterStyle ...></FooterStyle>
<Columns>
<asp:ButtonColumn Text="Infos1" ButtonType="PushButton" CommandName="Infos1">
</asp:ButtonColumn>
<asp:ButtonColumn Text="Infos2" ButtonType="PushButton" CommandName="Infos2">
</asp:ButtonColumn>
</Columns>
<PagerStyle ... Mode="NumericPages"></PagerStyle>
</asp:datagrid>
</td>
<td ...>
<asp:repeater id="Repeater1" runat="server">
<HeaderTemplate>
[début]<hr />
</HeaderTemplate>
<FooterTemplate>
<hr />
[fin]
</FooterTemplate>
<SeparatorTemplate>
<hr />
</SeparatorTemplate>
<ItemTemplate>
<%# Container.DataItem %>
<asp:Button runat="server" Text="Infos1" CommandName="infos1"></asp:Button>
<asp:Button runat="server" Text="Infos2" CommandName="infos2"></asp:Button></P>
</ItemTemplate>
</asp:repeater>
</td>
</tr>
</table>
<P><asp:label id="lblInfo" runat="server"></asp:label></P>
<P></P>
</form>
</body>
</HTML>
W powyższym kodzie pominięto kod formatowania (kolory, linie, rozmiary itp.)
9.2.4. Kod sterujący stroną
Kod sterujący aplikacją umieszczono w pliku [main.aspx.vb]:
Public Class main
Inherits System.Web.UI.Page
' elementy strony
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
' źródło danych
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
'połączenia ze źródłem danych
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
' wystąpiło zdarzenie na jednej z linii [datalist]
lblInfo.Text = "Vous avez cliqué sur le bouton [" + e.CommandName + "] de l'élément [" + e.Item.ItemIndex.ToString + "] du composant [DataList]"
End Sub
Private Sub Repeater1_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.RepeaterCommandEventArgs) Handles Repeater1.ItemCommand
' zdarzenie wystąpiło w jednym z wierszy pliku [repeater]
lblInfo.Text = "Vous avez cliqué sur le bouton [" + e.CommandName + "] de l'élément [" + e.Item.ItemIndex.ToString + "] du composant [Repeater]"
End Sub
Private Sub DataGrid1_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.ItemCommand
' wystąpiło zdarzenie w jednej z linii pliku [datagrid]
lblInfo.Text = "Vous avez cliqué sur le bouton [" + e.CommandName + "] de l'élément [" + e.Item.ItemIndex.ToString + "] de la page [" + DataGrid1.CurrentPageIndex.ToString() + "] du composant [DataGrid]"
End Sub
Private Sub DataGrid1_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles DataGrid1.PageIndexChanged
' zmiana strony
With DataGrid1
.CurrentPageIndex = e.NewPageIndex
.DataSource = textes
.DataBind()
End With
End Sub
End Class
Uwagi:
- źródło danych [textes] to prosta tablica ciągów znaków. Zostanie ono powiązane z trzema komponentami obecnymi na stronie
- powiązanie to jest tworzone w procedurze [Page_Load] podczas pierwszego żądania. W przypadku kolejnych żądań trzy komponenty odzyskają swoje wartości dzięki mechanizmowi [VIEW_STATE].
- Wszystkie trzy komponenty posiadają procedurę obsługi zdarzenia [ItemCommand]. Zdarzenie to występuje po kliknięciu przycisku lub linku w jednym z wierszy komponentu. Procedura obsługi otrzymuje dwie informacje:
- źródło: odwołanie do obiektu (przycisku lub linku), który wywołał zdarzenie
- a: informacje o zdarzeniu typu [DataListCommandEventArgs], [RepeaterCommandEventArgs], [DataGridCommandEventArgs], w zależności od przypadku. Argument a zawiera różne informacje. W tym przypadku interesują nas dwie z nich:
- a.Item: reprezentuje wiersz, w którym wystąpiło zdarzenie, typu [DataListItem], [DataGridItem] lub [RepeaterItem]. Niezależnie od dokładnego typu, element [Item] posiada atrybut [ItemIndex] wskazujący numer wiersza [Item] w kontenerze, do którego należy. Wyświetlamy tutaj ten numer wiersza
- a.CommandName: jest to atrybut [CommandName] przycisku (Button, LinkButton, ImageButton), który wywołał zdarzenie. Ta informacja w połączeniu z poprzednią pozwala nam ustalić, który przycisk w kontenerze jest źródłem zdarzenia [ItemCommand]
9.3. Aplikacja – zarządzanie listą subskrypcji
Teraz, gdy wiemy już, jak przechwytywać zdarzenia występujące wewnątrz kontenera danych, przedstawiamy przykład pokazujący, jak można nimi zarządzać.
9.3.1. Wprowadzenie
Przedstawiona aplikacja symuluje aplikację do subskrypcji list mailingowych. Listy te są definiowane przez obiekt [DataTable] zawierający trzy kolumny:
nazwa | typ | rola |
ciąg znaków | klucz główny | |
ciąg znaków | nazwa motywu listy | |
ciąg znaków | opis tematów poruszanych na liście |
Aby nie komplikować naszego przykładu, powyższy obiekt [DataTable] zostanie wygenerowany programowo w dowolny sposób. W rzeczywistej aplikacji zostałby on prawdopodobnie dostarczony przez metodę klasy zapewniającej dostęp do danych. Utworzenie tabeli list nastąpi w procedurze [Application_Start], a powstała tabela zostanie umieszczona w aplikacji. Nazwiemy ją tabelą [dtThèmes]. Użytkownik zasubskrybuje niektóre tematy z tej tabeli. Lista jego subskrypcji będzie przechowywana w obiekcie [DataTable] o nazwie [dtAbonnements], którego struktura będzie następująca:
nazwa | typ | rola |
ciąg znaków | klucz główny | |
ciąg znaków | nazwa motywu listy |
Aplikacja jednostronicowa wygląda następująco:
![]() |
nr | nazwa | typ | właściwości | rola |
DataGrid | listy mailingowe dostępne do subskrypcji | |||
DataList | lista subskrypcji użytkownika na powyższe listy | |||
panel | panel informacyjny dotyczący tematu wybranego przez użytkownika z linkiem [Plus d'informations] | |||
Etykieta | jest częścią [panelInfos] | nazwa tematu | ||
Etykieta | należy do [panelInfos] | opis tematu | ||
Etykieta | komunikat informacyjny aplikacji |
Nasz przykład ma na celu zilustrowanie wykorzystania komponentów [DataGrid] i [DataList], a w szczególności zarządzania zdarzeniami występującymi na poziomie wierszy tych kontenerów danych. Dlatego aplikacja nie posiada przycisku zatwierdzającego, który zapisywałby na przykład w bazie danych wybory dokonane przez użytkownika. Niemniej jednak jest to realistyczny przykład. Jesteśmy blisko aplikacji e-commerce, w której użytkownik dodawałby produkty (subskrypcje) do koszyka.
9.3.2. Działanie
Pierwszy ekran, który widzi użytkownik, wygląda następująco:
![]() |
Użytkownik klika na linki [Plus d'informations], aby uzyskać informacje na dany temat. Są one wyświetlane w [panelInfos]:

Klika na linki [S'abonner], aby subskrybować dany temat. Wybrane tematy są zapisywane w komponencie [dlAbonnements]:

Użytkownik może chcieć subskrybować listę, którą już subskrybuje. Pojawia się komunikat informujący go o tym:

Wreszcie użytkownik może zrezygnować z subskrypcji jednego z tematów za pomocą przycisków [Retirer] powyżej. Nie jest wymagane żadne potwierdzenie. Nie jest to tutaj konieczne, ponieważ użytkownik może łatwo ponownie subskrybować dany temat. Oto widok po usunięciu subskrypcji [thème1]:

9.3.3. Konfiguracja kontenerów danych
Komponent [dgThèmes] typu [DataGrid] jest powiązany ze źródłem typu [DataTable]. Jego formatowanie zostało wykonane za pomocą powiązania [Mise en forme automatique] w panelu właściwości komponentu [DataGrid]. Z kolei definicja jego właściwości została utworzona za pomocą linku [Générateur de proprités] w tym samym panelu. Wygenerowany kod wygląda następująco (kod formatowania został pominięty):
<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>
Należy zwrócić uwagę na następujące kwestie:
w sekcji <columns>...</columns> sami definiujemy kolumny, które mają być wyświetlane | |
w celu paginacji danych | |
definiuje kolumnę [thème] (HeaderText) z [DataGrid], która będzie powiązana z kolumną [thème] ze źródła danych (DataField) | |
definiuje dwie kolumny przycisków (lub linków). Aby odróżnić dwa linki w tym samym wierszu, wykorzystamy ich właściwość [CommandName]. |
Komponent [DataGrid] nie jest w pełni skonfigurowany. Zostanie to zrobione w kodzie kontrolera.
Komponent [dlAbonnements] typu [DataList] jest powiązany ze źródłem typu [DataTable]. Jego formatowanie zostało wykonane za pomocą linku [Mise en forme automatique] z panelu właściwości komponentu [DataList]. Definicja jego właściwości została natomiast wprowadzona bezpośrednio w kodzie prezentacji. Kod ten wygląda następująco (kod formatowania został pominięty):
<asp:DataList id="dlAbonnements" ... runat="server" >
<HeaderTemplate>
<div align="center">
Vos abonnements</div>
</HeaderTemplate>
<ItemStyle ...></ItemStyle>
<ItemTemplate>
<TABLE>
<TR>
<TD><%#Container.DataItem („motyw”)%></TD>
<TD>
<asp:Button id="lnkRetirer" CommandName="retirer" runat="server" Text="Retirer" /></TD>
</TR>
</TABLE>
</ItemTemplate>
<HeaderStyle ...></HeaderStyle>
</asp:DataList>
określa tekst nagłówka [DataList] | |
definiuje bieżący element [DataList] – w tym miejscu umieszczono tabelę składającą się z dwóch kolumn i jednego wiersza. Pierwsza komórka będzie zawierała nazwę tematu, który użytkownik chce subskrybować, a druga – przycisk [Retirer], umożliwiający cofnięcie wyboru. |
9.3.4. Strona prezentacyjna
Kod prezentacji [main.aspx] wygląda następująco:
<%@ 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. Kontrolery
Kontrola jest podzielona między pliki [global.asax] i [main.aspx]. Plik [global.asax] ma następującą treść:
Powiązany plik [global.asax.vb] zawiera następujący kod:
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)
' inicjowanie źródła danych
Dim thèmes As New DataTable
' kolumny
With thèmes.Columns
.Add("id", GetType(System.Int32))
.Add("thème", GetType(System.String))
.Add("description", GetType(System.String))
End With
' kolumna „id” będzie kluczem głównym
thèmes.Constraints.Add("cléprimaire", thèmes.Columns("id"), True)
' wiersze
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
' umieszczamy źródło danych w aplikacji
Application("thèmes") = thèmes
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' początek sesji – tworzymy pustą tabelę subskrypcji
Dim dtAbonnements As New DataTable
With dtAbonnements
' kolumny
.Columns.Add("id", GetType(String))
.Columns.Add("thème", GetType(String))
' klucz główny
.PrimaryKey = New DataColumn() {.Columns("id")}
End With
' tabela jest umieszczana w sesji
Session.Item("abonnements") = dtAbonnements
End Sub
End Class
Procedura [Application_Start], uruchamiana w momencie otrzymania przez aplikację pierwszego żądania, tworzy obiekt [DataTable] zawierający motywy, które można subskrybować. Jest on tworzony arbitralnie za pomocą kodu. Przypomnijmy sobie technikę, z którą już się spotkaliśmy. Tworzymy w następującej kolejności:
- pusty obiekt [DataTable], bez struktury i bez danych
- strukturę tabeli poprzez zdefiniowanie jej kolumn (nazwa i typ danych, które zawiera)
- wiersze tabeli, które reprezentują dane użytkowe
Dodaliśmy tutaj klucz główny. Kolumna „id” pełni rolę klucza głównego. Można to ująć na wiele sposobów. W tym przypadku zastosowaliśmy ograniczenie. W SQL ograniczenie to reguła, której muszą spełniać dane w wierszu, aby wiersz ten mógł zostać dodany do tabeli. Istnieje wiele różnych rodzajów ograniczeń. Ograniczenie „Primary Key” wymusza, aby kolumna, której dotyczy, zawierała unikalne i niepuste wartości. Klucz główny może w rzeczywistości składać się z wyrażenia uwzględniającego wartości z kilku kolumn. [DataTable].Constraints to zbiór ograniczeń danej tabeli. Aby dodać ograniczenie, używa się metody [DataTable.Constraints.Add]. Metoda ta ma kilka sygnatur. W tym przypadku użyto metody [Add(Byval nom as String, Byval colonne as DataColumn, Byval cléPrimaire as Boolean)]:
nazwa ograniczenia – może być dowolna | |
kolumna, która będzie kluczem głównym – typu [DataColumn] | |
musi wynosić [vrai], aby kolumna [colonne] stała się kluczem głównym. Jeśli wartość wynosi [cléPrimaire=false], obowiązuje jedynie ograniczenie unikalności dla kolumny [colonne] |
Aby kolumna o nazwie „id” stała się kluczem głównym tabeli [dtAbonnements], należy wpisać:
Procedura [Session_Start] jest uruchamiana, gdy aplikacja otrzymuje pierwsze zapytanie od klienta. Służy ona do tworzenia obiektów specyficznych dla każdego klienta, które muszą być zachowane w trakcie kolejnych zapytań tego klienta. Procedura ta tworzy tabelę [DataTable] zawierającą subskrypcje klienta. Tworzona jest jedynie jej struktura, ponieważ na początku tabela ta jest pusta. Będzie się ona wypełniać w miarę napływania żądań. Również w tym przypadku kolumna „id” pełni rolę klucza głównego. Do zadeklarowania tego ograniczenia zastosowano inną technikę:
to tablica kolumn tworzących klucz główny – w tym przypadku zadeklarowano tablicę zawierającą jeden element: kolumnę o nazwie „id” |
Gdy żądanie klienta dociera do kontrolera [main.aspx], oba obiekty [DataTable] są dostępne w aplikacji dla tabeli motywów oraz w sesji dla tabeli subskrypcji. Kontroler [main.aspx.vb] wygląda następująco:
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
Głównym zadaniem procedury [Page_Load] jest:
- pobranie dwóch tabel [dtThèmes] i [dtAbonnements], które znajdują się odpowiednio w aplikacji i sesji, w celu udostępnienia ich wszystkim metodom strony
- powiązanie danych z tych dwóch źródeł z ich odpowiednimi kontenerami. Odbywa się to wyłącznie przy pierwszym wywołaniu. Przy kolejnych wywołaniach powiązanie nie musi być wykonywane automatycznie, a jeśli jest konieczne, czasami trzeba poczekać na zdarzenie następujące po [Page_Load], aby je przeprowadzić.
Kod [Page_Load] wygląda następująco:
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' pobierane są źródła danych
dtThèmes = CType(Application("thèmes"), DataTable)
dtAbonnements = CType(Session("abonnements"), DataTable)
' powiązanie danych
If Not IsPostBack Then
liaisons()
End If
' niektóre informacje są ukryte
panelInfo.Visible = False
End Sub
Private Sub liaisons()
'– łączy się źródło danych z komponentem [datagrid]
With dgThèmes
.DataSource = dtThèmes
.DataKeyField = "id"
End With
' powiązanie źródła danych z komponentem [datalist]
With dlAbonnements
.DataSource = dtAbonnements
.DataKeyField = "id"
End With
' przypisuje się dane do komponentów
Page.DataBind()
End Sub
W procedurze [liaisons] wykorzystujemy właściwość [DataKeyField] komponentów [DataList] i [DataGrid] w celu zdefiniowania kolumny źródła danych, która posłuży do jednoznacznej identyfikacji wierszy kontenerów. Zazwyczaj kolumna ta jest kluczem głównym źródła danych, ale nie jest to obowiązkowe. Wystarczy, aby kolumna ta nie zawierała duplikatów ani pustych wartości. W przypadku kontenera [dgThèmes] kolumna „id” źródła [dtThèmes] będzie służyć jako klucz główny, a dla kontenera [dlAbonnements] będzie to kolumna „id” źródła [dtAbonnements]. Nie ma konieczności, aby kolumna pełniąca rolę klucza głównego w kontenerze była przez niego wyświetlana. W tym przypadku żaden z obu kontenerów nie wyświetla kolumny klucza głównego. Zaletą posiadania przez kontener klucza głównego jest to, że umożliwia on łatwe odnalezienie w źródle danych informacji związanych z wierszem kontenera, w którym wystąpiło zdarzenie. Często bowiem zdarza się, że na podstawie wiersza kontenera, w którym wystąpiło zdarzenie, konieczne jest podjęcie działań w odniesieniu do odpowiadającego mu wiersza w źródle danych, z którym jest on powiązany. Klucz podstawowy ułatwia tę pracę.
Paginacja [DataGrid] jest zarządzana w sposób standardowy:
Private Sub dgThèmes_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles dgThèmes.PageIndexChanged
' zmiana strony
dgThèmes.CurrentPageIndex = e.NewPageIndex
' powiązanie
liaisons()
End Sub
Operacje na linkach [Plus d'informations] i [S'abonner] są zarządzane przez procedurę [dgThèmes_ItemCommand]:
Private Sub dgThèmes_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles dgThèmes.ItemCommand
' zdarzenie w wierszu komponentu [datagrid]
Dim commande As String = e.CommandName
Select Case commande
Case "infos"
infos(dgThèmes.DataKeys(e.Item.ItemIndex))
Case "abonner"
abonner(dgThèmes.DataKeys(e.Item.ItemIndex))
End Select
' powiązanie
liaisons()
End Sub
W celu rozróżnienia obu linków wykorzystuje się fakt, że oba posiadają atrybut [CommandName]. W zależności od wartości tego atrybutu wywoływana jest procedura [infos] lub [abonner], przekazując w obu przypadkach klucz „id” powiązany z elementem [DataGrid], w którym wystąpiło zdarzenie. Dysponując tą informacją, procedura [info] wyświetli informacje dotyczące motywu wybranego przez użytkownika:
Private Sub infos(ByVal id As String)
' informacje na temat klucza id
' pobierany jest wiersz z [datatable] odpowiadający kluczowi
Dim ligne As DataRow
ligne = dtThèmes.Rows.Find(id)
If Not ligne Is Nothing Then
' wyświetla się informacja
lblThème.Text = CType(ligne("thème"), String)
lblDescription.Text = CType(ligne("description"), String)
panelInfo.Visible = True
End If
End Sub
Ponieważ tabela [dtThèmes] posiada klucz główny, metoda [dtThèmes.Rows.Find("P")] pozwala znaleźć wiersz o kluczu głównym P. Jeśli zostanie on znaleziony, otrzymujemy obiekt [DataRow]. W tym przypadku musimy wyszukać wiersz o kluczu głównym [id], przy czym jako parametr przekazano [id]. Jeśli wiersz zostanie znaleziony, umieszczamy informacje [thème] i [description] z tego wiersza w panelu informacyjnym, który następnie wyświetlamy.
Procedura [abonner(id)] ma za zadanie dodać motyw klucza [id] do listy subskrypcji. Jej kod jest następujący:
Private Sub abonner(ByVal id As String)
' subskrypcja motywu o identyfikatorze klucza
' pobieramy wiersz z [datatable] odpowiadający kluczowi
Dim ligne As DataRow
ligne = dtThèmes.Rows.Find(id)
If Not ligne Is Nothing Then
' sprawdzamy, czy nie jesteśmy już subskrybentami
Dim abonnement As DataRow
abonnement = dtAbonnements.Rows.Find(id)
If Not abonnement Is Nothing Then
' zgłaszamy błąd
lblInfo.Text = "Vous êtes déjà abonné au thème [" + ligne("thème") + "]"
Else
' dodajemy temat do subskrypcji
abonnement = dtAbonnements.NewRow
abonnement("id") = id
abonnement("thème") = ligne("thème")
dtAbonnements.Rows.Add(abonnement)
' tworzy się powiązania
liaisons()
End If
End If
End Sub
Na liście motywów [dtThèmes] najpierw szukamy wiersza klucza [id]. Jeśli zostanie ona znaleziona, sprawdzamy, czy ten motyw nie występuje już na liście subskrypcji, aby uniknąć podwójnego wpisania. Jeśli tak jest, wyświetlamy komunikat o błędzie. W przeciwnym razie dodajemy nową subskrypcję do tabeli [dtAbonnements] i tworzymy powiązania między komponentami listy danych a ich odpowiednimi źródłami.
Gdy użytkownik kliknie przycisk [Retirer], należy usunąć element z tabeli [dtAbonnements]. Odbywa się to za pomocą następującej procedury:
Private Sub dlAbonnements_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles dlAbonnements.ItemCommand
' usuwa się subskrypcję
Dim commande As String = e.CommandName
If commande = "retirer" Then
' usuwa się subskrypcję z [datatable]
With dtAbonnements.Rows
.Remove(.Find(dlAbonnements.DataKeys(e.Item.ItemIndex)))
End With
' powiązania
liaisons()
End If
End Sub
Najpierw sprawdzamy atrybut [CommandName] elementu, który wywołał zdarzenie. Jest to w rzeczywistości dość zbędne, ponieważ przycisk [Retirer] jest jedynym elementem sterującym zdolnym do wygenerowania zdarzenia w komponencie [DataList]. Nie ma więc żadnej niejasności. Aby usunąć wiersz z obiektu [DataTable], stosuje się metodę [DataList.Remove(DataRow)], która usuwa z tabeli wiersz typu [DataRow] przekazany jako parametr. Wiersz ten jest wyszukiwany przez metodę [DataList.Find], do której przekazano klucz główny szukanego wiersza. Po usunięciu wiersza następuje powiązanie danych z komponentami
9.4. Zarządzanie paginowanym komponentem [DataList]
Wracamy do poprzedniego przykładu, aby wprowadzić paginację w komponencie [DataList] przedstawiającym listę subskrypcji użytkownika. W przeciwieństwie do komponentu [DataGrid], komponent [DataList] nie oferuje żadnych funkcji ułatwiających paginację. Przekonamy się, że wdrożenie paginacji w tym komponencie jest skomplikowane, co pozwoli nam docenić zalety automatycznej paginacji w komponencie [DataGrid].
9.4.1. Działanie
Jedyną różnicą jest paginacja w komponencie [DataList]. Strony będą zawierały dwie subskrypcje. Jeśli użytkownik ma pięć subskrypcji, powstają trzy strony. Pierwsza z nich będzie wyglądała następująco:

Drugą stronę uzyskuje się poprzez link [Suivant]:

Trzecia strona:

Warto zauważyć, że linki [Précédent] i [Suivant] są widoczne tylko wtedy, gdy przed bieżącą stroną znajduje się poprzednia strona, a po niej następna.
9.4.2. Kod prezentacji
Linki [Précédent] i [Suivant] uzyskuje się poprzez dodanie tagu <FooterTemplate> do [DataList]:
<asp:datalist id="dlAbonnements" runat="server" ...>
....
<FooterTemplate>
<asp:LinkButton id="lnkPrecedent" runat="server" CommandName="precedent">Précédent</asp:LinkButton>
<asp:LinkButton id="lnkSuivant" runat="server" CommandName="suivant">Suivant</asp:LinkButton>
</FooterTemplate>
....
</asp:datalist>
9.4.3. Kod kontrolny
Powiązany plik [global.asax.vb] zmienia się w następujący sposób:
...
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)
' początek sesji – tworzy się pustą tabelę subskrypcji
Dim dtAbonnements As New DataTable
With dtAbonnements
' kolumny
.Columns.Add("id", GetType(String))
.Columns.Add("thème", GetType(String))
' klucz główny
.PrimaryKey = New DataColumn() {.Columns("id")}
End With
' tabela zostaje umieszczona w sesji
Session.Item("abonnements") = dtAbonnements
' bieżąca strona to strona 0
Session.Item("pAC") = 0
' liczba subskrypcji na tej stronie wynosi 0
Session.Item("nbAC") = 0
End Sub
Oprócz tabeli subskrypcji [dtAbonnements] do sesji dodaje się dwie inne informacje:
typu [Integer] – jest to numer bieżącej strony wyświetlonej podczas ostatniego żądania | |
typu [Integer] – liczba wierszy wyświetlonych na poprzedniej bieżącej stronie |
Na początku sesji numer bieżącej strony oraz liczba wierszy na tej stronie wynoszą zero.
Kontroler [main.aspx.vb] zmienia się w następujący sposób:
....
Public Class main
Inherits System.Web.UI.Page
....
' dane aplikacji
Protected dtThèmes As DataTable
Protected dtAbonnements As DataTable
Protected dtPA As DataTable ' la page d'abonnements affichée
Protected Const nbAP As Integer = 2 ' nbre abonnements par page
Protected pAC As Integer ' page abonnement courant
Protected nbAC As Integer ' nombre d'abonnements dans page courante
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
End Sub
Private Sub terminer()
...
End Sub
Private Sub dgThèmes_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles dgThèmes.PageIndexChanged
...
End Sub
Private Sub dgThèmes_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles dgThèmes.ItemCommand
...
End Sub
Private Sub infos(ByVal id As String)
...
End Sub
Private Sub abonner(ByVal id As String)
...
End Sub
Private Sub dlAbonnements_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles dlAbonnements.ItemCommand
...
End Sub
Private Sub changePAC()
...
End Sub
Private Sub setLiens(ByVal ctl As Control, ByVal blPrec As Boolean, ByVal blSuivant As Boolean)
...
End Sub
End Class
Zdefiniowano w nim nowe dane związane z paginacją subskrypcji:
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
Niektóre z tych informacji są przechowywane w sesji i pobierane przy każdym wywołaniu procedury [Page_Load]:
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' pobierane są źródła danych
dtThèmes = CType(Application("thèmes"), DataTable)
dtAbonnements = CType(Session("abonnements"), DataTable)
' oraz informacje dotyczące wyświetlania subskrypcji
pAC = CType(Session("pAC"), Integer)
nbAC = CType(Session("nbAC"), Integer)
' powiązanie danych
If Not IsPostBack Then
' wyświetlanie pustej listy subskrypcji
terminer()
End If
' ukrywanie niektórych informacji
panelInfo.Visible = False
End Sub
Pobrane informacje [pAC] i [nbAC] dotyczą strony subskrypcji wyświetlonej podczas poprzedniego zapytania:
jest to numer bieżącej strony wyświetlonej podczas poprzedniego żądania | |
liczba wierszy wyświetlanych na bieżącej stronie |
Metoda [terminer] ustanawia powiązania między komponentami a ich źródłami danych, podobnie jak robiła to metoda [liaisons] w poprzedniej aplikacji. Nowością jest tutaj powiązanie tabeli [DataList] z tabelą [dtPA], która stanowi stronę subskrypcji do wyświetlenia:
Private Sub terminer()
' powiązanie źródła danych z komponentem [datagrid]
With dgThèmes
.DataSource = dtThèmes
.DataKeyField = "id"
End With
' powiązanie strony subskrypcji z komponentem [datalist] z uwzględnieniem bieżącej strony pAC
changePAC()
' wyświetla się strona p
With dlAbonnements
.DataSource = dtPA
.DataKeyField = "id"
End With
' przypisuje się dane do komponentów
Page.DataBind()
' zarządzanie powiązaniami [précédent] i [suivant] z [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)
' zapisywanie informacji o bieżącej stronie w sesji
Session("pAC") = pAC
Session("nbAC") = dtPA.Rows.Count
End Sub
Należy zwrócić uwagę na następujące kwestie:
- źródło [dtPA] zależy od numeru bieżącej strony [pAC], która ma zostać wyświetlona. Zmienna [pAC] jest zmienną globalną klasy, obsługiwanej przez metody odpowiedzialne za zmianę tego numeru bieżącej strony. To metoda [changePAC] odpowiada za utworzenie tabeli [dtPA], która zostanie powiązana z komponentem [dlAbonnements].
- Metoda [setLiens] służy do wyświetlania lub ukrywania linków [Précédent] i [Suivant] w zależności od tego, czy wyświetlana strona bieżąca [pAC] ma przed sobą i za sobą inną stronę. Posiada cztery parametry:
- [dlAbonnements]: kontrolka [DataList], której drzewo kontrolki zostanie zbadane w celu znalezienia obu linków. W rzeczywistości, mimo że oba te linki znajdują się w konkretnym miejscu, jakim jest stopka elementu [DataList], nie wydaje się, aby istniał prosty sposób na bezpośrednie odwołanie się do nich. W każdym razie nie znaleziono go tutaj.
- [blPrecedent]: wartość logiczna, którą należy przypisać do właściwości [visible] linku [Precedent] – ma wartość „prawda”, jeśli bieżąca strona jest różna od 0
- [blSuivant]: wartość logiczna, którą należy przypisać do właściwości [visible] linku [Suivant] – ma wartość true, jeśli bieżąca strona nie jest ostatnią stroną na liście subskrypcji
- [nbLiensTrouvés]: parametr wyjściowy, który zlicza liczbę znalezionych linków. Gdy liczba ta osiągnie wartość dwa, metoda zostaje zakończona.
- Informacje [pAC] i [nbAC] są zapisywane w sesji na potrzeby następnego żądania.
Metoda [changePAC] tworzy tabelę [dtPA], która zostanie powiązana z komponentem [dlAbonnements]. Odbywa się to na podstawie numeru [pAC] bieżącej strony, która ma zostać wyświetlona. Tabela [dtPA] ma wyświetlać określone wiersze z tabeli subskrypcji [dtAbonnements]. Należy pamiętać, że tabela ta jest przechowywana w sesji i aktualizowana (zwiększana lub zmniejszana) w miarę napływających zapytań. Na początek należy określić przedział [premier,dernier] obejmujący numery wierszy z tabeli [dtAbonnements], które ma wyświetlać tabela [dtPA]:
Private Sub changePAC()
' ustawia stronę pAC jako bieżącą stronę subskrypcji
' zarządzanie stronami [datalist]
Dim nbAbonnements = dtAbonnements.Rows.Count
Dim dernièrePage = (nbAbonnements - 1) \ nbAP
' pierwsza i ostatnia subskrypcja
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
Po wykonaniu tej czynności można utworzyć tabelę [dtPA]. Najpierw definiujemy jej strukturę [id,thème], a następnie wypełniamy ją, kopiując do niej wiersze z tabeli [dtAbonnements], których numery mieszczą się w przedziale [premier,dernier] obliczonym wcześniej.
' tworzenie tabeli danych dtpa
dtPA = New DataTable
With dtPA
' kolumny
.Columns.Add("id", GetType(String))
.Columns.Add("thème", GetType(String))
' klucz główny
.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
Pod koniec metody [changePAC] utworzono tabelę [dtPA], którą można powiązać z komponentem [DataList]. Odbywa się to w metodzie [terminer]. W tej samej metodzie wykorzystuje się procedurę [setLiens] w celu ustalenia stanu powiązań [Précédent] i [Suivant] w ramach [DataList]. Kod tej procedury jest następujący:
Private Sub setLiens(ByVal ctl As Control, ByVal blPrec As Boolean, ByVal blSuivant As Boolean, ByRef nbLiensTrouvés As Integer)
' szukamy powiązań [précédent] i [suivant]
' w drzewie elementów kontrolnych [datalist]
' Czy znaleziono wszystkie linki?
If nbLiensTrouvés = 2 Then Exit Sub
' przegląd elementów podrzędnych
Dim c As Control
For Each c In ctl.Controls
' najpierw pracujemy dogłębnie – powiązania znajdują się na samym końcu drzewa
setLiens(c, blPrec, blSuivant, nbLiensTrouvés)
' link [Précédent]?
If c.ID = "lnkPrecedent" Then
CType(c, LinkButton).Visible = blPrec
nbLiensTrouvés += 1
End If
' link [Suivant]?
If c.ID = "lnkSuivant" Then
CType(c, LinkButton).Visible = blSuivant
nbLiensTrouvés += 1
End If
Next
End Sub
Procedura jest rekurencyjna. Najpierw przeszukuje wśród elementów potomnych komponentu [dlAbonnements] komponenty o nazwach [lnkPrecedent] i [lnkSuivant], które są identyfikatorami (atrybut ID) dwóch linków paginacji. Najpierw szuka ich, zaczynając od najniższego poziomu drzewa elementów sterujących, ponieważ właśnie tam się znajdują. Gdy tylko zostanie znaleziony link, licznik [nbLiensTrouvés] jest zwiększany, a właściwość [visible] linku jest wypełniana wartością przekazaną jako parametr procedury. Gdy tylko zostaną znalezione oba linki, drzewo elementów sterujących nie jest już przeszukiwane, a procedura rekurencyjna zostaje zakończona.
Wspomnieliśmy, że metoda [changePAC], która ustawia źródło danych [dtPA] dla komponentu [dlAbonnements], korzysta z numeru [pAC] bieżącej strony, która ma zostać wyświetlona. Kilka procedur modyfikuje ten numer:
Private Sub abonner(ByVal id As String)
' subskrypcja motywu o identyfikatorze klucza
..
' dodajemy motyw do subskrypcji
..
' aktualizacja numeru bieżącej strony – jest to teraz ostatnia strona
pAC = (dtAbonnements.Rows.Count - 1) \ nbAP
' powiązania danych
terminer()
End If
End Sub
Po dodaniu subskrypcji pojawia się ona na końcu listy subskrypcji. W związku z tym strona przechodzi na ostatnią stronę listy subskrypcji, aby użytkownik mógł zobaczyć dodaną pozycję.
Private Sub dlAbonnements_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles dlAbonnements.ItemCommand
' usuwanie subskrypcji
Dim commande As String = e.CommandName
Select Case commande
Case "retirer"
' usuwa się subskrypcję z [datatable]
With dtAbonnements.Rows
.Remove(.Find(dlAbonnements.DataKeys(e.Item.ItemIndex)))
End With
' czy należy zmienić bieżącą stronę?
nbAC -= 1
If nbAC = 0 Then pAC -= 1
Case "precedent"
' zmiana bieżącej strony
pAC -= 1
Case "suivant"
' zmiana bieżącej strony
pAC += 1
End Select
' powiązania danych
terminer()
End Sub
- [nbAC] to liczba wierszy wyświetlanych na bieżącej stronie przed anulowaniem subskrypcji. Jeśli nowa liczba wierszy na stronie wynosi 0, numer bieżącej strony [pAC] jest zmniejszany o jeden.
- W przypadku kliknięcia linku [Precedent] numer bieżącej strony [pAC] jest zmniejszany o jeden.
- W przypadku kliknięcia linku [Suivant] numer bieżącej strony [pAC] zostaje zwiększony o jeden.
Pozostałe procedury pozostają takie same jak poprzednio.
9.4.4. Wniosek
Na tym przykładzie pokazaliśmy, że można wprowadzić paginację w komponencie [DataList]. Paginacja ta jest skomplikowana i w miarę możliwości zaleca się korzystanie z automatycznej paginacji komponentu [DataGrid]. Ten przykład pokazał nam również, jak uzyskać dostęp do komponentów znajdujących się w stopce komponentu [DataList].
9.5. Klasa dostępu do bazy produktów
Ponownie zajmujemy się bazą danych ACCESS [produits], z której już korzystaliśmy. Przypomnijmy, że zawiera ona jedną tabelę o nazwie [liste], której struktura jest następująca:
![]() | ![]() |
Stworzymy klasę zapewniającą dostęp do tabeli [liste], która umożliwi jej odczyt i aktualizację. Stworzymy również klienta konsolowego, który będzie korzystał z tej klasy w celu aktualizacji tabeli. W kolejnym etapie stworzymy klienta internetowego, który wykona to samo zadanie.
9.5.1. Klasa ExceptionProduits
Klasa [Exception] posiada konstruktor, który przyjmuje komunikat o błędzie jako parametr. Chcemy tutaj dysponować klasą wyjątków posiadającą konstruktor przyjmujący listę komunikatów o błędach zamiast pojedynczego komunikatu o błędzie. Będzie to klasa [ExceptionProduits] przedstawiona poniżej:
Public Class ExceptionProduits
Inherits Exception
' komunikaty o błędach związanych z wyjątkiem
Private _erreurs As ArrayList
' konstruktor
Public Sub New(ByVal erreurs As ArrayList)
Me._erreurs = erreurs
End Sub
' właściwość
Public ReadOnly Property erreurs() As ArrayList
Get
Return _erreurs
End Get
End Property
End Class
9.5.2. Struktura [sProduit]
Struktura [produit] będzie reprezentować produkt [id, nom, prix]:
' struktura sProduit
Public Structure sProduit
' pola
Private _id As Integer
Private _nom As String
Private _prix As Double
' identyfikator właściwości
Public Property id() As Integer
Get
Return _id
End Get
Set(ByVal Value As Integer)
_id = Value
End Set
End Property
' właściwość nazwa
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
' właściwość „cena”
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
Struktura dopuszcza wyłącznie prawidłowe dane dla pól [nom] i [prix].
9.5.3. Klasa Produkty
Klasa [Produits] to klasa, która pozwoli nam zaktualizować tabelę [liste] w bazie produktów. Jej struktura jest następująca:
Public Class produits
' dane instancji
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
Dane instancji
Dane współdzielone przez różne metody klasy są następujące:
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
połączenie z bazą danych – zostanie otwarte w celu wykonania polecenia SQL, a następnie natychmiast zamknięte | |
zapytanie SQL [select] pobierające całą tabelę [liste] | |
zapytanie umożliwiające wstawienie wiersza (nazwa, cena) do tabeli [liste]. Należy zauważyć, że nie określa się pola [id]. Pole to jest bowiem automatycznie zwiększane przez SGBD, dlatego nie ma potrzeby jego określania. | |
zapytanie umożliwiające aktualizację pól (nazwa, cena) w wierszu tabeli [liste] o kluczu [id] | |
zapytanie umożliwiające usunięcie wiersza z tabeli [liste] o kluczu [id] | |
obiekt [OleDbCommand] wykonujący zapytanie [selectText] na połączeniu [connexion] | |
obiekt [OleDbCommand] wykonujący zapytanie [updateText] na połączeniu [connexion] | |
obiekt [OleDbCommand] wykonujący zapytanie [insertText] na połączeniu [connexion] | |
obiekt [OleDbCommand] wykonujący zapytanie [deleteText] na połączeniu [connexion] | |
obiekt umożliwiający pobranie wyniku wykonania [selectCommand] do obiektu [DataSet] |
Konstruktor
Konstruktor otrzymuje jeden parametr [chaineConnexionOLEDB], który jest ciągiem połączenia wskazującym bazę danych, z której mają być pobierane dane. Na jego podstawie przygotowuje się cztery polecenia odczytu i aktualizacji tabeli, a także adapter. Jest to jedynie przygotowanie i nie nawiązywane jest żadne połączenie.
Public Sub New(ByVal chaineConnexionOLEDB As String)
' przygotowuje się połączenie
connexion = New OleDbConnection(chaineConnexionOLEDB)
' przygotowywanie poleceń zapytań
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
' przygotowywanie adaptera dostępu do danych
adaptateur.SelectCommand = selectCommand
End Sub
Metoda getProduits
Metoda ta pozwala na przeniesienie zawartości tabeli [Liste] do obiektu [DataTable]. Jej kod jest następujący:
Public Function getProduits() As DataTable
' tabela [liste] jest umieszczana w [dataset]
Dim contenu As New DataSet
' tworzy się obiekt DataAdapter w celu odczytania danych ze źródła 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
' zwracamy wynik
Return contenu.Tables(0)
End Function
Zadanie realizują dwie poniższe instrukcje:
With adaptateur
.FillSchema(contenu, SchemaType.Source)
.Fill(contenu)
End With
Metoda [FillSchema] określa strukturę (kolumny, ograniczenia, relacje) tabeli [DataSet] oraz contenu na podstawie struktury bazy danych, do której odwołuje się [adaptateur.Connexion]. Pozwala to na pobranie struktury tabeli [liste], a w szczególności jej klucza głównego. Kolejna operacja [Fill] wypełnia tabele [Dataset] i contenu wierszami z tabeli [liste]. Dzięki tej jednej operacji uzyskamy wprawdzie dane i strukturę, ale nie klucz główny. A ten będzie nam potrzebny do aktualizacji tabeli [liste] w pamięci. W tym przypadku, podobnie jak w innych metodach, obsługujemy ewentualne błędy za pomocą klasy [ExceptionProduits], aby uzyskać listę (ArrayList) błędów zamiast pojedynczego błędu. Metoda [getProduits] zwraca tabelę [liste] w postaci obiektu [DataTable].
Metoda ajouterProduits
Metoda ta pozwala dodać wiersz (id, nazwa, cena) do tabeli [liste]. Informacje te są jej przekazywane w postaci struktury [sProduit] zawierającej pola [id, nom, prix]. Kod metody jest następujący:
Public Sub ajouterProduit(ByVal produit As sProduit)
' dodaje się produkt o nazwie [nom,prix]
' przygotowujemy parametry dodawania
With insertCommand.Parameters
.Clear()
.Add(New OleDbParameter("nom", produit.nom))
.Add(New OleDbParameter("prix", produit.prix))
End With
' dodano
Try
' nawiązanie połączenia
connexion.Open()
' wykonanie polecenia
insertCommand.ExecuteNonQuery()
Catch ex As Exception
' problem
Dim erreursCommande As New ArrayList
erreursCommande.Add(String.Format("Erreur lors de l'ajout : {0}", ex.Message))
Throw New ExceptionProduits(erreursCommande)
Finally
' zamknięcie połączenia
connexion.Close()
End Try
End Sub
Pola struktury [produit] są wprowadzane do parametrów polecenia [insertCommand]. Przypomnijmy aktualną konfigurację tego polecenia (patrz producent):
Private Const insertText As String = "insert into liste(nom,prix) values(?,?)"
insertCommand.Connexion=connexion
Tekst polecenia SQL [insert] zawiera parametry formalne „?”, które należy zastąpić parametrami rzeczywistymi. Odbywa się to za pomocą kolekcji [Parameters] klasy [OleDbCommand]. Zawiera ona elementy typu [OleDbParameter], które definiują parametry rzeczywiste, które mają zastąpić parametry formalne „?”. Ponieważ parametry rzeczywiste nie są nazwane, do ustalenia, który parametr rzeczywisty odpowiada danemu parametrowi formalnemu „?”, wykorzystuje się indeks parametrów rzeczywistych. W tym przypadku parametr rzeczywisty nr i w kolekcji [Parameters] zastąpi parametr formalny „?” nr i. Aby utworzyć parametr rzeczywisty typu [OleDbParameter], używa się tutaj konstruktora [OleDbParameter (Byval nom as String, Byval valeur as Object)], który definiuje nazwę i wartość parametru rzeczywistego. Nazwa może być dowolna. Ponadto w tym przypadku nie będzie ona wykorzystywana. Oba parametry instrukcji SQL i [insert] otrzymują jako wartości wartości pól [nom, prix] struktury [produit]. Po wykonaniu tej czynności wstawianie odbywa się za pomocą instrukcji [insertCommand.ExecuteNonQuery].
Metoda modifierProduits
Metoda ta umożliwia modyfikację wiersza w tabeli [liste]. Informacje niezbędne do jej działania są podane w strukturze [sProduit] w polach [id, nom, prix].
Public Sub modifierProduit(ByVal produit As sProduit)
' modyfikacja produktu [id,nom,prix]
' przygotowywanie parametrów aktualizacji
With updateCommand.Parameters
.Clear()
.Add(New OleDbParameter("nom", produit.nom))
.Add(New OleDbParameter("prix", produit.prix))
.Add(New OleDbParameter("id", produit.id))
End With
' wprowadzamy zmianę
Try
' otwarcie połączenia
connexion.Open()
' wykonanie polecenia
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
' problem
Dim erreursCommande As New ArrayList
erreursCommande.Add(String.Format("Erreur lors de la modification : {0}", ex.Message))
Throw New ExceptionProduits(erreursCommande)
Finally
' zamykanie połączenia
connexion.Close()
End Try
End Sub
Kod jest niemal identyczny z kodem metody [ajouterProduits], z tą różnicą, że przedmiotowe polecenie [OleDbCommand] to [updateCommand] zamiast [insertCommand].
Metoda supprimerProduits
Metoda ta pozwala na usunięcie wiersza z tabeli [liste] o kluczu [id] przekazanym jako parametr. Kod wygląda następująco:
Public Sub supprimerProduit(ByVal id As Integer)
' usuwa produkt klucza [id]
' przygotowuje się parametry usunięcia
With deleteCommand.Parameters
.Clear()
.Add(New OleDbParameter("id", id))
End With
' przeprowadzamy usunięcie
Try
' nawiązanie połączenia
connexion.Open()
' wykonanie polecenia
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
' problem
Dim erreursCommande As New ArrayList
erreursCommande.Add(String.Format("Erreur lors de la suppression : {0}", ex.Message))
Throw New ExceptionProduits(erreursCommande)
Finally
' zamykanie połączenia
connexion.Close()
End Try
End Sub
Postępujemy tak samo jak w przypadku poprzednich metod.
9.5.4. Testy klasy [produits]
Program testowy [testproduits.vb] typu konsolowego mógłby wyglądać następująco:
Option Explicit On
Option Strict On
' przestrzenie nazw
Imports System
Imports System.Data
Imports Microsoft.VisualBasic
Imports System.Collections
Namespace st.istia.univangers.fr
' strona testowa
Module testproduits
Dim contenu As DataTable
Sub Main(ByVal arguments() As String)
' wyświetla zawartość tabeli produktów
' tabela znajduje się w bazie ACCESS, której nazwa jest wykorzystywana jako nazwa pliku
Const syntaxe1 As String = "pg bdACCESS"
' sprawdzanie parametrów programu
If arguments.Length <> 1 Then
' komunikat o błędzie
Console.Error.WriteLine(syntaxe1)
' koniec
Environment.Exit(1)
End If
' przygotowywanie ciągu połączenia
Dim chaineConnexion As String = "Provider=Microsoft.Jet.OLEDB.4.0; Ole DB Services=-4; Data Source=" + arguments(0)
' tworzenie obiektu produktów
Dim objProduits As produits = New produits(chaineConnexion)
' wyświetlanie wszystkich produktów
afficheProduits(objProduits)
' dodawanie produktu
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
' wyświetlanie wszystkich produktów
afficheProduits(objProduits)
' pobieranie identyfikatora dodanego produktu
produit.id = CType(contenu.Rows(contenu.Rows.Count - 1)("id"), Integer)
' modyfikuje się dodany produkt
produit.prix = 200
Try
objProduits.modifierProduit(produit)
Catch ex As ExceptionProduits
afficheErreurs(ex.erreurs)
End Try
' wyświetla wszystkie produkty
afficheProduits(objProduits)
' usuwa dodany produkt
Try
objProduits.supprimerProduit(produit.id)
Catch ex As ExceptionProduits
afficheErreurs(ex.erreurs)
End Try
' wyświetla wszystkie produkty
afficheProduits(objProduits)
End Sub
Sub afficheProduits(ByRef objProduits As produits)
' pobieranie tabeli produktów do tabeli danych
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
' wiersz i tabeli
Console.Out.WriteLine(lignes(i).Item("id").ToString + "," + lignes(i).Item("nom").ToString + _
"," + lignes(i).Item("prix").ToString)
Next
' zatrzymuje strumień konsoli
Console.WriteLine("...")
Console.ReadLine()
End Sub
Sub afficheErreurs(ByRef erreurs As ArrayList)
' wyświetla błędy w konsoli
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
' zatrzymuje strumień konsoli
Console.WriteLine("...")
Console.ReadLine()
End Sub
End Module
End Namespace
Kompilujemy oba pliki źródłowe:
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
następnie sprawdzamy:
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
...
Prosimy czytelnika o porównanie powyższego wyniku wyświetlanego na ekranie z kodem programu testowego.
9.6. Aplikacja internetowa do aktualizacji tabeli produktów w pamięci podręcznej
9.6.1. Wprowadzenie
Tworzymy obecnie aplikację internetową służącą do aktualizacji tabeli produktów (dodawanie, usuwanie, modyfikowanie). Zaktualizowana tabela będzie przechowywana w pamięci w obiekcie [DataTable] i będzie udostępniona wszystkim użytkownikom. Chcemy zwrócić uwagę na dwie kwestie:
- zarządzanie obiektem [DataTable]
- problemy związane z jednoczesną aktualizacją tabeli przez wielu użytkowników.
Architektura aplikacji MVC będzie wyglądać następująco:
![]() |
9.6.2. Działanie i widoki
Ekran powitalny aplikacji wygląda następująco:

Ten widok, o nazwie [formulaire], umożliwia użytkownikowi zastosowanie kryteriów filtrowania produktów oraz określenie liczby produktów, które mają być wyświetlane na stronie.
nr | nazwa | typ | rola |
LinkButton | wyświetla widok [Formulaire], który służy do ustalenia warunku filtrowania | ||
LinkButton | wyświetla widok [Produits], który służy do przeglądania i aktualizowania tabeli produktów (modyfikacja i usuwanie) | ||
LinkButton | wyświetla widok [Ajout], który służy do dodawania produktu | ||
vueFormulaire | widok [Formulaire] | ||
TextBox | warunek filtrowania | ||
TextBox | liczba produktów na stronie | ||
RequiredFieldValidator | sprawdza, czy w polu [txtPages] znajduje się wartość | ||
RangeValidator | sprawdza, czy txtPages mieści się w przedziale [3,10] | ||
przycisk [submit], który powoduje wyświetlenie widoku [produits] przefiltrowanego według warunku (5) | |||
Etykieta | Tekst informacyjny w przypadku błędów |
Na przykład, jeśli widok [formulaire] jest wypełniony w następujący sposób:

otrzymujemy następujący wynik:
![]() |
nr | nazwa | typ | rola |
panel | |||
RadioButton | pozwala użytkownikowi ustawić żądaną kolejność sortowania po kliknięciu nagłówka jednej z kolumn [nom], [prix]. Oba przyciski należą do grupy [rdTri]. | ||
DataGrid | tabela wyświetlająca przefiltrowany widok tabeli produktów. Filtr jest taki sam, jak ten ustawiony w widoku [formulaire]. Mamy również .AllowPaging=true, .AllowSorting=true | ||
DataGrid | wyświetla całą tabelę produktów – umożliwia śledzenie aktualizacji | ||
Etykieta | tekst informacyjny, zwłaszcza w przypadku błędów | ||
DataGrid | wyświetli usunięte produkty w tabeli produktów | ||
DataGrid | wyświetli zmodyfikowane produkty w tabeli produktów | ||
DataGrid | wyświetli dodane produkty w tabeli produktów |
Na tej stronie znajduje się pięć kontenerów danych. Wszystkie wyświetlają tę samą tabelę [dtProduits] za pośrednictwem różnych widoków [DataView]. Widok reprezentuje podzbiór wierszy z tabeli źródłowej widoku. Podzbiór ten jest tworzony na podstawie właściwości [RowFilter] i [RowStateFilter] klasy [DataView]:
- [RowFilter] umożliwia zastosowanie filtru do wierszy, jak na przykład powyżej w przypadku [prix>30]. Ten rodzaj filtrowania będzie wykorzystywany przez [DataGrid1].
- [RowStateFilter] umożliwia ustawienie filtru w zależności od stanu wiersza w tabeli. Wskaźnik ten określa stan wiersza w odniesieniu do stanu pierwotnego, jaki miał on w momencie utworzenia widoku tabeli. W tym przypadku tabela [dtProduits] pochodzi z bazy danych. Początkowo wszystkie jej wiersze będą miały stan równy [Original], co oznacza, że są to oryginalne wiersze tabeli. Stan ten może się następnie zmieniać i przyjmować różne wartości, z których oto kilka:
- [Added]: wiersz został dodany – nie był częścią pierwotnej tabeli
- [Deleted]: wiersz został usunięty – nadal znajduje się w tabeli, ale jest „oznaczony” jako „do usunięcia”
- [Modified]: wiersz został zmodyfikowany
[RowStateFilter] pozwala wyświetlić wiersze tabeli o określonym statusie:
- (ciąg dalszy)
- [DataViewRowState.Added]: wyświetlane są wyłącznie dodane wiersze. Są one wyświetlane wraz z ich aktualnymi wartościami.
- [DataViewRowState.ModifiedOriginal]: wyświetlane są wyłącznie wiersze zmodyfikowane. Są one wyświetlane wraz z ich pierwotnymi wartościami.
- [DataViewRowState.ModifiedCurrent]: wyświetlane są wyłącznie wiersze zmodyfikowane. Są one wyświetlane wraz z ich aktualnymi wartościami.
- [DataViewRowState.Deleted]: wyświetlane są wyłącznie usunięte wiersze. Są one wyświetlane wraz z ich pierwotnymi wartościami.
- [DataViewRowState.CurrentRows]: wyświetlane są wiersze, które nie zostały usunięte. Są one wyświetlane wraz z ich aktualnymi wartościami.
W ten sposób filtr [RowStateFilter] będzie miał następujące wartości:
brak filtru dotyczącego stanu linii | ||
DataViewRowState.CurrentRows | wyświetla aktualny stan tabeli produktów | |
DataViewRowState.Deleted | wyświetla usunięte wiersze z tabeli produktów | |
DataViewRowState.ModifiedOriginal | wyświetla zmienione wiersze z tabeli produktów wraz z ich pierwotnymi wartościami | |
DataViewRowState.Added | wyświetla wiersze dodane do pierwotnej tabeli produktów |
Kontenery [DataGrid1-5] pozwolą nam śledzić aktualizację tabeli [dtProduits]. Komponent [DataGrid1] umożliwia modyfikację i usunięcie produktu. Zobaczymy, w jaki sposób konfiguracja komponentu pozwala na tę aktualizację. Jest on również podzielony na strony i posortowany. Te dwa aspekty zostały już omówione w poprzednim przykładzie.
Link [Ajout] zapewnia dostęp do formularza dodawania produktu:
![]() |
nr | nazwa | typ | rola |
panel | |||
TextBox | nazwa produktu | ||
RequiredFieldValidator | sprawdza, czy w [txtNom] występuje wartość | ||
TextBox | cena produktu | ||
RequiredFieldValidator | sprawdza, czy w polu [txtPrix] znajduje się wartość | ||
CompareValidator | sprawdza, czy cena >= 0 | ||
Przycisk | przycisk [submit] dodawania produktu | ||
Etykieta | tekst informacyjny dotyczący wyniku operacji dodawania |
Baza danych [produits] może być niedostępna podczas uruchamiania aplikacji. W takim przypadku użytkownikowi wyświetlany jest widok [erreurs]:
![]() |
nr | nazwa | typ | rola |
panel | |||
Repeater | lista błędów |
9.6.3. Konfiguracja kontenerów danych
Pięć kontenerów [DataGrid] zostało skonfigurowanych w ramach [WebMatrix]. Ich styl (kolory i obramowania) został dostosowany za pomocą linku [Configuration automatique] w panelu właściwości. Kontener [DataGrid1] został skonfigurowany za pomocą linku [Générateur de propriétés] w tym samym panelu. Zezwolono na sortowanie (zakładka [Général]):

Kolumny nie są generowane automatycznie, w przeciwieństwie do pozostałych czterech kontenerów. Zostały one zdefiniowane ręcznie za pomocą kreatora:

Najpierw utworzono dwie kolumny typu [Colonne connexe] o nazwach [nom] i [prix]. Zostały one przypisane odpowiednio do pól [nom] i [prix] w źródle danych, które będą wyświetlane przez kontenery. Oto przykładowa konfiguracja kolumny [nom]:

Wyrażenie sortujące to wyrażenie, które należy umieścić za klauzulą [order by] w instrukcji SQL [select], która zostanie wykonana, gdyużytkownik kliknie nazwę kolumny nagłówkowej [nom] powiązanej z polem [nom] w [DataGrid]. W tym miejscu wprowadziliśmy [nom], dzięki czemu klauzula sortowania będzie miała postać [order by nom]. Zobaczymy, że zmodyfikujemy ją tak, aby brzmiała [order by nom asc] lub [order by nom desc], w zależności od kolejności sortowania wybranej przez użytkownika.
Ponadto utworzyliśmy dwie kolumny przycisków:

Kolumna [Modifier, Mettre à jour, Annuler] pozwoli nam edytować produkt, a kolumna [Supprimer] – go usunąć. Każdą z tych kolumn można skonfigurować. Kolumna [Modifier, Mettre à jour, Annuler] oferuje następującą konfigurację:

Widać, że można edytować teksty przycisków. Jeśli chodzi o przyciski, mamy do wyboru linki i przyciski (lista rozwijana powyżej). W tym przypadku wybrano linki. Konfiguracja kolumny [Supprimer] jest analogiczna. Oprócz tego kreatora konfiguracji skorzystaliśmy bezpośrednio z okna właściwości [DataGrid], aby wypełnić właściwość [DataKeyField], która określa, z którym polem źródła danych indeksowane są wiersze [DataGrid]. W tym przypadku wykorzystywany jest klucz główny tabeli produktów:

Ostatecznie ta konfiguracja generuje następujący kod prezentacji:
<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>
Jak zawsze, po zdobyciu pewnego doświadczenia można napisać cały lub część powyższego kodu bezpośrednio.
Pozostałe kontenery [DataGrid] mają domyślną konfigurację uzyskaną poprzez automatyczne wygenerowanie kolumn [DataGrid] na podstawie kolumn źródła danych, z którym są powiązane.
Kontener [Repeater] służy do wyświetlania listy błędów. Jego konfiguracja odbywa się bezpośrednio w kodzie prezentacji:
<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>
Każdy wiersz komponentu wyświetla wartość [Container.DataItem], c.a.d. Odpowiada to wartości z listy danych. Lista ta będzie miała typ [ArrayList] i będzie zawierała listę błędów.
9.6.4. Kod prezentacji aplikacji
Znajduje się on w pliku [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>
Należy zwrócić uwagę na kilka następujących kwestii:
- strona składa się z czterech kontenerów (paneli) [vueFormulaire, vueProduits, vueAjout, vueErreurs], które będą tworzyć cztery widoki aplikacji.
- Przyciski lub linki typu [submit] posiadają właściwość [CausesValidation=false]. Właściwość [causesValidation=true] powoduje wykonanie wszystkich kontroli poprawności na stronie. Jednak w tym przypadku nie wszystkie kontrole poprawności powinny być przeprowadzane jednocześnie. Na przykład podczas dodawania danych nie chcemy, aby przeprowadzane były kontrole dotyczące liczby wierszy na stronie. Dlatego sami określimy, które kontrole poprawności mają zostać przeprowadzone.
9.6.5. Kod kontrolny [global.asax]
Kontroler [global.asax] wygląda następująco:
Powiązany kod [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)
' pobieramy informacje konfiguracyjne
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
' czy występują błędy konfiguracji?
If erreurs.Count <> 0 Then
' odnotowuje się błędy
Application("erreurs") = erreurs
' kończymy
Exit Sub
End If
' tutaj nie ma błędów konfiguracji
' tworzymy obiekt produktów
Dim dtProduits As DataTable
Try
dtProduits = New produits(chaînedeConnexion).getProduits
Catch ex As ExceptionProduits
'wystąpił błąd dostępu do produktów, odnotowuje się to w aplikacji
Application("erreurs") = ex.erreurs
Exit Sub
Catch ex As Exception
' błąd nieobsługiwany
erreurs.Add(ex.Message)
Application("erreurs") = erreurs
' wyjście z podprogramu
End Try
' tutaj nie wystąpiły żadne błędy inicjalizacji
' zapisujemy liczbę produktów na stronie
Application("defaultProduitsPage") = defaultProduitsPage
' zapisywanie tabeli produktów
Application("dtProduits") = dtProduits
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' inicjowanie zmiennych sesji
If IsNothing(Application("erreurs")) Then
' widok tabeli produktów
Session("dvProduits") = CType(Application("dtProduits"), DataTable).DefaultView
' liczba produktów na stronie
Session("nbProduitsPage") = Application("defaultProduitsPage")
' wyświetlana strona bieżąca
Session("pageCourante") = 0
End If
End Sub
End Class
W pliku [Application_Start] najpierw pobieramy dwie informacje z pliku konfiguracyjnego aplikacji [web.config]:
- OLEDBStringConnection: ciąg znaków OLEDB służący do połączenia z bazą produktów
- defaultProduitsPage: domyślna liczba produktów na wyświetlanej stronie
<?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>
Jeśli brakuje jednej z tych dwóch informacji, tworzona jest lista błędów i umieszczana w aplikacji. To samo ma miejsce, jeśli parametr [defaultProduitsPage] istnieje, ale jest nieprawidłowy. Jeśli oba oczekiwane parametry są obecne i poprawne, tworzona jest tabela [dtProduits] i umieszczana w aplikacji. To właśnie ta tabela będzie wykorzystywana i aktualizowana przez różnych klientów. Sama baza danych pozostanie niezmieniona. Kwestią jej aktualizacji zajmiemy się w kolejnej aplikacji. Tabela ta jest tworzona na podstawie instancji klasy [produits], omówionej wcześniej, oraz jej metody [getProduits]. Pobranie tabeli [dtProduits] może się nie powieść. W takim przypadku wiadomo, że klasa [produits] zgłasza wyjątek typu [ExceptionProduits]. Wyjątek ten jest tutaj przechwytywany, a powiązana lista błędów umieszczana jest w aplikacji powiązanej z kluczem [erreurs]. Obecność tego klucza w informacjach zapisanych w aplikacji będzie sprawdzana przy każdym przetwarzaniu żądania. Jeśli zostanie znaleziony, widok [erreurs] zostanie wysłany do klienta.
Jeśli tabela [dtProduits] jest współdzielona przez wszystkich klientów internetowych, każdy z nich będzie jednak miał swój własny widok [dvProduits] tej tabeli. W rzeczywistości każdy klient internetowy ma możliwość ustawienia filtru dla tabeli [dtProduits], a także kolejności sortowania. Te informacje, specyficzne dla każdego klienta internetowego, są przechowywane w widoku klienta. Dlatego też widok ten jest tworzony w tabeli [Session_Start], aby można go było umieścić w sesji właściwej dla każdego klienta. W celu uzyskania domyślnego widoku tabeli wykorzystuje się atrybut [DefaultView] z tabeli [dtProduits]. Początkowo nie ma ani filtra, ani kolejności sortowania. Ponadto do sesji zapisywane są również dwie informacje:
- liczba produktów na stronie o kluczu [nbProduitsPage]. Przy uruchomieniu sesji liczba ta jest równa wartości domyślnej zdefiniowanej w pliku konfiguracyjnym.
- numer bieżącej strony produktów. Początkowo jest to pierwsza strona.
9.6.6. Kod kontrolera [main.aspx.vb]
Szkielet kontrolera wygląda następująco:
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
' komponenty strony
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
' dane strony
Protected dtProduits As DataTable
Protected dvProduits As DataView
Protected defaultProduitsPage As Integer
Protected erreur As Boolean = False
' ładowanie strony
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. Dane instancji
Klasa [main] wykorzystuje następujące dane instancji:
Public Class main
Inherits System.Web.UI.Page
' elementy strony
Protected WithEvents txtPages As System.Web.UI.WebControls.TextBox
...
' dane strony
Protected dtProduits As DataTable
Protected dvProduits As DataView
Protected defaultProduitsPage As Integer
tabela produktów [DataTable] – wspólna dla wszystkich klientów | |
widok produktów [DataView] – specyficzny dla każdego klienta | |
domyślna liczba produktów na stronie |
9.6.8. Procedura [Page_Load] służąca do ładowania strony
Kod procedury [page_Load] jest następujący:
' ładowanie strony
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' sprawdzamy, czy w aplikacji wystąpił błąd
If Not IsNothing(Application("erreurs")) Then
' aplikacja nie zainicjowała się poprawnie
afficheErreurs(CType(Application("erreurs"), ArrayList))
Exit Sub
End If
' pobieramy odwołanie do tabeli produktów
dtProduits = CType(Application("dtProduits"), DataTable)
' pobierany jest widok produktów
dvProduits = CType(Session("dvProduits"), DataView)
' pobierana jest liczba produktów na stronie
defaultProduitsPage = CType(Application("defaultProduitsPage"), Integer)
' anulowanie ewentualnej trwającej aktualizacji
DataGrid1.EditItemIndex = -1
'pierwsze zapytanie
If Not IsPostBack Then
' wyświetla się formularz początkowy
txtPages.Text = defaultProduitsPage.ToString
afficheFormulaire()
End If
End Sub
- Najpierw sprawdzamy, czy tabela produktów została załadowana podczas uruchamiania aplikacji. Jeśli tak nie jest, wyświetlamy widok [erreurs] wraz z odpowiednimi komunikatami o błędach, a następnie kończymy procedurę po ustawieniu zmiennej logicznej [erreur] na wartość „prawda”. Wskaźnik ten będzie sprawdzany przez niektóre procedury.
- Pobieramy tabelę produktów [dtProduits] z aplikacji i umieszczamy ją w zmiennej instancji [dtProduits], aby była ona współdzielona przez wszystkie metody strony.
- Postępujemy analogicznie z widokiem [dvProduits], pobranym z sesji, oraz domyślną liczbą produktów na stronę, pobraną z aplikacji.
- Jeśli jest to pierwsze żądanie klienta, wyświetla się mu formularz do definiowania warunków filtrowania i paginacji z domyślną liczbą produktów na stronie wynoszącą [defaultProduitsPage].
- Anulujemy tryb edycji komponentu [dataGrid1]. Komponent ten posiada atrybut [EditItemIndex], który jest indeksem elementu aktualnie edytowanego. Jeśli [EditItemIndex]=-1, oznacza to, że żaden element nie jest aktualnie edytowany. Jeśli [EditItemIndex]=i, to element nr i komponentu [DataGrid] jest aktualnie edytowany. Jest on wówczas wyświetlany inaczej niż pozostałe elementy komponentu [dataGrid]:

Powyżej element [Produit8, 80] znajduje się w trybie [édition]. Jak widać powyżej, użytkownik może zatwierdzić wprowadzone zmiany za pomocą linku [Mettre à jour], a anulować je za pomocą linku [Annuler]. Może również korzystać z innych linków, które nie mają nic wspólnego z edytowanym elementem. Umieszczając [DataGrid1.EditItemIndex=-1] przy każdym ładowaniu strony, zapewniamy systematyczne anulowanie trybu edycji [DataGrid1]. Inną wartość przypiszemy mu tylko wtedy, gdy kliknięto link [Modifier]. Zrobimy to w procedurze obsługującej to zdarzenie. Mamy następujące sytuacje:
- kliknięto link [Modifier] w elemencie nr 8 z [DataGrid1]. [Page_Load] najpierw ustawia wartość -1 w [DataGrid1.EditItemIndex]. Następnie procedura obsługująca zdarzenie [Modifier] wprowadzi wartość 8 do [DataGrid1.EditItemIndex]. Ostatecznie, gdy strona zostanie zwrócona klientowi, element nr 8 będzie znajdował się w trybie [édition] i będzie wyświetlany tak, jak pokazano powyżej.
- Użytkownik modyfikuje produkt znajdujący się w trybie [édition] i zatwierdza zmianę za pomocą linku [Mettre à jour]. Procedura [Page_Load] zapisuje wartość -1 w [DataGrid1.EditItemIndex]. Następnie zostanie uruchomiona procedura obsługująca zdarzenie [Mettre à jour], a element nr 8 w [dtProduits] zostanie zaktualizowany. Gdy strona zostanie ponownie wysłana do klienta, żaden element z [DataGrid1] nie będzie znajdował się w trybie edycji (DataGrid.EditItemIndex=-1).
- Jeśli użytkownik rozpoczął aktualizację produktu, logiczne byłoby, gdyby zrezygnował z tej aktualizacji za pomocą [Annuler]. Nic jednak nie uniemożliwia mu kliknięcia innego linku. Musimy zatem uwzględnić ten przypadek. W tym przypadku, podobnie jak w poprzednich, [Page_Load] najpierw ustawia wartość -1 w [DataGrid1.EditItemIndex], a następnie uruchamia się procedura obsługująca zaistniałe zdarzenie. Nie zmieni ona właściwości [DataGrid1.EditItemIndex], która pozostanie zatem z wartością -1. Gdy strona zostanie odesłana do klienta, żaden element z [DataGrid1] nie będzie w trybie edycji.
Widać, że ustawiając [EditItemIndex] na -1 podczas ładowania strony, unikamy konieczności martwienia się o to, czy użytkownik był w trybie edycji, gdy kliknął link.
9.6.9. Wyświetlanie widoków [erreurs], [formulaire] i [ajout]
Wyświetlenie widoku [erreurs] jest wymagane podczas ładowania strony [Page_Load], jeśli okaże się, że aplikacja nie zainicjowała się poprawnie. Wyświetlenie to odbywa się poprzez powiązanie komponentu danych [rptErreurs] z listą błędów przekazaną w parametrach oraz poprzez uwidocznienie odpowiedniego kontenera. Na koniec trzy linki [Filtre, Mise à jour, Ajout] są ukrywane, ponieważ w przypadku błędu aplikacja nie może być używana.
Private Sub afficheErreurs(ByVal erreurs As ArrayList)
' powiązanie listy błędów z powtarzaczem rptErreurs
With rptErreurs
.DataSource = erreurs
.DataBind()
End With
'wyłączane są opcje
lnkAjout.Visible = False
lnkMisajour.Visible = False
lnkFiltre.Visible = False
' wyświetla się widok [erreurs]
vueErreurs.Visible = True
vueFormulaire.Visible = False
vueProduits.Visible = False
vueAjout.Visible = False
End Sub
Pozostałe widoki są wywoływane na podstawie opcji proponowanych użytkownikowi:

Wyświetlenie widoku [Ajout] następuje po kliknięciu przez użytkownika linku [Ajout] na stronie:
Private Sub lnkAjout_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkAjout.Click
' wyświetla się widok [ajout]
afficheAjout()
End Sub
Private Sub afficheAjout()
' wyświetla widok dodawania
vueAjout.Visible = True
vueFormulaire.Visible = False
vueProduits.Visible = False
vueErreurs.Visible = False
End Sub
Wyświetlenie widoku [Formulaire] następuje po kliknięciu przez użytkownika linku [Filtrage] na stronie. Należy wówczas wyświetlić widok umożliwiający zdefiniowanie
- filtr dla tabeli produktów
- liczby produktów wyświetlanych na każdej stronie internetowej
Wyświetlany jest formularz zawierający wartości zapisane w sesji:
Private Sub lnkFiltre_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkFiltre.Click
' zdefiniuj warunki filtrowania
txtFiltre.Text = dvProduits.RowFilter
' zdefiniuj paginację
txtPages.Text = CType(Session("nbProduitsPage"), String)
' wyświetla formularz filtrowania
afficheFormulaire()
End Sub
Warunek filtrowania widoku jest zdefiniowany w jego atrybucie [RowFilter]. Przypomnijmy, że przefiltrowany i paginowany widok nosi nazwę [dvProduits] i został pobrany do sesji podczas ładowania strony [Page_Load]. Warunek filtrowania jest zatem pobierany z [dvProduits.RowFilter]. Liczba produktów na stronie jest również pobierana z sesji. Jeśli użytkownik nigdy nie zdefiniował tej informacji, liczba ta jest równa domyślnej liczbie produktów na stronie. Następnie wyświetlany jest formularz umożliwiający zdefiniowanie tych dwóch informacji:
Private Sub afficheFormulaire()
' wyświetla widok [formulaire]
vueFormulaire.Visible = True
vueErreurs.Visible = False
vueProduits.Visible = False
vueAjout.Visible = False
End Sub

9.6.10. Weryfikacja widoku [formulaire]
Kliknięcie powyższego przycisku [Exécuter] jest obsługiwane przez następującą procedurę:
Private Sub btnExécuter_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnExécuter.Click
' czy strona jest poprawna?
rfvLignes.Validate()
rvLignes.Validate()
If Not rfvLignes.IsValid Or Not rvLignes.IsValid Then
afficheFormulaire()
Exit Sub
End If
' dołączanie przefiltrowanych danych do tabeli
Try
dvProduits.RowFilter = txtFiltre.Text.Trim
Catch ex As Exception
lblinfo1.Text = "Erreur de filtrage (" + ex.Message + ")"
afficheFormulaire()
Exit Sub
End Try
' zapisywanie liczby produktów na stronie
Session("nbProduitsPage") = txtPages.Text
'oraz bieżącą stronę
Session("pageCourante") = 0
' wszystko w porządku – wyświetla się dane
afficheProduits(0, CType(txtPages.Text, Integer))
End Sub
Na wstępie należy przypomnieć, że strona zawiera dwa elementy walidacyjne:
nr | nazwa | typ | rola |
RequiredFieldValidator | sprawdza, czy w polu [txtPages] znajduje się wartość | ||
RangeValidator | sprawdza, czy txtPages mieści się w przedziale [3,10] |
Procedura rozpoczyna się od uruchomienia kodu kontrolnego obu powyższych komponentów za pomocą ich metody [Validate], a następnie sprawdza wartość ich atrybutu [IsValid]. Atrybut ten przyjmuje wartość [true] wyłącznie wtedy, gdy sprawdzane dane zostały uznane za prawidłowe. Jeśli testy poprawności jednego z dwóch komponentów zakończyły się niepowodzeniem, ponownie wyświetlany jest widok [formulaire], aby użytkownik mógł skorygować błąd lub błędy. Warunek filtrowania jest stosowany do atrybutu [dvProduits.RowFilter]. W tym przypadku może wystąpić wyjątek, jeśli użytkownik wprowadził nieprawidłowe kryterium filtrowania, jak pokazano poniżej:

W takim przypadku ponownie wyświetlany jest widok [formulaire]. Jeśli obie wprowadzone informacje są poprawne, do sesji zapisywane są dwie wartości:
- liczba produktów na stronie wybrana przez użytkownika
- numer strony, która zostanie wyświetlona – początkowo strona 0
Te dwie informacje są wykorzystywane za każdym razem, gdy wyświetlany jest widok [produits].
9.6.11. Wyświetlanie widoku [produits]
Widok [produits] wygląda następująco:

Mamy 5 komponentów [DataGrid], z których każdy przedstawia konkretny widok tabeli produktów [dtProduits]. Zaczynając od lewej:
nazwa | rola |
tabela wyświetlająca przefiltrowany widok tabeli produktów. Filtr jest taki sam, jak ten ustawiony w widoku [formulaire]. Mamy również .AllowPaging=true, .AllowSorting=true | |
wyświetla całą tabelę produktów – umożliwia śledzenie aktualizacji | |
wyświetli produkty usunięte z tabeli produktów | |
wyświetli produkty, które zostały zmodyfikowane w tabeli produktów | |
wyświetli produkty dodane w tabeli produktów |
Procedura [afficheProduits] służy do wyświetlenia poprzedniego widoku:
Private Sub afficheProduits(ByVal page As Integer, ByVal taillePage As Integer)
' dane są dołączane do obu komponentów [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()
' wyświetlany jest widok [produits]
vueProduits.Visible = True
vueFormulaire.Visible = False
vueErreurs.Visible = False
vueAjout.Visible = False
' zapisujemy bieżącą stronę
Session("pageCourante") = page
End Sub
Procedura przyjmuje dwa parametry:
- numer strony [page], która ma zostać wyświetlona w [DataGrid1]
- liczba produktów [taillePage] na stronie
Powiązanie [DataGrid] z tabelą produktów odbywa się za pośrednictwem 5 różnych widoków.
- [DataGrid1] jest powiązana z widokiem paginowanym i posortowanym [dvProduits].
With DataGrid1
.DataSource = dvProduits
.PageSize = taillePage
.CurrentPageIndex = page
.DataBind()
End With
To właśnie podczas łączenia tabeli [DataGrid1] z jej źródłem danych potrzebne są dwie informacje przekazywane jako parametry do procedury.
- [DataGrid2] jest powiązany z widokiem wyświetlającym wszystkie aktualne elementy tabeli [dtProduits]. Podobnie jak [DataGrid1], przedstawia on zaktualizowany widok tabeli [dtProduits], ale bez paginacji i sortowania.
Dim dvCurrent As New DataView(dtProduits)
dvCurrent.RowStateFilter = DataViewRowState.CurrentRows
With DataGrid2
.DataSource = dvCurrent
.DataBind()
End With
W tym przypadku używamy atrybutu [RowStateFilter] z klasy [DataView]. Każdy wiersz tabeli lub, w tym przypadku, widoku posiada atrybut [RowState] wskazujący stan tego wiersza. Oto kilka przykładów:
- (ciąg dalszy)
- [Added]: wiersz został dodany
- [Modified]: wiersz został zmodyfikowany
- [Deleted]: wiersz został usunięty
- [Unchanged]: wiersz nie uległ zmianie
Kiedy tabela [dtProduits] została początkowo utworzona w [Application_Start], wszystkie jej wiersze znajdowały się w stanie [Unchanged]. Stan ten będzie się zmieniał w miarę aktualizacji dokonywanych przez klientów internetowych:
- (ciąg dalszy)
- gdy klient tworzy nowy produkt, do tabeli [dtProduits] dodawany jest wiersz. Będzie on znajdował się w stanie [Added].
- gdy produkt zostanie zmodyfikowany, jego stan zmienia się z [Unchanged] na [Modified].
- Gdy produkt zostanie usunięty, wiersz nie jest fizycznie usuwany. Zostaje on natomiast oznaczony jako przeznaczony do usunięcia, a jego stan zmienia się na [Deleted]. Usunięcie to można cofnąć.
Atrybut [RowStateFilter] klasy [DataView] umożliwia filtrowanie widoku według stanu [RowState] jego wierszy. Jego możliwe wartości to wartości z wyliczenia [DataRowViewState]:
- (ciąg dalszy)
- DataRowViewState.CurrentRows: wiersze, które nie zostały usunięte, są wyświetlane wraz z ich aktualnymi wartościami
- DataRowViewState.Added: dodane wiersze są wyświetlane wraz z ich aktualnymi wartościami
- DataRowViewState.Deleted: usunięte wiersze są wyświetlane wraz z ich pierwotnymi wartościami
- DataRowViewState.ModifiedOriginal: zmienione wiersze są wyświetlane wraz z ich pierwotnymi wartościami
- DataRowViewState.ModifiedCurrent: zmodyfikowane wiersze są wyświetlane wraz z ich aktualnymi wartościami
Zmodyfikowany wiersz ma dwie wartości: wartość pierwotną, czyli tę, którą wiersz miał przed pierwszą modyfikacją, oraz wartość aktualną, uzyskaną po jednej lub kilku modyfikacjach. Obie te wartości są przechowywane jednocześnie.
Wiersze od 2 do 5 w tabeli DataGrid przedstawiają widok tabeli [dtProduits] przefiltrowany według atrybutu [RowStateFilter]
DataGrid | Filtr |
RowStateFilter= DataRowViewState.CurrentRows | |
RowStateFilter= DataRowViewState.Deleted | |
RowStateFilter = DataRowViewState.ModifiedOriginal | |
RowStateFilter= DataRowViewState.Added |
Tabela [dtPoduits] jest aktualizowana jednocześnie przez różnych klientów internetowych, co może prowadzić do konfliktów dostępu do tabeli. Gdy klient wyświetla swoje widoki tabeli [dtProduits], należy unikać sytuacji, w której robi to w momencie, gdy tabela znajduje się w stanie niestabilnym, ponieważ jest właśnie modyfikowana przez innego klienta internetowego. Dlatego za każdym razem, gdy klient potrzebuje tabeli [dtProduits] do odczytu, jak powyżej, lub do zapisu w celu dodania, modyfikacji lub usunięcia produktów, zsynchronizuje się z innymi klientami za pomocą następującej sekwencji:
W kodzie aplikacji może występować kilka krytycznych sekcji:
Mechanizm działania jest następujący:
- jeden lub więcej klientów dociera do instrukcji [Application.Lock] w sekcji krytycznej 1. Jest to dystrybutor tokenu z unikalnym wejściem. Tylko jeden klient otrzymuje ten token. Nazwijmy go C1.
- Dopóki klient C1 nie zwróci tokenu za pośrednictwem [Application.Unlock], żaden inny klient nie ma uprawnień do wejścia do sekcji krytycznej kontrolowanej przez [Application.Lock]. W powyższym przykładzie żaden klient nie może zatem wejść do sekcji krytycznych 1 i 2.
- Klient C1 uruchamia proces [Application.Unlock] i w ten sposób zwraca token dostępu. Token ten może następnie zostać przekazany innemu klientowi. Kroki od 1 do 3 są powtarzane.
Dzięki temu mechanizmowi zapewnimy, że tylko jeden klient ma dostęp do tabeli [dtProduits], zarówno w trybie odczytu, jak i zapisu.
Link [Mise à jour] wyświetla również widok [Produits]:

Powiązana procedura jest następująca:
Private Sub lnkMisajour_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkMisajour.Click
' wyświetlanie widoku produktów
afficheProduits(CType(Session("pageCourante"), Integer), CType(Session("nbProduitsPage"), Integer))
End Sub
Należy wyświetlić widok [produits]. Odbywa się to za pomocą procedury [afficheProduits], która przyjmuje dwa parametry: numer bieżącej strony do wyświetlenia oraz liczbę produktów na stronie. Obie te informacje znajdują się w sesji, ewentualnie wraz z wartościami domyślnymi, jeśli użytkownik nigdy ich sam nie zdefiniował.
9.6.12. Paginacja i sortowanie w procedurze [DataGrid1]
W innym przykładzie zapoznaliśmy się już z procedurami zarządzającymi paginacją i sortowaniem w procedurze [DataGrid]. W przypadku zmiany strony wyświetlamy widok [produits], przekazując numer nowej strony jako parametr do procedury [afficheProduits].
Private Sub DataGrid1_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles DataGrid1.PageIndexChanged
' zmiana strony
afficheProduits(e.NewPageIndex, DataGrid1.PageSize)
End Sub
Procedura [DataGrid1_SortCommand] jest uruchamiana, gdy użytkownik kliknie nagłówek jednej z kolumn w widoku [DataGrid1]. Należy wówczas przypisać wyrażenie sortujące do atrybutu [Sort] widoku [dvProduits] wyświetlanego przez [DataGrid1]. Wyrażenie to jest pod względem składniowym równoważne wyrażeniu sortującemu umieszczonemu za klauzulą [order by] w instrukcji SQL SELECT. Argument [e] procedury posiada atrybut [SortExpression], który podaje wyrażenie sortujące powiązane z kolumną, której nagłówek został kliknięty. Wyrażenie to zostało zdefiniowane podczas tworzenia komponentu [DataGrid1]. Poniżej przedstawiono wyrażenie zdefiniowane dla kolumny [nom] w komponencie [DataGrid1]:

Jeśli wyrażenie sortowania nie zostało zdefiniowane podczas projektowania komponentu [DataGrid], wykorzystywana jest nazwa pola danych powiązanego z kolumną komponentu [DataGrid]. Kierunek sortowania (rosnąco, malejąco) jest tutaj ustalany przez użytkownika za pomocą przycisków opcji [rdCroissant, rd Décroissant]:

Procedura sortowania wygląda następująco:
Private Sub DataGrid1_SortCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridSortCommandEventArgs) Handles DataGrid1.SortCommand
' sortowanie widoku danych
With dvProduits
.Sort = e.SortExpression + " " + CType(IIf(rdCroissant.Checked, "asc", "desc"), String)
End With
' wyświetlanie
afficheProduits(0, DataGrid1.PageSize)
End Sub
9.6.13. Usunięcie produktu
Aby usunąć produkt, użytkownik korzysta z linku [Supprimer] znajdującego się w wierszu produktu:

Następnie uruchamiana jest procedura [DataGrid1_DeleteCommand]:
Private Sub DataGrid1_DeleteCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.DeleteCommand
' usunięcie produktu
' klucz produktu do usunięcia
Dim idProduit As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)
' usunięcie produktu
Dim erreur As Boolean = False
Try
supprimerProduit(idProduit)
Catch ex As Exception
' problem
lblInfo2.Text = ex.Message
erreur = True
End Try
' zmiana strony?
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
' wyświetlanie produktów
afficheProduits(page, DataGrid1.PageSize)
End Sub
Aktualizacja produktów nastąpi za pomocą klucza produktu [id]. W kolumnie [id] tabeli [dtProduits] znajduje się klucz główny, dzięki czemu w tabeli [dtProduits] możemy odnaleźć wiersz produktu, który jest aktualizowany w komponencie [DataGrid1]. Procedura rozpoczyna się zatem od pobrania klucza produktu, którego link [Supprimer] został kliknięty:
' klucz produktu do usunięcia
Dim idProduit As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)
Wiemy, że [e.Item] jest elementem [DataGrid1], który wywołał zdarzenie. Ogólnie rzecz biorąc, element ten stanowi wiersz w [DataGrid]. [e.Item.ItemIndex] to numer wiersza, który wywołał zdarzenie. Indeks ten odnosi się do aktualnie wyświetlanej strony. Tak więc pierwszy wiersz wyświetlanej strony ma właściwość [ItemIndex=0], nawet jeśli w tabeli produktów ma numer 17. [DataGrid1.DataKeys] to lista kluczy z [DataGrid]. Ponieważ podczas projektowania wpisaliśmy [DataGrid1.DataKey=id], klucze [DataGrid1] składają się z wartości kolumny [id] w tabeli [dtProduits], która jest jednocześnie kluczem głównym. Zatem [DataGrid1.DataKeys(e.Item.ItemIndex)] jest kluczem [id] produktu, który ma zostać usunięty. Po uzyskaniu tej informacji żądamy jego usunięcia za pomocą procedury [supprimerProduit]:
' usunięcie produktu
Dim erreur As Boolean = False
Try
supprimerProduit(idProduit)
Catch ex As Exception
' pb
lblInfo2.Text = ex.Message
erreur = True
End Try
W niektórych przypadkach usunięcie może się nie powieść. Zobaczymy, dlaczego. Wówczas generowany jest wyjątek. Jest on tutaj obsługiwany. W przypadku wystąpienia błędu kod [DataGrid1] nie musi ulec zmianie. Do widoku [produits] dodawany jest jedynie komunikat o błędzie. Jeśli nie wystąpił błąd, a użytkownik właśnie usunął jedyny produkt z bieżącej strony, wyświetlana jest poprzednia strona.
' zmiana strony?
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
W każdym przypadku widok [produits] jest ponownie wyświetlany za pomocą procedury [afficheProduits]:
Procedura [supprimerProduit] wygląda następująco:
Private Sub supprimerProduit(ByVal idProduit As Integer)
Dim erreur As String
Try
' synchronizacja
Application.Lock()
' wyszukiwanie wiersza do usunięcia
Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
If ligne Is Nothing Then
erreur = String.Format("Produit [{0}] inexistant", idProduit)
Else
' usunięcie wiersza
ligne.Delete()
End If
Catch ex As Exception
erreur = String.Format("Erreur de suppression : {0}", ex.Message)
Finally
' koniec synchronizacji
Application.UnLock()
End Try
' w przypadku błędu zgłaszany jest wyjątek
If erreur <> String.Empty Then Throw New Exception(erreur)
End Sub
Procedura przyjmuje jako parametr klucz produktu, który ma zostać usunięty. Gdyby aktualizacje miał przeprowadzać tylko jeden klient, usunięcie to mogłoby przebiegać w następujący sposób:
W przypadku, gdy aktualizacje są przeprowadzane jednocześnie przez wielu klientów, sprawa jest bardziej skomplikowana. Rozważmy bowiem następującą sekwencję zdarzeń:
![]() |
Czas | Czynność |
T1 | klient A odczytuje tabelę [dtProduits] – znajduje się tam produkt o kluczu 20 |
T2 | klient B odczytuje tabelę [dtProduits] – znajduje się tam produkt o kluczu 20 |
T3 | klient A usuwa produkt o kluczu 20 |
T4 | klient B usuwa produkt o kluczu 20 |
Gdy klienci A i B odczytują tabelę produktów i wyświetlają jej zawartość na stronie internetowej, tabela zawiera produkt o kluczu 20. Mogą więc chcieć wykonać jakąś operację na tym produkcie, na przykład go usunąć. Któryś z nich z pewnością zrobi to jako pierwszy. Ten, który pojawi się później, będzie chciał usunąć produkt, który już nie istnieje. Procedura [supprimerProduits] uwzględnia ten przypadek na kilka sposobów:
- po pierwsze następuje synchronizacja z procedurą [Application.Lock]. Oznacza to, że gdy jeden klient przekroczy tę barierę, żaden inny nie może modyfikować ani odczytywać tabeli produktów. Wszystkie te operacje są bowiem zsynchronizowane w ten sposób. Widzieliśmy to już w przypadku odczytu.
- Następnie sprawdzamy, czy produkt, który chcemy usunąć, istnieje. Jeśli tak, jest on usuwany, w przeciwnym razie generowany jest komunikat o błędzie.
' wyszukiwanie wiersza do usunięcia
Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
If ligne Is Nothing Then
erreur = String.Format("Produit [{0}] inexistant", idProduit)
Else
' usunięcie wiersza
ligne.Delete()
End If
- Wychodzimy z sekwencji krytycznej za pomocą [Application.Unlock], aby umożliwić innym klientom przeprowadzenie aktualizacji.
- Jeśli nie udało się usunąć wiersza, procedura zgłasza wyjątek powiązany z komunikatem o błędzie.
Spójrzmy na przykład. Uruchamiamy klienta [Mozilla] i natychmiast wybieramy opcję [Mise à jour] (widok częściowy):

To samo robimy z klientem [Internet Explorer] (widok częściowy):

W przypadku klienta [Mozilla] usuwamy produkt [produit2]. Otrzymujemy następującą nową stronę:

Operacja usunięcia przebiegła pomyślnie. Usunięty produkt pojawia się w sekcji [DataGrid] produktów usuniętych i nie pojawia się już na liście produktów komponentów [DataGrid1] oraz [DataGrid2], które wyświetlają produkty aktualnie znajdujące się w tabeli. Zróbmy to samo z [Interbet Explorer]. Usuwamy element [produit2]:

Otrzymana odpowiedź wygląda następująco:

Komunikat o błędzie informuje użytkownika, że produkt o kluczu [2] nie istnieje. Odpowiedź zwraca klientowi nowy widok odzwierciedlający aktualny stan tabeli. Widać, że produkt o kluczu [2] rzeczywiście znajduje się na liście usuniętych produktów. Widok [produits] odzwierciedla zatem aktualizacje wszystkich klientów, a nie tylko jednego.
9.6.14. Dodawanie produktu
Aby dodać produkt, użytkownik korzysta z linku [Ajout] w opcjach. Link ten po prostu wyświetla widok [Ajout]:


Dwie procedury związane z tą czynnością to:
Private Sub lnkAjout_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkAjout.Click
' wyświetla się widok [ajout]
afficheAjout()
End Sub
Private Sub afficheAjout()
' wyświetla widok dodawania
vueAjout.Visible = True
vueFormulaire.Visible = False
vueProduits.Visible = False
vueErreurs.Visible = False
End Sub
Przypomnijmy, że widok [Ajout] zawiera komponenty walidacyjne:
nazwa | typ | rola |
RequiredFieldValidator | sprawdza, czy w polu [txtNom] znajduje się wartość | |
RequiredFieldValidator | sprawdza, czy w polu [txtPrix] znajduje się wartość | |
CompareValidator | sprawdza, czy cena >= 0 |
Procedura obsługi kliknięcia przycisku [Ajouter] wygląda następująco:
Private Sub btnAjouter_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnAjouter.Click
' dodanie nowego elementu do tabeli produktów
' najpierw dane muszą być prawidłowe
rfvNom.Validate()
rfvPrix.Validate()
cvPrix.Validate()
If Not rfvNom.IsValid Or Not rfvPrix.IsValid Or Not cvPrix.IsValid Then
' ponownie formularz wprowadzania danych
afficheAjout()
Exit Sub
End If
' tworzymy wiersz
Dim produit As DataRow = dtProduits.NewRow
produit("nom") = txtNom.Text.Trim
produit("prix") = txtPrix.Text.Trim
' dodajemy wiersz do tabeli
Application.Lock()
Try
dtProduits.Rows.Add(produit)
lblInfo3.Text = "Ajout réussi"
' oczyszczanie
txtNom.Text = ""
txtPrix.Text = ""
Catch ex As Exception
lblInfo3.Text = String.Format("Erreur : {0}", ex.Message)
End Try
Application.UnLock()
End Sub
Najpierw przeprowadzane są kontrole poprawności trzech komponentów [rfvNom, rfvPrix, cvPrix]. Jeśli któraś z kontroli zakończy się niepowodzeniem, ponownie wyświetlany jest widok [Ajout] wraz z komunikatami o błędach dotyczących komponentów walidacyjnych. Oto przykład:

Jeśli dane są prawidłowe, przygotowuje się wiersz do wstawienia do tabeli [dtProduits].
' tworzymy wiersz
Dim produit As DataRow = dtProduits.NewRow
produit("nom") = txtNom.Text.Trim
produit("prix") = txtPrix.Text.Trim
Najpierw tworzymy nowy wiersz w tabeli [dtProduits] za pomocą metody [DataTable.NewRow]. Wiersz ten będzie zawierał trzy kolumny [id, nom, prix] z tabeli [dtProduits]. Kolumny [nom, prix] są wypełniane wartościami wprowadzonymi w formularzu dodawania. Kolumna [id] nie jest wypełniana. Kolumna ta ma typ [AutoIncrement], c.a.d, a SGBD będzie kluczem dla nowego produktu – jest to największy istniejący klucz powiększony o 1. Utworzony tutaj wiersz o kluczu [produit] jest oddzielony od tabeli [dtProduits]. Teraz musimy go wstawić do tej tabeli. Ponieważ zamierzamy zaktualizować tabelę [dtProduits], włączamy synchronizację między klientami:
Po wykonaniu tej czynności próbujemy dodać wiersz do tabeli [dtProduits]. Jeśli operacja się powiedzie, wyświetlamy komunikat o pomyślnym zakończeniu, w przeciwnym razie komunikat o błędzie.
Try
dtProduits.Rows.Add(produit)
lblInfo3.Text = "Ajout réussi"
' oczyszczanie
txtNom.Text = ""
txtPrix.Text = ""
Catch ex As Exception
lblInfo3.Text = String.Format("Erreur : {0}", ex.Message)
End Try
Zazwyczaj nie ma możliwości wystąpienia wyjątku. Jednakże, jako środek ostrożności, wdrożono mechanizm obsługi wyjątków. Po dodaniu danych opuszczamy sekcję krytyczną za pomocą [Application.Unlock].
9.6.15. Modyfikacja produktu
Aby zmodyfikować produkt, użytkownik korzysta z linku [Modifier] znajdującego się w wierszu produktu:

Następnie przechodzi się do trybu edycji:

Dzięki komponentowi [DataGrid] wynik ten uzyskuje się przy użyciu bardzo niewielkiej ilości kodu:
Private Sub DataGrid1_EditCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.EditCommand
' przełączamy bieżący element w tryb edycji
DataGrid1.EditItemIndex = e.Item.ItemIndex
' ponowne wyświetlenie produktów
afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
End Sub
Kliknięcie linku [Modifier] powoduje uruchomienie procedury [DataGrid1_EditCommand] po stronie serwera. Komponent [DataGrid1] posiada pole o indeksie [EditItemIndex]. Wiersz o indeksie równym wartości [EditItemIndex] przechodzi w tryb edycji. Każdą wartość w tak zaktualizowanym wierszu można zmodyfikować w polu wprowadzania danych, jak pokazano na powyższym zrzucie ekranu. Pobieramy zatem indeks produktu, na którym kliknięto link [Modifier], i przypisujemy go do atrybutu [EditItemIndex] komponentu [DataGrid1]. Pozostaje nam tylko ponownie wyświetlić widok [produits]. Będzie on wyglądał tak samo jak poprzednio, ale z jednym wierszem w trybie edycji.
Link [Annuler] dla edytowanego produktu pozwala użytkownikowi zrezygnować z aktualizacji. Procedura związana z tym linkiem jest następująca:
Private Sub DataGrid1_CancelCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.CancelCommand
' ponownie wyświetla produkty
afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
End Sub
Powoduje to po prostu ponowne wyświetlenie widoku [produits]. Można by ulec pokusie, aby wpisać -1 w [DataGrid1.EditItemIndex] w celu anulowania trybu aktualizacji. W rzeczywistości wiemy, że procedura [Page_Load] robi to automatycznie. Nie ma więc potrzeby powtarzania tej czynności.
Zmiana jest zatwierdzana za pomocą linku [Mettre à jour] w edytowanym wierszu. Następnie wykonywana jest następująca procedura:
Private Sub DataGrid1_UpdateCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.UpdateCommand
' klucz produktu do modyfikacji
Dim idProduit As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)
' zmienione elementy
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
' czy zmiany są prawidłowe?
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
' w przypadku błędu ponownie wyświetla się strona aktualizacji
If lblInfo2.Text <> String.Empty Then
' przywracamy wiersz do trybu aktualizacji
DataGrid1.EditItemIndex = e.Item.ItemIndex
' wyświetlanie produktów
afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
' koniec
Exit Sub
End If
' w przypadku braku błędu – modyfikujemy tabelę
Try
modifierProduit(idProduit, e.Item)
Catch ex As Exception
' problem
lblInfo2.Text = ex.Message
End Try
' wyświetlanie produktów
afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
End Sub
Podobnie jak w przypadku usuwania, musimy pobrać klucz produktu, który ma zostać zmodyfikowany, aby odnaleźć jego wiersz w tabeli produktów [dtProduits]:
' klucz produktu do modyfikacji
Dim idProduit As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)
Następnie musimy pobrać nowe wartości, które należy przypisać do wiersza. Znajdują się one w komponencie [DataGrid1]. Pomocny w tym będzie argument procedury o nazwie [e]. [e.Item] reprezentuje wiersz [DataGrid1], który jest źródłem zdarzenia. Jest to zatem wiersz aktualizowany w danej chwili, ponieważ powiązanie [Mettre à jour] istnieje wyłącznie w tym wierszu. Wiersz ten zawiera kolumny oznaczone przez zbiór [Cells] tego wiersza. Zatem [e.Item.Cells(0)] reprezentuje kolumnę 0 aktualizowanego wiersza. Wiemy, że nowe wartości znajdują się w polach wprowadzania danych. Zbiór [e.Item.Cells(i).Controls] reprezentuje zbiór elementów sterujących kolumny i w wierszu [e.Item]. Dwie poniższe instrukcje pobierają wartości z pól wprowadzania danych zaktualizowanego wiersza do [DataGrid1]:
' zmienione elementy
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
Mamy zatem nowe wartości zmodyfikowanego wiersza w postaci ciągów znaków. Następnie sprawdzamy, czy dane te są prawidłowe. Nazwa nie może być pusta, a cena musi być liczbą dodatnią lub zerem:
' czy zmiany są prawidłowe?
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
W przypadku błędu etykieta [lblInfo2] wyświetli komunikat o błędzie, a strona zostanie po prostu ponownie wyświetlona:
' w przypadku błędu ponownie wyświetla się strona aktualizacji
If lblInfo2.Text <> String.Empty Then
' przywracamy wiersz do trybu aktualizacji
DataGrid1.EditItemIndex = e.Item.ItemIndex
' wyświetlanie produktów
afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
' koniec
Exit Sub
End If
Powyższy kod nie pokazuje, że wprowadzone wartości zostają utracone. [DataGrid1] jest bowiem powiązany z danymi z tabeli [dtProduits], która zawiera pierwotne wartości zmodyfikowanego wiersza. Oto przykład.

Otrzymana odpowiedź wygląda następująco:

Widać, że utracono pierwotnie wprowadzone wartości. W profesjonalnej aplikacji byłoby to prawdopodobnie nie do przyjęcia. W tym przypadku napotykamy pewne ograniczenia standardowego trybu aktualizacji komponentu [DataGrid]. Lepiej byłoby dysponować widokiem [Modification] analogicznym do widoku [Ajout].
Jeśli dane są prawidłowe, są one wykorzystywane do aktualizacji tabeli [dtProduits]:
' w przypadku braku błędu – modyfikuje się tabelę
Try
modifierProduit(idProduit, e.Item)
Catch ex As Exception
' problem
lblInfo2.Text = ex.Message
End Try
' wyświetlanie produktów
afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
Z tego samego powodu, o którym mowa przy usuwaniu produktu, modyfikacja może się nie powieść. W rzeczywistości w okresie między momentem, w którym klient odczytał tabelę [dtProduits], a momentem, w którym zamierza zmodyfikować jeden ze swoich produktów, produkt ten mógł zostać usunięty przez innego klienta. Procedura [modifierProduit] obsługuje ten przypadek, generując wyjątek. Wyjątek ten jest tutaj obsługiwany. Po pomyślnym lub nieudanym zaktualizowaniu aplikacja zwraca klientowi widok [produits]. Pozostaje nam sprawdzić, w jaki sposób procedura [modifierProduit] realizuje aktualizację:
Private Sub modifierProduit(ByVal idProduit As Integer, ByVal item As DataGridItem)
Dim erreur As String
Try
' synchronizacja
Application.Lock()
' wyszukiwanie wiersza do modyfikacji
Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
If ligne Is Nothing Then
erreur = String.Format("Produit [{0}] inexistant", idProduit)
Else
' modyfikujemy wiersz
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
' koniec synchronizacji
Application.UnLock()
End Try
' w przypadku błędu zgłaszany jest wyjątek
If erreur <> String.Empty Then Throw New Exception(erreur)
End Sub
Nie będziemy wchodzić w szczegóły tej procedury, której kod jest analogiczny do kodu procedury [supprimerProduit], która została już szczegółowo omówiona. Przyjrzyjmy się po prostu dwóm przykładom. Najpierw zmienimy cenę produktu [produit1]:

Użycie powyższego linku [Mettre à jour] powoduje wyświetlenie następującej odpowiedzi:

Zmiana jest widoczna w komponentach [DataGrid] 1 i 2, które odzwierciedlają aktualny stan tabeli [dtProduits]. Jest ona również widoczna w komponencie [DataGrid4], który stanowi widok zmodyfikowanych wierszy, przy czym wiersze te są wyświetlane wraz z ich pierwotnymi wartościami. Przyjrzyjmy się teraz przypadkowi konfliktu dostępu. Podobnie jak w przykładzie usunięcia, wykorzystamy dwóch różnych klientów internetowych. Klient [Mozilla] odczytuje tabelę [dtProduits] i przechodzi do trybu edycji produktu [produit1]:

Klient o numerze [Internet Explorer] zamierza usunąć produkt o numerze [produit1]:

Klient [Internet Explorer] usuwa produkt [produit1]:

Warto zauważyć, że [produit1] nie znajduje się już w tabeli zmodyfikowanych wierszy, lecz w tabeli usuniętych wierszy. Klient [Mozilla] zatwierdza swoją aktualizację:

Klient [Mozilla] otrzymuje następującą odpowiedź na swoją aktualizację:

Może on stwierdzić, że [produit1] został usunięty, ponieważ znajduje się na liście usuniętych produktów.
9.7. Aplikacja internetowa do aktualizacji fizycznej tabeli produktów
9.7.1. Proponowane rozwiązania
Poprzednia aplikacja była raczej przykładem pokazującym zarządzanie obiektem [DataTable] w pamięci podręcznej niż typowym przypadkiem. W pewnym momencie konieczne jest bowiem zaktualizowanie rzeczywistego źródła danych. Można wybrać dwie różne strategie:
- wykorzystuje się pamięć podręczną [dtProduits] w pamięci operacyjnej do aktualizacji źródła danych. Można przewidzieć stronę umieszczoną w drzewie struktury sieciowej poprzedniej aplikacji, aby uzyskać dostęp do jej pamięci podręcznej [dtProduits]. Strona ta umożliwiłaby administratorowi przeniesienie zmian zawartych w pamięci podręcznej [dtProduits] na fizyczne źródło danych. W tym celu można by dodać nową metodę do klasy dostępu [produits], która przyjmowałaby jako parametr pamięć podręczną [dtProduits] i za pomocą tej pamięci aktualizowałaby fizyczne źródło danych.
- Fizyczne źródło danych jest aktualizowane jednocześnie z pamięcią podręczną.
Strategia nr 1 pozwala na otwarcie tylko jednego połączenia z fizycznym źródłem danych. Strategia nr 2 wymaga nawiązania połączenia przy każdej aktualizacji. W zależności od dostępności połączeń można preferować jedną lub drugą strategię. Ponieważ dysponujemy narzędziami do jej wdrożenia (klasa [produits]), wybieramy strategię nr 2.
9.7.2. Rozwiązanie 1
Ze względu na zachowanie spójności z poprzednią aplikacją wybieramy następującą strategię:
- źródło fizyczne jest aktualizowane jednocześnie z pamięcią podręczną
- pamięć podręczna jest tworzona tylko raz, podczas inicjalizacji aplikacji w klasie [global.asax.vb]. Oznacza to, że jeśli fizyczne źródło danych jest aktualizowane przez klientów innych niż klienci internetowi, ci ostatni nie widzą tych zmian. Widzą jedynie te zmiany, które sami wprowadzają do tabeli w pamięci podręcznej.
Aby zaktualizować fizyczne źródło danych, potrzebujemy instancji klasy dostępu do produktów. Każdy klient mógłby mieć swoją własną instancję. Można również współdzielić jedną instancję, która byłaby tworzona przez aplikację podczas jej uruchamiania. To właśnie to rozwiązanie wybieramy w tym przypadku. Kod kontrolny [global.asax.vb] zostaje zmodyfikowany w następujący sposób:
Imports System
Imports System.Web
...
Public Class Global
Inherits System.Web.HttpApplication
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
' pobieranie informacji konfiguracyjnych
Dim chaînedeConnexion As String = ConfigurationSettings.AppSettings("OLEDBStringConnection")
Dim defaultProduitsPage As String = ConfigurationSettings.AppSettings("defaultProduitsPage")
Dim erreurs As New ArrayList
...
' tutaj nie ma błędów konfiguracji
' tworzy się obiekt produktów
Dim objProduits As New produits(chaînedeConnexion)
Dim dtProduits As DataTable
Try
dtProduits = objProduits.getProduits
Catch ex As ExceptionProduits
'wystąpił błąd dostępu do produktów, odnotowuje się to w aplikacji
Application("erreurs") = ex.erreurs
Exit Sub
Catch ex As Exception
' błąd nieobsługiwany
erreurs.Add(ex.Message)
Application("erreurs") = erreurs
' wyjście z podprogramu
End Try
' tutaj nie wystąpiły żadne błędy inicjalizacji
...
' zapisujemy instancję dostępu do danych
Application("objProduits") = objProduits
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
...
End Sub
End Class
W aplikacji zapisano instancję klasy dostępu do danych, powiązaną z kluczem [objProduits]. Każdy klient będzie korzystał z tej instancji w celu uzyskania dostępu do fizycznego źródła danych. Zostanie ona pobrana w procedurze [Page_Load] z [main.aspx.vb]:
Protected objProduits As produits
....
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
' pobieramy instancję dostępu do produktów
objProduits = CType(Application("objProduits"), produits)
...
End Sub
Instancja dostępu do danych [objProduits] jest dostępna dla wszystkich metod na stronie. Będzie ona wykorzystywana do trzech operacji aktualizacji: dodawania, usuwania i modyfikacji.
Procedura dodawania została zmodyfikowana w następujący sposób:
Private Sub btnAjouter_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnAjouter.Click
...
' dodajemy wiersz
Application.Lock()
Try
' dodajemy wiersz do tabeli w pamięci podręcznej
dtProduits.Rows.Add(produit)
' dodaje się wiersz do źródła fizycznego
Dim nouveauProduit As sProduit
With nouveauProduit
.nom = CType(produit("nom"), String)
.prix = CType(produit("prix"), Double)
End With
objProduits.ajouterProduit(nouveauProduit)
' monitorowanie
lblInfo3.Text = "Ajout réussi"
' oczyszczanie
txtNom.Text = ""
txtPrix.Text = ""
Catch ex As Exception
' błąd
lblInfo3.Text = String.Format("Erreur : {0}", ex.Message)
End Try
Application.UnLock()
End Sub
Procedura modyfikacji zostaje zmieniona w następujący sposób:
Private Sub modifierProduit(ByVal idProduit As Integer, ByVal item As DataGridItem)
Dim erreur As String
Try
' synchronizacja
Application.Lock()
' wyszukiwanie wiersza do zmiany
Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
If ligne Is Nothing Then
erreur = String.Format("Produit [{0}] inexistant", idProduit)
Else
' modyfikacja wiersza w pamięci podręcznej
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
' zmiana wiersza w źródle fizycznym
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
' koniec synchronizacji
Application.UnLock()
End Try
' w przypadku błędu zgłaszany jest wyjątek
If erreur <> String.Empty Then Throw New Exception(erreur)
End Sub
Procedura usuwania zostaje zmieniona w następujący sposób:
Private Sub supprimerProduit(ByVal idProduit As Integer)
Dim erreur As String
Try
' synchronizacja
Application.Lock()
' wyszukiwanie wiersza do usunięcia
Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
If ligne Is Nothing Then
erreur = String.Format("Produit [{0}] inexistant", idProduit)
Else
' usunięcie wiersza z pamięci podręcznej
ligne.Delete()
' usunięcie wiersza ze źródła fizycznego
objProduits.supprimerProduit(idProduit)
End If
Catch ex As Exception
erreur = String.Format("Erreur de suppression : {0}", ex.Message)
Finally
' koniec synchronizacji
Application.UnLock()
End Try
' w przypadku błędu zgłaszany jest wyjątek
If erreur <> String.Empty Then Throw New Exception(erreur)
End Sub
9.7.3. Testy
Wychodzimy od następującej tabeli danych w pliku ACCESS:

Uruchamiany jest klient internetowy:

Dodajemy produkt:

Usuwamy [produit1]:

Zmieniamy cenę produktu [produit2]:

Po wprowadzeniu tych zmian sprawdzamy zawartość tabeli [liste] w bazie ACCESS:

Wszystkie trzy zmiany zostały poprawnie uwzględnione w tabeli fizycznej. Przyjrzyjmy się teraz sytuacji, w której dochodzi do konfliktu. Usuwamy, bezpośrednio pod wpisem ACCESS, wiersz [produit2]:

Wracamy do naszego klienta internetowego. Nie widzi on, że usunięcie zostało już wykonane, i sam chce usunąć [produit2]:

Otrzymuje następującą odpowiedź:

Wiersz [produit2] został rzeczywiście usunięty z pamięci podręcznej, co potwierdza lista usuniętych produktów. Usunięcie wiersza [produit2] ze źródła fizycznego nie powiodło się, co wskazuje komunikat o błędzie.
9.7.4. Rozwiązanie 2
W poprzednim rozwiązaniu klienci internetowi jednocześnie aktualizują fizyczne źródło danych, ale nie widzą zmian wprowadzonych przez innych klientów. Widzą jedynie swoje własne zmiany. Chcielibyśmy teraz, aby klient mógł widzieć fizyczne źródło danych w jego aktualnym stanie, a nie w stanie, w jakim znajdowało się w momencie uruchomienia aplikacji. W tym celu udostępnimy klientowi nową opcję:

Dzięki opcji [Rafraîchir] klient wymusza ponowne odczytanie fizycznego źródła danych. Aby nie miało to wpływu na innych klientów, tabela powstała w wyniku tego odczytu musi należeć do klienta dokonującego odświeżenia i nie może być współdzielona z innymi klientami. Jest to pierwsza różnica w stosunku do poprzedniej aplikacji. Pamięć podręczna [dtProduits] źródła danych zostanie utworzona przez każdego klienta, a nie przez samą aplikację. Zmiana została wprowadzona w [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)
' pobieranie informacji konfiguracyjnych
...
' brak błędów konfiguracji
' tworzy się obiekt produktów
Dim objProduits As New produits(chaînedeConnexion)
' zapisywana jest liczba produktów na stronie
Application("defaultProduitsPage") = defaultProduitsPage
' zapisywanie instancji dostępu do danych
Application("objProduits") = objProduits
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' inicjowanie zmiennych sesji
If IsNothing(Application("erreurs")) Then
' zapisujemy źródło danych w pamięci podręcznej
Dim dtProduits As DataTable
Try
Application.Lock()
dtProduits = CType(Application("objProduits"), produits).getProduits
Catch ex As ExceptionProduits
'wystąpił błąd dostępu do produktów, odnotowuje się to w sesji
Session("erreurs") = ex.erreurs
Exit Sub
Finally
Application.UnLock()
End Try
' zapisano pamięć podręczną [dtProduits] w sesji
Session("dtProduits") = dtProduits
' wyświetlono tabelę produktów
Session("dvProduits") = dtProduits.DefaultView
' liczba produktów na stronie
Session("nbProduitsPage") = Application("defaultProduitsPage")
' aktualnie wyświetlana strona
Session("pageCourante") = 0
End If
End Sub
End Class
Informacje z sesji są pobierane przy każdym żądaniu w procedurze [Page_Load]:
' dane strony
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
' sprawdzanie, czy w aplikacji wystąpił błąd
If Not IsNothing(Application("erreurs")) Then
' aplikacja nie zainicjowała się poprawnie
afficheErreurs(CType(Application("erreurs"), ArrayList))
Exit Sub
End If
' sprawdzanie, czy wystąpił błąd sesji
If Not IsNothing(Session("erreurs")) Then
' sesja nie została poprawnie zainicjowana
afficheErreurs(CType(Session("erreurs"), ArrayList))
Exit Sub
End If
' pobieramy odwołanie do tabeli produktów
dtProduits = CType(Session("dtProduits"), DataTable)
' pobierany jest widok produktów
dvProduits = CType(Session("dvProduits"), DataView)
' pobierana jest liczba produktów na stronie
nbProduitsPage = CType(Session("nbProduitsPage"), Integer)
' pobierana jest bieżąca strona
pageCourante = CType(Session("pageCourante"), Integer)
' pobieranie instancji dostępu do produktów
objProduits = CType(Application("objProduits"), produits)
' anulowanie ewentualnej trwającej aktualizacji
DataGrid1.EditItemIndex = -1
'pierwsze zapytanie
If Not IsPostBack Then
' wyświetla się formularz początkowy
txtPages.Text = nbProduitsPage.ToString
afficheFormulaire()
End If
End Sub
Informacje pobrane podczas sesji będą zapisywane na końcu tej samej sesji po zakończeniu każdego żądania:
Private Sub Page_PreRender(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.PreRender
' zapisuje się pewne informacje w sesji
Session("dtProduits") = dtProduits
Session("dvProduits") = dvProduits
Session("nbProduitsPage") = nbProduitsPage
Session("pageCourante") = pageCourante
End Sub
Zdarzenie [PreRender] sygnalizuje, że odpowiedź zostanie wysłana do klienta. Wykorzystujemy tę okazję, aby zapisać w sesji wszystkie dane, które należy zachować. Jest to nieco nadmierne, ponieważ dość często zmieniły się tylko wartości niektórych z nich. Takie systematyczne zapisywanie ma tę zaletę, że zwalnia nas z konieczności zarządzania sesją w pozostałych metodach strony.
Operację odświeżania pamięci podręcznej zapewnia następująca procedura:
Private Sub lnkRefresh_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkRefresh.Click
' należy odświeżyć pamięć podręczną [dtProduits] przy użyciu fizycznego źródła danych
' rozpoczęcie synchronizacji
Application.Lock()
Try
' tabela produktów
dtProduits = CType(Application("objProduits"), produits).getProduits
' zapisujemy aktualny filtr
Dim filtre As String = dvProduits.RowFilter
' tworzy się nowy widok z filtrem
dvProduits = New DataView(dtProduits)
' przywracamy filtr
dvProduits.RowFilter = filtre
Catch ex As ExceptionProduits
'wystąpił błąd dostępu do produktów, odnotowujemy to w sesji
Session("erreurs") = ex.erreurs
' wyświetla się widok [erreurs]
afficheErreurs(ex.erreurs)
' zakończono
Exit Sub
Finally
' zakończono synchronizację
Application.UnLock()
End Try
' wszystko poszło dobrze – wyświetlamy produkty od pierwszej strony
afficheProduits(0, nbProduitsPage)
End Sub
Procedura generuje nowe wartości dla pamięci podręcznej [dtProduits] oraz widoku [dvProduits]. Zostaną one umieszczone w sesji przez procedurę [Page_PreRender] opisaną powyżej. Po odbudowaniu pamięci podręcznej [dtProduits] wyświetlane są produkty z pierwszej strony.
Oto przykładowe wykonanie. Uruchamiany jest klient [mozilla], który wyświetla produkty:

Klient [Internet Explorer] postępuje analogicznie:

Klient [Mozilla] usuwa [produit1], modyfikuje [produit2] i dodaje nowy produkt. Otrzymuje następującą nową stronę:

Klient [Internet Explorer] chce usunąć [produit1].

Otrzymuje następującą odpowiedź:

Został poinformowany, że [produit1] już nie istnieje. Użytkownik postanawia zatem odświeżyć pamięć podręczną, korzystając z powyższego linku [Rafraîchir]. Otrzymuje następującą odpowiedź:

Ma teraz to samo źródło danych co klient [Mozilla].
9.8. Conclusion
W tym rozdziale poświęciliśmy sporo czasu na omówienie kontenerów danych i ich powiązań ze źródłami danych. Na zakończenie pokazaliśmy, jak można zaktualizować źródło danych za pomocą komponentu [DataGrid]. Jako źródło danych przyjęliśmy tabelę bazy danych. Chociaż komponent [DataGrid] nieco ułatwia prezentację danych, prawdziwa trudność nie leży w części prezentacyjnej, ale w zarządzaniu aktualizacjami źródła danych dokonywanymi przez różnych klientów. Mogą pojawić się konflikty dostępu, które należy rozwiązać. W tym przypadku rozwiązaliśmy je w kontrolerze za pomocą komponentu [Application.Lock]. Prawdopodobnie rozsądniej byłoby zsynchronizować dostęp do źródła danych w klasie dostępu do danych, tak aby kontroler nie musiał zajmować się takimi szczegółami, które nie leżą w jego zakresie kompetencji.
W praktyce tabele w bazie danych są ze sobą powiązane relacjami, a ich aktualizacja musi te relacje uwzględniać. Ma to wpływ przede wszystkim na klasę dostępu do danych, która staje się bardziej złożona niż ta wymagana w przypadku pojedynczej tabeli. Zazwyczaj ma to również wpływ na część prezentacyjną aplikacji internetowej, ponieważ często trzeba wyświetlać nie jedną tabelę, ale tabele powiązane ze sobą relacjami.
W niniejszym rozdziale przedstawiono ponadto różne struktury danych, takie jak [DataTable, DataView].









