9. ASP Server Components - 3
9.1. Introduction
We continue our work on the user interface by exploring the capabilities of the [DataList] and [DataGrid] components, particularly in the area of updating the data they display
9.2. Handling events associated with data in data-bound components
9.2.1. The Example
Consider the following page:
![]() |
The page includes three components associated with a data list:
- a [DataList] component named [DataList1]
- a [DataGrid] component named [DataGrid1]
- a [Repeater] component named [Repeater1]
The associated data list is the array {"zero", "one", "two", "three"}. Each of these data points is associated with a group of two buttons labeled [Info1] and [Info2]. When the user clicks one of the buttons, text displays the name of the clicked button. The goal here is to demonstrate how to manage a list of buttons or links.
9.2.2. Component configuration
The [DataList1] component is configured as follows:
<asp:datalist id="DataList1" ... runat="server">
<SelectedItemStyle ...</SelectedItemStyle>
<HeaderTemplate>
[start]
</HeaderTemplate>
<FooterTemplate>
[end]
</FooterTemplate>
<ItemStyle ...></ItemStyle>
<ItemTemplate>
<P><%# Container.DataItem %>
<asp:Button runat="server" Text="Info1" CommandName="info1"></asp:Button>
<asp:Button runat="server" Text="Info2" CommandName="info2"></asp:Button></P>
</ItemTemplate>
<FooterStyle ...></FooterStyle>
<HeaderStyle ...></HeaderStyle>
</asp:datalist>
We have omitted everything related to the appearance of the [DataList] to focus solely on its content:
- the <HeaderTemplate> section defines the header of the [DataList], and the <FooterTemplate> section defines its footer.
- The <ItemTemplate> section is the display template used for each item in the associated data list. It contains the following elements:
- the value of the current data item in the data list associated with the component: <%# Container.DataItem %>
- two buttons labeled [Info1] and [Info2], respectively. The [Button] class has a [CommandName] attribute that is used here. It will allow us to determine which button triggered an event in the [DataList]. To handle button clicks, we will have a single event handler linked to the [DataList] itself, not to the buttons. This handler will receive information indicating which row of the [DataList] the click occurred on. The [CommandName] attribute will let us know which button on that row triggered the click.
The [Repeater1] component is configured in a very similar way:
<asp:repeater id="Repeater1" runat="server">
<HeaderTemplate>
[start]<hr />
</HeaderTemplate>
<FooterTemplate>
<hr />
[end]
</FooterTemplate>
<SeparatorTemplate>
<hr />
</SeparatorTemplate>
<ItemTemplate>
<%# Container.DataItem %>
<asp:Button runat="server" Text="Info1" CommandName="info1"></asp:Button>
<asp:Button runat="server" Text="Info2" CommandName="info2"></asp:Button></P>
</ItemTemplate>
</asp:repeater>
We simply added a <SeparatorTemplate> section so that the successive data displayed by the component is separated by a horizontal bar.
Finally, the [DataGrid1] component is configured as follows:
<asp:datagrid id="DataGrid1" ... runat="server" PageSize="2" AllowPaging="True">
<SelectedItemStyle ...></SelectedItemStyle>
<AlternatingItemStyle ...></AlternatingItemStyle>
<ItemStyle ...></ItemStyle>
<HeaderStyle ...></HeaderStyle>
<FooterStyle ....></FooterStyle>
<Columns>
<asp:ButtonColumn Text="Info1" ButtonType="PushButton" CommandName="Info1">
</asp:ButtonColumn>
<asp:ButtonColumn Text="Info2" ButtonType="PushButton" CommandName="Info2">
</asp:ButtonColumn>
</Columns>
<PagerStyle .... Mode="NumericPages"></PagerStyle>
</asp:datagrid>
Here as well, we have omitted the style information (colors, widths, etc.). We are in automatic column generation mode, which is the default mode for the [DataGrid]. This means there will be as many columns as there are in the data source. Here, there is one. We have added two other columns marked with <asp:ButtonColumn>. We define information similar to that defined for the other two components, as well as the button type, which is [PushButton] here. The default type for is [LinkButton], i.e., a link. Additionally, the data will be paginated [AllowPaging=true] with a page size of two items [PageSize=2].
9.2.3. The page layout code
The presentation code for our example page has been placed in a file named [main.aspx]:
<%@ page codebehind="main.aspx.vb" inherits="vs.main" autoeventwireup="false" %>
<HTML>
<HEAD>
</HEAD>
<body>
<form runat="server">
<P>Handling events for components associated with data lists</P>
<HR width="100%" SIZE="1">
<table cellSpacing="1" cellPadding="1" bgColor="#ffcc00" border="1">
<td ...>DataList
<td ...>DataGrid
<td ...>Repeater
<td ...>
<asp:datalist id="DataList1" ... runat="server">
<HeaderTemplate>
[start]
</HeaderTemplate>
<FooterTemplate>
[end]
</FooterTemplate>
<ItemStyle ...></ItemStyle>
<ItemTemplate>
<P><%# Container.DataItem %>
<asp:Button runat="server" Text="Info1" CommandName="info1"></asp:Button>
<asp:Button runat="server" Text="Info2" CommandName="info2"></asp:Button></P>
</ItemTemplate>
<FooterStyle ...></FooterStyle>
<HeaderStyle ....></HeaderStyle>
</asp:datalist>
<P></P>
<td ...>
<asp:datagrid id="DataGrid1" ... runat="server" PageSize="2" AllowPaging="True">
<AlternatingItemStyle ...></AlternatingItemStyle>
<ItemStyle ...></ItemStyle>
<HeaderStyle ...></HeaderStyle>
<FooterStyle ...></FooterStyle>
<Columns>
<asp:ButtonColumn Text="Info1" ButtonType="PushButton" CommandName="Info1">
</asp:ButtonColumn>
<asp:ButtonColumn Text="Info2" ButtonType="PushButton" CommandName="Info2">
</asp:ButtonColumn>
</Columns>
<PagerStyle ... Mode="NumericPages"></PagerStyle>
</asp:datagrid>
<td ...>
<asp:repeater id="Repeater1" runat="server">
<HeaderTemplate>
[start]<hr />
</HeaderTemplate>
<FooterTemplate>
<hr />
[end]
</FooterTemplate>
<SeparatorTemplate>
<hr />
</SeparatorTemplate>
<ItemTemplate>
<%# Container.DataItem %>
<asp:Button runat="server" Text="Info1" CommandName="info1"></asp:Button>
<asp:Button runat="server" Text="Info2" CommandName="info2"></asp:Button></P>
</ItemTemplate>
</asp:repeater>
<P><asp:label id="lblInfo" runat="server"></asp:label></P>
<P></P>
</form>
</body>
</HTML>
In the code above, we have omitted the formatting code (colors, lines, sizes, etc.)
9.2.4. The page control code
The application control code has been placed in the [main.aspx.vb] file:
Public Class main
Inherits System.Web.UI.Page
' page components
Protected WithEvents DataList1 As System.Web.UI.WebControls.DataList
Protected WithEvents lblInfo As System.Web.UI.WebControls.Label
Protected WithEvents DataGrid1 As System.Web.UI.WebControls.DataGrid
Protected WithEvents Repeater1 As System.Web.UI.WebControls.Repeater
' the data source
Protected texts() As String = {"zero", "one", "two", "three"}
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
If Not IsPostBack Then
'bind to data source
DataList1.DataSource = texts
DataGrid1.DataSource = texts
Repeater1.DataSource = texts
Page.DataBind()
End If
End Sub
Private Sub DataList1_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles DataList1.ItemCommand
' An event occurred on one of the rows of the [datalist]
lblInfo.Text = "You clicked the [" + e.CommandName + "] button on item [" + e.Item.ItemIndex.ToString + "] of the [DataList] component"
End Sub
Private Sub Repeater1_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.RepeaterCommandEventArgs) Handles Repeater1.ItemCommand
' An event occurred on one of the rows of the [repeater]
lblInfo.Text = "You clicked the [" + e.CommandName + "] button on the [" + e.Item.ItemIndex.ToString + "] item of the [Repeater] component"
End Sub
Private Sub DataGrid1_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.ItemCommand
' An event occurred on one of the rows of the [DataGrid]
lblInfo.Text = "You clicked the [" + e.CommandName + "] button on the [" + e.Item.ItemIndex.ToString + "] item on page [" + DataGrid1.CurrentPageIndex.ToString() + "] of the [DataGrid] component"
End Sub
Private Sub DataGrid1_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles DataGrid1.PageIndexChanged
' page change
With DataGrid1
.CurrentPageIndex = e.NewPageIndex
.DataSource = texts
.DataBind()
End With
End Sub
End Class
Comments:
- The [textes] data source is a simple array of strings. It will be bound to the three components on the page
- This binding is established in the [Page_Load] procedure during the first request. For subsequent requests, the three components will retrieve their values via the [VIEW_STATE] mechanism.
- The three components have a handler for the [ItemCommand] event. This event occurs when a button or link is clicked in one of the component’s rows. The handler receives two pieces of information:
- source: the reference to the object (button or link) that triggered the event
- a: information about the event of type [DataListCommandEventArgs], [RepeaterCommandEventArgs], or [DataGridCommandEventArgs], as appropriate. The a argument carries various pieces of information. Here, two of them are of interest to us:
- a.Item: represents the row where the event occurred, of type [DataListItem], [DataGridItem], or [RepeaterItem]. Regardless of the exact type, the [Item] element has an [ItemIndex] attribute indicating the row number of the [Item] in the container to which it belongs. Here, we display this row number
- a.CommandName: is the [CommandName] attribute of the button (Button, LinkButton, ImageButton) that triggered the event. This information, combined with the previous one, allows us to determine which button in the container triggered the [ItemCommand] event
9.3. Application - Managing a Subscription List
Now that we know how to intercept events that occur within a data container, we present an example showing how to handle them.
9.3.1. Introduction
The application presented here simulates a mailing list subscription application. These are defined by a three-column [DataTable] object:
name | type | role |
string | primary key | |
string | list theme name | |
string | a description of the topics covered by the list |
To keep our example simple, the [DataTable] object above will be constructed programmatically in an arbitrary manner. In a real-world application, it would likely be provided by a method of a data access class. The list table will be constructed in the [Application_Start] procedure, and the resulting table will be stored in the application. We will call it the [dtThèmes] table. The user will subscribe to certain topics from this table. The list of their subscriptions will be stored in a [DataTable] object called [dtAbonnements], which will have the following structure:
name | type | role |
string | primary key | |
string | list theme name |
The single-page application is as follows:
![]() |
No. | name | type | properties | role |
DataGrid | mailing lists available for subscription | |||
DataList | list of the user's subscriptions to the previous lists | |||
panel | information panel on the topic selected by the user with a [More Information] link | |||
Label | part of [panelInfos] | theme name | ||
Label | part of [panelInfos] | theme description | ||
Label | application information message |
Our example aims to illustrate the use of the [DataGrid] and [DataList] components, particularly the handling of events that occur at the row level of these data containers. Therefore, the application does not have a submit button that would save the user's selections to a database, for example. Nevertheless, it is realistic. We are close to an e-commerce application where a user would add products (subscriptions) to their cart.
9.3.2. Functionality
The first view the user sees is as follows:
![]() |
The user clicks on the [More Information] links to get details about a topic. These are displayed in [panelInfos]:

They click the [Subscribe] links to subscribe to a topic. The selected topics are added to the [dlSubscriptions] component:

The user may want to subscribe to a list to which they are already subscribed. A message alerts them to this:

Finally, they can unsubscribe from any of the topics using the [Unsubscribe] buttons above. No confirmation is required. This is not necessary here because the user can easily resubscribe. Here is the view after unsubscribing from [topic1]:

