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] із трьома стовпцями:
назва | тип | роль |
рядок | первинний ключ | |
рядок | назва теми списку | |
рядок | опис тем, що розглядаються у списку |
Щоб не перевантажувати наш приклад, об’єкт [DataTable], наведений вище, буде сформовано програмно довільним чином. У реальному додатку його, ймовірно, надаватиме метод класу доступу до даних. Формування таблиці списків відбуватиметься у процедурі [Application_Start], а отримана таблиця буде розміщена в додатку. Ми назвемо її таблицею [dtThèmes]. Користувач підпишеться на деякі теми з цієї таблиці. Список його підписок також зберігатиметься в об’єкті [DataTable], який називається [dtAbonnements] і має таку структуру:
ім’я | тип | роль |
рядок | первинний ключ | |
рядок | назва теми списку |
Односторінковий додаток виглядає наступним чином:
![]() |
№ | назва | тип | властивості | роль |
DataGrid | списки розсилки, доступні для підписки | |||
DataList | список підписок користувача на вищезазначені списки | |||
панель | панель інформації про тему, обрану користувачем, із посиланням [Plus d'informations] | |||
Етикетка | є частиною [panelInfos] | назва теми | ||
Етикетка | є частиною [panelInfos] | опис теми | ||
Етикетка | інформаційне повідомлення програми |
Наш приклад має на меті проілюструвати використання компонентів [DataGrid] та [DataList], зокрема управління подіями, що відбуваються на рівні рядків цих контейнерів даних. Тому додаток не має кнопки підтвердження, яка б, наприклад, зберігала у базі даних вибір, зроблений користувачем. Проте він є реалістичним. Ми наближаємося до додатка електронної комерції, де користувач додає товари (підписки) до свого кошика.
9.3.2. Принцип роботи
Перше, що бачить користувач, виглядає так:
![]() |
Користувач натискає на посилання [Plus d'informations], щоб отримати інформацію про певну тему. Ця інформація відображається в [panelInfos]:

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

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

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

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ème" HeaderText="Thème"></asp:BoundColumn>
<asp:ButtonColumn Text="Plus d'informations" CommandName="infos"></asp:ButtonColumn>
<asp:ButtonColumn Text="S'abonner" CommandName="abonner"></asp:ButtonColumn>
</Columns>
<PagerStyle HorizontalAlign="Center" ... Mode="NumericPages"></PagerStyle>
</asp:datagrid>
Слід звернути увагу на наступні моменти:
ми самостійно визначаємо стовпці, які потрібно відобразити, у розділі <columns>...</columns> | |
для пагінації даних | |
визначає стовпець [thème] (HeaderText) з [DataGrid], який буде пов'язаний зі стовпцем [thème] джерела даних (DataField) | |
визначає два стовпці кнопок (або посилань). Щоб розрізнити два посилання в одному рядку, будемо використовувати їхню властивість [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>
визначає текст заголовка [DataList] | |
визначає поточний елемент [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] має такий вигляд:
Пов’язаний файл [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)]:
ім’я обмеження — може бути будь-яким | |
стовпець, який буде первинним ключем — типу [DataColumn] | |
повинно дорівнювати [vrai], щоб зробити [colonne] первинним ключем. Якщо [cléPrimaire=false], то на [colonne] діє лише обмеження на унікальність значень |
Щоб зробити стовпець із назвою «id» первинним ключем таблиці [dtAbonnements], потрібно написати:
Процедура [Session_Start] виконується, коли додаток отримує перший запит від клієнта. Вона служить для створення об’єктів, унікальних для кожного клієнта, які мають зберігатися протягом різних запитів цього клієнта. Процедура формує таблицю [DataTable] з підписками клієнта. Створюється лише її структура, оскільки спочатку ця таблиця порожня. Вона заповнюватиметься у міру надходження запитів. Тут також стовпець «id» слугує первинним ключем. Для оголошення цього обмеження було використано іншу техніку:
— це масив стовпців, що утворюють первинний ключ; тут оголошено масив з одним елементом: стовпцем із назвою «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]. Сторінки матимуть дві підписки. Якщо користувач має п’ять підписок, буде три сторінки. Перша з них матиме такий вигляд:

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

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

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

Цей екран, що має назву [formulaire], дозволяє користувачеві задати критерії фільтрації товарів та визначити кількість товарів на сторінці, яку він бажає бачити.
№ | назва | тип | роль |
LinkButton | відображає вікно [Formulaire], яке використовується для встановлення умови фільтрації | ||
LinkButton | відображає подання [Produits], яке використовується для перегляду та оновлення таблиці товарів (редагування та видалення) | ||
LinkButton | відображає вікно [Ajout], яке використовується для додавання товару | ||
vueFormulaire | погляд [Formulaire] | ||
TextBox | умова фільтрації | ||
TextBox | кількість товарів на сторінці | ||
RequiredFieldValidator | перевіряє наявність значення в [txtPages] | ||
RangeValidator | перевіряє, чи txtPages знаходиться в інтервалі [3,10] | ||
кнопка [submit], яка відображає вигляд [produits], відфільтрований за умовою (5) | |||
Мітка | Інформаційний текст у разі помилок |
Наприклад, якщо подання [formulaire] заповнено таким чином:

отримуємо такий результат:
![]() |
№ | назва | тип | роль |
панель | |||
RadioButton | дозволяє користувачеві встановити бажаний порядок сортування, коли він натискає на заголовок одного зі стовпців [nom], [prix]. Обидві кнопки входять до групи [rdTri]. | ||
DataGrid | сітка відображення відфільтрованого виду таблиці товарів. Фільтр встановлено за допомогою виду [formulaire]. Також встановлено .AllowPaging=true, .AllowSorting=true | ||
DataGrid | відображає всю таблицю продуктів — дозволяє відстежувати оновлення | ||
Етикетка | інформаційний текст, зокрема у разі помилок | ||
DataGrid | відображатиме видалені товари в таблиці товарів | ||
DataGrid | відобразить змінені товари в таблиці товарів | ||
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] матиме такі значення:
відсутність фільтра за станом ліній | ||
DataViewRowState.CurrentRows | відображає поточний стан таблиці товарів | |
DataViewRowState.Deleted | відображає видалені рядки з таблиці товарів | |
DataViewRowState.ModifiedOriginal | відображає змінені рядки таблиці товарів із їхніми початковими значеннями | |
DataViewRowState.Added | відображає рядки, додані до початкової таблиці товарів |
Контейнери [DataGrid1-5] дозволять нам відстежувати оновлення таблиці [dtProduits]. Компонент [DataGrid1] дозволяє змінювати та видаляти товар. Ми розглянемо, як конфігурація компонента забезпечує це оновлення. Він також має пагінацію та сортування. Ці два аспекти вже розглядалися в попередньому прикладі.
Посилання [Ajout] надає доступ до форми додавання товару:
![]() |
№ | назва | тип | роль |
панель | |||
TextBox | назва товару | ||
RequiredFieldValidator | перевіряє наявність значення в [txtNom] | ||
TextBox | ціна товару | ||
RequiredFieldValidator | перевіряє наявність значення в [txtPrix] | ||
CompareValidator | перевіряє, чи ціна >= 0 | ||
Кнопка | кнопка [submit] для додавання товару | ||
Етикетка | інформаційний текст про результат операції додавання |
База даних [produits] може бути недоступною під час запуску програми. У цьому випадку користувачеві відображається вікно [erreurs]:
![]() |
№ | назва | тип | роль |
панель | |||
Репітер | список помилок |
9.6.3. Налаштування контейнерів даних
П'ять контейнерів [DataGrid] було налаштовано в [WebMatrix]. Їх оформлення (кольори та рамки) було налаштовано за допомогою посилання [Configuration automatique] у панелі властивостей. Контейнер [DataGrid1] було налаштовано за допомогою посилання [Générateur de propriétés] у цій же панелі. Було дозволено сортування (вкладка [Général]):

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

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

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

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

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

