Skip to content

9. Серверні компоненти ASP — 3

9.1. Introduction

Ми продовжуємо роботу над користувацьким інтерфейсом, розширюючи можливості компонентів [DataList], [DataGrid], зокрема в області оновлення даних, які вони відображають

9.2. Управління подіями, пов’язаними з даними компонентів із зв’язком даних

9.2.1. Приклад

Розглянемо таку сторінку:

Сторінка містить три компоненти, пов’язані зі списком даних:

  • компонент [DataList] з назвою [DataList1]
  • компонент [DataGrid] з назвою [DataGrid1]
  • компонент [Repeater] з назвою [Repeater1]

Відповідний список даних — це масив {"нуль", "один", "два", "три"}. Кожному з цих елементів відповідає група з двох кнопок із назвами [Infos1] та [Infos2]. Користувач натискає одну з кнопок, і на екрані з’являється текст із назвою натиснутої кнопки. Тут ми покажемо, як керувати списком кнопок або посилань.

9.2.2. Налаштування компонентів

Компонент [DataList1] налаштований наступним чином:


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

Ми опустили все, що стосувалося зовнішнього вигляду [DataList], щоб зосередитися виключно на його змісті:

  • розділ <HeaderTemplate> визначає верхній колонтитул [DataList], а розділ <FooterTemplate> — нижній колонтитул.
  • секція <ItemTemplate> — це шаблон відображення, що використовується для кожного елемента зі списку пов’язаних даних. У ній містяться такі елементи:
    • значення поточного елемента зі списку даних, пов’язаного з компонентом: <%# Container.DataItem %>
    • дві кнопки з назвами відповідно [Infos1] та [Infos2]. Клас [Button] має атрибут [CommandName], який використовується тут. Він дозволить нам визначити, яка саме кнопка спричинила подію в [DataList]. Для обробки кліків по кнопках у нас буде лише один обробник подій, який буде пов’язаний із самим [DataList], а не з кнопками. Цей обробник отримає інформацію про те, на якому рядку [DataList] відбулося натискання. Атрибут [CommandName] дозволить нам дізнатися, на якій кнопці цього рядка воно відбулося.

Компонент [Repeater1] налаштовується дуже схожим чином:


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

Ми просто додали розділ <SeparatorTemplate>, щоб послідовні дані, що відображаються компонентом, були розділені горизонтальною рискою.

Нарешті, компонент [DataGrid1] налаштований наступним чином:


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

Тут ми також опустили інформацію про стилі (кольори, ширину тощо). Ми перебуваємо в режимі автоматичного формування стовпців, що є режимом за замовчуванням для [DataGrid]. Це означає, що стовпців буде стільки, скільки їх у джерелі даних. Тут є один стовпець. Ми додали ще два стовпці, позначені тегом <asp:ButtonColumn>. У них ми задаємо інформацію, аналогічну тій, що задана для двох інших компонентів, а також тип кнопки, у даному випадку [PushButton]. Типом за замовчуванням є [LinkButton], c.a.d — посилання. Крім того, дані будуть розбиті на сторінки [AllowPaging=true] з розміром сторінки у два елементи [PageSize=2].

9.2.3. Код оформлення сторінки