9.3.3. Configuring Data Containers
The [dgThèmes] component of type [DataGrid] is bound to a source of type [DataTable]. Its formatting was done using the [Auto Format] link in the [DataGrid] properties panel. Its properties were defined using the [Property Generator] link in the same panel. The generated code is as follows (the formatting code is omitted):
<asp:datagrid id="dgThemes" AutoGenerateColumns="False" AllowPaging="True" PageSize="5"
runat="server">
<ItemStyle ...></ItemStyle>
<HeaderStyle ...></HeaderStyle>
<FooterStyle ...></FooterStyle>
<Columns>
<asp:BoundColumn DataField="theme" HeaderText="Theme"></asp:BoundColumn>
<asp:ButtonColumn Text="More information" CommandName="info"></asp:ButtonColumn>
<asp:ButtonColumn Text="Subscribe" CommandName="subscribe"></asp:ButtonColumn>
</Columns>
<PagerStyle HorizontalAlign="Center" ... Mode="NumericPages"></PagerStyle>
</asp:datagrid>
Note the following points:
We define the columns to be displayed ourselves in the <columns>...</columns> section | |
for data pagination | |
defines the [theme] column (HeaderText) of the [DataGrid] that will be linked to the [theme] column of the data source (DataField) | |
defines two columns of buttons (or links). To distinguish between the two links in the same row, we will use their [CommandName] property. |
The [DataGrid] component is not fully configured. It will be configured in the controller code.
The [dlAbonnements] component of type [DataList] is linked to a source of type [DataTable]. Its formatting was done using the [AutoFormat] link in the [DataList] properties panel. Its properties were defined directly in the presentation code. This code is as follows (the formatting code is omitted):
<asp:DataList id="dlAbonnements" ... runat="server" >
<HeaderTemplate>
<div align="center">
Your subscriptions
</HeaderTemplate>
<ItemStyle ...></ItemStyle>
<ItemTemplate>
<TABLE>
<TR>
<TD><%#Container.DataItem("theme")%></TD>
<TD>
<asp:Button id="lnkRemove" CommandName="remove" runat="server" Text="Remove" /></TD>
</TR>
</TABLE>
</ItemTemplate>
<HeaderStyle ...></HeaderStyle>
</asp:DataList>
defines the header text of the [DataList] | |
defines the current item in the [DataList]—here we have placed a table with two columns and one row. The first cell will contain the name of the theme the user wants to subscribe to, and the other will contain the [Unsubscribe] button, which allows the user to cancel their selection. |
9.3.4. The presentation page
The presentation code [main.aspx] is as follows:
<%@ page src="main.aspx.vb" inherits="main" autoeventwireup="false" %>
<HTML>
<HEAD>
<title></title>
</HEAD>
<body>
<P>Select the topics you want to subscribe to:</P>
<HR width="100%" SIZE="1">
<form runat="server">
<td vAlign="top">
<asp:datagrid id="dgThemes" ... runat="server">
...
</asp:datagrid>
<td vAlign="top">
<asp:DataList id="dlSubscriptions" ... runat="server" GridLines="Horizontal" ShowFooter="False">
....
</asp:DataList>
<td vAlign="top">
<asp:Panel ID="panelInfo" Runat="server" EnableViewState="False">
<TABLE>
<TR>
<TD bgColor="#99cccc">
<asp:Label id="lblTheme" runat="server"></asp:Label></TD>
</TR>
<TR>
<TD bgColor="#ffff99">
<asp:Label id="lblDescription" runat="server"></asp:Label></TD>
</TR>
</TABLE>
</asp:Panel>
<P>
<asp:Label id="lblInfo" runat="server" EnableViewState="False"></asp:Label></P>
</form>
</body>
</HTML>
9.3.5. Controllers
Control logic is distributed between the [global.asax] and [main.aspx] files. The [global.asax] file is as follows:
The associated [global.asax.vb] file contains the following code:
Imports System.Web
Imports System.Web.SessionState
Imports System.Data
Imports System
Public Class global
Inherits System.Web.HttpApplication
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
' Initialize the data source
Dim themes As New DataTable
' columns
With themes.Columns
.Add("id", GetType(System.Int32))
.Add("theme", GetType(System.String))
.Add("description", GetType(System.String))
End With
' The 'id' column will be the primary key
themes.Constraints.Add("primary_key", themes.Columns("id"), True)
' rows
Dim row As DataRow
For i As Integer = 0 To 10
row = themes.NewRow
row.Item("id") = i.ToString
row.Item("theme") = "theme" + i.ToString
row.Item("description") = "theme description " + i.ToString
themes.Rows.Add(row)
Next
' Set the data source in the application
Application("themes") = themes
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' Start of session - create an empty subscriptions table
Dim dtSubscriptions As New DataTable
With dtSubscriptions
' the columns
.Columns.Add("id", GetType(String))
.Columns.Add("theme", GetType(String))
' the primary key
.PrimaryKey = New DataColumn() {.Columns("id")}
End With
' the table is added to the session
Session.Item("subscriptions") = dtSubscriptions
End Sub
End Class
The [Application_Start] procedure, executed when the application receives its very first request, builds the [DataTable] of topics that can be subscribed to. It is constructed arbitrarily using code. Recall the technique we have already encountered. We construct in the following order:
- an empty [DataTable] object, with no structure and no data
- the table structure by defining its columns (name and data type)
- the table rows that represent the useful data
Here we have added a primary key. The "id" column serves as the primary key. There are several ways to express this. Here, we have used a constraint. In SQL, a constraint is a rule that the data in a row must follow in order for that row to be added to a table. There are all sorts of possible constraints. The "Primary Key" constraint forces the column to which it is applied to have unique and non-empty values. A primary key can actually consist of an expression involving values from multiple columns. [DataTable].Constraints is the collection of constraints for a given table. To add a constraint, we use the [DataTable.Constraints.Add] method. This method has several signatures. Here, we used the [Add(Byval name as String, Byval column as DataColumn, Byval primaryKey as Boolean)] method:
constraint name - can be anything | |
column that will be the primary key - of type [DataColumn] | |
must be set to [true] to make [column] a primary key. If [primaryKey=false], only the unique constraint applies to [column] |
To make the column named "id" the primary key of the [dtAbonnements] table, we write:
The [Session_Start] procedure, executed when the application receives the first request from a client. It is used to create objects specific to each client that must persist across the client’s various requests. The procedure builds the [DataTable] of the client’s subscriptions. Only its structure is built, since this table is initially empty. It will be populated as requests are made. Here too, the "id" column serves as the primary key. We used a different technique to declare this constraint:
is the array of columns that make up the primary key—here we have declared a one-element array: the column named "id" |
When the client's request reaches the controller [main.aspx], both [DataTable] objects are available in the application for the themes table and in the session for the subscriptions table. The controller [main.aspx.vb] is as follows:
Imports System.Data
Public Class main
Inherits System.Web.UI.Page
Protected WithEvents dgThemes As System.Web.UI.WebControls.DataGrid
Protected WithEvents lblTheme As System.Web.UI.WebControls.Label
Protected WithEvents lblDescription As System.Web.UI.WebControls.Label
Protected WithEvents dlSubscriptions 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 dtThemes As DataTable
Protected dtSubscriptions As DataTable
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
End Sub
Private Sub links()
...
End Sub
Private Sub dgThemes_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles dgThemes.PageIndexChanged
...
End Sub
Private Sub dgThemes_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles dgThemes.ItemCommand
...
End Sub
Private Sub info(ByVal id As String)
...
End Sub
Private Sub subscribe(ByVal id As String)
...
End Sub
Private Sub dlSubscriptions_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles dlSubscriptions.ItemCommand
...
End Sub
End Class
The primary role of the [Page_Load] procedure is to:
- retrieve the two tables [dtThèmes] and [dtAbonnements], which are located in the application and session respectively, to make them available to all methods on the page
- binding the data from these two sources to their respective containers. This is done only during the first request. For subsequent requests, the binding does not need to be performed systematically, and when it is necessary, it may sometimes be necessary to wait for an event occurring after [Page_Load] to perform it.
The code for [Page_Load] is as follows:
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' retrieve the data sources
dtThemes = CType(Application("themes"), DataTable)
dtSubscriptions = CType(Session("subscriptions"), DataTable)
' data binding
If Not IsPostBack Then
bindings()
End If
' certain information is hidden
panelInfo.Visible = False
End Sub
Private Sub liaisons()
'Bind the data source to the [datagrid] component
With dgThemes
.DataSource = dtThemes
.DataKeyField = "id"
End With
' Bind the data source to the [datalist] component
With dlSubscriptions
.DataSource = dtSubscriptions
.DataKeyField = "id"
End With
' assign the data to the components
Page.DataBind()
End Sub
In the [Bindings] procedure, we use the [DataKeyField] property of the [DataList] and [DataGrid] components to define the column in the data source that will be used to uniquely identify the rows in the containers. Typically, this column is the primary key of the data source, but this is not mandatory. It is sufficient for the column to be free of duplicates and empty values. For the [dgThemes] container, the "id" column of the [dtThemes] source will serve as the primary key, and for the [dlSubscriptions] container, it will be the "id" column of the [dtSubscriptions] source. There is no need for the column serving as the container’s primary key to be displayed by the container itself. Here, neither container displays the primary key column. The benefit of a container having a primary key is that it allows you to easily retrieve, from the data source, the information associated with the container row on which an event occurred. In fact, it is common that, starting from a container row where an event occurred, you need to perform an action on the corresponding row in the linked data source. The primary key facilitates this task.
Pagination of the [DataGrid] is handled in the standard way:
Private Sub dgThèmes_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles dgThèmes.PageIndexChanged
' page change
dgThemes.CurrentPageIndex = e.NewPageIndex
' bind
bindings()
End Sub
Actions on the [More Information] and [Subscribe] links are handled by the [dgThèmes_ItemCommand] procedure:
Private Sub dgThemes_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles dgThemes.ItemCommand
' event on a row in the [DataGrid]
Dim command As String = e.CommandName
Select Case command
Case "info"
info(dgThemes.DataKeys(e.Item.ItemIndex))
Case "subscribe"
subscribe(dgThemes.DataKeys(e.Item.ItemIndex))
End Select
' link
links()
End Sub
We use the fact that both links have a [CommandName] attribute to distinguish them. Depending on the value of this attribute, we call the [info] or [subscribe] procedure, passing in both cases the "id" key associated with the [DataGrid] element where the event occurred. Armed with this information, the [info] procedure will display the details of the theme selected by the user:
Private Sub infos(ByVal id As String)
' information on the theme with key id
' retrieve the row from the [DataGrid] corresponding to the key
Dim row As DataRow
row = dtThemes.Rows.Find(id)
If Not row Is Nothing Then
' display the information
lblTheme.Text = CType(row("theme"), String)
lblDescription.Text = CType(row("description"), String)
panelInfo.Visible = True
End If
End Sub
Since the [dtThèmes] table has a primary key, the method [dtThèmes.Rows.Find("P")] allows us to find the row with the primary key P. If it is found, we obtain a [DataRow] object. Here, we need to find the row with the primary key [id], where [id] is passed as a parameter. If the row is found, we place the [theme] and [description] information from that row into the information panel, which we then make visible.
The [subscribe(id)] procedure must add the theme with key [id] to the subscription list. Its code is as follows:
Private Sub subscribe(ByVal id As String)
' Subscribe to the theme with key id
' retrieve the row from the [datatable] corresponding to the key
Dim row As DataRow
row = dtThemes.Rows.Find(id)
If Not row Is Nothing Then
' check if the user is not already subscribed
Dim subscription As DataRow
subscription = dtSubscriptions.Rows.Find(id)
If Not subscription Is Nothing Then
' display the error
lblInfo.Text = "You are already subscribed to the theme [" + Line("theme") + "]"
Else
' add the theme to subscriptions
subscription = dtSubscriptions.NewRow
subscription("id") = id
subscription("theme") = row("theme")
dtSubscriptions.Rows.Add(subscription)
' we make the connections
links()
End If
End If
End Sub
In the list of themes [dtThemes], we first search for the row with the key [id]. If found, we check that this theme is not already present in the list of subscriptions to avoid adding it twice. If it is, we display an error message. Otherwise, we add a new subscription to the [dtSubscriptions] table and link the data list components to their respective sources.
When the user clicks a [Remove] button, an item must be deleted from the [dtAbonnements] table. This is done by the following procedure:
Private Sub dlSubscriptions_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles dlSubscriptions.ItemCommand
' remove a subscription
Dim command As String = e.CommandName
If command = "remove" Then
' remove the subscription from the [datatable]
With dtSubscriptions.Rows
.Remove(.Find(dlSubscriptions.DataKeys(e.Item.ItemIndex)))
End With
' links
links()
End If
End Sub
First, we check the [CommandName] property of the element that triggered the event. This is actually quite unnecessary since the [Remove] button is the only control capable of generating an event in the [DataList] component. There is therefore no ambiguity. To delete a row from a [DataTable] object, we use the [DataList.Remove(DataRow)] method, which removes the [DataRow] row passed as a parameter from the table. This row is found by the [DataList.Find] method, to which we pass the primary key of the row being searched for. Once the row has been deleted, we bind the data to the components
9.4. Managing a paginated [DataList]
We’ll revisit the previous example to paginate the [DataList] component representing the user’s subscription list. Unlike the [DataGrid] component, the [DataList] component offers no built-in pagination functionality. We’ll see that implementing pagination is complex, which will help us appreciate the value of the [DataGrid]’s automatic pagination.
9.4.1. How it works
The only difference is the pagination of the [DataList]. Each page will display two subscriptions. If the user has five subscriptions, there will be three pages. The first page will look like this:

The second page is accessed via the [Next] link:

The third page:

Note that the [Previous] and [Next] links are only visible if there is a page preceding and a page following the current page, respectively.
9.4.2. Presentation code
The [Previous] and [Next] links are created by adding a <FooterTemplate> tag to the [DataList]:
<asp:datalist id="dlAbonnements" runat="server" ...>
....
<FooterTemplate>
<asp:LinkButton id="lnkPrecedent" runat="server" CommandName="precedent">Previous</asp:LinkButton>
<asp:LinkButton id="lnkNext" runat="server" CommandName="next">Next</asp:LinkButton>
</FooterTemplate>
....
</asp:datalist>
9.4.3. Control code
The associated file [global.asax.vb] changes as follows:
...
Public Class global
Inherits System.Web.HttpApplication
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
...
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' Start of session - create an empty subscription table
Dim dtSubscriptions As New DataTable
With dtSubscriptions
' the columns
.Columns.Add("id", GetType(String))
.Columns.Add("theme", GetType(String))
' the primary key
.PrimaryKey = New DataColumn() {.Columns("id")}
End With
' the table is added to the session
Session.Item("subscriptions") = dtSubscriptions
' the current page is page 0
Session.Item("pAC") = 0
' The number of subscriptions on this page is 0
Session.Item("nbAC") = 0
End Sub
In addition to the subscriptions table [dtAbonnements], two other pieces of information are stored in the session:
of type [Integer]—this is the number of the current page displayed during the last request | |
of type [Integer] - number of rows displayed on the previous current page |
At the start of a session, the current page number and the number of rows on that page are zero.
The [main.aspx.vb] controller evolves as follows:
....
Public Class main
Inherits System.Web.UI.Page
....
' application data
Protected dtThemes As DataTable
Protected dtSubscriptions As DataTable
Protected dtPA As DataTable ' the subscriptions page displayed
Protected Const nbAP As Integer = 2 ' number of subscriptions per page
Protected pAC As Integer ' current subscription page
Protected nbAC As Integer ' number of subscriptions on current page
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
End Sub
Private Sub terminate()
...
End Sub
Private Sub dgThemes_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles dgThemes.PageIndexChanged
...
End Sub
Private Sub dgThemes_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles dgThemes.ItemCommand
...
End Sub
Private Sub info(ByVal id As String)
...
End Sub
Private Sub subscribe(ByVal id As String)
...
End Sub
Private Sub dlSubscriptions_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles dlSubscriptions.ItemCommand
...
End Sub
Private Sub changePAC()
...
End Sub
Private Sub setLinks(ByVal ctl As Control, ByVal blPrec As Boolean, ByVal blNext As Boolean)
...
End Sub
End Class
Here, we define new data related to subscription pagination:
Protected dtPA As DataTable ' the displayed subscriptions page
Protected Const nbAP As Integer = 2 ' number of subscriptions per page
Protected pAC As Integer ' current subscription page
Protected nbAC As Integer ' number of subscriptions on the current page
Some of this information is stored in the session and is retrieved with each request in the [Page_Load] procedure:
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' retrieve the data sources
dtThemes = CType(Application("themes"), DataTable)
dtSubscriptions = CType(Session("subscriptions"), DataTable)
' and the subscription display information
pAC = CType(Session("pAC"), Integer)
nbAC = CType(Session("nbAC"), Integer)
' data binding
If Not IsPostBack Then
' Display an empty list of subscriptions
terminate()
End If
' Hide certain information
panelInfo.Visible = False
End Sub
The retrieved information [pAC] and [nbAC] are details about the subscriptions page displayed during the previous request:
is the number of the current page displayed during the previous request | |
number of rows displayed on this current page |
The [terminer] method binds the components to their data sources, just as the [liaisons] method did in the previous application. The new feature here is the binding of the [DataList] to the [dtPA] table, which is the subscription page to be displayed:
Private Sub finish()
' link the data source to the [datagrid] component
With dgThemes
.DataSource = dtThemes
.DataKeyField = "id"
End With
' Link the subscriptions page to the [datalist] component, taking into account the current page pAC
changePAC()
' display page p
With dlSubscriptions
.DataSource = dtPA
.DataKeyField = "id"
End With
' assign data to the components
Page.DataBind()
' manage the [previous] and [next] links of the [datalist]
Dim blprec As Boolean = pAC <> 0
Dim blnext As Boolean = pAC <> (dtSubscriptions.Rows.Count - 1) \ nbAP
Dim nbLinksFound As Integer = 0
setLinks(dlSubscriptions, blprev, blnext, nbLinksFound)
' Save the current page information to the session
Session("pAC") = pAC
Session("nbAC") = dtPA.Rows.Count
End Sub
The following points should be noted:
- The source [dtPA] depends on the current page number [pAC] to be displayed. The variable [pAC] is a global variable of the class, manipulated by methods that need to change this current page number. The [changePAC] method is responsible for constructing the [dtPA] table, which will be linked to the [dlAbonnements] component.
- The [setLiens] method is responsible for displaying or hiding the [Previous] and [Next] links depending on whether the current page [pAC] displayed is preceded and followed by a page. It has four parameters:
- [dlAbonnements]: the [DataList] control whose control tree we will explore to find the two links. Although these two links are located in a specific place—the footer of the [DataList]—there does not appear to be a simple way to reference them directly. In any case, none has been found here.
- [blPrecedent]: a Boolean to be assigned to the [visible] property of the [Precedent] link—is true if the current page is not 0
- [blNext]: a Boolean to be assigned to the [visible] property of the [Next] link—is true if the current page is not the last page in the subscription list
- [nbLiensTrouvés]: an output parameter that counts the number of links found. As soon as this count reaches two, the method terminates.
- The [pAC] and [nbAC] information is saved in the session for the next request.
The [changePAC] method constructs the [dtPA] table, which will be linked to the [dlAbonnements] component. It does so based on the [pAC] number of the current page to be displayed. The [dtPA] table must display certain rows from the [dtAbonnements] subscription table. Recall that this table is stored in the session and updated (increased or decreased) as requests are made. We begin by setting the [first,last] range of row numbers in the [dtAbonnements] table that the [dtPA] table must display:
Private Sub changePAC()
' makes the pAC page the current page for the
' manages the pages of the [datalist]
Dim nbSubscriptions = dtSubscriptions.Rows.Count
Dim lastPage = (nbSubscriptions - 1) \ nbAP
' first and last subscription
If pAC < 0 Then pAC = 0
If pAC > lastPage Then pAC = lastPage
Dim first As Integer = pAC * nbAP
Dim last As Integer = (pAC + 1) * nbAP - 1
If last > numSubscriptions - 1 Then last = numSubscriptions - 1
Once this is done, we can create the [dtPA] table. First, we define its structure [id, theme], then we populate it by copying the rows from [dtSubscriptions] whose numbers fall within the range [first, last] calculated earlier.
' creation of the dtpa DataTable
dtPA = New DataTable
With dtPA
' the columns
.Columns.Add("id", GetType(String))
.Columns.Add("theme", GetType(String))
' the primary key
.PrimaryKey = New DataColumn() {.Columns("id")}
End With
Dim subscription As DataRow
For i As Integer = first To last
subscription = dtPA.NewRow
With subscription
.Item("id") = dtSubscriptions.Rows(i).Item("id")
.Item("theme") = dtSubscriptions.Rows(i).Item("theme")
End With
dtPA.Rows.Add(subscription)
Next
End Sub
At the end of the [changePAC] method, the [dtPA] table has been built and can be bound to the [DataList] component. This is done in the [terminer] method. In this same method, the [setLiens] procedure is used to set the state of the [Previous] and [Next] links in the [DataList]. The code for this procedure is as follows:
Private Sub setLiens(ByVal ctl As Control, ByVal blPrec As Boolean, ByVal blSuivant As Boolean, ByRef nbLiensTrouvés As Integer)
' search for the [Previous] and [Next] links
' in the [DataList] control tree
'Have we found all the links?
If nbLinksFound = 2 Then Exit Sub
' Check child controls
Dim c As Control
For Each c In ctl.Controls
' First, we work in depth—the links are at the bottom of the tree
setLinks(c, blPrevious, blNext, nbLinksFound)
' [Previous] link?
If c.ID = "lnkPrecedent" Then
CType(c, LinkButton).Visible = blPrec
nbLinksFound += 1
End If
' [Next] link?
If c.ID = "lnkNext" Then
CType(c, LinkButton).Visible = blNext
nbLinksFound += 1
End If
Next
End Sub
The procedure is recursive. It first searches among the child controls of the [dlAbonnements] component for components named [lnkPrecedent] and [lnkSuivant], which are the IDs of the two pagination links. It starts the search from the bottom of the control tree because that is where they are located. As soon as a link is found, the [nbLiensTrouvés] counter is incremented, and the [visible] property of the link is set to a value passed as a parameter to the procedure. Once both links have been found, the control tree is no longer traversed, and the recursive procedure ends.
We mentioned that the [changePAC] method, which sets the data source [dtPA] for the [dlAbonnements] component, works with the [pAC] number of the current page to be displayed. Several procedures modify this number:
Private Sub subscribe(ByVal id As String)
' Subscribe to the theme with key id
..
' add the theme to subscriptions
..
' Update the current page number—it is now the last page
pAC = (dtSubscriptions.Rows.Count - 1) / nbAP
' data bindings
finish()
End If
End Sub
After adding a subscription, it appears at the end of the subscription list. Therefore, the view is set to the last page of subscriptions so that the user can see the addition that was made.
Private Sub dlAbonnements_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles dlAbonnements.ItemCommand
' Remove a subscription
Dim command As String = e.CommandName
Select Case command
Case "cancel"
' Remove the subscription from the [datatable]
With dtSubscriptions.Rows
.Remove(.Find(dlSubscriptions.DataKeys(e.Item.ItemIndex)))
End With
' Should we change the current page?
nbAC -= 1
If nbAC = 0 Then pAC -= 1
Case "previous"
' change current page
pAC -= 1
Case "next"
' change current page
pAC += 1
End Select
' data links
End()
End Sub
- [nbAC] is the number of lines displayed on the current page before a subscription is canceled. If the new number of lines on the page is equal to 0, the current page number [pAC] is decremented by one.
- If the [Previous] link is clicked, the current page number [pAC] is decremented by one.
- When the [Next] link is clicked, the current page number [pAC] is incremented by one.
The other procedures remain the same as before.
9.4.4. Conclusion
We have shown in this example that we can paginate a [DataList] component. This pagination is complex, and it is preferable to rely on the automatic pagination of the [DataGrid] component whenever possible. This example has also shown us how to access the components present in the footer of the [DataList] component.
9.5. Class for accessing a product database
We are once again focusing on the ACCESS [products] database that we have already used. Recall that it has a single table named [list] with the following structure:
![]() | ![]() |
We will create an access class for the [list] table that will allow us to read from and update it. We will also create a console client that will use the previous class to update the table. Next, we will create a web client to perform the same task.
9.5.1. The ProductException Class
The [Exception] class has a constructor that takes an error message as a parameter. Here, we want an exception class with a constructor that accepts a list of error messages rather than a single error message. This will be the [ExceptionProduits] class below:
Public Class ProductException
Inherits Exception
' error messages related to the exception
Private _errors As ArrayList
' constructor
Public Sub New(ByVal errors As ArrayList)
Me._errors = errors
End Sub
' property
Public ReadOnly Property errors() As ArrayList
Get
Return _errors
End Get
End Property
End Class
9.5.2. The [sProduct] structure
The [product] structure represents a product [id, name, price]:
' sProduct structure
Public Structure sProduct
' the fields
Private _id As Integer
Private _name As String
Private _price As Double
' id property
Public Property id() As Integer
Get
Return _id
End Get
Set(ByVal Value As Integer)
_id = Value
End Set
End Property
' property name
Public Property name() As String
Get
Return _name
End Get
Set(ByVal Value As String)
If IsNothing(Value) OrElse Value.Trim = String.Empty Then Throw New Exception
_name = Value
End Set
End Property
' price property
Public Property price() As Double
Get
Return _price
End Get
Set(ByVal Value As Double)
If IsNothing(Value) OrElse Value < 0 Then Throw New Exception
_price = Value
End Set
End Property
End Structure
The structure only accepts valid data for the [name] and [price] fields.
9.5.3. The Products Class
The [Products] class is the class that will allow us to update the [list] table in the product database. Its structure is as follows:
Public Class Products
' instance data
Public Sub New(ByVal OLEDBConnectionString As String)
....
End Sub
Public Function getProducts() As DataTable
....
End Function
Public Sub addProduct(ByVal product As sProduct)
...
End Sub
Public Sub modifyProduct(ByVal product As sProduct)
...
End Sub
Public Sub deleteProduct(ByVal id As Integer)
...
End Sub
End Class
Instance data
The data shared by the various methods of the class is as follows:
Private connection As OleDbConnection
Private Const selectText As String = "select id,name,price from list"
Private Const insertText As String = "insert into list(name,price) values(?,?)"
Private Const updateText As String = "update list set name=?,price=? where id=?"
Private Const deleteText As String = "delete from list where id=?"
Private selectCommand As New OleDbCommand
Dim insertCommand As New OleDbCommand
Dim updateCommand As New OleDbCommand
Dim deleteCommand As New OleDbCommand
Dim adapter As New OleDbDataAdapter
The database connection will be opened to execute an SQL command and then closed immediately afterward | |
SQL query [select] retrieving the entire table [list] | |
query allowing the insertion of a row (name, price) into the table [list]. Note that the [id] field is not specified. This is because the DBMS auto-increment this field, so we do not need to specify it. | |
Query to update the fields (name, price) of the row in the [list] table with the key [id] | |
query that deletes the row from the [list] table with the [id] key | |
[OleDbCommand] object that executes the [selectText] query on the [connection] connection | |
[OleDbCommand] object executing the [updateText] query on the [connection] connection | |
[OleDbCommand] object executing the [insertText] query on the [connection] connection | |
[OleDbCommand] object executing the [deleteText] query on the [connection] connection | |
object used to retrieve the result of executing [selectCommand] in a [DataSet] object |
The constructor
The constructor takes a single parameter [OLEDBConnectionString], which is the connection string specifying the database to be used. From this, the four commands for querying and updating the table, as well as the adapter, are prepared. This is merely a preparation step, and no connection is established.
Public Sub New(ByVal OLEDBConnectionString As String)
' prepare the connection
connection = New OleDbConnection(OLEDBConnectionString)
' prepare the query commands
Dim commands() As OleDbCommand = {selectCommand, insertCommand, updateCommand, deleteCommand}
Dim texts() As String = {selectText, insertText, updateText, deleteText}
For i As Integer = 0 To commands.Length - 1
With commands(i)
.CommandText = texts(i)
.Connection = connection
End With
Next
' Prepare the data access adapter
adapter.SelectCommand = selectCommand
End Sub
The getProducts method
This method retrieves the contents of the [List] table into a [DataTable] object. Its code is as follows:
Public Function getProducts() As DataTable
' Place the [Liste] table into a [DataSet]
Dim content As New DataSet
' Create a DataAdapter object to read data from the OLEDB source
Try
With adapter
.FillSchema(content, SchemaType.Source)
.Fill(content)
End With
Catch e As Exception
' pb
Dim commandErrors As New ArrayList
orderErrors.Add(String.Format("Database access error: {0}", e.Message))
Throw New ProductException(orderErrors)
End Try
' Return the result
Return content.Tables(0)
End Function
The work is done by the following two statements:
With adapter
.FillSchema(content, SchemaType.Source)
.Fill(content)
End With
The [FillSchema] method sets the structure (columns, constraints, relationships) of the contained [DataSet] based on the structure of the database referenced by [adapter.Connection]. This allows us to retrieve the structure of the [list] table, including its primary key. The [Fill] operation that follows populates the contained [DataSet] with the rows from the [list] table. With this single operation, we would have obtained the data and the structure but not the primary key. However, this will be useful for updating the [list] table in memory. Here, as in the other methods, we handle any errors using the [ProductExceptions] class to obtain a list (ArrayList) of errors rather than a single error. The [getProducts] method returns the [list] table as a [DataTable] object.
The addProducts method
This method allows you to add a row (id, name, price) to the [list] table. This information is provided to it in the form of a [sProduct] structure with fields [id, name, price]. The method’s code is as follows:
Public Sub addProduct(ByVal product As sProduct)
' Add a product [name, price]
' prepare the parameters for the addition
With insertCommand.Parameters
.Clear()
.Add(New OleDbParameter("name", product.name))
.Add(New OleDbParameter("name", product.name))
End With
' we make the addition
Try
' open connection
connection.Open()
' execute command
insertCommand.ExecuteNonQuery()
Catch ex As Exception
' problem
Dim commandErrors As New ArrayList
commandErrors.Add(String.Format("Error adding: {0}", ex.Message))
Throw New ProductException(commandErrors)
Finally
' Close connection
connexion.Close()
End Try
End Sub
The fields of the [product] structure are passed as parameters to the [insertCommand] command. Let’s review the current configuration of this command (see constructor):
Private Const insertText As String = "insert into list(name,price) values(?,?)"
insertCommand.Connection = connection
The text of the [insert] SQL command contains formal parameters ? that must be replaced with actual parameters. This is done using the [Parameters] collection of the [OleDbCommand] class. This collection contains elements of type [OleDbParameter] that define the actual parameters that must replace the formal parameters ?. Since these are not named, the index of the actual parameters is used to determine which formal parameter corresponds to a given actual parameter. Here, actual parameter #i in the [Parameters] collection will replace formal parameter ? #i. To create an actual parameter of type [OleDbParameter], we use the constructor [OleDbParameter (Byval name as String, Byval value as Object)], which defines the name and value of the actual parameter. The name can be anything. Furthermore, it will not be used here. The two parameters of the SQL [insert] statement receive as values those of the [name, price] fields of the [product] structure. Once this is done, the insertion is performed by the [insertCommand.ExecuteNonQuery] statement.
The modifyProducts method
This method allows you to modify a row in the [list] table. The information it requires is provided in the [sProduct] structure, which contains the fields [id, name, price].
Public Sub modifyProduct(ByVal product As sProduct)
' Modify a product [id, name, price]
' prepare the update parameters
With updateCommand.Parameters
.Clear()
.Add(New OleDbParameter("name", product.name))
.Add(New OleDbParameter("name", product.name))
.Add(New OleDbParameter("id", product.id))
End With
' make the change
Try
' open connection
connection.Open()
' execute command
Dim nbLines As Integer = updateCommand.ExecuteNonQuery()
If nbLines = 0 Then Throw New Exception(String.Format("The key [{0}] does not exist in the data table", product.id))
Catch ex As Exception
' pb
Dim orderErrors As New ArrayList
orderErrors.Add(String.Format("Error during modification: {0}", ex.Message))
Throw New ProductException(orderErrors)
Finally
' Close connection
connection.Close()
End Try
End Sub
The code is almost identical to that of the [addProducts] method, except that the relevant [OleDbCommand] is [updateCommand] instead of [insertCommand].
The [deleteProducts] method
This method deletes the row from the [list] table with the [id] key passed as a parameter. The code is as follows:
Public Sub deleteProduct(ByVal id As Integer)
' deletes the product with key [id]
' prepares the delete parameters
With deleteCommand.Parameters
.Clear()
.Add(New OleDbParameter("id", id))
End With
' Perform the delete
Try
' Open connection
connection.Open()
' execute command
Dim nbLines As Integer = deleteCommand.ExecuteNonQuery()
If nbLines = 0 Then Throw New Exception(String.Format("The key [{0}] does not exist in the data table", id))
Catch ex As Exception
' pb
Dim commandErrors As New ArrayList
orderErrors.Add(String.Format("Error during deletion: {0}", ex.Message))
Throw New ProductException(orderErrors)
Finally
' Close connection
connection.Close()
End Try
End Sub
The approach is the same as for the previous methods.
9.5.4. Testing the [products] class
A console-based test program [testproducts.vb] could look like this:
Option Explicit On
Option Strict On
' namespaces
Imports System
Imports System.Data
Imports Microsoft.VisualBasic
Imports System.Collections
Namespace st.istia.univangers.fr
' test page
Module testproducts
Dim content As DataTable
Sub Main(ByVal arguments() As String)
' displays the contents of a product table
' the table is in an ACCESS database whose file name is passed to the pg
Const syntax1 As String = "dbACCESS"
' checks the program parameters
If arguments.Length <> 1 Then
' error message
Console.Error.WriteLine(syntax1)
' end
Environment.Exit(1)
End If
' Prepare the connection string
Dim connectionString As String = "Provider=Microsoft.Jet.OLEDB.4.0; Ole DB Services=-4; Data Source=" + arguments(0)
' Create a Products object
Dim objProducts As Products = New Products(connectionString)
' Display all products
displayProducts(objProducts)
' insert a product
Dim product As New sProduct
With product
.name = "xxx"
.price = 1
End With
Try
objProducts.addProduct(product)
Catch ex As ProductException
displayErrors(ex.errors)
End Try
' Display all products
displayProducts(objProducts)
' retrieve the ID of the added product
product.id = CType(content.Rows(content.Rows.Count - 1)("id"), Integer)
' Modify the added product
product.price = 200
Try
objProducts.updateProduct(product)
Catch ex As ProductException
displayErrors(ex.errors)
End Try
' Display all products
displayProducts(objProducts)
' delete the added product
Try
productObject.deleteProduct(product.id)
Catch ex As ProductException
displayErrors(errors.ex)
End Try
' display all products
DisplayProducts(objProducts)
End Sub
Sub displayProducts(ByRef objProducts As products)
' Retrieve the product table into a DataTable
Try
content = objProducts.getProducts()
Catch ex As ProductException
displayErrors(ex.errors)
Environment.Exit(2)
End Try
Dim rows As DataRowCollection = content.Rows
For i As Integer = 0 To rows.Count - 1
' row i of the table
Console.Out.WriteLine(rows(i).Item("id").ToString + "," + rows(i).Item("name").ToString + _
"," + rows(i).Item("price").ToString)
Next
' stops console output
Console.WriteLine("...")
Console.ReadLine()
End Sub
Sub displayErrors(ByRef errors As ArrayList)
' displays errors on the console
If errors.Count <> 0 Then
Console.WriteLine("The following errors occurred:")
For i As Integer = 0 To errors.Count - 1
Console.WriteLine(String.Format("-- {0}", CType(errors(i), String)))
Next
End If
' stops the console output
Console.WriteLine("...")
Console.ReadLine()
End Sub
End Module
End Namespace
Compile the two source files:
dos>vbc /t:library /r:system.dll /r:system.data.dll /r:system.xml.dll products.vb
dos >vbc /r:products.dll /r:system.dll /r:system.data.dll /r:system.xml.dll testproducts.vb
dos>dir
04/07/2004 08:40 7,168 products.dll
04/04/2004 16:38 118,784 products.mdb
04/07/2004 08:31 6,209 products.vb
04/07/2004 08:40 5,120 testproducts.exe
04/03/2004 7:02 PM 3,312 testproducts.vb
Then we test:
dos>testproducts products.mdb
1,product1,10
2,product2,20
3,product3,30
...
1,product1,10
2,product2,20
3,product3,30
8,xxx,1
...
1,product1,10
2,product2,20
3,product3,30
8,xxx,200
...
1,product1,10
2,product2,20
3,product3,30
...
The reader is invited to compare the screen output above with the test program code.
9.6. Web application for updating the cached product table
9.6.1. Introduction
We are now writing a web application to update the product table (add, delete, modify). The updated table will remain in memory in a [DataTable] object and will be shared by all users. We want to highlight two points:
- the management of a [DataTable] object
- the challenges of simultaneous table updates by multiple users.
The application’s MVC architecture will be as follows:
![]() |
9.6.2. Functionality and Views
The application’s home view is as follows:

This view, called [form], allows the user to apply a filter condition to the products and set the number of products per page they wish to see.
No. | name | Type | role |
LinkButton | displays the [Form] view used to set the filter condition | ||
LinkButton | displays the [Products] view, which is used to view and update the product table (edit and delete) | ||
LinkButton | displays the [Add] view, which is used to add a product | ||
FormView | the [Form] view | ||
TextBox | the filter condition | ||
TextBox | number of products per page | ||
RequiredFieldValidator | checks for a value in [txtPages] | ||
RangeValidator | checks that txtPages is in the range [3,10] | ||
[submit] button that displays the [products] view filtered by condition (5) | |||
Label | Information text in case of errors |
For example, if the [form] view is filled out as follows:

the following result is obtained:
![]() |
No. | name | type | role |
panel | |||
RadioButton | allows the user to set the desired sort order when they click on the title of one of the columns [name], [price]. The two buttons are part of the [rdSort] group. | ||
DataGrid | A grid displaying a filtered view of the product table. The filter is the one set by the [form] view. We also have .AllowPaging=true, .AllowSorting=true | ||
DataGrid | displays the entire product table - allows tracking of updates | ||
Label | informational text, particularly in case of errors | ||
DataGrid | will display deleted products in the product table | ||
DataGrid | will display the modified products in the product table | ||
DataGrid | will display the products added to the product table |
There are five data containers on this page. They all display the same table [dtProduits] through a different [DataView]. A view represents a subset of the rows in the view’s source table. This subset is created using the [RowFilter] and [RowStateFilter] properties of the [DataView] class:
- [RowFilter] allows you to set a filter on the rows, such as [price>30] above. This type of filtering will be used by [DataGrid1].
- [RowStateFilter] allows you to set a filter based on the state of the table row. This indicates the row’s state relative to its original state when the view on the table was created. Here, the [dtProduits] table comes from a database. Initially, all its rows will have a state equal to [Original] to indicate that these are the original rows of the table. This state can then change and take on different values, some of which are listed below:
- [Added]: the row has been added—it was not part of the original table
- [Deleted]: the row has been deleted—it is still in the table but "marked" as "to be deleted"
- [Modified]: the row has been modified
[RowStateFilter] allows you to display the rows in the table that have a certain status:
- (continued)
- [DataViewRowState.Added]: only added rows are displayed. They are shown with their current values.
- [DataViewRowState.ModifiedOriginal]: only modified rows are displayed. They are displayed with their original values.
- [DataViewRowState.ModifiedCurrent]: Only modified rows are displayed. They are displayed with their current values.
- [DataViewRowState.Deleted]: Only deleted rows are displayed. They are displayed with their original values.
- [DataViewRowState.CurrentRows]: Non-deleted rows are displayed. They are displayed with their current values.
Thus, the [RowStateFilter] will have the following values:
no filter on row state | ||
DataViewRowState.CurrentRows | displays the current state of the product table | |
DataViewRowState.Deleted | displays the deleted rows of the product table | |
DataViewRowState.ModifiedOriginal | displays modified rows from the product table along with their original values | |
DataViewRowState.Added | displays the rows added to the original product table |
The [DataGrid1-5] containers will allow us to track updates to the [dtProduits] table. The [DataGrid1] component allows for the modification and deletion of a product. We will see how the component’s configuration enables this update. It is also paginated and sorted. These two features were already covered in a previous example.
The [Add] link provides access to a product addition form:
![]() |
No. | name | type | role |
panel | |||
TextBox | product name | ||
RequiredFieldValidator | checks for a value in [txtName] | ||
TextBox | product price | ||
RequiredFieldValidator | checks for a value in [txtPrice] | ||
CompareValidator | checks that price >= 0 | ||
Button | [submit] button to add the product | ||
Label | Information text about the result of the Add operation |
The [products] database may be unavailable when the application starts. In this case, the [errors] view is displayed to the user:
![]() |
No. | name | type | role |
panel | |||
Repeater | list of errors |
9.6.3. Data container configuration
The five [DataGrid] containers were configured in [WebMatrix]. They were formatted (colors and borders) using the [Auto Configuration] link in their properties panel. The [DataGrid1] container was configured using the [Property Generator] link in the same panel. Sorting was enabled (the [General] tab):

The columns are not generated automatically, unlike in the other four containers. They were defined manually using the wizard:

First, two [Related Column] columns named [name] and [price] were created. They were associated respectively with the [name] and [price] fields of the data source that the containers will display. Here, for example, is the configuration of the [name] column:

The sort expression is the expression that must be placed after the [order by] clause in the SQL [select] statement that will be executed when the user clicks on the header column [name] associated with the [name] field of the [DataGrid]. Here we have entered [name] so that the sort clause will be [order by name]. We will see that we will modify this to be [order by name asc] or [order by name desc] depending on the sort order chosen by the user.
We have also created two columns of buttons:

The [Edit, Update, Cancel] column will allow us to edit a product, and the [Delete] column to delete it. Each of these columns can be configured. The [Edit, Update, Cancel] column offers the following configuration:

We can see that the button texts can be modified. Regarding buttons, we have the choice between links and buttons (drop-down list above). Links have been chosen here. The configuration of the [Delete] column is similar. In addition to this configuration wizard, we used the [DataGrid] properties window directly to set the [DataKeyField] property, which specifies which field in the data source is used to index the rows of the [DataGrid]. Here, the primary key of the product table is used:

Ultimately, this configuration generates the following presentation code:
<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="name" SortExpression="name" HeaderText="name"></asp:BoundColumn>
<asp:BoundColumn DataField="price" SortExpression="price" HeaderText="price"></asp:BoundColumn>
<asp:EditCommandColumn ButtonType="LinkButton" UpdateText="Update" CancelText="Cancel" EditText="Edit"></asp:EditCommandColumn>
<asp:ButtonColumn Text="Delete" CommandName="Delete"></asp:ButtonColumn>
</Columns>
<PagerStyle NextPageText="Next" PrevPageText="Previous" ...></PagerStyle>
</asp:DataGrid>
As always, once you have gained some experience, you can write all or part of the code above directly.
The other [DataGrid] containers have the default configuration obtained by automatically generating the [DataGrid] columns from those of the data source to which it is associated.
The [Repeater] container is used to display a list of errors. Its configuration is done directly in the presentation code:
<asp:Repeater id="rptErrors" runat="server" EnableViewState="False">
<HeaderTemplate>
The following errors occurred:
</HeaderTemplate>
<ItemTemplate>
<%# Container.DataItem %>
</ItemTemplate>
<FooterTemplate>
</FooterTemplate>
</asp:Repeater>
Each row of the component displays the value [Container.DataItem], i.e., the corresponding value from the data list. This will be of type [ArrayList] and will represent a list of errors.
9.6.4. The application's presentation code
This is located in the [main.aspx] file:
<%@ Page src="main.aspx.vb" inherits="main" autoeventwireup="false" Language="vb" %>
<HTML>
<HEAD>
</HEAD>
<body>
<form id="Form1" runat="server">
<P>
<FONT size="6">Options:</FONT>
<asp:linkbutton id="lnkFilter" runat="server" CausesValidation="False">
Filter
</asp:linkbutton>
<asp:linkbutton id="lnkUpdate" runat="server" CausesValidation="False">
Update
</asp:linkbutton>
<asp:linkbutton id="lnkAjout" runat="server" CausesValidation="False">
Add
</asp:linkbutton>
</P>
<HR width="100%" SIZE="1">
<asp:panel id="vueFormulaire" runat="server">
<P>Filter condition on the LISTE table. Example: price<100 and
price>50</P>
<P>
<asp:TextBox id="txtFilter" runat="server" Columns="60"></asp:TextBox></P>
<P>Number of rows per page:
<asp:TextBox id="txtPages" runat="server" Columns="3">5</asp:TextBox>
<asp:RequiredFieldValidator id="rfvLines" runat="server" Display="Dynamic"
ControlToValidate="txtPages" ErrorMessage="Enter the number of lines per page"
EnableClientScript="False">
</asp:RequiredFieldValidator></P>
<P>
<asp:RangeValidator id="rvLines" runat="server" Display="Dynamic"
ControlToValidate="txtPages" ErrorMessage="You must enter a number between 3 and 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="btnExecute" runat="server" CausesValidation="False"
EnableViewState="False" Text="Execute">
</asp:Button></P>
</asp:panel>
<asp:panel id="productView" runat="server">
<TABLE>
<TR>
<TD align="center" bgColor="#ff9966">
<P>Sort
<asp:RadioButton id="rdCroissant" runat="server" Text="ascending"
GroupName="rdTri" Checked="True">
</asp:RadioButton>
<asp:RadioButton id="rdDescending" runat="server" Text="Descending"
GroupName="rdSort">
</asp:RadioButton></P>
</TD>
<TD align="center" bgColor="#ff9966">All products
</TD>
<TD align="center" bgColor="#ff9966">Removals
</TD>
<TD align="center" bgColor="#ff9966">Changes
</TD>
<TD align="center" bgColor="#ff9966">Additions
</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="name" SortExpression="name" HeaderText="name">
</asp:BoundColumn>
<asp:BoundColumn DataField="price" SortExpression="price" HeaderText="price">
</asp:BoundColumn>
<asp:EditCommandColumn ButtonType="LinkButton" UpdateText="Update"
CancelText="Cancel" EditText="Edit">
</asp:EditCommandColumn>
<asp:ButtonColumn Text="Delete" CommandName="Delete">
</asp:ButtonColumn>
</Columns>
<PagerStyle NextPageText="Next" PrevPageText="Previous" ....>
</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="errorView" runat="server">
<asp:Repeater id="rptErrors" runat="server" EnableViewState="False">
<HeaderTemplate>
The following errors occurred:
</HeaderTemplate>
<ItemTemplate>
<%# Container.DataItem %>
</ItemTemplate>
<FooterTemplate>
</FooterTemplate>
</asp:Repeater>
</asp:panel>
<asp:panel id="vueAjout" EnableViewState="False" Runat="server">
<P>Add a product</P>
<P>
<TABLE ... border="1">
<TR>
<TD>name</TD>
<TD>
<asp:TextBox id="txtName" runat="server" Columns="30"></asp:TextBox>
<asp:RequiredFieldValidator id="rfvName" runat="server" ControlToValidate="txtName"
ErrorMessage="You must enter a name">
</asp:RequiredFieldValidator>
</TD>
</TR>
<TR>
<TD>price</TD>
<TD>
<asp:TextBox id="txtPrice" runat="server" Columns="10"></asp:TextBox>
<asp:RequiredFieldValidator id="rfvPrice" runat="server" ControlToValidate="txtPrice"
ErrorMessage="You must enter a price">
</asp:RequiredFieldValidator>
<asp:CompareValidator id="cvPrice" runat="server" ControlToValidate="txtPrice"
ErrorMessage="CompareValidator" Type="Double" Operator="GreaterThanEqual"
ValueToCompare="0">
</asp:CompareValidator>
</TD>
</TR>
</TABLE>
</P>
<P>
<asp:Button id="btnAdd" runat="server" CausesValidation="False"
Text="Add">
</asp:Button></P>
<P>
<asp:Label id="lblInfo3" runat="server"></asp:Label></P>
</asp:panel>
</form>
</body>
</HTML>
Note the following points:
- The page consists of four containers (panels) [vueFormulaire, vueProduits, vueAjout, vueErreurs] that will form the four views of the application.
- Buttons or links of type [submit] have the property [CausesValidation=false]. The [CausesValidation=true] property triggers the execution of all validation checks on the page. However, in this case, not all validation checks need to be performed at the same time. For example, when adding an item, we do not want the checks regarding the number of rows per page to be executed. We will therefore specify ourselves which validation checks should be performed.
9.6.5. The [global.asax] control code
The [global.asax] controller is as follows:
The associated code [global.asax.vb]:
Imports System
Imports System.Web
Imports System.Web.SessionState
Imports st.istia.univangers.fr
Imports System.Configuration
Imports System.Data
Imports Microsoft.VisualBasic
Imports System.Collections
Public Class Global
Inherits System.Web.HttpApplication
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
' Retrieve configuration information
Dim connectionString As String = ConfigurationSettings.AppSettings("OLEDBStringConnection")
Dim defaultProductsPage As String = ConfigurationSettings.AppSettings("defaultProductsPage")
Dim errors As New ArrayList
If IsNothing(connectionString) Then errors.Add("The [OLEDBStringConnection] parameter has not been initialized")
If IsNothing(defaultProductsPage) Then
errors.Add("The parameter [defaultProductsPage] has not been initialized")
Else
Try
Dim defProductsPage As Integer = CType(defaultProductsPage, Integer)
If defProductsPage <= 0 Then Throw New Exception
Catch ex As Exception
errors.Add("The [defaultProductsPage] parameter has an incorrect value")
End Try
End If
' Any configuration errors?
If errors.Count <> 0 Then
' Log the errors
Application("errors") = errors
' exit
Exit Sub
End If
' No configuration errors here
' create a products object
Dim dtProducts As DataTable
Try
dtProducts = New Products(connectionString).GetProducts
Catch ex As ProductException
'If there was an error accessing the products, log it in the application
Application("errors") = ex.errors
Exit Sub
Catch ex As Exception
' unhandled error
errors.Add(ex.Message)
Application("errors") = errors
' exit sub
End Try
'No initialization errors here
' we store the number of products per page
Application("defaultProductsPage") = defaultProductsPage
' store the product table
Application("productTable") = productTable
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' initialize session variables
If IsNothing(Application("errors")) Then
' view the product table
Session("dvProducts") = CType(Application("dtProducts"), DataTable).DefaultView
' number of products per page
Session("nbProductsPerPage") = Application("defaultProductsPage")
' current page displayed
Session("currentPage") = 0
End If
End Sub
End Class
In [Application_Start], we start by retrieving two pieces of information from the application's [web.config] configuration file:
- OLEDBStringConnection: the OLEDB connection string to the product database
- defaultProductsPage: the default number of products per page displayed
<?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="defaultProductsPerPage" value="5" />
</appSettings>
</configuration>
If either of these two pieces of information is missing, an error list is generated and added to the application. The same applies if the [defaultProduitsPage] parameter exists but is incorrect. If both expected parameters are present and correct, a [dtProduits] table is created and added to the application. This table will be used and updated by the various clients. The database itself will remain unchanged. We will address updating the database in a future application. This table is constructed from an instance of the [products] class discussed earlier and its [getProducts] method. Retrieving the [dtProducts] table may fail. In this case, we know that the [products] class throws an exception of type [ProductException]. It is intercepted here, and the associated error list is stored in the application under the key [errors]. The presence of this key in the information stored in the application will be checked with each request. If it is found, the [errors] view will be sent to the client.
If the [dtProducts] table is shared by all web clients, each of them will nevertheless have its own [dvProducts] view of it. This is because each web client can set a filter on the [dtProducts] table as well as a sort order. This information specific to each web client is stored in the client’s view. Therefore, this view is created in [Session_Start] so that it can be placed in the session specific to each client. We use the [DefaultView] attribute of the [dtProduits] table to have a default view of the table. Initially, there is neither a filter nor a sort order. Additionally, two pieces of information are also placed in the session:
- the number of products per page, identified by the key [nbProduitsPage]. At the start of the session, this number is equal to the default value defined in the configuration file.
- the number of the current product page. Initially, this is the first page.
9.6.6. The controller code [main.aspx.vb]
The controller skeleton is as follows:
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
' page components
Protected WithEvents txtPages As System.Web.UI.WebControls.TextBox
Protected WithEvents btnExecute As System.Web.UI.WebControls.Button
Protected WithEvents formView As System.Web.UI.WebControls.Panel
Protected WithEvents DataGrid1 As System.Web.UI.WebControls.DataGrid
Protected WithEvents lnkErrors As System.Web.UI.WebControls.LinkButton
Protected WithEvents errorView As System.Web.UI.WebControls.Panel
Protected WithEvents rdAscending As System.Web.UI.WebControls.RadioButton
Protected WithEvents rdDescending As System.Web.UI.WebControls.RadioButton
Protected WithEvents txtFilter 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 lblErrors As System.Web.UI.WebControls.Label
Protected WithEvents DataGrid2 As System.Web.UI.WebControls.DataGrid
Protected WithEvents productView As System.Web.UI.WebControls.Panel
Protected WithEvents AddView As System.Web.UI.WebControls.Panel
Protected WithEvents lnkUpdate As System.Web.UI.WebControls.LinkButton
Protected WithEvents lnkFilter As System.Web.UI.WebControls.LinkButton
Protected WithEvents txtName As System.Web.UI.WebControls.TextBox
Protected WithEvents priceTextBox As System.Web.UI.WebControls.TextBox
Protected WithEvents btnAdd As System.Web.UI.WebControls.Button
Protected WithEvents lblInfo3 As System.Web.UI.WebControls.Label
Protected WithEvents rfvLines As System.Web.UI.WebControls.RequiredFieldValidator
Protected WithEvents rvLines As System.Web.UI.WebControls.RangeValidator
Protected WithEvents rfvName As System.Web.UI.WebControls.RequiredFieldValidator
Protected WithEvents rfvPrice As System.Web.UI.WebControls.RequiredFieldValidator
Protected WithEvents cvPrice As System.Web.UI.WebControls.CompareValidator
Protected WithEvents lnkAdd 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
' page data
Protected dtProducts As DataTable
Protected dvProducts As DataView
Protected defaultProductsPage As Integer
Protected error As Boolean = False
' page load
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
End Sub
Private Sub displayErrors()
...
End Sub
Private Sub displayForm()
...
End Sub
Private Sub btnExecute_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnExecute.Click
....
End Sub
Private Sub displayProducts(ByVal page As Integer, ByVal pageSize 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 deleteProduct(ByVal productId As Integer)
...
End Sub
Private Sub UpdateProduct(ByVal productId 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 displayAdd()
...
End Sub
Private Sub lnkFilter_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkFilter.Click
....
End Sub
Private Sub btnAdd_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnAdd.Click
....
End Sub
End Class
9.6.7. Instance data
The [main] class uses the following instance data:
Public Class main
Inherits System.Web.UI.Page
' page components
Protected WithEvents txtPages As System.Web.UI.WebControls.TextBox
...
' page data
Protected dtProducts As DataTable
Protected dvProducts As DataView
Protected defaultProductsPage As Integer
the [DataTable] table for products—shared by all clients | |
the [DataView] for products - specific to each customer | |
default number of products per page |
9.6.8. The [Page_Load] procedure for loading the page
The code for the [Page_Load] procedure is as follows:
' page load
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' Check if the application has errors
If Not IsNothing(Application("errors")) Then
' the application did not initialize correctly
displayErrors(CType(Application("errors"), ArrayList))
Exit Sub
End If
' retrieve a reference to the product table
productTable = CType(Application("productTable"), DataTable)
' retrieve the product view
dvProducts = CType(Session("dvProducts"), DataView)
' Retrieve the number of products per page
defaultProductsPage = CType(Application("defaultProductsPage"), Integer)
' cancel any ongoing updates
DataGrid1.EditItemIndex = -1
'First query
If Not IsPostBack Then
' display the initial form
txtPages.Text = defaultProductsPage.ToString
displayForm()
End If
End Sub
- First, we check whether the product table was loaded when the application started. If not, we display the [errors] view with the appropriate error messages, then exit the procedure after setting the [error] boolean to true. This indicator will be checked by certain procedures.
- We retrieve the product table [dtProduits] from the application and store it in the instance variable [dtProduits] so that it can be shared by all methods on the page.
- We do the same with the [dvProducts] view retrieved from the session and the default number of products per page retrieved from the application.
- If this is the client’s first request, we display the form for defining filtering and pagination conditions with a default pagination of [defaultProduitsPage] products per page.
- The edit mode of the [dataGrid1] component is canceled. This component has an [EditItemIndex] attribute, which is the index of the item currently being edited. If [EditItemIndex]=-1, then no item is currently being edited. If [EditItemIndex]=i, then item number i of the [DataGrid] component is currently being edited. It is then displayed differently from the other items in the [dataGrid]:

Above, the [Product8, 80] item is in [edit] mode. As shown above, the user can confirm the update using the [Update] link or cancel it using the [Cancel] link. They can also use other links that have nothing to do with the item currently being edited. By setting [DataGrid1.EditItemIndex=-1] each time the page loads, we ensure that the edit mode of [DataGrid1] is systematically canceled. We will only assign it a different value if an [Edit] link has been clicked. We will do this in the procedure handling this event. We will have the following situations:
- The [Edit] link for item #8 in [DataGrid1] has been clicked. [Page_Load] first sets [DataGrid1.EditItemIndex] to -1. Then, the procedure handling the [Edit] event will set [DataGrid1.EditItemIndex] to 8. Ultimately, when the page is sent back to the client, item #8 will indeed be in [edit] mode and will appear as shown above.
- The user edits the product that is in [edit] mode and confirms the change using the [Update] link. [Page_Load] sets [DataGrid1.EditItemIndex] to -1. Then the procedure handling the [Update] event will execute, and item #8 in [dtProduits] will be updated. When the page is sent back to the client, no item in [DataGrid1] will be in edit mode (DataGrid.EditItemIndex=-1).
- If a user has started updating a product, it would make sense for them to cancel this update by clicking [Cancel]. However, nothing prevents them from clicking another link. We must therefore account for this scenario. In this case, as in the previous ones, [Page_Load] begins by setting [DataGrid1.EditItemIndex] to -1, then the procedure handling the event that occurred will execute. It will not modify the [DataGrid1.EditItemIndex] property, which will therefore remain at -1. When the page is sent back to the client, no item in [DataGrid1] will be in edit mode.
We can see that by setting [EditItemIndex] to -1 when the page loads, we avoid having to worry about whether or not the user was in edit mode when they clicked a link.
9.6.9. Displaying the [errors], [form], and [add] views
The [errors] view is displayed when the page loads [Page_Load] if it is detected that the application failed to initialize properly. This is done by binding the [rptErrors] data component to the list of errors passed as parameters and making the appropriate container visible. Finally, the three links [Filter, Update, Add] are hidden because the application cannot be used in the event of an error.
Private Sub displayErrors(ByVal errors As ArrayList)
' we bind the error list to the rptErreurs repeater
With rptErrors
.DataSource = errors
.DataBind()
End With
'Disable the options
lnkAdd.Visible = False
lnkUpdate.Visible = False
lnkFilter.Visible = False
' display the [errors] view
errorView.Visible = True
formView.Visible = False
ProductsView.Visible = False
AddView.Visible = False
End Sub
The other views are requested based on the options presented to the user:

The [Add] view is displayed when the user clicks the [Add] link on the page:
Private Sub lnkAdd_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkAdd.Click
' display the [Add] view
displayAdd()
End Sub
Private Sub displayAdd()
' displays the Add view
viewAdd.Visible = True
formView.Visible = False
productView.Visible = False
errorView.Visible = False
End Sub
The [Form] view is displayed when the user clicks the [Filter] link on the page. We must then display the view that allows the user to define
- the filter for the product table
- the number of products displayed on each web page
A form is displayed that retrieves the values stored in the session:
Private Sub lnkFiltre_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkFiltre.Click
' define the filter conditions
txtFilter.Text = dvProducts.RowFilter
' set the pagination
txtPages.Text = CType(Session("nbProduitsPage"), String)
' Display the filter form
displayForm()
End Sub
A view's filter condition is defined in its [RowFilter] property. Recall that the filtered and paginated view is named [dvProducts] and was retrieved from the session when the page loaded [Page_Load]. The filter condition is therefore retrieved from [dvProduits.RowFilter]. The number of products per page is also retrieved from the session. If the user has never defined this information, this number is equal to the default number of products per page. Once this is done, we display the form that allows the user to define these two pieces of information:
Private Sub displayForm()
' display the [form] view
vueFormulaire.Visible = True
errorView.Visible = False
productView.Visible = False
AddView.Visible = False
End Sub

9.6.10. Validating the [form] view
Clicking the [Execute] button above is handled by the following procedure:
Private Sub btnExecute_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnExecute.Click
' Is the page valid?
rfvLines.Validate()
rvLines.Validate()
If Not rfvLines.IsValid Or Not rvLines.IsValid Then
displayForm()
Exit Sub
End If
' attach filtered data to the grid
Try
dvProducts.RowFilter = txtFilter.Text.Trim
Catch ex As Exception
lblinfo1.Text = "Filtering error (" + ex.Message + ")"
displayForm()
Exit Sub
End Try
' store the number of products per page
Session("nbProductsPerPage") = txtPages.Text
'and the current page
Session("currentPage") = 0
' everything is fine - display the data
displayProducts(0, CType(txtPages.Text, Integer))
End Sub
First, let's recall that the page has two validation components:
No. | name | type | role |
RequiredFieldValidator | checks for a value in [txtPages] | ||
RangeValidator | checks that txtPages is in the range [3,10] |
The procedure begins by executing the validation code for the two components above using their [Validate] method, then checks the value of their [IsValid] attribute. This attribute is set to [true] only if the validated data was found to be valid. If the validity checks for either component fail, the [form] view is re-displayed so the user can correct the error(s). The filter condition is applied to the [dvProduits.RowFilter] attribute. Here, an exception may occur if the user has entered an incorrect filter criterion, as shown below:

In this case, the [form] view is redisplayed. If both pieces of entered information are correct, then two pieces of data are placed in the session:
- the number of products per page selected by the user
- the page number to be displayed—initially page 0
These two pieces of information are used every time the [products] view is displayed.
9.6.11. Displaying the [products] view
The [products] view is as follows:

We have 5 [DataGrid] components, each displaying a specific view of the [dtProduits] product table. Starting from the left:
name | role |
Grid displaying a filtered view of the product table. The filter is the one set by the [form] view. We also have .AllowPaging=true, .AllowSorting=true | |
Displays the entire product table—allows tracking of updates | |
will display the deleted products in the product table | |
will display the modified products in the product table | |
will display the added products in the product table |
The [displayProducts] procedure is responsible for displaying the previous view:
Private Sub displayProducts(ByVal page As Integer, ByVal pageSize As Integer)
' bind the data to the two [DataGrid] components
Application.Lock()
With DataGrid1
.DataSource = dvProduits
.PageSize = pageSize
.CurrentPageIndex = page
.DataBind()
End With
Dim dvCurrent As New DataView(dtProducts)
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()
' display the [products] view
ProductView.Visible = True
formView.Visible = False
errorView.Visible = False
viewAdd.Visible = False
' Save the current page
Session("currentPage") = page
End Sub
The procedure accepts two parameters:
- the page number [page] to display in [DataGrid1]
- the number of products [pageSize] per page
The [DataGrid] is linked to the product table through 5 different views.
- [DataGrid1] is linked to the paginated and sorted view [dvProduits].
With DataGrid1
.DataSource = dvProducts
.PageSize = pageSize
.CurrentPageIndex = page
.DataBind()
End With
It is during this binding of [DataGrid1] to its data source that we need the two pieces of information passed as parameters to the procedure.
- [DataGrid2] is bound to a view displaying all current items in the [dtProduits] table. Like [DataGrid1], it presents an up-to-date view of the [dtProduits] table but without pagination or sorting.
Dim dvCurrent As New DataView(dtProducts)
dvCurrent.RowStateFilter = DataViewRowState.CurrentRows
With DataGrid2
.DataSource = dvCurrent
.DataBind()
End With
Here we are using the [RowStateFilter] property of the [DataView] class. A row in a table—or in this case, a view—has a [RowState] property that indicates the row’s state. Here are a few examples:
- (continued)
- [Added]: the row has been added
- [Modified]: the row has been modified
- [Deleted]: the row has been deleted
- [Unchanged]: the row has not changed
When the [dtProduits] table was initially created in [Application_Start], all its rows were in the [Unchanged] state. This state will change as web clients update the data:
- (continued)
- When a client creates a new product, a row is added to the [dtProduits] table. It will be in the [Added] state.
- When a product is modified, its status changes from [Unchanged] to [Modified].
- When a product is deleted, the row is not physically deleted. Instead, it is marked for deletion and its state changes to [Deleted]. It is possible to undo this deletion.
The [RowStateFilter] attribute of the [DataView] class allows you to filter a view based on the [RowState] of its rows. Its possible values are those of the [DataRowViewState] enumeration:
- (continued)
- DataRowViewState.CurrentRows: Rows that have not been deleted are displayed with their current values
- DataRowViewState.Added: Added rows are displayed with their current values
- DataRowViewState.Deleted: Deleted rows are displayed with their original values
- DataRowViewState.ModifiedOriginal: Modified rows are displayed with their original values
- DataRowViewState.ModifiedCurrent: Modified rows are displayed with their current values
A modified row has two values: the original value, which is the value the row had before the first modification, and the current value, which is the value obtained after one or more modifications. Both of these values are retained simultaneously.
DataGrids 2 through 5 display a view of the [dtProduits] table filtered by the [RowStateFilter] attribute
DataGrid | Filter |
RowStateFilter= DataRowViewState.CurrentRows | |
RowStateFilter = DataRowViewState.Deleted | |
RowStateFilter = DataRowViewState.ModifiedOriginal | |
RowStateFilter= DataRowViewState.Added |
The [dtProducts] table is updated simultaneously by different web clients, which can lead to conflicts when accessing the table. When a client displays its views of the [dtProducts] table, we want to prevent it from doing so while the table is in an unstable state because it is currently being modified by another web client. Therefore, whenever a client needs the [dtProducts] table for reading, as above, or for writing when adding, modifying, or deleting products, it will synchronize with the other clients using the following sequence:
There may be several critical sections in the application code:
The mechanism works as follows:
- One or more clients arrive at the [Application.Lock] statement in critical section 1. This is a single-entry token dispenser. A single client obtains this token. Let’s call it C1.
- Until client C1 returns the token via [Application.Unlock], no other client is allowed to enter a critical section controlled by [Application.Lock]. In the example above, therefore, no client can enter critical sections 1 and 2.
- Client C1 executes [Application.Unlock] and thus returns the entry token. This token can then be given to another client. Steps 1 through 3 are repeated.
With this mechanism, we ensure that only one client has access to the [dtProduits] table, whether for reading or writing.
The [Update] link also displays the [Products] view:

The associated procedure is as follows:
Private Sub lnkMisajour_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkMisajour.Click
' display products view
displayProducts(CType(Session("currentPage"), Integer), CType(Session("productsPerPage"), Integer))
End Sub
We need to display the [products] view. This is done using the [displayProducts] procedure, which takes two parameters: the number of the current page to display and the number of products per page to display. Both of these pieces of information are in the session, possibly with their default values if the user has never defined them themselves.
9.6.12. Pagination and Sorting of [DataGrid1]
We have already encountered the procedures that handle pagination and sorting of a [DataGrid] in another example. When the page changes, we display the [products] view by passing the new page number as a parameter to the [displayProducts] procedure.
Private Sub DataGrid1_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles DataGrid1.PageIndexChanged
' page change
displayProducts(e.NewPageIndex, DataGrid1.PageSize)
End Sub
The [DataGrid1_SortCommand] procedure is executed when the user clicks on the header of one of the columns in [DataGrid1]. The sort expression must then be assigned to the [Sort] attribute of the [dvProduits] view displayed by [DataGrid1]. This expression is syntactically equivalent to the sort expression placed after the [order by] clause in the SQL SELECT statement. The [e] argument of the procedure has a [SortExpression] attribute that provides the sort expression associated with the column whose header was clicked. When the [DataGrid1] component's [ ] was created, this sort expression was defined. Below is the one defined for the [name] column of [DataGrid1]:

If the sort expression was not defined during the design of the [DataGrid], the name of the data field associated with the [DataGrid] column is used. The sort order (ascending, descending) is set here by the user using the radio buttons [rdAscending, rdDescending]:

The sorting procedure is as follows:
Private Sub DataGrid1_SortCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridSortCommandEventArgs) Handles DataGrid1.SortCommand
' sort the DataGrid
With dvProduits
.Sort = e.SortExpression + " " + CType(IIf(rdAscending.Checked, "asc", "desc"), String)
End With
' display it
displayProducts(0, DataGrid1.PageSize)
End Sub
9.6.13. Deleting a product
To delete a product, the user clicks the [Delete] link in the product row:

The [DataGrid1_DeleteCommand] procedure is then executed:
Private Sub DataGrid1_DeleteCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.DeleteCommand
' Delete a product
' key of the product to delete
Dim productId As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)
' Delete the product
Dim error As Boolean = False
Try
deleteProduct(productId)
Catch ex As Exception
' error
lblInfo2.Text = ex.Message
error = True
End Try
' Change page?
Dim page As Integer = DataGrid1.CurrentPageIndex
If Not error AndAlso DataGrid1.Items.Count = 1 Then
page = DataGrid1.CurrentPageIndex - 1
If page < 0 Then page = 0
End If
' display products
displayProducts(page, DataGrid1.PageSize)
End Sub
Products will be updated using the product's [id] key. The [id] column in the [dtProduits] table is the primary key, and using it, we can locate the product row in the [dtProduits] table that is being updated in the [DataGrid1] component. The procedure therefore begins by retrieving the key of the product whose [Delete] link was clicked:
' ID of the product to be deleted
Dim idProduct As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)
We know that [e.Item] is the element in [DataGrid1] that triggered the event. Roughly speaking, this element is a row in the [DataGrid]. [e.Item.ItemIndex] is the number of the row that triggered the event. This index is relative to the currently displayed page. Thus, the first row of the displayed page has the [ItemIndex=0] property, even if it has the number 17 in the product table. [DataGrid1.DataKeys] is the list of keys for the [DataGrid]. Because we set [DataGrid1.DataKey=id] during design time, the keys for [DataGrid1] consist of the values from the [id] column of the [dtProduits] table, which is also the primary key. Thus, [DataGrid1.DataKeys(e.Item.ItemIndex)] is the [id] key of the product to be deleted. With this obtained, we request its deletion using the [deleteProduct] procedure:
' Delete the product
Dim error As Boolean = False
Try
deleteProduct(productId)
Catch ex As Exception
' error
lblInfo2.Text = ex.Message
error = True
End Try
This deletion may fail in some cases. We'll see why. An exception is then thrown. It is handled here. If there is an error, the [DataGrid1] does not need to change. Only an error message is added to the [products] view. If there is no error and the user has just deleted the only product on the current page, then the previous page is displayed.
' Change page?
Dim page As Integer = DataGrid1.CurrentPageIndex
If Not error And Also DataGrid1.Items.Count = 1 Then
page = DataGrid1.CurrentPageIndex - 1
If page < 0 Then page = 0
End If
In any case, the [products] view is redrawn using the [displayProducts] procedure:
The [deleteProduct] procedure is as follows:
Private Sub deleteProduct(ByVal productId As Integer)
Dim error As String
Try
' synchronization
Application.Lock()
' search for the row to delete
Dim row As DataRow = dtProducts.Rows.Find(productId)
If row Is Nothing Then
error = String.Format("Product [{0}] does not exist", productId)
Else
' Delete the row
row.Delete()
End If
Catch ex As Exception
error = String.Format("Delete error: {0}", ex.Message)
Finally
' end of synchronization
Application.UnLock()
End Try
' throw an exception if an error occurs
If error <> String.Empty Then Throw New Exception(error)
End Sub
The procedure receives the key of the product to be deleted as a parameter. If there were only one client performing the updates, this deletion could be done as follows:
With multiple clients performing updates at the same time, it is more complicated. Indeed, consider the following sequence of events:
![]() |
Time | Action |
T1 | Client A reads the [dtProducts] table—there is a product with key 20 |
T2 | Client B reads the [dtProducts] table - there is a product with key 20 |
T3 | Customer A deletes the product with key 20 |
T4 | Client B deletes the product with key 20 |
When clients A and B read the product table and display its contents on a web page, the table contains the product with key 20. They may therefore want to perform an action on this product, such as deleting it. One of them is bound to do so first. The one who comes next will then try to delete a product that no longer exists. This scenario is handled by the [deleteProducts] procedure in several ways:
- First, there is synchronization using [Application.Lock]. This means that when a client crosses this barrier, no other client can modify or read the product table. In fact, all such operations are synchronized in this way. We have already seen this for reading.
- Next, we check whether the product we want to delete exists. If so, it is deleted; otherwise, an error message is generated.
<div class="odt-code-rich" data-linenums="false" style="counter-reset: odtline 0;"><pre><code class="language-csharp">
<span class="odt-code-line"> ' Search for the row to delete</span>
<span class="odt-code-line"> <span style="color:#0000ff">Dim </span>row <span style="color:#0000ff">As </span>DataRow = dtProducts.Rows.Find(productId)</span>
<span class="odt-code-line"> <span style="color:#0000ff">If </span>row <span style="color:#0000ff">Is Nothing Then</span></span>
<span class="odt-code-line"> error = <span style="color:#0000ff">String</span>.Format("Product [{0}] does not exist", productId)</span>
<span class="odt-code-line"> <span style="color:#0000ff">Else</span><span style="color:#0000ff"></span></span>
<span class="odt-code-line"> ' Delete the row</span>
<span class="odt-code-line"> row.Delete()</span>
<span class="odt-code-line"> <span style="color:#0000ff">End If</span></span>
</code></pre></div>
- We exit the critical section using [Application.Unlock] to allow other clients to perform their updates.
- If the row could not be deleted, the procedure throws an exception associated with an error message.
Let's look at an example. We launch a [Mozilla] client and immediately select the [Update] option (partial view):