У підсумку ця конфігурація генерує такий код для відображення:
<asp:DataGrid id="DataGrid1" runat="server" AllowSorting="True" PageSize="4" AllowPaging="True" AutoGenerateColumns="False" DataKeyField="id">
<SelectedItemStyle ...></SelectedItemStyle>
<HeaderStyle ...></HeaderStyle>
<FooterStyle ...></FooterStyle>
<Columns>
<asp:BoundColumn DataField="nom" SortExpression="nom" HeaderText="nom"></asp:BoundColumn>
<asp:BoundColumn DataField="prix" SortExpression="prix" HeaderText="prix"></asp:BoundColumn>
<asp:EditCommandColumn ButtonType="LinkButton" UpdateText="Mettre à jour" CancelText="Annuler" EditText="Modifier"></asp:EditCommandColumn>
<asp:ButtonColumn Text="Supprimer" CommandName="Delete"></asp:ButtonColumn>
</Columns>
<PagerStyle NextPageText="Suivant" PrevPageText="Précédent" ...></PagerStyle>
</asp:DataGrid>
Як завжди, набувши певного досвіду, можна написати весь або частину наведеного вище коду безпосередньо.
Інші контейнери [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 la table LISTE. Exemple : prix<100 and
prix>50</P>
<P>
<asp:TextBox id="txtFiltre" runat="server" Columns="60"></asp:TextBox></P>
<P>Nombre de lignes par page :
<asp:TextBox id="txtPages" runat="server" Columns="3">5</asp:TextBox>
<asp:RequiredFieldValidator id="rfvLignes" runat="server" Display="Dynamic"
ControlToValidate="txtPages" ErrorMessage="Indiquez le nombre de lignes par page"
EnableClientScript="False">
</asp:RequiredFieldValidator></P>
<P>
<asp:RangeValidator id="rvLignes" runat="server" Display="Dynamic"
ControlToValidate="txtPages" ErrorMessage="Vous devez indiquer un nombre entre 3 et 10"
EnableClientScript="False" MaximumValue="10" MinimumValue="3" Type="Integer"
EnableViewState="False">
</asp:RangeValidator></P>
<P>
<asp:Label id="lblinfo1" runat="server"></asp:Label></P>
<P>
<asp:Button id="btnExécuter" runat="server" CausesValidation="False"
EnableViewState="False" Text="Exécuter">
</asp:Button></P>
</asp:panel>
<asp:panel id="vueProduits" runat="server">
<TABLE>
<TR>
<TD align="center" bgColor="#ff9966">
<P>Tri
<asp:RadioButton id="rdCroissant" runat="server" Text="croissant"
GroupName="rdTri" Checked="True">
</asp:RadioButton>
<asp:RadioButton id="rdDécroissant" runat="server" Text="décroissant"
GroupName="rdTri">
</asp:RadioButton></P>
</TD>
<TD align="center" bgColor="#ff9966">Tous les produits
</TD>
<TD align="center" bgColor="#ff9966">Suppressions
</TD>
<TD align="center" bgColor="#ff9966">Modifications
</TD>
<TD align="center" bgColor="#ff9966">Ajouts
</TD>
<TR>
<TD vAlign="top">
<P>
<asp:DataGrid id="DataGrid1" runat="server" AllowSorting="True" PageSize="4"
AllowPaging="True" .... AutoGenerateColumns="False" DataKeyField="id">
<ItemStyle ...></ItemStyle>
<HeaderStyle ...></HeaderStyle>
<FooterStyle ...></FooterStyle>
<Columns>
<asp:BoundColumn DataField="nom" SortExpression="nom" HeaderText="nom">
</asp:BoundColumn>
<asp:BoundColumn DataField="prix" SortExpression="prix" HeaderText="prix">
</asp:BoundColumn>
<asp:EditCommandColumn ButtonType="LinkButton" UpdateText="Mettre à jour"
CancelText="Annuler" EditText="Modifier">
</asp:EditCommandColumn>
<asp:ButtonColumn Text="Supprimer" CommandName="Delete">
</asp:ButtonColumn>
</Columns>
<PagerStyle NextPageText="Suivant" PrevPageText="Précédent" ....>
</PagerStyle>
</asp:DataGrid>
</P>
</TD>
<TD vAlign="top">
<asp:DataGrid id="DataGrid2" runat="server" ...>
<AlternatingItemStyle ...></AlternatingItemStyle>
<ItemStyle ...></ItemStyle>
<HeaderStyle ....></HeaderStyle>
<FooterStyle ...></FooterStyle>
<PagerStyle ... Mode="NumericPages"></PagerStyle>
</asp:DataGrid>
</TD>
<TD vAlign="top">
<asp:DataGrid id="DataGrid3" runat="server" ...>
<ItemStyle ...></ItemStyle>
<HeaderStyle ...></HeaderStyle>
<FooterStyle ...></FooterStyle>
<PagerStyle ...></PagerStyle>
</asp:DataGrid>
</TD>
<TD vAlign="top">
<asp:DataGrid id="DataGrid4" runat="server" ...>
<ItemStyle ....></ItemStyle>
<HeaderStyle ....></HeaderStyle>
<FooterStyle ...></FooterStyle>
<PagerStyle .... Mode="NumericPages"></PagerStyle>
</asp:DataGrid>
</TD>
<TD vAlign="top">
<asp:DataGrid id="DataGrid5" runat="server....>
<AlternatingItemStyle ...></AlternatingItemStyle>
<HeaderStyle ..></HeaderStyle>
<FooterStyle ...></FooterStyle>
<PagerStyle ....></PagerStyle>
</asp:DataGrid>
</TD>
</TR>
</TABLE>
<P></P>
<P>
<asp:Label id="lblInfo2" runat="server"></asp:Label></P>
</asp:panel>
<asp:panel id="vueErreurs" runat="server">
<asp:Repeater id="rptErreurs" runat="server" EnableViewState="False">
<HeaderTemplate>
Les erreurs suivantes se sont produites :
<ul>
</HeaderTemplate>
<ItemTemplate>
<li>
<%# Container.DataItem %>
</li>
</ItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</asp:Repeater>
</asp:panel>
<asp:panel id="vueAjout" EnableViewState="False" Runat="server">
<P>Ajout d'un produit</P>
<P>
<TABLE ... border="1">
<TR>
<TD>nom</TD>
<TD>
<asp:TextBox id="txtNom" runat="server" Columns="30"></asp:TextBox>
<asp:RequiredFieldValidator id="rfvNom" runat="server" ControlToValidate="txtNom"
ErrorMessage="Vous devez indiquer un nom">
</asp:RequiredFieldValidator>
</TD>
</TR>
<TR>
<TD>prix</TD>
<TD>
<asp:TextBox id="txtPrix" runat="server" Columns="10"></asp:TextBox>
<asp:RequiredFieldValidator id="rfvPrix" runat="server" ControlToValidate="txtPrix"
ErrorMessage="Vous devez indiquer un prix">
</asp:RequiredFieldValidator>
<asp:CompareValidator id="cvPrix" runat="server" ControlToValidate="txtPrix"
ErrorMessage="CompareValidator" Type="Double" Operator="GreaterThanEqual"
ValueToCompare="0">
</asp:CompareValidator>
</TD>
</TR>
</TABLE>
</P>
<P>
<asp:Button id="btnAjouter" runat="server" CausesValidation="False"
Text="Ajouter">
</asp:Button></P>
<P>
<asp:Label id="lblInfo3" runat="server"></asp:Label></P>
</asp:panel>
</td>
</tr>
</table>
</form>
</body>
</HTML>
Звернімо увагу на наступні моменти:
- сторінка складається з чотирьох контейнерів (панелей) [vueFormulaire, vueProduits, vueAjout, vueErreurs], які утворюють чотири вікна додатка.
- кнопки або посилання типу [submit] мають властивість [CausesValidation=false]. Властивість [causesValidation=true] викликає виконання всіх перевірок валідності на сторінці. Однак у даному випадку не всі перевірки валідності потрібно виконувати одночасно. Наприклад, під час додавання даних ми не хочемо, щоб виконувалися перевірки, що стосуються кількості рядків на сторінці. Тому ми самі вкажемо, які перевірки повинні бути виконані.
9.6.5. Код перевірки [global.asax]
Контролер [global.asax] має такий вигляд:
Відповідний код [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
таблиця [DataTable] з товарами — спільна для всіх клієнтів | |
перегляд [DataView] товарів — індивідуальний для кожного клієнта | |
кількість товарів на сторінці, запропонована за замовчуванням |
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]:

На малюнку вище елемент [Produit8, 80] перебуває в режимі [édition]. Як видно вище, користувач може підтвердити оновлення за посиланням [Mettre à jour] або скасувати його за посиланням [Annuler]. Він також може використовувати інші посилання, що не мають жодного стосунку до елемента, який редагується. Вставляючи [DataGrid1.EditItemIndex=-1] при кожному завантаженні сторінки, ми гарантуємо, що режим редагування [DataGrid1] буде систематично скасовано. Інше значення йому буде присвоєно лише в тому випадку, якщо було натиснуто посилання [Modifier]. Це буде зроблено в процедурі, що обробляє цю подію. Можуть виникнути такі ситуації:
- було натиснуто посилання [Modifier] в елементі № 8 [DataGrid1]. [Page_Load] спочатку встановлює значення -1 у [DataGrid1.EditItemIndex]. Потім процедура, що обробляє подію [Modifier], встановить значення 8 у [DataGrid1.EditItemIndex]. У підсумку, коли сторінка буде відправлена клієнту, елемент № 8 перебуватиме в режимі [édition] і відображатиметься так, як показано вище.
- Користувач змінює товар, який перебуває в режимі [édition], і підтверджує зміни за допомогою посилання [Mettre à jour]. [Page_Load] записує -1 у [DataGrid1.EditItemIndex]. Потім виконається процедура, що обробляє подію [Mettre à jour], і елемент № 8 у [dtProduits] буде оновлено. Коли сторінка буде відправлена клієнту, жоден елемент [DataGrid1] не перебуватиме в режимі редагування (DataGrid.EditItemIndex=-1).
- Якщо користувач перейшов до оновлення продукту, логічно було б, щоб він скасував це оновлення за допомогою [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
Інші види запитуються на основі опцій, запропонованих користувачеві:

Відображення виду [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

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
Насамперед нагадаємо, що сторінка має два компоненти перевірки:
№ | ім’я | тип | роль |
RequiredFieldValidator | перевіряє наявність значення в [txtPages] | ||
RangeValidator | перевіряє, чи txtPages знаходиться в інтервалі [3,10] |
Процедура починається з виконання коду перевірки двох вищезазначених компонентів за допомогою їхнього методу [Validate], а потім перевіряє значення їхнього атрибута [IsValid]. Це значення дорівнює [true] лише в тому випадку, якщо перевірені дані визнано дійсними. Якщо перевірка дійсності одного з двох компонентів завершилася невдало, знову відображається вікно [formulaire], щоб користувач міг виправити свою помилку або помилки. Умова фільтрації застосовується до атрибута [dvProduits.RowFilter]. Тут може виникнути виняток, якщо користувач вказав неправильний критерій фільтрації, як показано нижче:

У цьому випадку знову відображається вікно [formulaire]. Якщо обидва введені параметри є правильними, у сесію записуються два значення:
- кількість товарів на сторінці, обрана користувачем
- номер сторінки, яка буде відображена — спочатку це сторінка 0
Ці два параметри використовуються щоразу, коли відображається вид [produits].
9.6.11. Відображення подання [produits]
Вигляд [produits] має такий вигляд:

Ми маємо 5 компонентів [DataGrid], кожен з яких відображає окремий вигляд таблиці товарів [dtProduits]. Починаючи зліва:
назва | роль |
сітка відображення відфільтрованого виду таблиці товарів. Фільтр відповідає тому, що встановлено у вигляді [formulaire]. Також маємо .AllowPaging=true, .AllowSorting=true | |
відображає повну таблицю товарів — дозволяє стежити за оновленнями | |
відобразить видалені товари в таблиці товарів | |
відобразить змінені товари у таблиці товарів | |
відобразить товари, додані до таблиці товарів |
Процедура [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 | Фільтр |
RowStateFilter= DataRowViewState.CurrentRows | |
RowStateFilter= DataRowViewState.Deleted | |
RowStateFilter= DataRowViewState.ModifiedOriginal | |
RowStateFilter= DataRowViewState.Added |
Таблиця [dtPoduits] оновлюється одночасно різними веб-клієнтами, що може призвести до конфліктів доступу до таблиці. Коли клієнт переглядає свої записи в таблиці [dtProduits], потрібно уникнути ситуації, коли він робить це в той час, коли таблиця перебуває в нестабільному стані через те, що її саме редагує інший веб-клієнт. Також щоразу, коли клієнту потрібна таблиця [dtProduits] для читання, як зазначено вище, або для запису під час додавання, редагування та видалення товарів, він синхронізується з іншими клієнтами за допомогою такої послідовності дій:
У коді додатка може бути кілька критичних ділянок:
Механізм роботи такий:
- один або кілька клієнтів доходять до інструкції [Application.Lock] у критичній секції 1. Це дистриб’ютор, що видає єдиний вхідний токен. Цей токен отримує лише один клієнт. Назвемо його C1.
- доки клієнт C1 не поверне токен через [Application.Unlock], жоден інший клієнт не має права входити в критичну секцію, контрольовану [Application.Lock]. Отже, у наведеному вище прикладі жоден клієнт не може увійти до критичних секцій 1 і 2.
- Клієнт C1 виконує [Application.Unlock] і, отже, повертає токен доступу. Після цього цей токен може бути переданий іншому клієнту. Етапи 1–3 повторюються.
Завдяки цьому механізму ми гарантуємо, що лише один клієнт матиме доступ до таблиці [dtProduits] — як для читання, так і для запису.
Посилання [Mise à jour] також відображає представлення [Produits]:

Відповідна процедура така:
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]:

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

Процедура сортування така:
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] у рядку товару:

Після цього виконується процедура [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]:
Процедура [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
Процедура отримує як параметр ключ продукту, який потрібно видалити. Якби оновлення виконував лише один клієнт, це видалення можна було б здійснити наступним чином:
Якщо оновлення виконують одночасно кілька клієнтів, ситуація ускладнюється. Розглянемо таку послідовність подій:
![]() |
Час | Дія |
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] (частковий вигляд):

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

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

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

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

Повідомлення про помилку повідомляє користувачеві, що товар із ключем [2] не існує. Відповідь повертає клієнту нове представлення, що відображає поточний стан таблиці. Ми бачимо, що товар із ключем [2] дійсно знаходиться у списку видалених товарів. Отже, представлення [produits] відображає оновлення всіх клієнтів, а не лише одного.
9.6.14. Додавання товару
Щоб додати товар, користувач використовує посилання [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
Нагадаємо, що погляд [Ajout] має такі компоненти перевірки:
ім’я | тип | роль |
RequiredFieldValidator | перевіряє наявність значення в [txtNom] | |
RequiredFieldValidator | перевіряє наявність значення в [txtPrix] | |
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] із повідомленнями про помилки компонентів валідації. Ось приклад:

Якщо дані є правильними, готується рядок для вставки в таблицю [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], активуємо міжклієнтську синхронізацію:
Зробивши це, ми намагаємося додати рядок до таблиці [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] у рядку товару:

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

Завдяки компоненту [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], яка містить вихідні значення зміненого рядка. Ось приклад.

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

Бачимо, що втрачено значення, введені спочатку. У професійному додатку це, ймовірно, було б неприйнятним. Тут ми стикаємося з певними обмеженнями стандартного режиму оновлення компонента [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]:

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

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

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

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

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

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

Він може переконатися, що [produit1] було видалено, оскільки воно знаходиться у списку видалених продуктів.
9.7. Веб-додаток для оновлення фізичної таблиці продуктів
9.7.1. Пропоновані рішення
Попередній приклад був скоріше навчальним, покликаним продемонструвати управління об’єктом [DataTable] у кеші, ніж типовим випадком. Адже рано чи пізно доводиться оновлювати фактичне джерело даних. Можна обрати дві різні стратегії:
- використовувати кеш [dtProduits] у пам’яті для оновлення джерела даних. Можна передбачити сторінку, розташовану в веб-дереві попереднього додатка, щоб отримати доступ до його кешу [dtProduits]. Ця сторінка надавала б адміністратору можливість відобразити у фізичному джерелі даних зміни, внесені в кеш [dtProduits]. Для цього можна додати новий метод до класу доступу [produits], який приймав би як параметр кеш [dtProduits] і за допомогою цього кешу оновлював би фізичне джерело даних.
- Фізичне джерело даних оновлюється одночасно з кешем.
Стратегія № 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:

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

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

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

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

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

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

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

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

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

За допомогою опції [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], який відображає товари:

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

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

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

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

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

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