Код оформлення нашої прикладної сторінки розміщено у файлі [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>

У наведеному вище коді ми опустили код форматування (кольори, лінії, розміри тощо)

9.2.4. Код управління сторінкою

Код управління додатком розміщено у файлі [main.aspx.vb]:

Public Class main
    Inherits System.Web.UI.Page

     ' компоненти сторінки
    Protected WithEvents DataList1 As System.Web.UI.WebControls.DataList
    Protected WithEvents lblInfo As System.Web.UI.WebControls.Label
    Protected WithEvents DataGrid1 As System.Web.UI.WebControls.DataGrid
    Protected WithEvents Repeater1 As System.Web.UI.WebControls.Repeater

     ' джерело даних
    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
             'звязки з джерелом даних
            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
         ' у одній із ліній [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
         ' подія відбулася на одній із ліній [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
         ' подія відбулася в одному з рядків файлу [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
         ' перехід на іншу сторінку
        With DataGrid1
            .CurrentPageIndex = e.NewPageIndex
            .DataSource = textes
            .DataBind()
        End With
    End Sub
End Class

Коментарі:

  • джерело даних [textes] — це простий масив символьних рядків. Воно буде пов’язане з трьома компонентами, що містяться на сторінці
  • це зв’язування здійснюється у процедурі [Page_Load] під час першого запиту. Для наступних запитів ці три компоненти отримуватимуть свої значення за допомогою механізму [VIEW_STATE].
  • Усі три компоненти мають обробник події [ItemCommand]. Ця подія виникає, коли користувач натискає кнопку або посилання в одному з рядків компонента. Обробник отримує дві інформації:
    • джерело: посилання на об’єкт (кнопку або посилання), що спричинив подію
    • a: інформація про подію типу [DataListCommandEventArgs], [RepeaterCommandEventArgs], [DataGridCommandEventArgs] залежно від випадку. Аргумент a містить різну інформацію. Тут нас цікавлять дві з них:
      • a.Item: позначає рядок, у якому відбулася подія, типу [DataListItem], [DataGridItem] або [RepeaterItem]. Незалежно від точного типу, елемент [Item] має атрибут [ItemIndex], що вказує номер рядка [Item] контейнера, до якого він належить. Тут ми відображаємо цей номер рядка
      • a.CommandName: це атрибут [CommandName] кнопки (Button, LinkButton, ImageButton), яка ініціювала подію. Ця інформація у поєднанні з попередньою дозволяє нам дізнатися, яка саме кнопка контейнера є джерелом події [ItemCommand]

9.3. Додаток — управління списком підписок

Тепер, коли ми знаємо, як перехоплювати події, що відбуваються всередині контейнера даних, наведемо приклад, який демонструє, як ними керувати.

9.3.1. Вступ

Представлений додаток імітує додаток для підписки на списки розсилки. Вони визначаються об’єктом [DataTable] із трьома стовпцями:

назва
тип
роль
id
рядок
первинний ключ
thème
рядок
назва теми списку
description
рядок
опис тем, що розглядаються у списку

Щоб не перевантажувати наш приклад, об’єкт [DataTable], наведений вище, буде сформовано програмно довільним чином. У реальному додатку його, ймовірно, надаватиме метод класу доступу до даних. Формування таблиці списків відбуватиметься у процедурі [Application_Start], а отримана таблиця буде розміщена в додатку. Ми назвемо її таблицею [dtThèmes]. Користувач підпишеться на деякі теми з цієї таблиці. Список його підписок також зберігатиметься в об’єкті [DataTable], який називається [dtAbonnements] і має таку структуру:

ім’я
тип
роль
id
рядок
первинний ключ
thème
рядок
назва теми списку

Односторінковий додаток виглядає наступним чином:

назва
тип
властивості
роль
1
dgThèmes
DataGrid
 
списки розсилки, доступні для підписки
2
dlAbonnements
DataList
 
список підписок користувача на вищезазначені списки
3
panelInfos
панель
 
панель інформації про тему, обрану користувачем, із посиланням [Plus d'informations]
4
lblThème
Етикетка
є частиною [panelInfos]
назва теми
5
lblDescription
Етикетка
є частиною [panelInfos]
опис теми
6
lblInfo
Етикетка
 
інформаційне повідомлення програми

Наш приклад має на меті проілюструвати використання компонентів [DataGrid] та [DataList], зокрема управління подіями, що відбуваються на рівні рядків цих контейнерів даних. Тому додаток не має кнопки підтвердження, яка б, наприклад, зберігала у базі даних вибір, зроблений користувачем. Проте він є реалістичним. Ми наближаємося до додатка електронної комерції, де користувач додає товари (підписки) до свого кошика.

9.3.2. Принцип роботи

Перше, що бачить користувач, виглядає так:

Користувач натискає на посилання [Plus d'informations], щоб отримати інформацію про певну тему. Ця інформація відображається в [panelInfos]:

Image

Він натискає на посилання [S'abonner], щоб підписатися на тему. Обрані теми додаються до компонента [dlAbonnements]:

Image

Користувач може захотіти підписатися на список, на який він уже підписаний. Про це йому повідомляє таке повідомлення:

Image

Нарешті, він може скасувати підписку на одну з тем за допомогою кнопок [Retirer], наведених вище. Підтвердження не вимагається. Це тут не потрібно, оскільки користувач може легко підписатися знову. Ось як виглядає сторінка після скасування підписки на [thème1]:

Image

9.3.3. Налаштування контейнерів даних

Компонент [dgThèmes] типу [DataGrid] пов’язаний із джерелом типу [DataTable]. Його форматування було виконано за допомогою посилання [Mise en forme automatique] на панелі властивостей [DataGrid]. Визначення його властивостей було виконано за допомогою посилання [Générateur de proprités] на тій самій панелі. Згенерований код має такий вигляд (код форматування опущено):


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

Слід звернути увагу на наступні моменти:

AutoGenerateColumns=false
ми самостійно визначаємо стовпці, які потрібно відобразити, у розділі <columns>...</columns>
AllowPaging=true
PageSize=5
для пагінації даних
<asp:BoundColumn>
визначає стовпець [thème] (HeaderText) з [DataGrid], який буде пов'язаний зі стовпцем [thème] джерела даних (DataField)
<asp:ButtonColumn>
визначає два стовпці кнопок (або посилань). Щоб розрізнити два посилання в одному рядку, будемо використовувати їхню властивість [CommandName].

Компонент [DataGrid] не налаштований повністю. Його налаштування відбуватиметься в коді контролера.

Компонент [dlAbonnements] типу [DataList] пов’язаний із джерелом типу [DataTable]. Його форматування було виконано за допомогою посилання [Mise en forme automatique] на панелі властивостей [DataList]. Визначення його властивостей було виконано безпосередньо в коді представлення. Цей код є таким (код форматування опущено):


                        <asp:DataList id="dlAbonnements" ... runat="server" >
                            <HeaderTemplate>
                                <div align="center">
                                    Vos abonnements</div>
                            </HeaderTemplate>
                            <ItemStyle ...></ItemStyle>
                            <ItemTemplate>
                                <TABLE>
                                    <TR>
                                        <TD><%#Container.DataItem («тема»)%></TD>
                                        <TD>
                                            <asp:Button id="lnkRetirer" CommandName="retirer" runat="server" Text="Retirer" /></TD>
                                    </TR>
                                </TABLE>
                            </ItemTemplate>
                            <HeaderStyle ...></HeaderStyle>
                        </asp:DataList>
<HeaderTemplate>
визначає текст заголовка [DataList]
<ItemTemplate>
визначає поточний елемент [DataList] — тут розміщено таблицю з двома стовпцями та одним рядком. Перша комірка призначена для імені теми, на яку користувач хоче підписатися, а інша — для кнопки [Retirer], яка дозволяє скасувати вибір.

9.3.4. Сторінка відображення

Код відображення [main.aspx] має такий вигляд:


<%@ 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. Контролери

Контроль розподіляється між файлами [global.asax] та [main.aspx]. Файл [global.asax] має такий вигляд:

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

Пов’язаний файл [global.asax.vb] містить такий код:


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)
        ' ініціалізується джерело даних
        Dim thèmes As New DataTable
        ' стовпці
        With thèmes.Columns
            .Add("id", GetType(System.Int32))
            .Add("thème", GetType(System.String))
            .Add("description", GetType(System.String))
        End With
        ' стовпець id буде первинним ключем
        thèmes.Constraints.Add("cléprimaire", thèmes.Columns("id"), True)
        ' рядки
        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
        ' додаємо джерело даних до програми
        Application("thèmes") = thèmes
    End Sub

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' початок сеансу — створюємо порожню таблицю підписок
        Dim dtAbonnements As New DataTable
        With dtAbonnements
            ' стовпці
            .Columns.Add("id", GetType(String))
            .Columns.Add("thème", GetType(String))
            ' первинний ключ
            .PrimaryKey = New DataColumn() {.Columns("id")}
        End With
        ' таблиця додається до сесії
        Session.Item("abonnements") = dtAbonnements
    End Sub

End Class

Процедура [Application_Start], що виконується, коли додаток отримує свій перший запит, формує [DataTable] із тем, на які можна підписатися. Він формується довільно за допомогою коду. Згадаймо техніку, з якою ми вже стикалися. У порядку формуються:

  • порожній об’єкт [DataTable] без структури та даних
  • структуру таблиці, визначаючи її стовпці (назва та тип даних, що містяться в ній)
  • рядки таблиці, що містять корисні дані

Тут ми додали первинний ключ. В якості первинного ключа використовується стовпець «id». Про це можна сказати по-різному. У цьому випадку ми використали обмеження. У SQL обмеження — це правило, якого повинні дотримуватися дані рядка, щоб його можна було додати до таблиці. Існують найрізноманітніші види обмежень. Обмеження «Primary Key» змушує стовпець, до якого воно застосовується, містити унікальні та непорожні значення. Первинний ключ може фактично складатися з виразу, що включає значення з декількох стовпців. [DataTable].Constraints — це набір обмежень для певної таблиці. Для додавання обмеження використовується метод [DataTable.Constraints.Add]. Він має кілька сигнатур. У цьому прикладі використано метод [Add(Byval nom as String, Byval colonne as DataColumn, Byval cléPrimaire as Boolean)]:

nom
ім’я обмеження — може бути будь-яким
colonne
стовпець, який буде первинним ключем — типу [DataColumn]
cléPrimaire
повинно дорівнювати [vrai], щоб зробити [colonne] первинним ключем. Якщо [cléPrimaire=false], то на [colonne] діє лише обмеження на унікальність значень

Щоб зробити стовпець із назвою «id» первинним ключем таблиці [dtAbonnements], потрібно написати:

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

Процедура [Session_Start] виконується, коли додаток отримує перший запит від клієнта. Вона служить для створення об’єктів, унікальних для кожного клієнта, які мають зберігатися протягом різних запитів цього клієнта. Процедура формує таблицю [DataTable] з підписками клієнта. Створюється лише її структура, оскільки спочатку ця таблиця порожня. Вона заповнюватиметься у міру надходження запитів. Тут також стовпець «id» слугує первинним ключем. Для оголошення цього обмеження було використано іншу техніку:

[DataTable].PrimaryKey
— це масив стовпців, що утворюють первинний ключ; тут оголошено масив з одним елементом: стовпцем із назвою «id»

Коли запит клієнта надходить до контролера [main.aspx], обидва об’єкти [DataTable] доступні в додатку для таблиці тем та в сесії для таблиці підписок. Контролер [main.aspx.vb] має такий вигляд:


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

Основна функція процедури [Page_Load] полягає в:

  • отримати дві таблиці [dtThèmes] та [dtAbonnements], які знаходяться відповідно в додатку та сесії, щоб зробити їх доступними для всіх методів сторінки
  • зв’язати дані з цих двох джерел із відповідними контейнерами. Це відбувається лише під час першого запиту. Під час наступних запитів зв’язування не потрібно виконувати систематично, а коли його потрібно виконати, іноді доводиться чекати на подію, що відбувається після виконання процедури [Page_Load].

Код [Page_Load] виглядає так:


    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' отримуємо джерела даних
        dtThèmes = CType(Application("thèmes"), DataTable)
        dtAbonnements = CType(Session("abonnements"), DataTable)
        ' зв'язок даних
        If Not IsPostBack Then
            liaisons()
        End If
        ' — певна інформація прихована
        panelInfo.Visible = False
    End Sub

    Private Sub liaisons()
        '— пов'язується джерело даних із компонентом [datagrid]
        With dgThèmes
            .DataSource = dtThèmes
            .DataKeyField = "id"
        End With
        ' здійснюється прив'язка джерела даних до компонента [datalist]
        With dlAbonnements
            .DataSource = dtAbonnements
            .DataKeyField = "id"
        End With
        ' присвоюються дані компонентам
        Page.DataBind()
    End Sub

У процедурі [liaisons] ми використовуємо властивість [DataKeyField] компонентів [DataList] та [DataGrid] для визначення стовпця джерела даних, який слугуватиме для однозначної ідентифікації рядків контейнерів. Зазвичай цей стовпець є первинним ключем джерела даних, але це не є обов’язковим. Достатньо, щоб у стовпці не було дублікатів та порожніх значень. Для контейнера [dgThèmes] в якості первинного ключа використовується стовпець «id» джерела [dtThèmes], а для контейнера [dlAbonnements] — стовпець «id» джерела [dtAbonnements]. Не обов’язково, щоб стовпець, який слугує первинним ключем для контейнера, відображався в ньому. У даному випадку жоден із двох контейнерів не відображає стовпець первинного ключа. Перевага наявності первинного ключа в контейнері полягає в тому, що він дозволяє легко знайти у джерелі даних інформацію, пов’язану з рядком контейнера, у якому відбулася подія. Дійсно, часто трапляється так, що, виходячи з рядка контейнера, у якому відбулася подія, доводиться виконувати операції з відповідним рядком джерела даних, пов’язаним із ним. Первинний ключ полегшує цю роботу.

Пагінація [DataGrid] здійснюється у традиційний спосіб:


    Private Sub dgThèmes_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles dgThèmes.PageIndexChanged
        ' перехід на іншу сторінку
        dgThèmes.CurrentPageIndex = e.NewPageIndex
        ' зв'язок
        liaisons()
    End Sub

Операції з посиланнями [Plus d'informations] та [S'abonner] керуються процедурою [dgThèmes_ItemCommand]:


    Private Sub dgThèmes_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles dgThèmes.ItemCommand
        ' подія на рядку [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
        ' зв'язок
        liaisons()
    End Sub

Для розрізнення цих двох посилань використовується той факт, що вони мають атрибут [CommandName]. Залежно від значення цього атрибута викликається процедура [infos] або [abonner], передаючи в обох випадках ключ «id», пов’язаний з елементом [DataGrid], де відбулася подія. Отримавши цю інформацію, процедура [info] відобразить відомості про тему, обрану користувачем:


    Private Sub infos(ByVal id As String)
        ' інформація про ключ id
        ' витягується рядок з [datatable], що відповідає ключу
        Dim ligne As DataRow
        ligne = dtThèmes.Rows.Find(id)
        If Not ligne Is Nothing Then
            ' відображається інформація
            lblThème.Text = CType(ligne("thème"), String)
            lblDescription.Text = CType(ligne("description"), String)
            panelInfo.Visible = True
        End If
    End Sub

Оскільки таблиця [dtThèmes] має первинний ключ, метод [dtThèmes.Rows.Find("P")] дозволяє знайти рядок із первинним ключем P. Якщо його знайдено, отримуємо об’єкт [DataRow]. Тут нам потрібно знайти рядок із первинним ключем [id], де [id] передається як параметр. Якщо рядок знайдено, інформація [thème] та [description] з цього рядка додається до інформаційної панелі, яку потім роблять видимою.

Процедура [abonner(id)] повинна додати тему ключа [id] до списку підписок. Її код такий:


    Private Sub abonner(ByVal id As String)
         ' підписка на тему з ідентифікатором ключа
         ' отримуємо рядок з [datatable], що відповідає ключу
        Dim ligne As DataRow
        ligne = dtThèmes.Rows.Find(id)
        If Not ligne Is Nothing Then
             ' перевіряється, чи не існує вже підписки
            Dim abonnement As DataRow
            abonnement = dtAbonnements.Rows.Find(id)
            If Not abonnement Is Nothing Then
                 ' повідомляємо про помилку
                lblInfo.Text = "Vous êtes déjà abonné au thème [" + ligne("thème") + "]"
            Else
                 ' додаємо тему до списку підписок
                abonnement = dtAbonnements.NewRow
                abonnement("id") = id
                abonnement("thème") = ligne("thème")
                dtAbonnements.Rows.Add(abonnement)
                 ' встановлюються зв’язки
                liaisons()
            End If
        End If
    End Sub

У списку тем [dtThèmes] спочатку шукається рядок ключа [id]. Якщо її знайдено, перевіряється, чи ця тема вже не присутня у списку підписок, щоб уникнути її подвійного внесення. Якщо це так, виводиться повідомлення про помилку. В іншому випадку до таблиці [dtAbonnements] додається нова підписка, а компоненти списку даних пов’язуються з відповідними джерелами.

Коли користувач натискає кнопку [Retirer], необхідно видалити елемент із таблиці [dtAbonnements]. Це здійснюється за допомогою такої процедури:


    Private Sub dlAbonnements_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles dlAbonnements.ItemCommand
        ' скасовується підписка
        Dim commande As String = e.CommandName
        If commande = "retirer" Then
            ' видалення підписки з [datatable]
            With dtAbonnements.Rows
                .Remove(.Find(dlAbonnements.DataKeys(e.Item.ItemIndex)))
            End With
            ' зв’язки
            liaisons()
        End If
    End Sub

Спочатку перевіряється атрибут [CommandName] елемента, що спричинив подію. Насправді це досить зайве, оскільки кнопка [Retirer] є єдиним елементом управління, здатним генерувати подію в компоненті [DataList]. Отже, двозначності немає. Щоб видалити рядок з об’єкта [DataTable], використовується метод [DataList.Remove(DataRow)], який видаляє з таблиці рядок типу [DataRow], переданий як параметр. Цей запис знаходить метод [DataList.Find], якому передано первинний ключ шуканого запису. Після видалення запису виконується прив’язка даних до компонентів

9.4. Управління компонентом [DataList] з пагінацією

Повернемося до попереднього прикладу, щоб розбити на сторінки компонент [DataList], що представляє список підписок користувача. На відміну від компонента [DataGrid], компонент [DataList] не надає жодних засобів для розбиття на сторінки. Ми побачимо, що реалізація пагінації є складною, що допоможе нам по-справжньому оцінити переваги автоматичної пагінації компонента [DataGrid].

9.4.1. Принцип роботи

Єдина відмінність полягає в пагінації компонента [DataList]. Сторінки матимуть дві підписки. Якщо користувач має п’ять підписок, буде три сторінки. Перша з них матиме такий вигляд:

Image

Другу сторінку можна відкрити за посиланням [Suivant]:

Image

Третя сторінка:

Image

Слід зауважити, що посилання [Précédent] та [Suivant] відображаються лише у тому випадку, якщо перед поточною сторінкою є попередня сторінка, а після неї — наступна.

9.4.2. Код оформлення

Посилання [Précédent] та [Suivant] отримуються шляхом додавання тегу <FooterTemplate> до [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. Контрольний код

Відповідний файл [global.asax.vb] змінюється наступним чином:

...

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)
         ' початок сеансу  створюється порожня таблиця підписок
        Dim dtAbonnements As New DataTable
        With dtAbonnements
             ' стовпці
            .Columns.Add("id", GetType(String))
            .Columns.Add("thème", GetType(String))
             ' первинний ключ
            .PrimaryKey = New DataColumn() {.Columns("id")}
        End With
         ' таблиця додається до сесії
        Session.Item("abonnements") = dtAbonnements
         ' поточна сторінка  це сторінка 0
        Session.Item("pAC") = 0
         ' кількість підписок на цій сторінці дорівнює 0
        Session.Item("nbAC") = 0
    End Sub

Окрім таблиці передплат [dtAbonnements], у сесію додаються ще дві інформації:

pAC
типу [Integer] — це номер поточної сторінки, що відображалася під час останнього запиту
nbAC
типу [Integer] — кількість рядків, що відображалися на попередній поточній сторінці

На початку сеансу номер поточної сторінки, а також кількість рядків на цій сторінці дорівнюють нулю.

Контролер [main.aspx.vb] змінюється наступним чином:

....

Public Class main
    Inherits System.Web.UI.Page

....

     ' дані додатка
    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

Тут визначаються нові дані, пов’язані з пагінацією передплат:


    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

Деякі з цих даних зберігаються в сесії та витягуються під час кожного запиту в процедурі [Page_Load]:


    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' здійснюється отримання джерел даних
        dtThèmes = CType(Application("thèmes"), DataTable)
        dtAbonnements = CType(Session("abonnements"), DataTable)
        ' та інформацію для відображення підписок
        pAC = CType(Session("pAC"), Integer)
        nbAC = CType(Session("nbAC"), Integer)
        ' зв'язок даних
        If Not IsPostBack Then
            ' відображення порожнього списку підписок
            terminer()
        End If
        ' приховування певної інформації
        panelInfo.Visible = False
    End Sub

Отримані дані [pAC] та [nbAC] — це інформація про сторінку підписок, яка відображалася під час попереднього запиту:

pAC
це номер поточної сторінки, яка відображалася під час попереднього запиту
nbAC
кількість рядків, що відображаються на цій поточній сторінці

Метод [terminer] встановлює зв’язки між компонентами та їхніми джерелами даних, як це робив метод [liaisons] у попередньому додатку. Новим елементом тут є зв’язок [DataList] із таблицею [dtPA], яка є сторінкою підписок, що має відображатися:


    Private Sub terminer()
        ' зв'язування джерела даних із компонентом [datagrid]
        With dgThèmes
            .DataSource = dtThèmes
            .DataKeyField = "id"
        End With
        ' прив'язування сторінки підписок до компонента [datalist] з урахуванням поточної сторінки pAC
        changePAC()
        ' відображається сторінка p
        With dlAbonnements
            .DataSource = dtPA
            .DataKeyField = "id"
        End With
        ' присвоюються дані компонентам
        Page.DataBind()
        ' управління посиланнями [précédent] та [suivant] з [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)
        ' зберігання інформації про поточну сторінку в сесії
        Session("pAC") = pAC
        Session("nbAC") = dtPA.Rows.Count
    End Sub

Слід звернути увагу на наступні моменти:

  • джерело [dtPA] залежить від номера поточної сторінки [pAC], що має відображатися. Змінна [pAC] є глобальною змінною класу, з якою працюють методи, що змінюють цей номер поточної сторінки. Саме метод [changePAC] відповідає за побудову таблиці [dtPA], яка буде пов’язана з компонентом [dlAbonnements].
  • Метод [setLiens] призначений для відображення або приховування посилань [Précédent] та [Suivant] залежно від того, чи перед поточною сторінкою [pAC], що відображається, є попередня сторінка, а за нею — наступна. Вона має чотири параметри:
    • [dlAbonnements]: елемент управління [DataList], дерево елементів управління якого ми будемо досліджувати, щоб знайти ці два посилання. Дійсно, хоча ці два посилання знаходяться в конкретному місці — у нижньому колонтитулі [DataList], — схоже, що немає простого способу безпосередньо посилатися на них. У всякому разі, тут його не було знайдено.
    • [blPrecedent]: логічне значення, яке слід присвоїти властивості [visible] посилання [Precedent] — має значення «true», якщо поточна сторінка відмінна від 0
    • [blSuivant]: логічне значення, яке слід присвоїти властивості [visible] посилання [Suivant] — приймає значення «true», якщо поточна сторінка не є останньою сторінкою у списку підписок
    • [nbLiensTrouvés]: вихідний параметр, що підраховує кількість знайдених посилань. Як тільки ця кількість дорівнює двом, метод завершується.
  • Інформація [pAC] та [nbAC] зберігається в сесії для наступного запиту.

Метод [changePAC] створює таблицю [dtPA], яка буде пов’язана з компонентом [dlAbonnements]. Це відбувається на основі номера [pAC] поточної сторінки, що відображається. Таблиця [dtPA] повинна відображати певні рядки з таблиці підписок [dtAbonnements]. Нагадаємо, що ця таблиця зберігається в сесії та оновлюється (доповнюється або скорочується) у міру надходження запитів. Спочатку встановлюємо діапазон [premier,dernier] номерів рядків таблиці [dtAbonnements], які має відображати таблиця [dtPA]:


    Private Sub changePAC()
        ' призначає сторінку pAC поточною сторінкою підписок
        ' управління сторінками [datalist]
        Dim nbAbonnements = dtAbonnements.Rows.Count
        Dim dernièrePage = (nbAbonnements - 1) \ nbAP
        ' перша та остання підписка
        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

Після цього можна створити таблицю [dtPA]. Спочатку визначаємо її структуру [id,thème], а потім заповнюємо її, скопіювавши туди рядки з таблиці [dtAbonnements], номери яких знаходяться в інтервалі [premier,dernier], розрахованому раніше.


         ' створення таблиці даних dtpa
        dtPA = New DataTable
        With dtPA
            ' стовпці
            .Columns.Add("id", GetType(String))
            .Columns.Add("thème", GetType(String))
            ' первинний ключ
            .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

Наприкінці методу [changePAC] таблиця [dtPA] була сформована і може бути пов’язана з компонентом [DataList]. Це здійснюється в методі [terminer]. У цьому ж методі використовується процедура [setLiens] для встановлення стану зв’язків [Précédent] та [Suivant] компонента [DataList]. Код цієї процедури такий:


    Private Sub setLiens(ByVal ctl As Control, ByVal blPrec As Boolean, ByVal blSuivant As Boolean, ByRef nbLiensTrouvés As Integer)
        ' шукаємо зв’язки [précédent] та [suivant]
        ' у дереві елементів управління [datalist]
        ' Чи знайдені всі посилання?
        If nbLiensTrouvés = 2 Then Exit Sub
        ' перевірка дочірніх елементів
        Dim c As Control
        For Each c In ctl.Controls
            ' спочатку працюємо поглиблено — посилання знаходяться в глибині дерева
            setLiens(c, blPrec, blSuivant, nbLiensTrouvés)
            ' посилання [Précédent]?
            If c.ID = "lnkPrecedent" Then
                CType(c, LinkButton).Visible = blPrec
                nbLiensTrouvés += 1
            End If
            ' посилання [Suivant]?
            If c.ID = "lnkSuivant" Then
                CType(c, LinkButton).Visible = blSuivant
                nbLiensTrouvés += 1
            End If
        Next
    End Sub

Процедура є рекурсивною. Спочатку вона шукає, серед дочірніх елементів управління компонента [dlAbonnements] компоненти з іменами [lnkPrecedent] та [lnkSuivant], які є ідентифікаторами (атрибут ID) двох посилань на пагінацію. Спочатку вона шукає їх, починаючи з нижньої частини дерева елементів управління, оскільки саме там вони знаходяться. Як тільки посилання знайдено, лічильник [nbLiensTrouvés] збільшується, а властивість [visible] посилання заповнюється значенням, переданим як параметр процедури. Як тільки обидва посилання знайдено, дерево елементів управління більше не обстежується, і рекурсивна процедура завершується.

Ми вже зазначали, що метод [changePAC], який встановлює джерело даних [dtPA] для компонента [dlAbonnements], працює з номером [pAC] поточної сторінки, що відображається. Цей номер змінюють кілька процедур:


    Private Sub abonner(ByVal id As String)
        ' підписка на тему за ідентифікатором ключа
..
            ' додаємо тему до підписок
..
             ' оновлення номера поточної сторінки — тепер це остання сторінка
            pAC = (dtAbonnements.Rows.Count - 1) \ nbAP
             ' зв’язки даних
            terminer()
        End If
    End Sub

Після додавання підписки вона з’являється в кінці списку підписок. Тому курсор переміщується на останню сторінку списку підписок, щоб користувач міг побачити додану підписку.


    Private Sub dlAbonnements_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles dlAbonnements.ItemCommand
        ' видалення підписки
        Dim commande As String = e.CommandName
        Select Case commande
            Case "retirer"
                ' видалення підписки з [datatable]
                With dtAbonnements.Rows
                    .Remove(.Find(dlAbonnements.DataKeys(e.Item.ItemIndex)))
                End With
                ' чи потрібно змінити поточну сторінку?
                nbAC -= 1
                If nbAC = 0 Then pAC -= 1
            Case "precedent"
                ' зміна поточної сторінки
                pAC -= 1
            Case "suivant"
                ' зміна поточної сторінки
                pAC += 1
        End Select
        ' зв'язки даних
        terminer()
    End Sub
  • [nbAC] — це кількість рядків, що відображаються на поточній сторінці до скасування підписки. Якщо нова кількість рядків на сторінці дорівнює 0, номер поточної сторінки [pAC] зменшується на одиницю.
  • у разі натискання на посилання [Precedent] номер поточної сторінки [pAC] зменшується на одиницю.
  • у разі натискання на посилання [Suivant] номер поточної сторінки [pAC] збільшується на одиницю.

Решта процедур залишаються такими ж, як і раніше.

9.4.4. Висновок

На цьому прикладі ми показали, що можна виконати нумерацію сторінок для компонента [DataList]. Ця процедура є досить складною, тому, коли це можливо, краще покладатися на автоматичну нумерацію сторінок компонента [DataGrid]. Цей приклад також продемонстрував, як отримати доступ до компонентів, що знаходяться у нижньому колонтитулі компонента [DataList].

9.5. Клас доступу до бази даних продуктів

Ми знову звертаємося до вже використаної бази даних ACCESS [produits]. Нагадаємо, що вона містить єдину таблицю з назвою [liste], структура якої така:

Ми створимо клас доступу до таблиці [liste], який дозволить читати та оновлювати її. Ми також створимо консольний клієнт, який використовуватиме цей клас для оновлення таблиці. На другому етапі ми створимо веб-клієнт для виконання тієї ж задачі.

9.5.1. Клас ExceptionProduits

Клас [Exception] має конструктор, який приймає повідомлення про помилку як параметр. Нам потрібно створити клас винятків, конструктор якого приймає список повідомлень про помилки, а не одне повідомлення. Це буде клас [ExceptionProduits], наведений нижче:


    Public Class ExceptionProduits
        Inherits Exception

        ' повідомлення про помилки, пов'язані з винятком
        Private _erreurs As ArrayList

        ' конструктор
        Public Sub New(ByVal erreurs As ArrayList)
            Me._erreurs = erreurs
        End Sub

        ' властивість
        Public ReadOnly Property erreurs() As ArrayList
            Get
                Return _erreurs
            End Get
        End Property
    End Class

9.5.2. Структура [sProduit]

Структура [produit] представлятиме товар [id, nom, prix]:


     ' структура sProduit
    Public Structure sProduit
         ' поля
        Private _id As Integer
        Private _nom As String
        Private _prix As Double

         ' ідентифікатор властивості
        Public Property id() As Integer
            Get
                Return _id
            End Get
            Set(ByVal Value As Integer)
                _id = Value
            End Set
        End Property

        ' властивість «назва»
        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

        ' властивість «ціна»
        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

Структура допускає лише дійсні дані для полів [nom] та [prix].

9.5.3. Клас «Продукти»

Клас [Produits] — це клас, який дозволить нам оновлювати таблицю [liste] у базі даних продуктів. Його структура така:

    Public Class produits
         ' дані екземпляра

        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

Дані екземпляра

Дані, спільні для різних методів класу, є такими:


        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
з'єднання з базою даних — буде відкрито для виконання команди SQL, а потім одразу ж закрито
selectText
запит SQL [select], що витягує всю таблицю [liste]
insertText
запит, що дозволяє вставити рядок (назва, ціна) у таблицю [liste]. Слід зауважити, що поле [id] не вказується. Адже це поле автоматично інкрементується за допомогою SGBD, тому нам не потрібно його вказувати.
updateText
запит, що дозволяє оновити поля (назва, ціна) рядка таблиці [liste] із ключем [id]
deleteText
запит, що дозволяє видалити рядок з таблиці [liste] з ключем [id]
selectCommand
об’єкт [OleDbCommand], що виконує запит [selectText] через з’єднання [connexion]
updateCommand
об'єкт [OleDbCommand], що виконує запит [updateText] через з'єднання [connexion]
insertCommand
об’єкт [OleDbCommand], що виконує запит [insertText] через з’єднання [connexion]
deleteCommand
об'єкт [OleDbCommand], що виконує запит [deleteText] через з'єднання [connexion]
adaptateur
об'єкт, що дозволяє отримати результат виконання [selectCommand] в об'єкті [DataSet]

Конструктор

Конструктор отримує єдиний параметр [chaineConnexionOLEDB], який є рядком підключення, що вказує на базу даних, з якою потрібно працювати. На основі цього рядка готуються чотири команди для роботи з таблицею та її оновлення, а також адаптер. Це лише підготовка, і жодного підключення не відбувається.


        Public Sub New(ByVal chaineConnexionOLEDB As String)
            ' підготовка з'єднання
            connexion = New OleDbConnection(chaineConnexionOLEDB)
            ' підготовка команд запитів
            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
            ' підготовка адаптера доступу до даних
            adaptateur.SelectCommand = selectCommand
        End Sub

Метод getProduits

Цей метод дозволяє отримати вміст таблиці [Liste] в об’єкті [DataTable]. Його код такий:


        Public Function getProduits() As DataTable
            ' таблиця [liste] поміщена в [dataset]
            Dim contenu As New DataSet
            ' створено об’єкт DataAdapter для зчитування даних із джерела 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
            ' повертаємо результат
            Return contenu.Tables(0)
        End Function

Робота виконується за допомогою двох наступних команд:


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

Метод [FillSchema] визначає структуру (стовпці, обмеження, зв’язки) таблиці [DataSet] contenu на основі структури бази даних, на яку посилається [adaptateur.Connexion]. Це дозволяє нам отримати структуру таблиці [liste], зокрема її первинний ключ. Наступна операція [Fill] заповнює таблиці [Dataset] та contenu записами з таблиці [liste]. За допомогою лише цієї операції ми б отримали дані та структуру, але не первинний ключ. Однак він нам знадобиться для оновлення таблиці [liste] у пам’яті. Тут, як і в інших методах, ми обробляємо можливі помилки за допомогою класу [ExceptionProduits], щоб отримати список (ArrayList) помилок, а не одну помилку. Метод [getProduits] повертає таблицю [liste] у вигляді об’єкта [DataTable].

Метод ajouterProduits

Цей метод дозволяє додати рядок (id, ім’я, ціна) до таблиці [liste]. Ця інформація передається йому у вигляді структури [sProduit] з полями [id, nom, prix]. Код методу такий:


        Public Sub ajouterProduit(ByVal produit As sProduit)
            ' додається товар [nom,prix]
            ' готуємо параметри додавання
            With insertCommand.Parameters
                .Clear()
                .Add(New OleDbParameter("nom", produit.nom))
                .Add(New OleDbParameter("prix", produit.prix))
            End With
            ' додано
            Try
                ' вхід у систему
                connexion.Open()
                ' виконання команди
                insertCommand.ExecuteNonQuery()
            Catch ex As Exception
                ' проблема
                Dim erreursCommande As New ArrayList
                erreursCommande.Add(String.Format("Erreur lors de l'ajout : {0}", ex.Message))
                Throw New ExceptionProduits(erreursCommande)
            Finally
                ' закриття з'єднання
                connexion.Close()
            End Try
        End Sub

Поля структури [produit] вводяться в параметри команди [insertCommand]. Нагадаємо поточну конфігурацію цієї команди (див. виробника):


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

Текст команди SQL [insert] містить формальні параметри ?, які потрібно замінити на фактичні параметри. Це робиться за допомогою колекції [Parameters] класу [OleDbCommand]. Вона містить елементи типу [OleDbParameter], які визначають фактичні параметри, що мають замінити формальні параметри «?». Оскільки останні не мають імен, для визначення, якому формальному параметру відповідає той чи інший фактичний параметр, використовується індекс фактичних параметрів. У даному випадку фактичний параметр № i у колекції [Parameters] замінить формальний параметр «?» № i. Для створення фактичного параметра типу [OleDbParameter] тут використовується конструктор [OleDbParameter (Byval nom as String, Byval valeur as Object)], який визначає ім’я та значення фактичного параметра. Ім’я може бути будь-яким. Крім того, тут воно не використовуватиметься. Обидва параметри інструкції SQL [insert] отримують у якості значень значення полів [nom, prix] структури [produit]. Після цього вставка виконується за допомогою інструкції [insertCommand.ExecuteNonQuery].

Метод modifierProduits

Цей метод дозволяє змінити рядок таблиці [liste]. Необхідна для цього інформація надається у структурі [sProduit] з полів [id, nom, prix].


        Public Sub modifierProduit(ByVal produit As sProduit)
            ' змінюємо товар [id,nom,prix]
            ' підготовка параметрів оновлення
            With updateCommand.Parameters
                .Clear()
                .Add(New OleDbParameter("nom", produit.nom))
                .Add(New OleDbParameter("prix", produit.prix))
                .Add(New OleDbParameter("id", produit.id))
            End With
            ' здійснюється зміна
            Try
                ' відкриття з'єднання
                connexion.Open()
                ' виконання команди
                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
                ' проблема
                Dim erreursCommande As New ArrayList
                erreursCommande.Add(String.Format("Erreur lors de la modification : {0}", ex.Message))
                Throw New ExceptionProduits(erreursCommande)
            Finally
                ' закриття з'єднання
                connexion.Close()
            End Try
        End Sub

Код майже ідентичний коду методу [ajouterProduits], за винятком того, що відповідна команда [OleDbCommand] має вигляд [updateCommand] замість [insertCommand].

Метод supprimerProduits

Цей метод дозволяє видалити рядок із таблиці [liste] за ключем [id], переданим як параметр. Код виглядає наступним чином:


        Public Sub supprimerProduit(ByVal id As Integer)
            ' видаляє продукт ключа [id]
            ' підготовка параметрів видалення
            With deleteCommand.Parameters
                .Clear()
                .Add(New OleDbParameter("id", id))
            End With
            ' виконується видалення
            Try
                ' відкриття з'єднання
                connexion.Open()
                ' виконання команди
                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
                ' проблема
                Dim erreursCommande As New ArrayList
                erreursCommande.Add(String.Format("Erreur lors de la suppression : {0}", ex.Message))
                Throw New ExceptionProduits(erreursCommande)
            Finally
                ' закриття з'єднання
                connexion.Close()
            End Try
        End Sub

Тут використовується той самий підхід, що й у попередніх методах.

9.5.4. Тести класу [produits]

Програма тестування [testproduits.vb] у консольному режимі може виглядати так:

Option Explicit On 
Option Strict On

' простори імен
Imports System
Imports System.Data
Imports Microsoft.VisualBasic
Imports System.Collections

Namespace st.istia.univangers.fr

     ' тестова сторінка
    Module testproduits
        Dim contenu As DataTable

        Sub Main(ByVal arguments() As String)
             ' відображення вмісту таблиці товарів
             ' таблиця знаходиться в базі даних ACCESS, ім'я файлу якої використовується як ім'я сторінки
            Const syntaxe1 As String = "pg bdACCESS"

             ' перевірка параметрів програми
            If arguments.Length <> 1 Then
                 ' повідомлення про помилку
                Console.Error.WriteLine(syntaxe1)
                 ' кінець
                Environment.Exit(1)
            End If
             ' підготовка рядка з'єднання
            Dim chaineConnexion As String = "Provider=Microsoft.Jet.OLEDB.4.0; Ole DB Services=-4; Data Source=" + arguments(0)
             ' створення об’єкта «товари»
            Dim objProduits As produits = New produits(chaineConnexion)
             ' відображення всіх товарів
            afficheProduits(objProduits)
             ' додається товар
            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
             ' відображення всіх товарів
            afficheProduits(objProduits)
             ' отримання ідентифікатора доданого товару
            produit.id = CType(contenu.Rows(contenu.Rows.Count - 1)("id"), Integer)
             ' редагування доданого товару
            produit.prix = 200
            Try
                objProduits.modifierProduit(produit)
            Catch ex As ExceptionProduits
                afficheErreurs(ex.erreurs)
            End Try
             ' відображаються всі товари
            afficheProduits(objProduits)
             ' видалення доданого товару
            Try
                objProduits.supprimerProduit(produit.id)
            Catch ex As ExceptionProduits
                afficheErreurs(ex.erreurs)
            End Try
             ' відображаються всі товари
            afficheProduits(objProduits)
        End Sub

        Sub afficheProduits(ByRef objProduits As produits)
             ' завантажується таблиця товарів у datatable
            Try
                contenu = objProduits.getProduits()
            Catch ex As ExceptionProduits
                afficheErreurs(ex.erreurs)
                Environment.Exit(2)
            End Try
            Dim lignes As DataRowCollection = contenu.Rows
            For i As Integer = 0 To lignes.Count - 1
                 ' рядок i таблиці
                Console.Out.WriteLine(lignes(i).Item("id").ToString + "," + lignes(i).Item("nom").ToString + _
                "," + lignes(i).Item("prix").ToString)
            Next
             ' зупиняє виведення даних у консоль
            Console.WriteLine("...")
            Console.ReadLine()
        End Sub

        Sub afficheErreurs(ByRef erreurs As ArrayList)
             ' виводить помилки на консоль
            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
             ' зупиняє виведення на консоль
            Console.WriteLine("...")
            Console.ReadLine()
        End Sub
    End Module
End Namespace

Компілюємо обидва вихідні файли:

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

потім перевіряємо:

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

Читачеві пропонується порівняти наведений вище вивід на екрані з кодом тестової програми.

9.6. Веб-додаток для оновлення кешованої таблиці товарів

9.6.1. Вступ

Зараз ми пишемо веб-додаток для оновлення таблиці товарів (додавання, видалення, редагування). Оновлена таблиця зберігатиметься в пам’яті в об’єкті [DataTable] і буде спільною для всіх користувачів. Ми хочемо звернути увагу на два моменти:

  • управління об’єктом [DataTable]
  • проблеми одночасного оновлення таблиці кількома користувачами.

Архітектура додатка MVC буде такою:

9.6.2. Функціонування та види

Головна сторінка додатка виглядає так:

Image

 

Цей екран, що має назву [formulaire], дозволяє користувачеві задати критерії фільтрації товарів та визначити кількість товарів на сторінці, яку він бажає бачити.

назва
тип
роль
1
lnkFiltre
LinkButton
відображає вікно [Formulaire], яке використовується для встановлення умови фільтрації
2
lnkMisaJour
LinkButton
відображає подання [Produits], яке використовується для перегляду та оновлення таблиці товарів (редагування та видалення)
3
lnkAjout
LinkButton
відображає вікно [Ajout], яке використовується для додавання товару
4
panel
vueFormulaire
погляд [Formulaire]
5
txtFiltre
TextBox
умова фільтрації
6
txtPages
TextBox
кількість товарів на сторінці
7
rfvLignes
RequiredFieldValidator
перевіряє наявність значення в [txtPages]
8
rvLignes
RangeValidator
перевіряє, чи txtPages знаходиться в інтервалі [3,10]
9
btnExécuter
 
кнопка [submit], яка відображає вигляд [produits], відфільтрований за умовою (5)
10
lblInfo1
Мітка
Інформаційний текст у разі помилок

Наприклад, якщо подання [formulaire] заповнено таким чином:

Image

отримуємо такий результат:

назва
тип
роль
1
vueProduits
панель
 
2
rdCroissant
rdDécroissant
RadioButton
дозволяє користувачеві встановити бажаний порядок сортування, коли він натискає на заголовок одного зі стовпців [nom], [prix]. Обидві кнопки входять до групи [rdTri].
3
DataGrid1
DataGrid
сітка відображення відфільтрованого виду таблиці товарів. Фільтр встановлено за допомогою виду [formulaire]. Також встановлено .AllowPaging=true, .AllowSorting=true
4
DataGrid2
DataGrid
відображає всю таблицю продуктів — дозволяє відстежувати оновлення
5
LblInfo2
Етикетка
інформаційний текст, зокрема у разі помилок
6
DataGrid3
DataGrid
відображатиме видалені товари в таблиці товарів
7
DataGrid4
DataGrid
відобразить змінені товари в таблиці товарів
8
DataGrid5
DataGrid
відобразить додані товари у таблиці товарів

На цій сторінці є п’ять контейнерів даних. Усі вони відображають одну й ту саму таблицю [dtProduits] через різні подання [DataView]. Вигляд представляє підмножину рядків таблиці-джерела цього вигляду. Ця підмножина створюється на основі властивостей [RowFilter] та [RowStateFilter] класу [DataView]:

  • [RowFilter] дозволяє встановити фільтр для рядків, як, наприклад, вище [prix>30]. Цей тип фільтрації буде використовуватися [DataGrid1].
  • [RowStateFilter] дозволяє встановити фільтр залежно від стану рядка таблиці. Цей фільтр вказує стан рядка порівняно з його початковим станом на момент створення подання таблиці. У даному випадку таблиця [dtProduits] походить із бази даних. Спочатку всі її рядки матимуть стан, рівний [Original], що вказує на те, що це оригінальні рядки таблиці. Згодом цей стан може змінюватися та набувати різних значень, ось деякі з них:
    • [Added]: рядок було додано — він не входив до початкової таблиці
    • [Deleted]: рядок було видалено — він все ще знаходиться в таблиці, але «позначений» як «підлягає видаленню»
    • [Modified]: рядок було змінено

[RowStateFilter] дозволяє відобразити рядки таблиці з певним статусом:

  • (продовження)
    • [DataViewRowState.Added]: відображаються лише додані рядки. Вони відображаються з їхніми поточними значеннями.
    • [DataViewRowState.ModifiedOriginal]: відображаються лише змінені рядки. Вони відображаються з їхніми початковими значеннями.
    • [DataViewRowState.ModifiedCurrent]: відображаються лише змінені рядки. Вони відображаються з їхніми поточними значеннями.
    • [DataViewRowState.Deleted]: відображаються лише видалені рядки. Вони відображаються з їхніми початковими значеннями.
    • [DataViewRowState.CurrentRows]: відображаються рядки, що не були видалені. Вони відображаються з їхніми поточними значеннями.

Таким чином, фільтр [RowStateFilter] матиме такі значення:

DataGrid1
відсутність фільтра за станом ліній
 
DataGrid2
DataViewRowState.CurrentRows
відображає поточний стан таблиці товарів
DataGrid3
DataViewRowState.Deleted
відображає видалені рядки з таблиці товарів
DataGrid4
DataViewRowState.ModifiedOriginal
відображає змінені рядки таблиці товарів із їхніми початковими значеннями
DataGrid5
DataViewRowState.Added
відображає рядки, додані до початкової таблиці товарів

Контейнери [DataGrid1-5] дозволять нам відстежувати оновлення таблиці [dtProduits]. Компонент [DataGrid1] дозволяє змінювати та видаляти товар. Ми розглянемо, як конфігурація компонента забезпечує це оновлення. Він також має пагінацію та сортування. Ці два аспекти вже розглядалися в попередньому прикладі.

Посилання [Ajout] надає доступ до форми додавання товару:

назва
тип
роль
1
vueAjout
панель
 
2
txtNom
TextBox
назва товару
3
rfvNom
RequiredFieldValidator
перевіряє наявність значення в [txtNom]
4
txtPrix
TextBox
ціна товару
5
rfvPrix
RequiredFieldValidator
перевіряє наявність значення в [txtPrix]
6
cvPrix
CompareValidator
перевіряє, чи ціна >= 0
7
btnAjouter
Кнопка
кнопка [submit] для додавання товару
8
lblInfo3
Етикетка
інформаційний текст про результат операції додавання

База даних [produits] може бути недоступною під час запуску програми. У цьому випадку користувачеві відображається вікно [erreurs]:

назва
тип
роль
1
vueErreurs
панель
 
2
rptErreurs
Репітер
список помилок

9.6.3. Налаштування контейнерів даних

П'ять контейнерів [DataGrid] було налаштовано в [WebMatrix]. Їх оформлення (кольори та рамки) було налаштовано за допомогою посилання [Configuration automatique] у панелі властивостей. Контейнер [DataGrid1] було налаштовано за допомогою посилання [Générateur de propriétés] у цій же панелі. Було дозволено сортування (вкладка [Général]):

Image

На відміну від інших чотирьох контейнерів, стовпці не генеруються автоматично. Їх було визначено вручну за допомогою майстра:

Image

Спочатку було створено два стовпці типу [Colonne connexe] з назвами [nom] та [prix]. Їх було пов’язано відповідно з полями [nom] та [prix] джерела даних, які контейнери будуть відображати. Ось, наприклад, конфігурація стовпця [nom]:

Image

Вираз сортування — це вираз, який слід розмістити після клаузули [order by] у інструкції SQL [select], яка виконуватиметься, коликористувач клацне на назві заголовного стовпця [nom], пов’язаного з полем [nom] у [DataGrid]. Тут ми вказали [nom], отже, умова сортування буде [order by nom]. Пізніше ми змінимо її на [order by nom asc] або [order by nom desc] залежно від порядку сортування, обраного користувачем.

Крім того, ми створили два стовпці з кнопками:

Image

Стовпець [Modifier, Mettre à jour, Annuler] дозволить нам редагувати товар, а стовпець [Supprimer] — видалити його. Кожен із цих стовпців можна налаштувати. Стовпець [Modifier, Mettre à jour, Annuler] має таке налаштування:

Image

Як бачимо, можна змінювати тексти кнопок. Щодо кнопок, то ми можемо вибирати між посиланнями та кнопками (випадаючий список вище). Тут було обрано посилання. Налаштування стовпця [Supprimer] є аналогічним. Окрім цього майстра налаштування, ми безпосередньо використовували вікно властивостей [DataGrid] для заповнення властивості [DataKeyField], яка вказує, за яким полем джерела даних індексуються рядки [DataGrid]. Тут використовується первинний ключ таблиці товарів:

Image

У підсумку ця конфігурація генерує такий код для відображення:


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

Як завжди, набувши певного досвіду, можна написати весь або частину наведеного вище коду безпосередньо.

Інші контейнери [DataGrid] мають конфігурацію за замовчуванням, отриману шляхом автоматичного формування стовпців [DataGrid] на основі стовпців джерела даних, з яким він пов’язаний.

Контейнер [Repeater] слугує для відображення списку помилок. Його конфігурація здійснюється безпосередньо в коді представлення:


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

Кожен рядок компонента відображає значення [Container.DataItem], c.a.d. Відповідне значення зі списку даних. Воно матиме тип [ArrayList] і представлятиме список помилок.

9.6.4. Код оформлення додатка

Він розміщений у файлі [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>

Звернімо увагу на наступні моменти:

  • сторінка складається з чотирьох контейнерів (панелей) [vueFormulaire, vueProduits, vueAjout, vueErreurs], які утворюють чотири вікна додатка.
  • кнопки або посилання типу [submit] мають властивість [CausesValidation=false]. Властивість [causesValidation=true] викликає виконання всіх перевірок валідності на сторінці. Однак у даному випадку не всі перевірки валідності потрібно виконувати одночасно. Наприклад, під час додавання даних ми не хочемо, щоб виконувалися перевірки, що стосуються кількості рядків на сторінці. Тому ми самі вкажемо, які перевірки повинні бути виконані.

9.6.5. Код перевірки [global.asax]

Контролер [global.asax] має такий вигляд:

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

Відповідний код [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)
        ' отримуємо інформацію про конфігурацію
        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
        ' чи є помилки конфігурації?
        If erreurs.Count <> 0 Then
            ' фіксуємо помилки
            Application("erreurs") = erreurs
            ' завершуємо роботу
            Exit Sub
        End If
        ' тут немає помилок конфігурації
        ' створюється об’єкт «товари»
        Dim dtProduits As DataTable
        Try
            dtProduits = New produits(chaînedeConnexion).getProduits
        Catch ex As ExceptionProduits
            'сталася помилка доступу до товарів, її фіксують у програмі
            Application("erreurs") = ex.erreurs
            Exit Sub
        Catch ex As Exception
            ' необроблена помилка
            erreurs.Add(ex.Message)
            Application("erreurs") = erreurs
            ' вихід із підпрограми
        End Try
        ' тут немає помилок ініціалізації
        ' запам'ятовується кількість товарів на сторінці
        Application("defaultProduitsPage") = defaultProduitsPage
        ' зберігаємо таблицю товарів
        Application("dtProduits") = dtProduits
    End Sub

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' ініціалізація змінних сесії
        If IsNothing(Application("erreurs")) Then
            ' перегляд таблиці товарів
            Session("dvProduits") = CType(Application("dtProduits"), DataTable).DefaultView
            ' кількість товарів на сторінці
            Session("nbProduitsPage") = Application("defaultProduitsPage")
            ' поточна сторінка, що відображається
            Session("pageCourante") = 0
        End If
    End Sub
End Class

У [Application_Start] спочатку отримують дві інформації з конфігураційного файлу [web.config] додатка:

  • OLEDBStringConnection: рядок OLEDB для підключення до бази даних товарів
  • defaultProduitsPage: кількість продуктів на сторінці за замовчуванням

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

Якщо одна з цих двох відомостей відсутня, формується список помилок, який додається до програми. Те саме відбувається, якщо параметр [defaultProduitsPage] існує, але є неправильним. Якщо обидва очікувані параметри присутні та правильні, створюється таблиця [dtProduits] і розміщується в додатку. Саме ця таблиця буде використовуватися та оновлюватися різними клієнтами. Сама база даних залишиться незмінною. Питання її оновлення ми розглянемо в наступному додатку. Ця таблиця створюється на основі екземпляра класу [produits], який ми розглядали раніше, та його методу [getProduits]. Отримання таблиці [dtProduits] може завершитися невдачею. У цьому випадку відомо, що клас [produits] генерує виняток типу [ExceptionProduits]. Він перехоплюється, а відповідний список помилок заноситься до додатка, пов’язаного з ключем [erreurs]. Наявність цього ключа в інформації, записаній у додатку, перевірятиметься під час кожної обробки запиту. Якщо його буде знайдено, клієнту буде надіслано представлення [erreurs].

Якщо таблиця [dtProduits] є спільною для всіх веб-клієнтів, кожен із них, тим не менш, матиме власний вигляд [dvProduits] на основі цієї таблиці. Дійсно, кожен веб-клієнт має можливість встановити фільтр для таблиці [dtProduits], а також порядок сортування. Ця інформація, що є індивідуальною для кожного веб-клієнта, зберігається у поданні клієнта. Тому це подання створюється в таблиці [Session_Start], щоб бути розміщеним у сесії кожного окремого клієнта. Для отримання перегляду таблиці за замовчуванням використовується атрибут [DefaultView] таблиці [dtProduits]. Спочатку фільтр та порядок сортування відсутні. Крім того, у сесію також заносяться дві величини:

  • кількість товарів на сторінці з ключем [nbProduitsPage]. На початку сеансу ця кількість дорівнює значенню за замовчуванням, визначеному у файлі конфігурації.
  • номер поточної сторінки товарів. Спочатку це перша сторінка.

9.6.6. Код контролера [main.aspx.vb]

Скелет контролера має такий вигляд:


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

    ' компоненти сторінки
    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

    ' дані сторінки
    Protected dtProduits As DataTable
    Protected dvProduits As DataView
    Protected defaultProduitsPage As Integer
    Protected erreur As Boolean = False

    ' завантаження сторінки
    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. Дані екземпляра

Клас [main] використовує такі дані екземпляра:

Public Class main
    Inherits System.Web.UI.Page

     ' компоненти сторінки
    Protected WithEvents txtPages As System.Web.UI.WebControls.TextBox
...

     ' дані сторінки
    Protected dtProduits As DataTable
    Protected dvProduits As DataView
    Protected defaultProduitsPage As Integer
dtProduits
таблиця [DataTable] з товарами — спільна для всіх клієнтів
dvProduits
перегляд [DataView] товарів — індивідуальний для кожного клієнта
defaultProduitsPage
кількість товарів на сторінці, запропонована за замовчуванням

9.6.8. Процедура [Page_Load] для завантаження сторінки

Код процедури [page_Load] такий:


    ' завантаження сторінки
    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' перевіряється, чи є помилка в роботі програми
        If Not IsNothing(Application("erreurs")) Then
            ' додаток не ініціалізувався належним чином
            afficheErreurs(CType(Application("erreurs"), ArrayList))
            Exit Sub
        End If
        ' отримано посилання на таблицю товарів
        dtProduits = CType(Application("dtProduits"), DataTable)
         ' отримуємо перегляд товарів
        dvProduits = CType(Session("dvProduits"), DataView)
         ' отримуємо кількість товарів на сторінку
        defaultProduitsPage = CType(Application("defaultProduitsPage"), Integer)
         ' скасовується можливе оновлення, що виконується
        DataGrid1.EditItemIndex = -1
        'перший запит
        If Not IsPostBack Then
            ' відображається початкова форма
            txtPages.Text = defaultProduitsPage.ToString
            afficheFormulaire()
        End If
    End Sub
  • Спочатку перевіряється, чи вдалося завантажити таблицю товарів під час запуску програми. Якщо ні, відображається вікно [erreurs] з відповідними повідомленнями про помилки, після чого процедура завершується після встановлення логічного значення [erreur] на «true». Цей індикатор перевірятиметься деякими процедурами.
  • З додатка отримуємо таблицю товарів [dtProduits] і зберігаємо її в змінній екземпляра [dtProduits], щоб вона була спільною для всіх методів сторінки.
  • Те саме робимо з поданням [dvProduits], яке отримуємо з сесії, та кількістю товарів на сторінку за замовчуванням, яку отримуємо з додатка.
  • Якщо це перший запит клієнта, йому показується форма для визначення умов фільтрації та пагінації з пагінацією за замовчуванням [defaultProduitsPage] товарів на сторінку.
  • Скасовується режим редагування компонента [dataGrid1]. Цей компонент має атрибут [EditItemIndex], який є індексом елемента, що редагується. Якщо [EditItemIndex]=-1, то жоден елемент не редагується. Якщо [EditItemIndex]=i, то елемент № i компонента [DataGrid] перебуває в процесі редагування. У цьому випадку він відображається інакше, ніж інші елементи [dataGrid]:

Image

На малюнку вище елемент [Produit8, 80] перебуває в режимі [édition]. Як видно вище, користувач може підтвердити оновлення за посиланням [Mettre à jour] або скасувати його за посиланням [Annuler]. Він також може використовувати інші посилання, що не мають жодного стосунку до елемента, який редагується. Вставляючи [DataGrid1.EditItemIndex=-1] при кожному завантаженні сторінки, ми гарантуємо, що режим редагування [DataGrid1] буде систематично скасовано. Інше значення йому буде присвоєно лише в тому випадку, якщо було натиснуто посилання [Modifier]. Це буде зроблено в процедурі, що обробляє цю подію. Можуть виникнути такі ситуації:

  1. було натиснуто посилання [Modifier] в елементі № 8 [DataGrid1]. [Page_Load] спочатку встановлює значення -1 у [DataGrid1.EditItemIndex]. Потім процедура, що обробляє подію [Modifier], встановить значення 8 у [DataGrid1.EditItemIndex]. У підсумку, коли сторінка буде відправлена клієнту, елемент № 8 перебуватиме в режимі [édition] і відображатиметься так, як показано вище.
  2. Користувач змінює товар, який перебуває в режимі [édition], і підтверджує зміни за допомогою посилання [Mettre à jour]. [Page_Load] записує -1 у [DataGrid1.EditItemIndex]. Потім виконається процедура, що обробляє подію [Mettre à jour], і елемент № 8 у [dtProduits] буде оновлено. Коли сторінка буде відправлена клієнту, жоден елемент [DataGrid1] не перебуватиме в режимі редагування (DataGrid.EditItemIndex=-1).
  3. Якщо користувач перейшов до оновлення продукту, логічно було б, щоб він скасував це оновлення за допомогою [Annuler]. Однак ніщо не заважає йому натиснути на інше посилання. Тому нам потрібно передбачити цей випадок. У цьому випадку, як і в попередніх, [Page_Load] спочатку встановлює значення -1 у [DataGrid1.EditItemIndex], а потім виконується процедура, що обробляє подію, яка відбулася. Вона не змінить властивість [DataGrid1.EditItemIndex], яка, отже, залишиться зі значенням -1. Коли сторінка буде відправлена клієнту, жоден елемент [DataGrid1] не перебуватиме в режимі редагування.

Як бачимо, присвоївши [EditItemIndex] значення -1 під час завантаження сторінки, ми уникаємо необхідності турбуватися про те, чи перебував користувач у режимі оновлення, коли натиснув на посилання.

9.6.9. Відображення переглядів [erreurs], [formulaire] та [ajout]

Відображення подання [erreurs] ініціюється під час завантаження сторінки [Page_Load], якщо виявляється, що додаток неініціалізувався належним чином. Це відображення здійснюється шляхом прив’язування компонента даних [rptErreurs] до списку помилок, переданого в параметрах, та виведення відповідного контейнера на екран. Нарешті, три посилання [Filtre, Mise à jour, Ajout] приховуються, оскільки в разі помилки додаток не можна використовувати.


    Private Sub afficheErreurs(ByVal erreurs As ArrayList)
        ' прив'язуємо список помилок до ретранслятора rptErreurs
        With rptErreurs
            .DataSource = erreurs
            .DataBind()
        End With
        '— блокуються параметри
        lnkAjout.Visible = False
        lnkMisajour.Visible = False
        lnkFiltre.Visible = False
        ' відображається вікно [erreurs]
        vueErreurs.Visible = True
        vueFormulaire.Visible = False
        vueProduits.Visible = False
        vueAjout.Visible = False
    End Sub

Інші види запитуються на основі опцій, запропонованих користувачеві:

Image

Відображення виду [Ajout] запитується, коли користувач натискає на посилання [Ajout] на сторінці:


    Private Sub lnkAjout_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkAjout.Click
        ' відображається вид [ajout]
        afficheAjout()
    End Sub

    Private Sub afficheAjout()
        ' відображає вікно додавання
        vueAjout.Visible = True
        vueFormulaire.Visible = False
        vueProduits.Visible = False
        vueErreurs.Visible = False
    End Sub

Відображення подання [Formulaire] відбувається, коли користувач натискає на посилання [Filtrage] на сторінці. У цьому випадку має з’явитися подання, що дозволяє визначити

  • фільтр для таблиці товарів
  • кількість товарів, що відображаються на кожній веб-сторінці

Відображається форма, що містить значення, збережені в сесії:


    Private Sub lnkFiltre_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkFiltre.Click
        ' визначити умови фільтрації
        txtFiltre.Text = dvProduits.RowFilter
        ' визначити пагінацію
        txtPages.Text = CType(Session("nbProduitsPage"), String)
        ' відображається форма фільтрації
        afficheFormulaire()
    End Sub

Умова фільтрації подання визначається в його атрибуті [RowFilter]. Нагадаємо, що відфільтроване та розбиті на сторінки подання називається [dvProduits] і було отримано в сесії під час завантаження сторінки [Page_Load]. Отже, умова фільтрації отримується з [dvProduits.RowFilter]. Кількість товарів на сторінці також отримується з сесії. Якщо користувач ніколи не вказував цю інформацію, ця кількість дорівнює стандартній кількості товарів на сторінці за замовчуванням. Після цього відображається форма, що дозволяє вказати ці два параметри:


    Private Sub afficheFormulaire()
        ' відображається вид [formulaire]
        vueFormulaire.Visible = True
        vueErreurs.Visible = False
        vueProduits.Visible = False
        vueAjout.Visible = False
    End Sub

Image

9.6.10. Перевірка подання [formulaire]

Натискання на кнопку [Exécuter] вище обробляється за допомогою такої процедури:


    Private Sub btnExécuter_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnExécuter.Click
        ' сторінка дійсна?
        rfvLignes.Validate()
        rvLignes.Validate()
        If Not rfvLignes.IsValid Or Not rvLignes.IsValid Then
            afficheFormulaire()
            Exit Sub
        End If
        ' додавання відфільтрованих даних до таблиці
        Try
            dvProduits.RowFilter = txtFiltre.Text.Trim
        Catch ex As Exception
            lblinfo1.Text = "Erreur de filtrage (" + ex.Message + ")"
            afficheFormulaire()
            Exit Sub
        End Try
        ' запам'ятовується кількість товарів на сторінці
        Session("nbProduitsPage") = txtPages.Text
        'та поточну сторінку
        Session("pageCourante") = 0
        ' все гаразд — відображаються дані
        afficheProduits(0, CType(txtPages.Text, Integer))
    End Sub

Насамперед нагадаємо, що сторінка має два компоненти перевірки:

ім’я
тип
роль
7
rfvLignes
RequiredFieldValidator
перевіряє наявність значення в [txtPages]
8
rvLignes
RangeValidator
перевіряє, чи txtPages знаходиться в інтервалі [3,10]

Процедура починається з виконання коду перевірки двох вищезазначених компонентів за допомогою їхнього методу [Validate], а потім перевіряє значення їхнього атрибута [IsValid]. Це значення дорівнює [true] лише в тому випадку, якщо перевірені дані визнано дійсними. Якщо перевірка дійсності одного з двох компонентів завершилася невдало, знову відображається вікно [formulaire], щоб користувач міг виправити свою помилку або помилки. Умова фільтрації застосовується до атрибута [dvProduits.RowFilter]. Тут може виникнути виняток, якщо користувач вказав неправильний критерій фільтрації, як показано нижче:

Image

У цьому випадку знову відображається вікно [formulaire]. Якщо обидва введені параметри є правильними, у сесію записуються два значення:

  • кількість товарів на сторінці, обрана користувачем
  • номер сторінки, яка буде відображена — спочатку це сторінка 0

Ці два параметри використовуються щоразу, коли відображається вид [produits].

9.6.11. Відображення подання [produits]

Вигляд [produits] має такий вигляд:

Image

Ми маємо 5 компонентів [DataGrid], кожен з яких відображає окремий вигляд таблиці товарів [dtProduits]. Починаючи зліва:

назва
роль
DataGrid1
сітка відображення відфільтрованого виду таблиці товарів. Фільтр відповідає тому, що встановлено у вигляді [formulaire]. Також маємо .AllowPaging=true, .AllowSorting=true
DataGrid2
відображає повну таблицю товарів — дозволяє стежити за оновленнями
DataGrid3
відобразить видалені товари в таблиці товарів
DataGrid4
відобразить змінені товари у таблиці товарів
DataGrid5
відобразить товари, додані до таблиці товарів

Процедура [afficheProduits] відповідає за відображення попереднього виду:


    Private Sub afficheProduits(ByVal page As Integer, ByVal taillePage As Integer)
        ' дані прив'язуються до обох компонентів [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()
        ' відображається вид [produits]
        vueProduits.Visible = True
        vueFormulaire.Visible = False
        vueErreurs.Visible = False
        vueAjout.Visible = False
        ' зберігається поточна сторінка
        Session("pageCourante") = page
    End Sub

Процедура приймає два параметри:

  • номер сторінки [page], яку потрібно відобразити в [DataGrid1]
  • кількість товарів [taillePage] на сторінці

Зв'язок [DataGrid] з таблицею товарів здійснюється через 5 різних подань.

  • [DataGrid1] пов’язана з переглядом із пагінацією та сортуванням [dvProduits].

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

Саме під час зв’язування [DataGrid1] з його джерелом даних потрібні обидві величини, що передаються як параметри до процедури.

  • [DataGrid2] пов’язаний із поданням, що відображає всі поточні елементи таблиці [dtProduits]. Він, як і [DataGrid1], представляє оновлене подання таблиці [dtProduits], але без пагінації та сортування.

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

Тут ми використовуємо атрибут [RowStateFilter] класу [DataView]. Рядок таблиці або, в даному випадку, подання має атрибут [RowState], що вказує на стан рядка. Ось декілька прикладів:

  • (продовження)
    • [Added]: рядок було додано
    • [Modified]: рядок було змінено
    • [Deleted]: рядок було видалено
    • [Unchanged]: рядок не змінився

Коли таблиця [dtProduits] була спочатку створена в [Application_Start], усі її рядки перебували у стані [Unchanged]. Цей стан змінюватиметься у міру оновлень веб-клієнтів:

  • (продовження)
    • коли клієнт створює новий товар, до таблиці [dtProduits] додається рядок. Він матиме стан [Added].
    • коли товар змінюється, його стан змінюється з [Unchanged] на [Modified].
    • Коли товар видаляється, рядок фізично не видаляється. Натомість він позначається як такий, що підлягає видаленню, і його стан змінюється на [Deleted]. Це видалення можна скасувати.

Атрибут [RowStateFilter] класу [DataView] дозволяє фільтрувати перегляд за станом [RowState] його рядків. Його можливі значення відповідають переліку [DataRowViewState]:

  • (продовження)
    • DataRowViewState.CurrentRows: рядки, що не були видалені, відображаються з їхніми поточними значеннями
    • DataRowViewState.Added: додані рядки відображаються з їхніми поточними значеннями
    • DataRowViewState.Deleted: видалені рядки відображаються з їхніми початковими значеннями
    • DataRowViewState.ModifiedOriginal: змінені рядки відображаються разом із їхніми початковими значеннями
    • DataRowViewState.ModifiedCurrent: змінені рядки відображаються разом із їхніми поточними значеннями

Змінений рядок має два значення: початкове значення, яке рядок мав до першої зміни, та поточне значення, отримане після однієї або кількох змін. Ці два значення зберігаються одночасно.

DataGrid 2–5 відображають вигляд таблиці [dtProduits], відфільтрований за атрибутом [RowStateFilter]

DataGrid
Фільтр
DataGrid2
RowStateFilter= DataRowViewState.CurrentRows
DataGrid3
RowStateFilter= DataRowViewState.Deleted
DataGrid4
RowStateFilter= DataRowViewState.ModifiedOriginal
DataGrid5
RowStateFilter= DataRowViewState.Added

Таблиця [dtPoduits] оновлюється одночасно різними веб-клієнтами, що може призвести до конфліктів доступу до таблиці. Коли клієнт переглядає свої записи в таблиці [dtProduits], потрібно уникнути ситуації, коли він робить це в той час, коли таблиця перебуває в нестабільному стані через те, що її саме редагує інший веб-клієнт. Також щоразу, коли клієнту потрібна таблиця [dtProduits] для читання, як зазначено вище, або для запису під час додавання, редагування та видалення товарів, він синхронізується з іншими клієнтами за допомогою такої послідовності дій:

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

У коді додатка може бути кілька критичних ділянок:

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

Механізм роботи такий:

  1. один або кілька клієнтів доходять до інструкції [Application.Lock] у критичній секції 1. Це дистриб’ютор, що видає єдиний вхідний токен. Цей токен отримує лише один клієнт. Назвемо його C1.
  2. доки клієнт C1 не поверне токен через [Application.Unlock], жоден інший клієнт не має права входити в критичну секцію, контрольовану [Application.Lock]. Отже, у наведеному вище прикладі жоден клієнт не може увійти до критичних секцій 1 і 2.
  3. Клієнт C1 виконує [Application.Unlock] і, отже, повертає токен доступу. Після цього цей токен може бути переданий іншому клієнту. Етапи 1–3 повторюються.

Завдяки цьому механізму ми гарантуємо, що лише один клієнт матиме доступ до таблиці [dtProduits] — як для читання, так і для запису.

Посилання [Mise à jour] також відображає представлення [Produits]:

Image

Відповідна процедура така:


    Private Sub lnkMisajour_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkMisajour.Click
        ' відображення перегляду товарів
        afficheProduits(CType(Session("pageCourante"), Integer), CType(Session("nbProduitsPage"), Integer))
    End Sub

Необхідно відобразити вікно [produits]. Це здійснюється за допомогою процедури [afficheProduits], яка приймає два параметри: номер поточної сторінки, яку потрібно відобразити, та кількість товарів на сторінці. Ці два параметри містяться в сесії, можливо, зі своїми початковими значеннями, якщо користувач ніколи не задавав їх самостійно.

9.6.12. Пагінація та сортування за допомогою [DataGrid1]

Ми вже знайомилися з процедурами, що керують пагінацією та сортуванням [DataGrid], в іншому прикладі. У разі зміни сторінки ми відображаємо представлення [produits], передаючи номер нової сторінки як параметр процедурі [afficheProduits].


    Private Sub DataGrid1_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles DataGrid1.PageIndexChanged
        ' перехід на іншу сторінку
        afficheProduits(e.NewPageIndex, DataGrid1.PageSize)
    End Sub

Процедура [DataGrid1_SortCommand] виконується, коли користувач клацає на заголовку одного зі стовпців [DataGrid1]. У цьому випадку до атрибуту [Sort] подання [dvProduits], яке відображається процедурою [DataGrid1], слід присвоїти вираз сортування. Цей вираз синтаксично еквівалентний виразу сортування, розміщеному після клаузули [order by] в інструкції SQL SELECT. Аргумент [e] процедури має атрибут [SortExpression], який надає нам вираз сортування, пов’язаний із стовпцем, заголовок якого було натиснуто. Під час створення компонента [DataGrid1] цей вираз сортування було визначено. Нижче наведено вираз, визначений для стовпця [nom] компонента [DataGrid1]:

Image

Якщо вираз сортування не було визначено під час проектування [DataGrid], використовується назва поля даних, пов’язаного зі стовпцем [DataGrid]. Напрямок сортування (за зростанням, за спаданням) тут встановлюється користувачем за допомогою перемикачів [rdCroissant, rd Décroissant]:

Image

Процедура сортування така:


    Private Sub DataGrid1_SortCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridSortCommandEventArgs) Handles DataGrid1.SortCommand
        ' сортування датвью
        With dvProduits
            .Sort = e.SortExpression + " " + CType(IIf(rdCroissant.Checked, "asc", "desc"), String)
        End With
        ' відображення
        afficheProduits(0, DataGrid1.PageSize)
    End Sub

9.6.13. Видалення товару

Щоб видалити товар, користувач використовує посилання [Supprimer] у рядку товару:

Image

Після цього виконується процедура [DataGrid1_DeleteCommand]:


    Private Sub DataGrid1_DeleteCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.DeleteCommand
        ' видалення товару
        ' ключ товару, який потрібно видалити
        Dim idProduit As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)
        ' видалення товару
        Dim erreur As Boolean = False
        Try
            supprimerProduit(idProduit)
        Catch ex As Exception
            ' проблема
            lblInfo2.Text = ex.Message
            erreur = True
        End Try
        ' зміна сторінки?
        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
        ' перегляд товарів
        afficheProduits(page, DataGrid1.PageSize)
    End Sub

Оновлення продуктів відбуватиметься за допомогою ключа [id] продукту. Дійсно, стовпець [id] у таблиці [dtProduits] є первинним ключем, і завдяки йому ми можемо знайти в таблиці [dtProduits] рядок продукту, який оновлюється в компоненті [DataGrid1]. Отже, процедура починається з отримання ключа товару, посилання на який ([Supprimer]) було натиснуто:


         ' ключ товару, який потрібно видалити
        Dim idProduit As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)

Ми знаємо, що [e.Item] є елементом [DataGrid1], який спричинив подію. Загалом цей елемент є рядком у [DataGrid]. [e.Item.ItemIndex] — це номер рядка, що спричинив подію. Цей індекс є відносним до поточної сторінки, що відображається. Таким чином, перший рядок відображеної сторінки має властивість [ItemIndex=0], навіть якщо він має номер 17 у таблиці товарів. [DataGrid1.DataKeys] — це список ключів [DataGrid]. Оскільки під час проектування ми вказали [DataGrid1.DataKey=id], ключі [DataGrid1] складаються зі значень стовпця [id] таблиці [dtProduits], який одночасно є первинним ключем. Отже, [DataGrid1.DataKeys(e.Item.ItemIndex)] є ключем [id] продукту, який потрібно видалити. Отримавши цей ключ, ми подаємо запит на його видалення за допомогою процедури [supprimerProduit]:


         ' видалення товару
        Dim erreur As Boolean = False
        Try
            supprimerProduit(idProduit)
        Catch ex As Exception
            ' проблема
            lblInfo2.Text = ex.Message
            erreur = True
        End Try

У деяких випадках це видалення може завершитися невдачею. Ми розглянемо, чому. У такому разі генерується виняток. Він обробляється тут. У разі помилки [DataGrid1] не потрібно змінювати. До подання [produits] додається лише повідомлення про помилку. Якщо помилки немає і користувач щойно видалив єдиний товар з поточної сторінки, то відображається попередня сторінка.


         ' зміна сторінки?
        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

У будь-якому випадку, погляд [produits] знову відображається за допомогою процедури [afficheProduits]:

         ' перегляд товарів
        afficheProduits(page, DataGrid1.PageSize)

Процедура [supprimerProduit] виглядає наступним чином:


    Private Sub supprimerProduit(ByVal idProduit As Integer)
        Dim erreur As String
        Try
            ' синхронізація
            Application.Lock()
            ' пошук рядка для видалення
            Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
            If ligne Is Nothing Then
                erreur = String.Format("Produit [{0}] inexistant", idProduit)
            Else
                ' видалення рядка
                ligne.Delete()
            End If
        Catch ex As Exception
            erreur = String.Format("Erreur de suppression : {0}", ex.Message)
        Finally
            ' завершення синхронізації
            Application.UnLock()
        End Try
        ' у разі помилки генерується виняток
        If erreur <> String.Empty Then Throw New Exception(erreur)
    End Sub

Процедура отримує як параметр ключ продукту, який потрібно видалити. Якби оновлення виконував лише один клієнт, це видалення можна було б здійснити наступним чином:

             ' видалення рядка [idProduit]
            dtProduits.Rows.Find(idProduit).Delete()

Якщо оновлення виконують одночасно кілька клієнтів, ситуація ускладнюється. Розглянемо таку послідовність подій:

Час
Дія
T1
клієнт A зчитує таблицю [dtProduits] — там є запис із ключем 20
T2
клієнт B зчитує таблицю [dtProduits] — там є товар із ключем 20
T3
клієнт A видаляє товар із ключем 20
T4
клієнт B видаляє товар із ключем 20

Коли клієнти A та B читають таблицю товарів і відображають її вміст на веб-сторінці, таблиця містить товар із ключем 20. Тому вони можуть захотіти виконати якусь дію щодо цього товару, наприклад, видалити його. Обов’язково хтось із них зробить це першим. Той, хто підійде наступним, захоче видалити товар, якого вже не існує. Цей випадок враховується процедурою [supprimerProduits] кількома способами:

  • по-перше, відбувається синхронізація з процедурою [Application.Lock]. Це означає, що коли один клієнт проходить цей бар’єр, ніхто інший не може змінювати або читати таблицю товарів. Адже всі ці операції синхронізуються саме таким чином. Ми вже розглядали це на прикладі читання.
  • Потім перевіряється, чи існує товар, який потрібно видалити. Якщо так, його видаляють, інакше формується повідомлення про помилку.

             ' пошук рядка для видалення
            Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
            If ligne Is Nothing Then
                erreur = String.Format("Produit [{0}] inexistant", idProduit)
            Else
                 ' видалення рядка
                ligne.Delete()
            End If
  • виходимо з критичної послідовності за допомогою [Application.Unlock], щоб інші клієнти могли виконати свої оновлення.
  • якщо рядок не вдалося видалити, процедура генерує виняток із відповідним повідомленням про помилку.

Розглянемо приклад. Ми запускаємо клієнт [Mozilla] і відразу ж вибираємо опцію [Mise à jour] (частковий вигляд):

Image

Те саме робимо з клієнтом [Internet Explorer] (частковий вигляд):

Image

Для клієнта [Mozilla] ми видаляємо товар [produit2]. Отримуємо таку нову сторінку:

Image

Операція видалення пройшла успішно. Видалений продукт [DataGrid] відображається у списку видалених продуктів і більше не з’являється у списку продуктів компонентів [DataGrid1] та [DataGrid2], які відображають продукти, що наразі присутні в таблиці. Зробимо те саме з [Interbet Explorer]. Видаляємо елемент [produit2]:

Image

Отримано такий результат:

Image

Повідомлення про помилку повідомляє користувачеві, що товар із ключем [2] не існує. Відповідь повертає клієнту нове представлення, що відображає поточний стан таблиці. Ми бачимо, що товар із ключем [2] дійсно знаходиться у списку видалених товарів. Отже, представлення [produits] відображає оновлення всіх клієнтів, а не лише одного.

9.6.14. Додавання товару

Щоб додати товар, користувач використовує посилання [Ajout] у меню опцій. Воно просто відображає представлення [Ajout]:

Image

Image

Ця дія включає дві процедури:


    Private Sub lnkAjout_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkAjout.Click
        ' виводиться вікно [ajout]
        afficheAjout()
    End Sub

    Private Sub afficheAjout()
        ' відображення вікна додавання
        vueAjout.Visible = True
        vueFormulaire.Visible = False
        vueProduits.Visible = False
        vueErreurs.Visible = False
    End Sub

Нагадаємо, що погляд [Ajout] має такі компоненти перевірки:

ім’я
тип
роль
rfvNom
RequiredFieldValidator
перевіряє наявність значення в [txtNom]
rfvPrix
RequiredFieldValidator
перевіряє наявність значення в [txtPrix]
cvPrix
CompareValidator
перевіряє, чи ціна >= 0

Процедура обробки натискання кнопки [Ajouter] є такою:


    Private Sub btnAjouter_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnAjouter.Click
        ' додавання нового елемента до таблиці товарів
        ' спочатку дані мають бути правильними
        rfvNom.Validate()
        rfvPrix.Validate()
        cvPrix.Validate()
        If Not rfvNom.IsValid Or Not rfvPrix.IsValid Or Not cvPrix.IsValid Then
            ' знову форма введення даних
            afficheAjout()
            Exit Sub
        End If
        ' створюємо рядок
        Dim produit As DataRow = dtProduits.NewRow
        produit("nom") = txtNom.Text.Trim
        produit("prix") = txtPrix.Text.Trim
        ' додаємо рядок до таблиці
        Application.Lock()
        Try
            dtProduits.Rows.Add(produit)
            lblInfo3.Text = "Ajout réussi"
            ' очищення
            txtNom.Text = ""
            txtPrix.Text = ""
        Catch ex As Exception
            lblInfo3.Text = String.Format("Erreur : {0}", ex.Message)
        End Try
        Application.UnLock()
    End Sub

Спочатку виконується перевірка валідності трьох компонентів [rfvNom, rfvPrix, cvPrix]. Якщо хоча б одна з перевірок завершиться невдало, знову відображається вікно [Ajout] із повідомленнями про помилки компонентів валідації. Ось приклад:

Image

Якщо дані є правильними, готується рядок для вставки в таблицю [dtProduits].


         ' створюємо рядок
        Dim produit As DataRow = dtProduits.NewRow
        produit("nom") = txtNom.Text.Trim
        produit("prix") = txtPrix.Text.Trim

Спочатку за допомогою методу [DataTable.NewRow] створюється новий рядок у таблиці [dtProduits]. Цей рядок міститиме три стовпці [id, nom, prix] з таблиці [dtProduits]. Столбці [nom, prix] заповнюються значеннями, введеними у форму додавання. Столбець [id] не заповнюється. Цей стовпець має тип [AutoIncrement], c.a.d, причому SGBD буде ключем для нового товару — це максимальний існуючий ключ, збільшений на 1. Створений тут рядок [produit] відокремлений від таблиці [dtProduits]. Тепер нам потрібно вставити його в цю таблицю. Оскільки ми будемо оновлювати таблицю [dtProduits], активуємо міжклієнтську синхронізацію:

        Application.Lock()

Зробивши це, ми намагаємося додати рядок до таблиці [dtProduits]. Якщо це вдається, ми виводимо повідомлення про успіх, інакше — повідомлення про помилку.

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

Зазвичай винятків бути не може. Проте, з міркувань обережності, було впроваджено механізм обробки винятків. Після додавання даних ми виходимо з критичного розділу за допомогою [Application.Unlock].

9.6.15. Редагування товару

Щоб змінити товар, користувач використовує посилання [Modifier] у рядку товару:

Image

Після цього ми переходимо в режим редагування:

Image

Завдяки компоненту [DataGrid] цей результат досягається за допомогою мінімальної кількості коду:


    Private Sub DataGrid1_EditCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.EditCommand
        ' переводимо поточний елемент у режим редагування
        DataGrid1.EditItemIndex = e.Item.ItemIndex
        ' повторно відображаються товари
        afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
    End Sub

Натискання на посилання [Modifier] призводить до виконання на стороні сервера процедури [DataGrid1_EditCommand]. Компонент [DataGrid1] має поле [EditItemIndex]. Рядок, індекс якого дорівнює значенню [EditItemIndex], переводиться в режим редагування. Кожне значення такого оновленого рядка можна змінити у полі введення, як показано на знімку екрана вище. Отже, ми отримуємо індекс товару, на якому відбулося натискання на посилання [Modifier], і присвоюємо його атрибуту [EditItemIndex] компонента [DataGrid1]. Залишається лише знову відобразити вікно [produits]. Воно виглядатиме так само, як і раніше, але з одним рядком у режимі редагування.

Посилання [Annuler] для продукту, що редагується, дозволяє користувачеві скасувати його оновлення. Процедура, пов’язана з цим посиланням, така:


    Private Sub DataGrid1_CancelCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.CancelCommand
        ' повторно відображаються товари
        afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
    End Sub

Вона просто повторно відображає вікно [produits]. Може виникнути спокуса ввести -1 у [DataGrid1.EditItemIndex], щоб скасувати цей режим оновлення. Насправді ми знаємо, що процедура [Page_Load] робить це автоматично. Тому немає потреби повторювати цю дію.

Зміна підтверджується за допомогою посилання [Mettre à jour] у рядку, що редагується. Після цього виконується наступна процедура:


    Private Sub DataGrid1_UpdateCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.UpdateCommand
        ' ключ товару, який потрібно змінити
        Dim idProduit As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)
        ' змінені елементи
        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
        ' чи зміни дійсні?
        lblInfo2.Text = ""
        If nom = String.Empty Then lblInfo2.Text += "[Indiquez un nom]"
        If prix = String.Empty Then
            lblInfo2.Text += "[Indiquez un prix]"
        Else
            Dim nouveauPrix As Double
            Try
                nouveauPrix = CType(prix, Double)
                If nouveauPrix < 0 Then Throw New Exception
            Catch ex As Exception
                lblInfo2.Text += "[prix invalide]"
            End Try
        End If
        '  якщо помилка, перезавантажуємо сторінку оновлення
        If lblInfo2.Text <> String.Empty Then
            ' повернути рядок у режим оновлення
            DataGrid1.EditItemIndex = e.Item.ItemIndex
            ' відображення товарів
            afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
            ' кінець
            Exit Sub
        End If
        ' якщо помилки немає — змінюємо таблицю
        Try
            modifierProduit(idProduit, e.Item)
        Catch ex As Exception
            ' проблема
            lblInfo2.Text = ex.Message
        End Try
        ' відображення товарів
        afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
    End Sub

Як і у випадку видалення, нам потрібно отримати ключ продукту, який потрібно змінити, щоб знайти його рядок у таблиці продуктів [dtProduits]:


         ' ключ товару, що підлягає зміні
        Dim idProduit As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)