We do the same with an [Internet Explorer] client (partial view):

With the [Mozilla] client, we delete the product [product2]. We get the following new page:

The deletion operation was successful. The deleted product appears in the [DataGrid] of deleted products and no longer appears in the product lists of the [DataGrid1] and [DataGrid2] components, which display the products currently present in the table. Let’s do the same with [Internet Explorer]. We delete the [product2] item:

The response received is as follows:

An error message informs the user that the product with key [2] does not exist. The response returns a new view to the client reflecting the current state of the table. We can see that the product with key [2] is indeed in the list of deleted products. The [products] view therefore reflects updates from all clients, not just one.
9.6.14. Adding a product
To add a product, the user clicks the [Add] link in the options. This simply displays the [Add] view:


The two procedures involved in this action are as follows:
Private Sub lnkAjout_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkAjout.Click
' Display the [Add] view
displayAdd()
End Sub
Private Sub displayAdd()
' displays the Add view
AddView.Visible = True
formView.Visible = False
productView.Visible = False
errorView.Visible = False
End Sub
Note that the [Add] view has validation components:
name | type | role |
RequiredFieldValidator | checks for a value in [txtName] | |
RequiredFieldValidator | checks for a value in [txtPrice] | |
CompareValidator | checks that price >= 0 |
The procedure for handling the click on the [Add] button is as follows:
Private Sub btnAdd_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnAdd.Click
' Add a new item to the product table
' First, the data must be valid
rfvName.Validate()
rfvPrice.Validate()
cvPrice.Validate()
If Not rfvName.IsValid Or Not rfvPrice.IsValid Or Not cvPrice.IsValid Then
' Display the input form again
displayAdd()
Exit Sub
End If
' create a row
Dim product As DataRow = dtProducts.NewRow
product("name") = txtName.Text.Trim
product("price") = txtPrice.Text.Trim
' Add the row to the table
Application.Lock()
Try
dtProducts.Rows.Add(product)
lblInfo3.Text = "Added successfully"
' cleanup
txtName.Text = ""
txtPrice.Text = ""
Catch ex As Exception
lblInfo3.Text = String.Format("Error: {0}", ex.Message)
End Try
Application.UnLock()
End Sub
First, we perform validity checks on the three components [rfvNom, rfvPrix, cvPrix]. If any of the checks fails, the [Add] view is redisplayed with error messages for the validation components. Here is an example:

If the data is valid, we prepare the row to be inserted into the [dtProduits] table.
' create a row
Dim product As DataRow = dtProducts.NewRow
product("name") = txtName.Text.Trim
product("price") = txtPrice.Text.Trim
First, we create a new row in the [dtProducts] table using the [DataTable.NewRow] method. This row will have the three columns [id, name, price] from the [dtProducts] table. The [name, price] columns are populated with the values entered in the add form. The [id] column is not populated. This column is of type [AutoIncrement], meaning that the DBMS will assign the maximum existing key plus 1 as the key for a new product. The [product] row created here is detached from the [dtProducts] table. We now need to insert it into this table. Since we are going to update the [dtProduits] table, we enable inter-client synchronization:
Once this is done, we attempt to add the row to the [dtProduits] table. If successful, we display a success message; otherwise, an error message.
Try
dtProducts.Rows.Add(product)
lblInfo3.Text = "Added successfully"
' cleanup
txtName.Text = ""
txtPrice.Text = ""
Catch ex As Exception
lblInfo3.Text = String.Format("Error: {0}", ex.Message)
End Try
Normally, no exceptions should occur. However, exception handling has been implemented as a precaution. Once the addition is complete, we exit the critical section using [Application.Unlock].
9.6.15. Editing a product
To modify a product, the user clicks the [Edit] link in the product row:

This switches to edit mode:

Thanks to the [DataGrid] component, this result is achieved with very little code:
Private Sub DataGrid1_EditCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.EditCommand
' Set the current item to edit mode
DataGrid1.EditItemIndex = e.Item.ItemIndex
' Redisplay the products
displayProducts(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
End Sub
Clicking the [Edit] link triggers the execution, on the server side, of the [DataGrid1_EditCommand] procedure. The [DataGrid1] component has an [EditItemIndex] field. The row with the index value of [EditItemIndex] is set to edit mode. Each value in the row thus updated can be modified in an input box, as shown in the screenshot above. We therefore retrieve the index of the product on which the [Edit] link was clicked and assign it to the [EditItemIndex] property of the [DataGrid1] component. All that remains is to reload the [products] view. It will appear identical to before but with one row in edit mode.
The [Cancel] link for the product being edited allows the user to abandon the update. The procedure associated with this link is as follows:
Private Sub DataGrid1_CancelCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.CancelCommand
' Redisplay the products
displayProducts(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
End Sub
It simply re-displays the [products] view. One might be tempted to set [DataGrid1.EditItemIndex] to -1 to cancel the update mode. In fact, we know that the [Page_Load] procedure does this automatically. There is therefore no need to do it again.
The change is validated by the [Update] link on the row being edited. The following procedure is then executed:
Private Sub DataGrid1_UpdateCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.UpdateCommand
' Key of the product to be modified
Dim idProduct As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)
' modified elements
Dim name As String = CType(e.Item.Cells(0).Controls(0), TextBox).Text.Trim
Dim price As String = CType(e.Item.Cells(1).Controls(0), TextBox).Text.Trim
' Are the changes valid?
lblInfo2.Text = ""
If name = String.Empty Then lblInfo2.Text += "[Enter a name]"
If price = String.Empty Then
lblInfo2.Text += "[Enter a price]"
Else
Dim newPrice As Double
Try
newPrice = CType(price, Double)
If newPrice < 0 Then Throw New Exception
Catch ex As Exception
lblInfo2.Text += "[invalid price]"
End Try
End If
' if an error occurs, reload the update page
If lblInfo2.Text <> String.Empty Then
' set the row back to update mode
DataGrid1.EditItemIndex = e.Item.ItemIndex
' display products
displayProducts(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
' end
Exit Sub
End If
' if no error - update the table
Try
modifyProduct(productId, e.Item)
Catch ex As Exception
' problem
lblInfo2.Text = ex.Message
End Try
' display products
DisplayProducts(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
End Sub
As with deletion, we need to retrieve the key of the product to be modified in order to find its row in the product table [dtProduits]:
' Key of the product to modify
Dim productId As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)
Next, we need to retrieve the new values to assign to the row. These are in the [DataGrid1] component. The [e] argument of the procedure is there to help us. [e.Item] represents the row in [DataGrid1] that triggered the event. This is therefore the row currently being updated, since the [Update] link exists only on this row. This row contains columns designated by the row’s [Cells] collection. Thus, [e.Item.Cells(0)] represents column 0 of the row being updated. We know that the new values are in text boxes. The collection [e.Item.Cells(i).Controls] represents the collection of controls in column i of the [e.Item] row. The following two statements retrieve the values from the text boxes of the row being updated in [DataGrid1]:
' modified elements
Dim name As String = CType(e.Item.Cells(0).Controls(0), TextBox).Text.Trim
Dim price As String = CType(e.Item.Cells(1).Controls(0), TextBox).Text.Trim
We now have the new values for the modified row as strings. We then check if this data is valid. The name must not be empty, and the price must be a positive number or zero:
' Valid changes?
lblInfo2.Text = ""
If name = String.Empty Then lblInfo2.Text += "[Enter a name]"
If price = String.Empty Then
lblInfo2.Text += "[Enter a price]"
Else
Dim newPrice As Double
Try
newPrice = CType(price, Double)
If newPrice < 0 Then Throw New Exception
Catch ex As Exception
lblInfo2.Text += "[invalid price]"
End Try
End If
If an error occurs, the label [lblInfo2] will display an error message, and the same page will simply be reloaded:
' If an error occurs, reload the update page
If lblInfo2.Text <> String.Empty Then
' Set the row back to update mode
DataGrid1.EditItemIndex = e.Item.ItemIndex
' display products
displayProducts(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
' end
Exit Sub
End If
What the code above does not show is that the entered values are lost. This is because [DataGrid1] is linked to the data in the [dtProduits] table, which contains the original values of the modified row. Here is an example.

The result is as follows:

We can see that the initially entered values have been lost. In a professional application, this would likely be unacceptable. Here, we encounter certain limitations of the standard update mode for the [DataGrid] component. It would be preferable to have an [Edit] view analogous to the [Add] view.
If the data is valid, it is used to update the [dtProducts] table:
' if no error - update the table
Try
modifyProduct(productId, e.Item)
Catch ex As Exception
' problem
lblInfo2.Text = ex.Message
End Try
' display products
displayProducts(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
For the same reason mentioned when deleting a product, the modification may fail. Indeed, between the time the client reads the [dtProduits] table and the time they attempt to modify one of their products, that product may have been deleted by another client. The [modifyProduct] procedure's " " handles this case by throwing an exception. This exception is handled here. After the update succeeds or fails, the application returns the [products] view to the client. We still need to see how the [modifyProduct] procedure performs the update:
Private Sub modifyProduct(ByVal productId As Integer, ByVal item As DataGridItem)
Dim error As String
Try
' synchronization
Application.Lock()
' search for the row to modify
Dim row As DataRow = dtProducts.Rows.Find(productId)
If line Is Nothing Then
error = String.Format("Product [{0}] does not exist", productId)
Else
' modify the row
With line
.Item("name") = CType(item.Cells(0).Controls(0), TextBox).Text.Trim
.Item("price") = CType(item.Cells(1).Controls(0), TextBox).Text.Trim
End With
End If
Catch ex As Exception
error = String.Format("Error during modification: {0}", ex.Message)
Finally
' end of synchronization
Application.UnLock()
End Try
' throw an exception if error
If error <> String.Empty Then Throw New Exception(error)
End Sub
We won’t go into the details of this procedure, whose code is similar to that of the [deleteProduct] procedure, which has been explained at length. Let’s just look at two examples. First, we’ll change the price of product [product1]:

Clicking the [Update] link above produces the following response:

The change is clearly visible in [DataGrid] components 1 and 2, which reflect the current state of the [dtProducts] table. It is also visible in the [DataGrid4] component, which displays a view of the modified rows, showing them with their original values. Now let’s look at a case of a concurrency conflict. As with the deletion example, we will use two different web clients. A client [Mozilla] reads the [dtProduits] table and begins editing the product [product1]:

An [Internet Explorer] client is about to delete product [product1]:

The [Internet Explorer] client deletes [product1]:

Note that [product1] is no longer in the table of modified rows but in the table of deleted rows. The [Mozilla] client validates its update:

The client [Mozilla] receives the following response to its update:

They can see that [product1] has been deleted since it is in the list of deleted products.
9.7. Web application for updating the physical product table
9.7.1. Proposed solutions
The previous application was more of a textbook example intended to demonstrate the management of a cached [DataTable] object than a real-world scenario. Indeed, at some point, the actual data source must be updated. Two different strategies can be chosen:
- We use the [dtProduits] cache in memory to update the data source. A page can be created within the web tree of the previous application to provide access to its [dtProduits] cache. This page would allow an administrator to synchronize changes made to the [dtProduits] cache with the physical data source. To do this, we could add a new method to the [products] access class that takes the [dtProduits] cache as a parameter and uses this cache to update the physical data source.
- The physical data source is updated at the same time as the cache.
Strategy #1 allows opening only one connection to the physical data source. Strategy #2 requires a connection for each update. Depending on connection availability, one strategy may be preferred over the other. Since we have the tools to implement it (the [products] class), we choose Strategy #2.
9.7.2. Solution 1
For the sake of continuity with the previous application, we choose the following strategy:
- the physical source is updated at the same time as the cache
- the cache is built only once during application initialization in [global.asax.vb]. This means that if the physical data source is updated by clients other than web clients, the web clients do not see these changes. They see only the changes they themselves make to the cached table.
To update the physical data source, we need an instance of the product access class. Each client could have its own. We can also share a single instance that would be created by the application at startup. This is the solution we are choosing here. The control code [global.asax.vb] is modified as follows:
Imports System
Imports System.Web
...
Public Class Global
Inherits System.Web.HttpApplication
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
' retrieve configuration information
Dim connectionString As String = ConfigurationSettings.AppSettings("OLEDBStringConnection")
Dim defaultProductsPage As String = ConfigurationSettings.AppSettings("defaultProductsPage")
Dim errors As New ArrayList
...
' No configuration errors here
' create a products object
Dim objProducts As New Products(connectionString)
Dim dtProducts As DataTable
Try
ProductsDT = ProductObj.GetProducts
Catch ex As ProductException
'If there was an error accessing the products, we log it in the application
Application("errors") = ex.errors
Exit Sub
Catch ex As Exception
' unhandled error
errors.Add(ex.Message)
Application("errors") = errors
' exit sub
End Try
' no initialization errors here
...
' we store the data access instance
Application("objProducts") = objProducts
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
...
End Sub
End Class
An instance of the data access class has been stored in the application, associated with the [objProducts] key. Each client will use this instance to access the physical data source. It will be retrieved in the [Page_Load] procedure of [main.aspx.vb]:
Protected objProducts As products
....
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
' retrieve the product access instance
objProducts = CType(Application("objProducts"), products)
...
End Sub
The data access instance [objProducts] is accessible to all methods on the page. It will be used for the three update operations: adding, deleting, and modifying.
The add procedure is modified as follows:
Private Sub btnAdd_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnAdd.Click
...
' add the line
Application.Lock()
Try
' Add the row to the in-memory table
dtProducts.Rows.Add(product)
' add the row to the physical source
Dim newProduct As sProduct
With newProduct
.name = CType(product("name"), String)
.price = CType(product("price"), Double)
End With
objProducts.addProduct(newProduct)
' follow-up
lblInfo3.Text = "Added successfully"
' cleanup
txtName.Text = ""
txtPrice.Text = ""
Catch ex As Exception
' error
lblInfo3.Text = String.Format("Error: {0}", ex.Message)
End Try
Application.UnLock()
End Sub
The modification procedure is modified as follows:
Private Sub modifyProduct(ByVal productId As Integer, ByVal item As DataGridItem)
Dim error As String
Try
' synchronization
Application.Lock()
' search for the row to modify
Dim row As DataRow = dtProducts.Rows.Find(productId)
If row Is Nothing Then
error = String.Format("Product [{0}] does not exist", productId)
Else
' update the row in the cache
With row
.Item("name") = CType(item.Cells(0).Controls(0), TextBox).Text.Trim
.Item("price") = CType(item.Cells(1).Controls(0), TextBox).Text.Trim
End With
' Modify the row in the physical source
Dim newProduct As Product
With newProduct
.id = productId
.name = CType(line.Item("name"), String)
.price = CType(line.Item("price"), Double)
End With
objProducts.updateProduct(newProduct)
End If
Catch ex As Exception
error = String.Format("Error during modification: {0}", ex.Message)
Finally
' end of synchronization
Application.UnLock()
End Try
' throw an exception if error
If error <> String.Empty Then Throw New Exception(error)
End Sub
The delete procedure is modified as follows:
Private Sub deleteProduct(ByVal productId As Integer)
Dim error As String
Try
' synchronization
Application.Lock()
' search for the row to delete
Dim row As DataRow = dtProducts.Rows.Find(productId)
If row Is Nothing Then
error = String.Format("Product [{0}] does not exist", productId)
Else
' Delete the row from the cache
row.Delete()
' Delete the row from the physical source
productObj.deleteProduct(productId)
End If
Catch ex As Exception
error = String.Format("Deletion error: {0}", ex.Message)
Finally
' end of synchronization
Application.UnLock()
End Try
' throw an exception if error
If error <> String.Empty Then Throw New Exception(error)
End Sub
9.7.3. Tests
We start with the following data table in an ACCESS file:

A web client is launched:

We add a product:

We are discontinuing [product1]:

We are changing the price of [product2]:

After these changes, we look at the contents of the [list] table in the ACCESS database:

The three changes have been correctly reflected in the physical table. Now let’s examine a conflict scenario. We delete the row for [product2] directly in Access:

We return to our web client. The client does not see the deletion that was made and wants to delete [product2] as well:

They receive the following response:

The [product2] row has indeed been removed from the cache, as shown in the list of deleted products. However, the deletion of [product2] in the physical source failed, as indicated by the error message.
9.7.4. Solution 2
In the previous solution, web clients simultaneously update the physical data source but do not see the changes made by other clients. They only see their own. We now want a client to be able to see the physical data source as it currently is, not as it was when the application was launched. To do this, we will offer the client a new option:

With the [Refresh] option, the client forces a re-read of the physical data source. To ensure this does not affect other clients, the table resulting from this read must belong to the client performing the refresh and must not be shared with other clients. This is the first difference from the previous application. The [dtProduits] cache of the data source will be built by each client and not by the application itself. The modification is made in [global.asax.vb]:
Imports System
Imports System.Web
Imports System.Web.SessionState
Imports st.istia.univangers.fr
Imports System.Configuration
Imports System.Data
Imports Microsoft.VisualBasic
Imports System.Collections
Public Class Global
Inherits System.Web.HttpApplication
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
' Retrieve configuration information
...
' no configuration errors here
' create a products object
Dim objProducts As New Products(connectionString)
' store the number of products per page
Application("defaultProductsPage") = defaultProductsPage
' Store the data access instance
Application("objProducts") = objProducts
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' initialize session variables
If IsNothing(Application("errors")) Then
' cache the data source
Dim dtProducts As DataTable
Try
Application.Lock()
dtProducts = CType(Application("objProducts"), products).getProducts
Catch ex As ProductException
'If there was an error accessing the products, we log it in the session
Session("errors") = ex.errors
Exit Sub
Finally
Application.UnLock()
End Try
' we put the [dtProducts] cache into the session
Session("dtProduits") = dtProduits
' view of the products table
Session("dvProducts") = dtProducts.DefaultView
' number of products per page
Session("nbProduitsPage") = Application("defaultProduitsPage")
' current page displayed
Session("currentPage") = 0
End If
End Sub
End Class
The information stored in the session is retrieved with each request in the [Page_Load] procedure:
' page data
Protected dtProducts As DataTable
Protected dvProducts As DataView
Protected objProducts As Products
Protected nbProductsPerPage As Integer
Protected currentPage As Integer
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' Check if the application has encountered an error
If Not IsNothing(Application("errors")) Then
' the application did not initialize correctly
displayErrors(CType(Application("errors"), ArrayList))
Exit Sub
End If
' check if the session has errors
If Not IsNothing(Session("errors")) Then
' the session did not initialize correctly
displayErrors(CType(Session("errors"), ArrayList))
Exit Sub
End If
' retrieve a reference to the product table
productTable = CType(Session("productTable"), DataTable)
' retrieve the product view
dvProducts = CType(Session("dvProducts"), DataView)
' Retrieve the number of products per page
nbProductsPerPage = CType(Session("nbProductsPerPage"), Integer)
' Retrieve the current page
currentPage = CType(Session("currentPage"), Integer)
' Retrieve the product access instance
productObject = CType(Application("productObject"), products)
' cancel any updates currently in progress
DataGrid1.EditItemIndex = -1
'First query
If Not IsPostBack Then
' display the initial form
txtPages.Text = nbProductsPerPage.ToString
displayForm()
End If
End Sub
The information retrieved during the session will be saved at the end of the session after each request:
Private Sub Page_PreRender(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.PreRender
' we store certain information in the session
Session("dtProduits") = dtProduits
Session("dvProducts") = dvProducts
Session("nbProduitsPage") = nbProduitsPage
Session("currentPage") = currentPage
End Sub
The [PreRender] event signals that the response is about to be sent to the client. We take this opportunity to save all the data that needs to be retained in the session. This is excessive, since quite often only some of the data has changed. This systematic saving has the advantage of relieving us of session management in the page’s other methods.
The cache refresh operation is handled by the following procedure:
Private Sub lnkRefresh_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkRefresh.Click
' we must refresh the [dtProduits] cache with the physical data source
' start sync
Application.Lock()
Try
' the products table
dtProducts = CType(Application("objProducts"), products).getProducts
' save the current filter
Dim filter As String = dvProducts.RowFilter
' create the new filtered view
dvProducts = New DataView(dtProducts)
' Reapply the filter
dvProducts.RowFilter = filter
Catch ex As ExceptionProducts
'There was an error accessing the products; we log it in the session
Session("errors") = ex.errors
' display the [errors] view
displayErrors(ex.errors)
' done
Exit Sub
Finally
' end sync
Application.UnLock()
End Try
' It went well - display products starting from the first page
displayProducts(0, numProductsPerPage)
End Sub
The procedure regenerates new values for the cache [dtProduits] and the view [dvProduits]. These will be placed in the session by the [Page_PreRender] procedure described above. Once the cache [dtProduits] has been rebuilt, we display the products starting from the first page.
Here is an example of execution. A client [mozilla] is launched and displays the products:

A client [Internet Explorer] does the same:

The [Mozilla] client deletes [product1], modifies [product2], and adds a new product. It gets the following new page:

The [Internet Explorer] client wants to delete [product1].

It receives the following response:

It was notified that [product1] no longer exists. The user then decides to refresh their cache using the [Refresh] link above. They receive the following response:

He now has the same data source as the [Mozilla] client.
9.8. Conclusion
In this chapter, we spent a lot of time on data containers and their connections to data sources. We concluded by showing how to update a data source using a [DataGrid] component. We used a database table as our data source. Even though the [DataGrid] component makes data presentation a bit easier, the real challenge lies not in the presentation layer but in managing updates to the data source made by different clients. Access conflicts can arise and must be managed. Here, we handled them in the controller using [Application.Lock]. It would likely be wiser to synchronize access to the data source within the data access class so that the controller does not have to worry about such details that are outside its scope.
In practice, tables in a database are linked to one another through relationships, and their updates must take these into account. This primarily affects the data access class, which becomes more complex than what is required for a standalone table. It also generally impacts the presentation layer of the web application, as it is often necessary to display not a single table but tables linked to one another through relationships.
This chapter has also introduced various data structures such as [DataTable, DataView].









