Skip to content

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
id
ciąg znaków
klucz główny
thème
ciąg znaków
nazwa motywu listy
description
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
id
ciąg znaków
klucz główny
thème
ciąg znaków
nazwa motywu listy

Aplikacja jednostronicowa wygląda następująco:

nr
nazwa
typ
właściwości
rola
1
dgThèmes
DataGrid
 
listy mailingowe dostępne do subskrypcji
2
dlAbonnements
DataList
 
lista subskrypcji użytkownika na powyższe listy
3
panelInfos
panel
 
panel informacyjny dotyczący tematu wybranego przez użytkownika z linkiem [Plus d'informations]
4
lblThème
Etykieta
jest częścią [panelInfos]
nazwa tematu
5
lblDescription
Etykieta
należy do [panelInfos]
opis tematu
6
lblInfo
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]:

Image

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

Image

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

Image

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

Image

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&#232;me" HeaderText="Th&#232;me"></asp:BoundColumn>
                                <asp:ButtonColumn Text="Plus d'informations" CommandName="infos"></asp:ButtonColumn>
                                <asp:ButtonColumn Text="S'abonner" CommandName="abonner"></asp:ButtonColumn>
                            </Columns>
                            <PagerStyle HorizontalAlign="Center" ... Mode="NumericPages"></PagerStyle>
                        </asp:datagrid>

Należy zwrócić uwagę na następujące kwestie:

AutoGenerateColumns=false
w sekcji <columns>...</columns> sami definiujemy kolumny, które mają być wyświetlane
AllowPaging=true
PageSize=5
w celu paginacji danych
<asp:BoundColumn>
definiuje kolumnę [thème] (HeaderText) z [DataGrid], która będzie powiązana z kolumną [thème] ze źródła danych (DataField)
<asp:ButtonColumn>
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>
<HeaderTemplate>
określa tekst nagłówka [DataList]
<ItemTemplate>
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ść:

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

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

nom
nazwa ograniczenia – może być dowolna
colonne
kolumna, która będzie kluczem głównym – typu [DataColumn]
cléPrimaire
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ć:

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

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

[DataTable].PrimaryKey
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:

Image

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

Image

Trzecia strona:

Image

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:

pAC
typu [Integer] – jest to numer bieżącej strony wyświetlonej podczas ostatniego żądania
nbAC
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:

pAC
jest to numer bieżącej strony wyświetlonej podczas poprzedniego żądania
nbAC
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
connexion
połączenie z bazą danych – zostanie otwarte w celu wykonania polecenia SQL, a następnie natychmiast zamknięte
selectText
zapytanie SQL [select] pobierające całą tabelę [liste]
insertText
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.
updateText
zapytanie umożliwiające aktualizację pól (nazwa, cena) w wierszu tabeli [liste] o kluczu [id]
deleteText
zapytanie umożliwiające usunięcie wiersza z tabeli [liste] o kluczu [id]
selectCommand
obiekt [OleDbCommand] wykonujący zapytanie [selectText] na połączeniu [connexion]
updateCommand
obiekt [OleDbCommand] wykonujący zapytanie [updateText] na połączeniu [connexion]
insertCommand
obiekt [OleDbCommand] wykonujący zapytanie [insertText] na połączeniu [connexion]
deleteCommand
obiekt [OleDbCommand] wykonujący zapytanie [deleteText] na połączeniu [connexion]
adaptateur
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:

Image

 

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
1
lnkFiltre
LinkButton
wyświetla widok [Formulaire], który służy do ustalenia warunku filtrowania
2
lnkMisaJour
LinkButton
wyświetla widok [Produits], który służy do przeglądania i aktualizowania tabeli produktów (modyfikacja i usuwanie)
3
lnkAjout
LinkButton
wyświetla widok [Ajout], który służy do dodawania produktu
4
panel
vueFormulaire
widok [Formulaire]
5
txtFiltre
TextBox
warunek filtrowania
6
txtPages
TextBox
liczba produktów na stronie
7
rfvLignes
RequiredFieldValidator
sprawdza, czy w polu [txtPages] znajduje się wartość
8
rvLignes
RangeValidator
sprawdza, czy txtPages mieści się w przedziale [3,10]
9
btnExécuter
 
przycisk [submit], który powoduje wyświetlenie widoku [produits] przefiltrowanego według warunku (5)
10
lblInfo1
Etykieta
Tekst informacyjny w przypadku błędów

Na przykład, jeśli widok [formulaire] jest wypełniony w następujący sposób:

Image

otrzymujemy następujący wynik:

nr
nazwa
typ
rola
1
vueProduits
panel
 