Далі нам потрібно отримати нові значення, які слід присвоїти цьому рядку. Вони містяться в компоненті [DataGrid1]. Аргумент [e] процедури призначений саме для цього. [e.Item] представляє рядок [DataGrid1], який є джерелом події. Отже, це рядок, що оновлюється, оскільки посилання [Mettre à jour] існує лише в цьому рядку. Цей рядок містить стовпці, позначені колекцією [Cells] цього рядка. Таким чином, [e.Item.Cells(0)] представляє стовпець 0 оновлюваного рядка. Ми знаємо, що нові значення знаходяться у полях введення. Колекція [e.Item.Cells(i).Controls] представляє колекцію елементів управління стовпця i рядка [e.Item]. Дві наступні інструкції отримують значення полів введення оновленого рядка в [DataGrid1]:


         ' змінені елементи
        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

Отже, ми маємо нові значення зміненого рядка у вигляді символьних рядків. Далі ми перевіряємо, чи є ці дані дійсними. Назва має бути не порожньою, а ціна — додатним числом або нулем:


         ' чи зміни дійсні?
        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

У разі помилки мітка [lblInfo2] відобразить повідомлення про помилку, і сторінка просто перезавантажиться:


          ' якщо помилка, перезавантажуємо сторінку оновлення
        If lblInfo2.Text <> String.Empty Then
            ' повернути рядок у режим оновлення
            DataGrid1.EditItemIndex = e.Item.ItemIndex
            ' відображення товарів
            afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
             ' кінець
            Exit Sub
        End If

У наведеному вище коді не показано, що введені значення втрачаються. Справа в тому, що [DataGrid1] пов'язаний з даними таблиці [dtProduits], яка містить вихідні значення зміненого рядка. Ось приклад.

Image

Отримано такий результат:

Image

Бачимо, що втрачено значення, введені спочатку. У професійному додатку це, ймовірно, було б неприйнятним. Тут ми стикаємося з певними обмеженнями стандартного режиму оновлення компонента [DataGrid]. Було б краще мати погляд [Modification], аналогічний погляду [Ajout].

Якщо дані є дійсними, вони використовуються для оновлення таблиці [dtProduits]:


         ' якщо помилки немає — змінюємо таблицю
        Try
            modifierProduit(idProduit, e.Item)
        Catch ex As Exception
            ' проблема
            lblInfo2.Text = ex.Message
        End Try
        ' відображення товарів
        afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)

З тієї ж причини, що згадувалася під час видалення товару, зміна може завершитися невдачею. Адже між моментом, коли клієнт прочитав таблицю [dtProduits], і моментом, коли він збирається змінити один зі своїх товарів, цей товар міг бути видалений іншим клієнтом. Процедура [modifierProduit] обробляє цей випадок, генеруючи виняток. Цей виняток тут обробляється. Після успішного або невдалого оновлення додаток повертає клієнту представлення [produits]. Залишається з’ясувати, як процедура [modifierProduit] здійснює оновлення:


    Private Sub modifierProduit(ByVal idProduit As Integer, ByVal item As DataGridItem)
        Dim erreur As String
        Try
            ' синхронізація
            Application.Lock()
            ' пошук рядка для редагування
            Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
            If ligne Is Nothing Then
                erreur = String.Format("Produit [{0}] inexistant", idProduit)
            Else
                ' змінюємо рядок
                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
            ' завершення синхронізації
            Application.UnLock()
        End Try
        ' у разі помилки генерується виняток
        If erreur <> String.Empty Then Throw New Exception(erreur)
    End Sub