2
rdCroissant
rdDécroissant
RadioButton
pozwala użytkownikowi ustawić żądaną kolejność sortowania po kliknięciu nagłówka jednej z kolumn [nom], [prix]. Oba przyciski należą do grupy [rdTri].
3
DataGrid1
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
4
DataGrid2
DataGrid
wyświetla całą tabelę produktów – umożliwia śledzenie aktualizacji
5
LblInfo2
Etykieta
tekst informacyjny, zwłaszcza w przypadku błędów
6
DataGrid3
DataGrid
wyświetli usunięte produkty w tabeli produktów
7
DataGrid4
DataGrid
wyświetli zmodyfikowane produkty w tabeli produktów
8
DataGrid5
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:

DataGrid1
brak filtru dotyczącego stanu linii
 
DataGrid2
DataViewRowState.CurrentRows
wyświetla aktualny stan tabeli produktów
DataGrid3
DataViewRowState.Deleted
wyświetla usunięte wiersze z tabeli produktów
DataGrid4
DataViewRowState.ModifiedOriginal
wyświetla zmienione wiersze z tabeli produktów wraz z ich pierwotnymi wartościami
DataGrid5
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
1
vueAjout
panel
 
2
txtNom
TextBox
nazwa produktu
3
rfvNom
RequiredFieldValidator
sprawdza, czy w [txtNom] występuje wartość
4
txtPrix
TextBox
cena produktu
5
rfvPrix
RequiredFieldValidator
sprawdza, czy w polu [txtPrix] znajduje się wartość
6
cvPrix
CompareValidator
sprawdza, czy cena >= 0
7
btnAjouter
Przycisk
przycisk [submit] dodawania produktu
8
lblInfo3
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
1
vueErreurs
panel
 
2
rptErreurs
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]):

Image

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

Image

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

Image

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:

Image

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

Image

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:

Image

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 &#224; jour" CancelText="Annuler" EditText="Modifier"></asp:EditCommandColumn>
          <asp:ButtonColumn Text="Supprimer" CommandName="Delete"></asp:ButtonColumn>
  </Columns>
<PagerStyle NextPageText="Suivant" PrevPageText="Pr&#233;c&#233;dent" ...></PagerStyle>
</asp:DataGrid>

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

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

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

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

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

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:

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

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
dtProduits
tabela produktów [DataTable] – wspólna dla wszystkich klientów
dvProduits
widok produktów [DataView] – specyficzny dla każdego klienta
defaultProduitsPage
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]:

Image

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:

  1. 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.
  2. 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).
  3. 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:

Image

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

Image

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
7
rfvLignes
RequiredFieldValidator
sprawdza, czy w polu [txtPages] znajduje się wartość
8
rvLignes
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:

Image

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:

Image

Mamy 5 komponentów [DataGrid], z których każdy przedstawia konkretny widok tabeli produktów [dtProduits]. Zaczynając od lewej:

nazwa
rola
DataGrid1
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
DataGrid2
wyświetla całą tabelę produktów – umożliwia śledzenie aktualizacji
DataGrid3
wyświetli produkty usunięte z tabeli produktów
DataGrid4
wyświetli produkty, które zostały zmodyfikowane w tabeli produktów
DataGrid5
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
DataGrid2
RowStateFilter= DataRowViewState.CurrentRows
DataGrid3
RowStateFilter= DataRowViewState.Deleted
DataGrid4
RowStateFilter = DataRowViewState.ModifiedOriginal
DataGrid5
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:

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

W kodzie aplikacji może występować kilka krytycznych sekcji:

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

Mechanizm działania jest następujący:

  1. 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.
  2. 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.
  3. 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]:

Image

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

Image

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

Image

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:

Image

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

         ' wyświetlanie produktów
        afficheProduits(page, DataGrid1.PageSize)

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:

             ' usunięcie wiersza [idProduit]
            dtProduits.Rows.Find(idProduit).Delete()

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

Image

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

Image

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

Image

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

Image

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

Image

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

Image

Image

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
rfvNom
RequiredFieldValidator
sprawdza, czy w polu [txtNom] znajduje się wartość
rfvPrix
RequiredFieldValidator
sprawdza, czy w polu [txtPrix] znajduje się wartość
cvPrix
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:

Image

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:

        Application.Lock()

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:

Image

Następnie przechodzi się do trybu edycji:

Image

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.

Image

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

Image

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

Image

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

Image

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

Image

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

Image

Klient [Internet Explorer] usuwa produkt [produit1]:

Image

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

Image

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

Image

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:

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

Image

Uruchamiany jest klient internetowy:

Image

Dodajemy produkt:

Image

Usuwamy [produit1]:

Image

Zmieniamy cenę produktu [produit2]:

Image

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

Image

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

Image

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

Image

Otrzymuje następującą odpowiedź:

Image

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

Image

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:

Image

Klient [Internet Explorer] postępuje analogicznie:

Image

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

Image

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

Image

Otrzymuje następującą odpowiedź:

Image

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

Image

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