Ми не будемо вдаватися в подробиці цієї процедури, код якої аналогічний коду процедури [supprimerProduit], що вже було докладно пояснено. Розглянемо лише два приклади. Спочатку змінимо ціну товару [produit1]:

Image

Використання наведеного вище посилання [Mettre à jour] дає такий результат:

Image

Зміна дійсно відображена в компонентах [DataGrid] 1 і 2, які відображають поточний стан таблиці [dtProduits]. Вона також присутня в компоненті [DataGrid4], який є переглядом змінених рядків, що відображаються разом із їхніми початковими значеннями. Тепер розглянемо випадок конфлікту доступу. Як і в прикладі видалення, ми будемо використовувати два різних веб-клієнти. Клієнт [Mozilla] зчитує таблицю [dtProduits] і переходить у режим редагування продукту [produit1]:

Image

Клієнт [Internet Explorer] збирається видалити товар [produit1]:

Image

Клієнт [Internet Explorer] видаляє [produit1]:

Image

Слід зауважити, що [produit1] більше не знаходиться в таблиці змінених рядків, а в таблиці видалених рядків. Клієнт [Mozilla] підтверджує його оновлення:

Image

Клієнт [Mozilla] отримує таку відповідь на своє оновлення:

Image

Він може переконатися, що [produit1] було видалено, оскільки воно знаходиться у списку видалених продуктів.

9.7. Веб-додаток для оновлення фізичної таблиці продуктів

9.7.1. Пропоновані рішення

Попередній приклад був скоріше навчальним, покликаним продемонструвати управління об’єктом [DataTable] у кеші, ніж типовим випадком. Адже рано чи пізно доводиться оновлювати фактичне джерело даних. Можна обрати дві різні стратегії:

  1. використовувати кеш [dtProduits] у пам’яті для оновлення джерела даних. Можна передбачити сторінку, розташовану в веб-дереві попереднього додатка, щоб отримати доступ до його кешу [dtProduits]. Ця сторінка надавала б адміністратору можливість відобразити у фізичному джерелі даних зміни, внесені в кеш [dtProduits]. Для цього можна додати новий метод до класу доступу [produits], який приймав би як параметр кеш [dtProduits] і за допомогою цього кешу оновлював би фізичне джерело даних.
  2. Фізичне джерело даних оновлюється одночасно з кешем.

Стратегія № 1 дозволяє відкривати лише одне з’єднання з фізичним джерелом даних. Стратегія № 2 вимагає з’єднання під час кожного оновлення. Залежно від доступності з’єднань можна віддати перевагу тій чи іншій стратегії. Оскільки ми маємо інструменти для її реалізації (клас [produits]), ми обираємо стратегію № 2.

9.7.2. Рішення 1

З метою забезпечення сумісності з попереднім додатком ми обираємо таку стратегію:

  • фізичне джерело оновлюється одночасно з кешем
  • кеш формується лише один раз під час ініціалізації додатка в [global.asax.vb]. Це означає, що якщо фізичне джерело даних оновлюється іншими клієнтами, крім веб-клієнтів, останні не бачать цих змін. Вони бачать лише ті зміни, які самі вносять до таблиці в кеші.

Щоб оновити фізичне джерело даних, нам потрібен екземпляр класу доступу до товарів. Кожен клієнт міг би мати свій власний екземпляр. Також можна використовувати спільний екземпляр, який створюватиметься додатком під час його запуску. Саме це рішення ми обираємо в даному випадку. Код модуля управління [global.asax.vb] змінюється наступним чином:


Imports System
Imports System.Web
...

Public Class Global
    Inherits System.Web.HttpApplication

    Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' отримано інформацію про конфігурацію
        Dim chaînedeConnexion As String = ConfigurationSettings.AppSettings("OLEDBStringConnection")
        Dim defaultProduitsPage As String = ConfigurationSettings.AppSettings("defaultProduitsPage")
        Dim erreurs As New ArrayList
...
        ' тут немає помилок конфігурації
        ' створюється об’єкт «продукти»
        Dim objProduits As New produits(chaînedeConnexion)
        Dim dtProduits As DataTable
        Try
            dtProduits = objProduits.getProduits
        Catch ex As ExceptionProduits
            'сталася помилка доступу до товарів, це фіксується в додатку
            Application("erreurs") = ex.erreurs
            Exit Sub
        Catch ex As Exception
            ' необроблена помилка
            erreurs.Add(ex.Message)
            Application("erreurs") = erreurs
            ' вихід із підпрограми
        End Try
        ' тут немає помилок ініціалізації
...
        ' зберігаємо екземпляр доступу до даних
        Application("objProduits") = objProduits
    End Sub

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

Екземпляр класу доступу до даних було збережено в додатку, пов’язаному з ключем [objProduits]. Кожен клієнт використовуватиме цей екземпляр для доступу до фізичного джерела даних. Його буде отримано в процедурі [Page_Load] з [main.aspx.vb]:


    Protected objProduits As produits
....
    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
        ' отримано екземпляр доступу до товарів
        objProduits = CType(Application("objProduits"), produits)
...
    End Sub

Інстанція доступу до даних [objProduits] доступна для всіх методів сторінки. Вона буде використовуватися для трьох операцій оновлення: додавання, видалення, редагування.

Процедура додавання змінюється наступним чином:


    Private Sub btnAjouter_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnAjouter.Click
...
        ' додаємо рядок
        Application.Lock()
        Try
            ' додаємо рядок до таблиці в кеші
            dtProduits.Rows.Add(produit)
             ' додається рядок до фізичного джерела
            Dim nouveauProduit As sProduit
            With nouveauProduit
                .nom = CType(produit("nom"), String)
                .prix = CType(produit("prix"), Double)
            End With
            objProduits.ajouterProduit(nouveauProduit)
            ' моніторинг
            lblInfo3.Text = "Ajout réussi"
            ' очищення
            txtNom.Text = ""
            txtPrix.Text = ""
        Catch ex As Exception
            ' помилка
            lblInfo3.Text = String.Format("Erreur : {0}", ex.Message)
        End Try
        Application.UnLock()
    End Sub

Процедура внесення змін змінюється наступним чином:


    Private Sub modifierProduit(ByVal idProduit As Integer, ByVal item As DataGridItem)
        Dim erreur As String
        Try
            ' синхронізація
            Application.Lock()
            ' пошук рядка, який потрібно змінити
            Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
            If ligne Is Nothing Then
                erreur = String.Format("Produit [{0}] inexistant", idProduit)
            Else
                ' зміна рядка в кеші
                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
                 ' зміна рядка у фізичному джерелі
                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
            ' завершення синхронізації
            Application.UnLock()
        End Try
        ' у разі помилки генерується виняток
        If erreur <> String.Empty Then Throw New Exception(erreur)
    End Sub

Процедура видалення змінюється наступним чином:


    Private Sub supprimerProduit(ByVal idProduit As Integer)
        Dim erreur As String
        Try
            ' синхронізація
            Application.Lock()
            ' пошук рядка для видалення
            Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
            If ligne Is Nothing Then
                erreur = String.Format("Produit [{0}] inexistant", idProduit)
            Else
                ' видалення рядка з кешу
                ligne.Delete()
                 ' видалення рядка з фізичного джерела
                objProduits.supprimerProduit(idProduit)
            End If
        Catch ex As Exception
            erreur = String.Format("Erreur de suppression : {0}", ex.Message)
        Finally
            ' завершення синхронізації
            Application.UnLock()
        End Try
        ' у разі помилки генерується виняток
        If erreur <> String.Empty Then Throw New Exception(erreur)
    End Sub

9.7.3. Тести

Ми виходимо з наступної таблиці даних у файлі ACCESS:

Image

Запускається веб-клієнт:

Image

Ми додаємо товар:

Image

Видаляємо [produit1]:

Image

Ми змінюємо ціну на [produit2]:

Image

Після цих змін переглядаємо вміст таблиці [liste] у базі даних ACCESS:

Image

Усі три зміни були правильно відображені у фізичній таблиці. Тепер розглянемо випадок конфлікту. Ми видаляємо, безпосередньо під записом ACCESS, рядок [produit2]:

Image

Повернемося до нашого веб-клієнта. Він не бачить, що запис було видалено, і хоче, у свою чергу, видалити [produit2]:

Image

Він отримує таку відповідь:

Image

Рядок [produit2] дійсно було видалено з кешу, як вказує список видалених продуктів. Натомість видалення [produit2] у фізичному джерелі завершилося невдачею, як вказує повідомлення про помилку.

9.7.4. Рішення 2

У попередньому рішенні веб-клієнти одночасно оновлюють фізичне джерело даних, але не бачать змін, внесених іншими клієнтами. Вони бачать лише свої зміни. Тепер ми хочемо, щоб клієнт міг бачити фізичне джерело даних таким, яким воно є на даний момент, а не таким, яким воно було під час запуску додатка. Для цього ми запропонуємо клієнту нову опцію:

Image

За допомогою опції [Rafraîchir] клієнт примусово ініціює повторне зчитування фізичного джерела даних. Щоб це не вплинуло на інших клієнтів, таблиця, отримана в результаті цього зчитування, має належати клієнту, який здійснює оновлення, і не повинна бути спільною для інших клієнтів. Це перша відмінність від попереднього додатка. Кеш [dtProduits] джерела даних буде створюватися кожним клієнтом, а не самою програмою. Зміна вноситься у [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)
        ' отримано інформацію про конфігурацію
...
        ' тут немає помилок конфігурації
        ' створюється об’єкт «товари»
        Dim objProduits As New produits(chaînedeConnexion)
        ' зберігається кількість товарів на сторінці
        Application("defaultProduitsPage") = defaultProduitsPage
        ' зберігається екземпляр доступу до даних
        Application("objProduits") = objProduits
    End Sub

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' ініціалізація змінних сесії
        If IsNothing(Application("erreurs")) Then
            ' закешовується джерело даних
            Dim dtProduits As DataTable
            Try
                Application.Lock()
                dtProduits = CType(Application("objProduits"), produits).getProduits
            Catch ex As ExceptionProduits
                'сталася помилка доступу до товарів, це фіксується в сесії
                Session("erreurs") = ex.erreurs
                Exit Sub
            Finally
                Application.UnLock()
            End Try
            ' кеш [dtProduits] додано до сесії
            Session("dtProduits") = dtProduits
            ' перегляд таблиці товарів
            Session("dvProduits") = dtProduits.DefaultView
            ' кількість товарів на сторінці
            Session("nbProduitsPage") = Application("defaultProduitsPage")
            ' поточна сторінка, що відображається
            Session("pageCourante") = 0
        End If
    End Sub

End Class

Інформація, що зберігається в сесії, отримується при кожному запиті в процедурі [Page_Load]:


     ' дані сторінки
    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
        ' перевіряється, чи є помилки в роботі додатка
        If Not IsNothing(Application("erreurs")) Then
            ' додаток не ініціалізувався належним чином
            afficheErreurs(CType(Application("erreurs"), ArrayList))
            Exit Sub
        End If
        ' перевіряється, чи є помилка в сесії
        If Not IsNothing(Session("erreurs")) Then
            ' сесія не ініціалізувалася належним чином
            afficheErreurs(CType(Session("erreurs"), ArrayList))
            Exit Sub
        End If
        ' отримується посилання на таблицю товарів
        dtProduits = CType(Session("dtProduits"), DataTable)
        ' отримується перегляд товарів
        dvProduits = CType(Session("dvProduits"), DataView)
        ' отримуємо кількість товарів на сторінку
        nbProduitsPage = CType(Session("nbProduitsPage"), Integer)
        ' отримано поточну сторінку
        pageCourante = CType(Session("pageCourante"), Integer)
        ' отримуємо екземпляр доступу до товарів
        objProduits = CType(Application("objProduits"), produits)
        ' скасовується можливе оновлення, що виконується
        DataGrid1.EditItemIndex = -1
        'перший запит
        If Not IsPostBack Then
            ' відображається початкова форма
            txtPages.Text = nbProduitsPage.ToString
            afficheFormulaire()
        End If
    End Sub

Інформація, отримана під час сеансу, зберігатиметься в цьому ж кінці сеансу після кожного завершення запиту:


    Private Sub Page_PreRender(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.PreRender
        ' зберігаємо певну інформацію в сесії
        Session("dtProduits") = dtProduits
        Session("dvProduits") = dvProduits
        Session("nbProduitsPage") = nbProduitsPage
        Session("pageCourante") = pageCourante
    End Sub

Подія [PreRender] сигналізує про те, що відповідь буде надіслана клієнту. Користуючись нагодою, ми зберігаємо в сесії всі дані, які потрібно зберегти. Це надмірний захід, оскільки досить часто змінюються лише значення деяких із них. Таке систематичне збереження має ту перевагу, що звільняє нас від управління сесією в інших методах сторінки.

Оновлення кешу здійснюється за допомогою такої процедури:


    Private Sub lnkRefresh_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkRefresh.Click
        ' необхідно оновити кеш [dtProduits] з фізичного джерела даних
        ' початок синхронізації
        Application.Lock()
        Try
            ' таблиця товарів
            dtProduits = CType(Application("objProduits"), produits).getProduits
            ' зберігаємо поточний фільтр
            Dim filtre As String = dvProduits.RowFilter
            ' створено нове відфільтроване представлення
            dvProduits = New DataView(dtProduits)
            ' відновлюємо фільтр
            dvProduits.RowFilter = filtre
        Catch ex As ExceptionProduits
            'сталася помилка доступу до товарів, це фіксується в сесії
            Session("erreurs") = ex.erreurs
            ' відображається перегляд [erreurs]
            afficheErreurs(ex.erreurs)
            ' завершено
            Exit Sub
        Finally
            ' завершено синхронізацію
            Application.UnLock()
        End Try
        ' все пройшло успішно — відображаємо товари, починаючи з першої сторінки
        afficheProduits(0, nbProduitsPage)
    End Sub

Процедура генерує нові значення для кешу [dtProduits] та подання [dvProduits]. Вони будуть розміщені в сесії процедурою [Page_PreRender], описаною вище. Після відновлення кешу [dtProduits] товари відображаються, починаючи з першої сторінки.

Ось приклад виконання. Запускається клієнт [mozilla], який відображає товари:

Image

Клієнт [Internet Explorer] робить те саме:

Image

Клієнт [Mozilla] видаляє [produit1], змінює [produit2] та додає новий товар. У результаті отримує таку нову сторінку:

Image

Клієнт [Internet Explorer] хоче видалити [produit1].

Image

Він отримує таку відповідь:

Image

Йому повідомили, що [produit1] більше не існує. Тоді користувач вирішує оновити свій кеш за допомогою наведеного вище посилання [Rafraîchir]. Він отримує таку відповідь:

Image

Тепер він має те саме джерело даних, що й клієнт [Mozilla].

9.8. Conclusion

У цьому розділі ми приділили багато уваги контейнерам даних та їхнім зв’язкам із джерелами даних. На завершення ми показали, як можна оновити джерело даних за допомогою компонента [DataGrid]. Як джерело даних ми взяли таблицю бази даних. Хоча компонент [DataGrid] дещо спрощує представлення даних, справжня складність полягає не в частині візуалізації, а саме в управлінні оновленнями джерела даних, що здійснюються різними клієнтами. Можуть виникати конфлікти доступу, які необхідно вирішувати. У даному випадку ми вирішили їх у контролері за допомогою компонента [Application.Lock]. Ймовірно, розумніше було б синхронізувати доступ до джерела даних у класі доступу до них, щоб контролер не мав займатися такими деталями, які не входять до його компетенції.

На практиці таблиці бази даних пов’язані між собою відношеннями, і їх оновлення має враховувати ці відношення. Це, насамперед, впливає на клас доступу до даних, який стає складнішим, ніж той, що необхідний для окремої таблиці. Зазвичай це також впливає на презентаційну частину веб-додатку, оскільки часто потрібно відображати не одну таблицю, а таблиці, пов’язані між собою відношеннями.

Крім того, у цьому розділі було представлено різні структури даних, такі як [DataTable, DataView].