9. Componentes de servidor ASP - 3
9.1. Introdução
Continuamos o nosso trabalho na interface do utilizador explorando as capacidades dos componentes [DataList] e [DataGrid], particularmente na área da atualização dos dados que apresentam
9.2. Tratamento de eventos associados a dados em componentes ligados a dados
9.2.1. O Exemplo
Considere a seguinte página:
![]() |
A página inclui três componentes associados a uma lista de dados:
- um componente [DataList] denominado [DataList1]
- um componente [DataGrid] denominado [DataGrid1]
- um componente [Repeater] denominado [Repeater1]
A lista de dados associada é a matriz {"zero", "um", "dois", "três"}. Cada um destes pontos de dados está associado a um grupo de dois botões rotulados [Info1] e [Info2]. Quando o utilizador clica num dos botões, o texto exibe o nome do botão clicado. O objetivo aqui é demonstrar como gerir uma lista de botões ou links.
9.2.2. Configuração do componente
O componente [DataList1] está configurado da seguinte forma:
<asp:datalist id="DataList1" ... runat="server">
<SelectedItemStyle ...</SelectedItemStyle>
<HeaderTemplate>
[début]
</HeaderTemplate>
<FooterTemplate>
[fin]
</FooterTemplate>
<ItemStyle ...></ItemStyle>
<ItemTemplate>
<P><%# Container.DataItem %>
<asp:Button runat="server" Text="Infos1" CommandName="infos1"></asp:Button>
<asp:Button runat="server" Text="Infos2" CommandName="infos2"></asp:Button></P>
</ItemTemplate>
<FooterStyle ...></FooterStyle>
<HeaderStyle ...></HeaderStyle>
</asp:datalist>
Omitimos tudo o que está relacionado com a aparência da [DataList] para nos concentrarmos exclusivamente no seu conteúdo:
- a secção <HeaderTemplate> define o cabeçalho do [DataList] e a secção <FooterTemplate> define o seu rodapé.
- A secção <ItemTemplate> é o modelo de apresentação utilizado para cada item na lista de dados associada. Contém os seguintes elementos:
- o valor do item de dados atual na lista de dados associada ao componente: <%# Container.DataItem %>
- dois botões rotulados [Info1] e [Info2], respetivamente. A classe [Button] possui um atributo [CommandName] que é utilizado aqui. Isto permitir-nos-á determinar qual o botão que desencadeou um evento na [DataList]. Para lidar com os cliques nos botões, teremos um único manipulador de eventos ligado à própria [DataList], e não aos botões. Este manipulador receberá informações indicando em que linha da [DataList] ocorreu o clique. O atributo [CommandName] permitir-nos-á saber qual o botão nessa linha que desencadeou o clique.
O componente [Repeater1] é configurado de forma muito semelhante:
<asp:repeater id="Repeater1" runat="server">
<HeaderTemplate>
[début]<hr />
</HeaderTemplate>
<FooterTemplate>
<hr />
[fin]
</FooterTemplate>
<SeparatorTemplate>
<hr />
</SeparatorTemplate>
<ItemTemplate>
<%# Container.DataItem %>
<asp:Button runat="server" Text="Infos1" CommandName="infos1"></asp:Button>
<asp:Button runat="server" Text="Infos2" CommandName="infos2"></asp:Button></P>
</ItemTemplate>
</asp:repeater>
Simplesmente adicionámos uma secção <SeparatorTemplate> para que os dados sucessivos apresentados pelo componente sejam separados por uma barra horizontal.
Por fim, o componente [DataGrid1] é configurado da seguinte forma:
<asp:datagrid id="DataGrid1" ... runat="server" PageSize="2" AllowPaging="True">
<SelectedItemStyle ...></SelectedItemStyle>
<AlternatingItemStyle ...></AlternatingItemStyle>
<ItemStyle ...></ItemStyle>
<HeaderStyle ...></HeaderStyle>
<FooterStyle ....></FooterStyle>
<Columns>
<asp:ButtonColumn Text="Infos1" ButtonType="PushButton" CommandName="Infos1">
</asp:ButtonColumn>
<asp:ButtonColumn Text="Infos2" ButtonType="PushButton" CommandName="Infos2">
</asp:ButtonColumn>
</Columns>
<PagerStyle .... Mode="NumericPages"></PagerStyle>
</asp:datagrid>
Aqui também omitimos as informações de estilo (cores, larguras, etc.). Estamos no modo de geração automática de colunas, que é o modo predefinido para o [DataGrid]. Isto significa que haverá tantas colunas quantas existirem na fonte de dados. Aqui, existe uma. Adicionámos duas outras colunas marcadas com <asp:ButtonColumn>. Definimos informações semelhantes às definidas para os outros dois componentes, bem como o tipo de botão, que aqui é [PushButton]. O tipo predefinido é [LinkButton], ou seja, um link. Além disso, os dados serão paginados [AllowPaging=true] com um tamanho de página de dois itens [PageSize=2].
9.2.3. O código de layout da página
O código de apresentação da nossa página de exemplo foi colocado num ficheiro chamado [main.aspx]:
<%@ page codebehind="main.aspx.vb" inherits="vs.main" autoeventwireup="false" %>
<HTML>
<HEAD>
</HEAD>
<body>
<form runat="server">
<P>Gestion d'événements de composants associés à des listes de données</P>
<HR width="100%" SIZE="1">
<table cellSpacing="1" cellPadding="1" bgColor="#ffcc00" border="1">
<tr>
<td ...>DataList</td>
<td ...>DataGrid</td>
<td ...>Repeater</td>
</tr>
<tr>
<td ...>
<asp:datalist id="DataList1" ... runat="server">
<HeaderTemplate>
[début]
</HeaderTemplate>
<FooterTemplate>
[fin]
</FooterTemplate>
<ItemStyle ...></ItemStyle>
<ItemTemplate>
<P><%# Container.DataItem %>
<asp:Button runat="server" Text="Infos1" CommandName="infos1"></asp:Button>
<asp:Button runat="server" Text="Infos2" CommandName="infos2"></asp:Button></P>
</ItemTemplate>
<FooterStyle ...></FooterStyle>
<HeaderStyle ....></HeaderStyle>
</asp:datalist>
<P></P>
</td>
<td ...>
<asp:datagrid id="DataGrid1" ... runat="server" PageSize="2" AllowPaging="True">
<AlternatingItemStyle ...></AlternatingItemStyle>
<ItemStyle ...></ItemStyle>
<HeaderStyle ...></HeaderStyle>
<FooterStyle ...></FooterStyle>
<Columns>
<asp:ButtonColumn Text="Infos1" ButtonType="PushButton" CommandName="Infos1">
</asp:ButtonColumn>
<asp:ButtonColumn Text="Infos2" ButtonType="PushButton" CommandName="Infos2">
</asp:ButtonColumn>
</Columns>
<PagerStyle ... Mode="NumericPages"></PagerStyle>
</asp:datagrid>
</td>
<td ...>
<asp:repeater id="Repeater1" runat="server">
<HeaderTemplate>
[début]<hr />
</HeaderTemplate>
<FooterTemplate>
<hr />
[fin]
</FooterTemplate>
<SeparatorTemplate>
<hr />
</SeparatorTemplate>
<ItemTemplate>
<%# Container.DataItem %>
<asp:Button runat="server" Text="Infos1" CommandName="infos1"></asp:Button>
<asp:Button runat="server" Text="Infos2" CommandName="infos2"></asp:Button></P>
</ItemTemplate>
</asp:repeater>
</td>
</tr>
</table>
<P><asp:label id="lblInfo" runat="server"></asp:label></P>
<P></P>
</form>
</body>
</HTML>
No código acima, omitimos o código de formatação (cores, linhas, tamanhos, etc.)
9.2.4. O código de controlo da página
O código de controlo da aplicação foi colocado no ficheiro [main.aspx.vb]:
Public Class main
Inherits System.Web.UI.Page
' components page
Protected WithEvents DataList1 As System.Web.UI.WebControls.DataList
Protected WithEvents lblInfo As System.Web.UI.WebControls.Label
Protected WithEvents DataGrid1 As System.Web.UI.WebControls.DataGrid
Protected WithEvents Repeater1 As System.Web.UI.WebControls.Repeater
' the data source
Protected textes() As String = {"zéro", "un", "deux", "trois"}
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
If Not IsPostBack Then
'data source links
DataList1.DataSource = textes
DataGrid1.DataSource = textes
Repeater1.DataSource = textes
Page.DataBind()
End If
End Sub
Private Sub DataList1_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles DataList1.ItemCommand
' an event has occurred on one of the [datalist] lines
lblInfo.Text = "Vous avez cliqué sur le bouton [" + e.CommandName + "] de l'élément [" + e.Item.ItemIndex.ToString + "] du composant [DataList]"
End Sub
Private Sub Repeater1_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.RepeaterCommandEventArgs) Handles Repeater1.ItemCommand
' an event has occurred on one of the [repeater] lines
lblInfo.Text = "Vous avez cliqué sur le bouton [" + e.CommandName + "] de l'élément [" + e.Item.ItemIndex.ToString + "] du composant [Repeater]"
End Sub
Private Sub DataGrid1_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.ItemCommand
' an event has occurred on one of the [datagrid] lines
lblInfo.Text = "Vous avez cliqué sur le bouton [" + e.CommandName + "] de l'élément [" + e.Item.ItemIndex.ToString + "] de la page [" + DataGrid1.CurrentPageIndex.ToString() + "] du composant [DataGrid]"
End Sub
Private Sub DataGrid1_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles DataGrid1.PageIndexChanged
' change page
With DataGrid1
.CurrentPageIndex = e.NewPageIndex
.DataSource = textes
.DataBind()
End With
End Sub
End Class
Comentários:
- A fonte de dados [textes] é uma matriz simples de cadeias de caracteres. Será ligada aos três componentes da página
- Esta ligação é estabelecida no procedimento [Page_Load] durante o primeiro pedido. Para pedidos subsequentes, os três componentes irão recuperar os seus valores através do mecanismo [VIEW_STATE].
- Os três componentes possuem um manipulador para o evento [ItemCommand]. Este evento ocorre quando se clica num botão ou num link numa das linhas do componente. O manipulador recebe duas informações:
- source: a referência ao objeto (botão ou link) que desencadeou o evento
- a: informações sobre o evento do tipo [DataListCommandEventArgs], [RepeaterCommandEventArgs] ou [DataGridCommandEventArgs], conforme apropriado. O argumento a contém várias informações. Aqui, duas delas são do nosso interesse:
- a.Item: representa a linha onde o evento ocorreu, do tipo [DataListItem], [DataGridItem] ou [RepeaterItem]. Independentemente do tipo exato, o elemento [Item] possui um atributo [ItemIndex] que indica o número da linha do [Item] no contentor ao qual pertence. Aqui, exibimos esse número de linha
- a.CommandName: é o atributo [CommandName] do botão (Button, LinkButton, ImageButton) que desencadeou o evento. Esta informação, combinada com a anterior, permite-nos determinar qual o botão no contentor que desencadeou o evento [ItemCommand]
9.3. Aplicação - Gestão de uma lista de subscrições
Agora que sabemos como interceptar eventos que ocorrem dentro de um contentor de dados, apresentamos um exemplo que mostra como lidar com eles.
9.3.1. Introdução
A aplicação aqui apresentada simula uma aplicação de subscrição de uma lista de correio. Estas são definidas por um objeto [DataTable] de três colunas:
nome | tipo | função |
string | chave primária | |
string | lista nome do tema | |
cadeia | uma descrição dos tópicos abordados pela lista |
Para manter o nosso exemplo simples, o objeto [DataTable] acima será construído programaticamente de forma arbitrária. Numa aplicação real, seria provavelmente fornecido por um método de uma classe de acesso a dados. A tabela da lista será construída no procedimento [Application_Start], e a tabela resultante será armazenada na aplicação. Chamaremos-lhe a tabela [dtThèmes]. O utilizador subscreverá determinados tópicos desta tabela. A lista das suas subscrições será armazenada num objeto [DataTable] chamado [dtAbonnements], que terá a seguinte estrutura:
nome | tipo | função |
string | chave primária | |
string | lista nome do tema |
A aplicação de página única é a seguinte:
![]() | |||||
N.º | nome | tipo | propriedades | função | |
DataGrid | listas de correio disponíveis para subscrição | ||||
DataList | lista das subscrições do utilizador às listas anteriores | ||||
painel | painel de informações sobre o tópico selecionado pelo utilizador com um link [Mais informações] | ||||
Label | parte de [panelInfos] | nome do tema | |||
Rótulo | parte de [panelInfos] | descrição do tema | |||
Rótulo | mensagem de informação da aplicação | ||||
O nosso exemplo visa ilustrar a utilização dos componentes [DataGrid] e [DataList], em particular o tratamento de eventos que ocorrem ao nível das linhas destes contentores de dados. Por conseguinte, a aplicação não possui um botão de envio que guardaria as seleções do utilizador numa base de dados, por exemplo. No entanto, é realista. Estamos próximos de uma aplicação de comércio eletrónico em que um utilizador adicionaria produtos (assinaturas) ao seu carrinho.
9.3.2. Funcionalidade
A primeira vista que o utilizador vê é a seguinte:
![]() |
O utilizador clica nos links [Mais Informações] para obter detalhes sobre um tópico. Estes são apresentados em [panelInfos]:

Clicam nos links [Subscrever] para subscrever um tópico. Os tópicos selecionados são adicionados ao componente [dlSubscriptions]:

O utilizador pode querer subscrever uma lista à qual já está subscrito. Uma mensagem alerta-o para isso:

Por fim, pode cancelar a subscrição de qualquer um dos tópicos utilizando os botões [Cancelar subscrição] acima. Não é necessária qualquer confirmação. Isto não é necessário aqui porque o utilizador pode facilmente voltar a subscrever. Aqui está a visualização após cancelar a subscrição de [topic1]:

9.3.3. Configurar contentores de dados
O componente [dgThèmes] do tipo [DataGrid] está associado a uma fonte do tipo [DataTable]. A sua formatação foi realizada através do link [Auto Format] no painel de propriedades do [DataGrid]. As suas propriedades foram definidas através do link [Property Generator] no mesmo painel. O código gerado é o seguinte (o código de formatação foi omitido):
<asp:datagrid id="dgThèmes" AutoGenerateColumns="False" AllowPaging="True" PageSize="5"
runat="server">
<ItemStyle ...></ItemStyle>
<HeaderStyle ...></HeaderStyle>
<FooterStyle ...></FooterStyle>
<Columns>
<asp:BoundColumn DataField="thème" HeaderText="Thème"></asp:BoundColumn>
<asp:ButtonColumn Text="Plus d'informations" CommandName="infos"></asp:ButtonColumn>
<asp:ButtonColumn Text="S'abonner" CommandName="abonner"></asp:ButtonColumn>
</Columns>
<PagerStyle HorizontalAlign="Center" ... Mode="NumericPages"></PagerStyle>
</asp:datagrid>
Tenha em atenção os seguintes pontos:
Definimos nós próprios as colunas a apresentar na secção <columns>...</columns> | |
para paginação de dados | |
define a coluna [theme] (HeaderText) do [DataGrid] que será ligada à coluna [theme] da fonte de dados (DataField) | |
define duas colunas de botões (ou links). Para distinguir entre os dois links na mesma linha, utilizaremos a sua propriedade [CommandName]. |
O componente [DataGrid] não está totalmente configurado. Será configurado no código do controlador.
O componente [dlAbonnements] do tipo [DataList] está ligado a uma fonte do tipo [DataTable]. A sua formatação foi feita utilizando o link [AutoFormat] no painel de propriedades do [DataList]. As suas propriedades foram definidas diretamente no código de apresentação. Este código é o seguinte (o código de formatação foi omitido):
<asp:DataList id="dlAbonnements" ... runat="server" >
<HeaderTemplate>
<div align="center">
Vos abonnements</div>
</HeaderTemplate>
<ItemStyle ...></ItemStyle>
<ItemTemplate>
<TABLE>
<TR>
<TD><%#Container.DataItem("thème")%></TD>
<TD>
<asp:Button id="lnkRetirer" CommandName="retirer" runat="server" Text="Retirer" /></TD>
</TR>
</TABLE>
</ItemTemplate>
<HeaderStyle ...></HeaderStyle>
</asp:DataList>
define o texto do cabeçalho da [DataList] | |
define o item atual na [DataList] — aqui colocámos uma tabela com duas colunas e uma linha. A primeira célula conterá o nome do tema ao qual o utilizador pretende subscrever-se, e a outra conterá o botão [Cancelar subscrição], que permite ao utilizador cancelar a sua seleção. |
9.3.4. A página de apresentação
O código de apresentação [main.aspx] é o seguinte:
<%@ page src="main.aspx.vb" inherits="main" autoeventwireup="false" %>
<HTML>
<HEAD>
<title></title>
</HEAD>
<body>
<P>Indiquez les thèmes auxquels vous voulez vous abonner :</P>
<HR width="100%" SIZE="1">
<form runat="server">
<table>
<tr>
<td vAlign="top">
<asp:datagrid id="dgThèmes" ... runat="server">
...
</asp:datagrid>
</td>
<td vAlign="top">
<asp:DataList id="dlAbonnements" ... runat="server" GridLines="Horizontal" ShowFooter="False">
....
</asp:DataList>
</td>
<td vAlign="top">
<asp:Panel ID="panelInfo" Runat="server" EnableViewState="False">
<TABLE>
<TR>
<TD bgColor="#99cccc">
<asp:Label id="lblThème" runat="server"></asp:Label></TD>
</TR>
<TR>
<TD bgColor="#ffff99">
<asp:Label id="lblDescription" runat="server"></asp:Label></TD>
</TR>
</TABLE>
</asp:Panel>
</td>
</tr>
</table>
<P>
<asp:Label id="lblInfo" runat="server" EnableViewState="False"></asp:Label></P>
</form>
</body>
</HTML>
9.3.5. Controladores
A lógica de controlo está distribuída entre os ficheiros [global.asax] e [main.aspx]. O ficheiro [global.asax] é o seguinte:
O ficheiro [global.asax.vb] associado contém o seguinte código:
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 thèmes As New DataTable
' columns
With thèmes.Columns
.Add("id", GetType(System.Int32))
.Add("thème", GetType(System.String))
.Add("description", GetType(System.String))
End With
' column id will be primary key
thèmes.Constraints.Add("cléprimaire", thèmes.Columns("id"), True)
' lines
Dim ligne As DataRow
For i As Integer = 0 To 10
ligne = thèmes.NewRow
ligne.Item("id") = i.ToString
ligne.Item("thème") = "thème" + i.ToString
ligne.Item("description") = "description du thème " + i.ToString
thèmes.Rows.Add(ligne)
Next
' put the data source in the application
Application("thèmes") = thèmes
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' start of session - create an empty subscriptions table
Dim dtAbonnements As New DataTable
With dtAbonnements
' the columns
.Columns.Add("id", GetType(String))
.Columns.Add("thème", GetType(String))
' the primary key
.PrimaryKey = New DataColumn() {.Columns("id")}
End With
' the table is placed in the session
Session.Item("abonnements") = dtAbonnements
End Sub
End Class
O procedimento [Application_Start], executado quando a aplicação recebe o seu primeiro pedido, constrói a [DataTable] de tópicos que podem ser subscritos. É construída arbitrariamente utilizando código. Recorde a técnica que já abordámos. Construímos na seguinte ordem:
- um objeto [DataTable] vazio, sem estrutura e sem dados
- a estrutura da tabela, definindo as suas colunas (nome e tipo de dados)
- as linhas da tabela que representam os dados úteis
Aqui adicionámos uma chave primária. A coluna «id» serve como chave primária. Existem várias formas de expressar isto. Aqui, utilizámos uma restrição. Em SQL, uma restrição é uma regra que os dados numa linha devem seguir para que essa linha seja adicionada a uma tabela. Existem todo o tipo de restrições possíveis. A restrição «Primary Key» obriga a coluna à qual é aplicada a ter valores únicos e não vazios. Uma chave primária pode, na verdade, consistir numa expressão que envolva valores de várias colunas. [DataTable].Constraints é a coleção de restrições para uma determinada tabela. Para adicionar uma restrição, usamos o método [DataTable.Constraints.Add]. Este método tem várias assinaturas. Aqui, usámos o método [Add(Byval name as String, Byval column as DataColumn, Byval primaryKey as Boolean)]:
nome da restrição - pode ser qualquer coisa | |
coluna que será a chave primária - do tipo [DataColumn] | |
deve ser definido como [true] para tornar [coluna] uma chave primária. Se [primaryKey=false], apenas a restrição de unicidade se aplica a [coluna] |
Para tornar a coluna denominada "id" a chave primária da tabela [dtAbonnements], escrevemos:
O procedimento [Session_Start], executado quando a aplicação recebe o primeiro pedido de um cliente. É utilizado para criar objetos específicos para cada cliente que devem persistir ao longo dos vários pedidos do cliente. O procedimento constrói a [DataTable] das subscrições do cliente. Apenas a sua estrutura é construída, uma vez que esta tabela está inicialmente vazia. Será preenchida à medida que forem feitos pedidos. Também aqui, a coluna "id" serve como chave primária. Utilizámos uma técnica diferente para declarar esta restrição:
é a matriz de colunas que compõem a chave primária — aqui declarámos uma matriz de um elemento: a coluna denominada «id» |
Quando a solicitação do cliente chega ao controlador [main.aspx], ambos os objetos [DataTable] estão disponíveis na aplicação para a tabela de temas e na sessão para a tabela de assinaturas. O controlador [main.aspx.vb] é o seguinte:
Imports System.Data
Public Class main
Inherits System.Web.UI.Page
Protected WithEvents dgThèmes As System.Web.UI.WebControls.DataGrid
Protected WithEvents lblThème As System.Web.UI.WebControls.Label
Protected WithEvents lblDescription As System.Web.UI.WebControls.Label
Protected WithEvents dlAbonnements As System.Web.UI.WebControls.DataList
Protected WithEvents lblInfo As System.Web.UI.WebControls.Label
Protected WithEvents panelInfo As System.Web.UI.WebControls.Panel
Protected dtThèmes As DataTable
Protected dtAbonnements As DataTable
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
End Sub
Private Sub liaisons()
...
End Sub
Private Sub dgThèmes_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles dgThèmes.PageIndexChanged
...
End Sub
Private Sub dgThèmes_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles dgThèmes.ItemCommand
...
End Sub
Private Sub infos(ByVal id As String)
...
End Sub
Private Sub abonner(ByVal id As String)
...
End Sub
Private Sub dlAbonnements_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles dlAbonnements.ItemCommand
...
End Sub
End Class
A principal função do procedimento [Page_Load] é:
- recuperar as duas tabelas [dtThèmes] e [dtAbonnements], que se encontram na aplicação e na sessão, respetivamente, para as disponibilizar a todos os métodos da página
- vincular os dados destas duas fontes aos seus respetivos contentores. Isto é feito apenas durante o primeiro pedido. Para pedidos subsequentes, a vinculação não precisa de ser realizada sistematicamente e, quando for necessário, poderá por vezes ser necessário aguardar que ocorra um evento após [Page_Load] para a realizar.
O código para [Page_Load] é o seguinte:
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' retrieve data sources
dtThèmes = CType(Application("thèmes"), DataTable)
dtAbonnements = CType(Session("abonnements"), DataTable)
' data link
If Not IsPostBack Then
liaisons()
End If
' we hide certain information
panelInfo.Visible = False
End Sub
Private Sub liaisons()
'link the data source to the [datagrid] component
With dgThèmes
.DataSource = dtThèmes
.DataKeyField = "id"
End With
' link the data source to the [datalist] component
With dlAbonnements
.DataSource = dtAbonnements
.DataKeyField = "id"
End With
' assign data to components
Page.DataBind()
End Sub
No procedimento [Bindings], utilizamos a propriedade [DataKeyField] dos componentes [DataList] e [DataGrid] para definir a coluna na fonte de dados que será utilizada para identificar de forma única as linhas nos contentores. Normalmente, esta coluna é a chave primária da fonte de dados, mas isso não é obrigatório. Basta que a coluna não contenha duplicados nem valores vazios. Para o contentor [dgThemes], a coluna "id" da fonte [dtThemes] servirá como chave primária e, para o contentor [dlSubscriptions], será a coluna "id" da fonte [dtSubscriptions]. Não é necessário que a coluna que serve como chave primária do contentor seja exibida pelo próprio contentor. Aqui, nenhum dos contentores exibe a coluna da chave primária. A vantagem de um contentor ter uma chave primária é que permite recuperar facilmente, a partir da fonte de dados, a informação associada à linha do contentor na qual ocorreu um evento. Na verdade, é comum que, a partir de uma linha do contentor onde ocorreu um evento, seja necessário realizar uma ação na linha correspondente na fonte de dados associada. A chave primária facilita esta tarefa.
A paginação do [DataGrid] é tratada da forma padrão:
Private Sub dgThèmes_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles dgThèmes.PageIndexChanged
' change page
dgThèmes.CurrentPageIndex = e.NewPageIndex
' link
liaisons()
End Sub
As ações nos links [Mais informações] e [Subscrever] são tratadas pelo procedimento [dgThemes_ItemCommand]:
Private Sub dgThèmes_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles dgThèmes.ItemCommand
' evt on a [datagrid] line
Dim commande As String = e.CommandName
Select Case commande
Case "infos"
infos(dgThèmes.DataKeys(e.Item.ItemIndex))
Case "abonner"
abonner(dgThèmes.DataKeys(e.Item.ItemIndex))
End Select
' link
liaisons()
End Sub
Utilizamos o facto de ambos os links possuírem um atributo [CommandName] para os distinguir. Dependendo do valor deste atributo, chamamos o procedimento [info] ou [subscribe], passando em ambos os casos a chave "id" associada ao elemento [DataGrid] onde o evento ocorreu. Com esta informação, o procedimento [info] irá apresentar os detalhes do tema selecionado pelo utilizador:
Private Sub infos(ByVal id As String)
' information on the theme of key id
' we retrieve the line from the [datatable] corresponding to the key
Dim ligne As DataRow
ligne = dtThèmes.Rows.Find(id)
If Not ligne Is Nothing Then
' display info
lblThème.Text = CType(ligne("thème"), String)
lblDescription.Text = CType(ligne("description"), String)
panelInfo.Visible = True
End If
End Sub
Uma vez que a tabela [dtThèmes] possui uma chave primária, o método [dtThèmes.Rows.Find("P")] permite-nos encontrar a linha com a chave primária P. Se for encontrada, obtemos um objeto [DataRow]. Aqui, precisamos de encontrar a linha com a chave primária [id], onde [id] é passado como parâmetro. Se a linha for encontrada, colocamos as informações [theme] e [description] dessa linha no painel de informações, que depois tornamos visível.
O procedimento [subscribe(id)] deve adicionar o tema com a chave [id] à lista de subscrições. O seu código é o seguinte:
Private Sub abonner(ByVal id As String)
' id key theme subscription
' we retrieve the line from the [datatable] corresponding to the key
Dim ligne As DataRow
ligne = dtThèmes.Rows.Find(id)
If Not ligne Is Nothing Then
' check if you are not already a subscriber
Dim abonnement As DataRow
abonnement = dtAbonnements.Rows.Find(id)
If Not abonnement Is Nothing Then
' report the error
lblInfo.Text = "Vous êtes déjà abonné au thème [" + ligne("thème") + "]"
Else
' add the theme to the subscriptions
abonnement = dtAbonnements.NewRow
abonnement("id") = id
abonnement("thème") = ligne("thème")
dtAbonnements.Rows.Add(abonnement)
' we make the connections
liaisons()
End If
End If
End Sub
Na lista de temas [dtThemes], procuramos primeiro a linha com a chave [id]. Se encontrada, verificamos se este tema já não está presente na lista de subscrições para evitar adicioná-lo duas vezes. Se estiver, exibimos uma mensagem de erro. Caso contrário, adicionamos uma nova subscrição à tabela [dtSubscriptions] e ligamos os componentes da lista de dados às suas respetivas fontes.
Quando o utilizador clica num botão [Remover], um item deve ser eliminado da tabela [dtAbonnements]. Isto é feito através do seguinte procedimento:
Private Sub dlAbonnements_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles dlAbonnements.ItemCommand
' withdraw a subscription
Dim commande As String = e.CommandName
If commande = "retirer" Then
' we remove the [datatable] subscription
With dtAbonnements.Rows
.Remove(.Find(dlAbonnements.DataKeys(e.Item.ItemIndex)))
End With
' links
liaisons()
End If
End Sub
Primeiro, verificamos a propriedade [CommandName] do elemento que desencadeou o evento. Na verdade, isto é bastante desnecessário, uma vez que o botão [Remove] é o único controlo capaz de gerar um evento no componente [DataList]. Não há, portanto, qualquer ambiguidade. Para eliminar uma linha de um objeto [DataTable], utilizamos o método [DataList.Remove(DataRow)], que remove da tabela a linha [DataRow] passada como parâmetro. Esta linha é encontrada pelo método [DataList.Find], ao qual passamos a chave primária da linha que estamos a procurar. Assim que a linha for eliminada, ligamos os dados aos componentes
9.4. Gerir um [DataList] paginado
Vamos revisitar o exemplo anterior para paginar o componente [DataList] que representa a lista de subscrições do utilizador. Ao contrário do componente [DataGrid], o componente [DataList] não oferece nenhuma funcionalidade de paginação incorporada. Veremos que implementar a paginação é complexo, o que nos ajudará a apreciar o valor da paginação automática do [DataGrid].
9.4.1. Como funciona
A única diferença é a paginação do [DataList]. Cada página exibirá duas subscrições. Se o utilizador tiver cinco subscrições, haverá três páginas. A primeira página terá este aspeto:

A segunda página é acedida através do link [Next]:

A terceira página:

Note que os links [Anterior] e [Seguinte] só são visíveis se houver, respetivamente, uma página anterior e uma página seguinte à página atual.
9.4.2. Código de apresentação
Os links [Anterior] e [Seguinte] são criados adicionando uma tag <FooterTemplate> à [DataList]:
<asp:datalist id="dlAbonnements" runat="server" ...>
....
<FooterTemplate>
<asp:LinkButton id="lnkPrecedent" runat="server" CommandName="precedent">Précédent</asp:LinkButton>
<asp:LinkButton id="lnkSuivant" runat="server" CommandName="suivant">Suivant</asp:LinkButton>
</FooterTemplate>
....
</asp:datalist>
9.4.3. Código de controlo
O ficheiro associado [global.asax.vb] é alterado da seguinte forma:
...
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 subscriptions table
Dim dtAbonnements As New DataTable
With dtAbonnements
' the columns
.Columns.Add("id", GetType(String))
.Columns.Add("thème", GetType(String))
' the primary key
.PrimaryKey = New DataColumn() {.Columns("id")}
End With
' the table is placed in the session
Session.Item("abonnements") = dtAbonnements
' 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
Para além da tabela de subscrições [dtAbonnements], são armazenadas na sessão duas outras informações:
do tipo [Integer] — este é o número da página atual exibida durante a última solicitação | |
do tipo [Integer] — número de linhas exibidas na página atual anterior |
No início de uma sessão, o número da página atual e o número de linhas nessa página são zero.
O controlador [main.aspx.vb] evolui da seguinte forma:
....
Public Class main
Inherits System.Web.UI.Page
....
' application data
Protected dtThèmes As DataTable
Protected dtAbonnements As DataTable
Protected dtPA As DataTable ' subscription page displayed
Protected Const nbAP As Integer = 2 ' subscriptions per page
Protected pAC As Integer ' current subscription page
Protected nbAC As Integer ' number of subscriptions in current page
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
End Sub
Private Sub terminer()
...
End Sub
Private Sub dgThèmes_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles dgThèmes.PageIndexChanged
...
End Sub
Private Sub dgThèmes_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles dgThèmes.ItemCommand
...
End Sub
Private Sub infos(ByVal id As String)
...
End Sub
Private Sub abonner(ByVal id As String)
...
End Sub
Private Sub dlAbonnements_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles dlAbonnements.ItemCommand
...
End Sub
Private Sub changePAC()
...
End Sub
Private Sub setLiens(ByVal ctl As Control, ByVal blPrec As Boolean, ByVal blSuivant As Boolean)
...
End Sub
End Class
Aqui, definimos novos dados relacionados com a paginação da subscrição:
Protected dtPA As DataTable ' la page d'abonnements affichée
Protected Const nbAP As Integer = 2 ' nbre abonnements par page
Protected pAC As Integer ' page abonnement courant
Protected nbAC As Integer ' nombre d'abonnements dans page courante
Algumas destas informações são armazenadas na sessão e são recuperadas com cada pedido no procedimento [Page_Load]:
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' retrieve data sources
dtThèmes = CType(Application("thèmes"), DataTable)
dtAbonnements = CType(Session("abonnements"), DataTable)
' and subscription display information
pAC = CType(Session("pAC"), Integer)
nbAC = CType(Session("nbAC"), Integer)
' data link
If Not IsPostBack Then
' display an empty subscription list
terminer()
End If
' we hide certain information
panelInfo.Visible = False
End Sub
As informações recuperadas [pAC] e [nbAC] são detalhes sobre a página de subscrições apresentada durante o pedido anterior:
é o número da página atual exibida durante a solicitação anterior | |
é o número de linhas exibidas nesta página atual |
O método [terminer] vincula os componentes às suas fontes de dados, tal como o método [liaisons] fez na aplicação anterior. A novidade aqui é a vinculação da [DataList] à tabela [dtPA], que é a página de subscrições a ser apresentada:
Private Sub terminer()
' link the data source to the [datagrid] component
With dgThèmes
.DataSource = dtThèmes
.DataKeyField = "id"
End With
' link the subscriptions page to the [datalist] component, taking into account the current page pAC
changePAC()
' page p is displayed
With dlAbonnements
.DataSource = dtPA
.DataKeyField = "id"
End With
' assign data to components
Page.DataBind()
' management of [previous] and [next] links in the [datalist]
Dim blprec As Boolean = pAC <> 0
Dim blsuivant As Boolean = pAC <> (dtAbonnements.Rows.Count - 1) \ nbAP
Dim nbLiensTrouvés As Integer = 0
setLiens(dlAbonnements, blprec, blsuivant, nbLiensTrouvés)
' save current page information in the session
Session("pAC") = pAC
Session("nbAC") = dtPA.Rows.Count
End Sub
Os seguintes pontos devem ser observados:
- A fonte [dtPA] depende do número da página atual [pAC] a ser exibida. A variável [pAC] é uma variável global da classe, manipulada por métodos que precisam alterar este número da página atual. O método [changePAC] é responsável pela construção da tabela [dtPA], que será ligada ao componente [dlAbonnements].
- O método [setLiens] é responsável por exibir ou ocultar os links [Anterior] e [Seguinte], dependendo se a página atual [pAC] exibida é precedida e seguida por uma página. Possui quatro parâmetros:
- [dlAbonnements]: o controlo [DataList] cuja árvore de controlo iremos explorar para encontrar os dois links. Embora estes dois links estejam localizados num local específico — o rodapé do [DataList] — não parece haver uma forma simples de os referenciar diretamente. De qualquer forma, não foi encontrada nenhuma aqui.
- [blPrecedent]: um valor booleano a atribuir à propriedade [visible] do link [Precedent] — é verdadeiro se a página atual não for 0
- [blNext]: um valor booleano a ser atribuído à propriedade [visible] do link [Next] — é verdadeiro se a página atual não for a última página da lista de assinaturas
- [nbLiensTrouvés]: um parâmetro de saída que conta o número de links encontrados. Assim que esta contagem atingir dois, o método termina.
- As informações [pAC] e [nbAC] são guardadas na sessão para o próximo pedido.
O método [changePAC] constrói a tabela [dtPA], que será ligada ao componente [dlAbonnements]. Faz-o com base no número [pAC] da página atual a ser exibida. A tabela [dtPA] deve exibir determinadas linhas da tabela de subscrições [dtAbonnements]. Lembre-se de que esta tabela é armazenada na sessão e atualizada (aumentada ou diminuída) à medida que as solicitações são feitas. Começamos por definir o intervalo [first,last] de números de linha na tabela [dtAbonnements] que a tabela [dtPA] deve exibir:
Private Sub changePAC()
' makes the pAC page the current subscription page
' datalist] page management
Dim nbAbonnements = dtAbonnements.Rows.Count
Dim dernièrePage = (nbAbonnements - 1) \ nbAP
' first and last subscription
If pAC < 0 Then pAC = 0
If pAC > dernièrePage Then pAC = dernièrePage
Dim premier As Integer = pAC * nbAP
Dim dernier As Integer = (pAC + 1) * nbAP - 1
If dernier > nbAbonnements - 1 Then dernier = nbAbonnements - 1
Depois de feito isto, podemos criar a tabela [dtPA]. Primeiro, definimos a sua estrutura [id, theme] e, em seguida, preenchemo-la copiando as linhas de [dtSubscriptions] cujos números se encontram no intervalo [first, last] calculado anteriormente.
' creation of the datatable dtpa
dtPA = New DataTable
With dtPA
' the columns
.Columns.Add("id", GetType(String))
.Columns.Add("thème", GetType(String))
' the primary key
.PrimaryKey = New DataColumn() {.Columns("id")}
End With
Dim abonnement As DataRow
For i As Integer = premier To dernier
abonnement = dtPA.NewRow
With abonnement
.Item("id") = dtAbonnements.Rows(i).Item("id")
.Item("thème") = dtAbonnements.Rows(i).Item("thème")
End With
dtPA.Rows.Add(abonnement)
Next
End Sub
No final do método [changePAC], a tabela [dtPA] foi criada e pode ser associada ao componente [DataList]. Isto é feito no método [terminer]. Neste mesmo método, o procedimento [setLiens] é utilizado para definir o estado dos links [Anterior] e [Seguinte] no [DataList]. O código para este procedimento é o seguinte:
Private Sub setLiens(ByVal ctl As Control, ByVal blPrec As Boolean, ByVal blSuivant As Boolean, ByRef nbLiensTrouvés As Integer)
' search for [previous] and [next] links
' in the [datalist] control tree
' have all the links been found?
If nbLiensTrouvés = 2 Then Exit Sub
' review of child controls
Dim c As Control
For Each c In ctl.Controls
' we work deep down first - the links are at the bottom of the tree
setLiens(c, blPrec, blSuivant, nbLiensTrouvés)
' link [Previous] ?
If c.ID = "lnkPrecedent" Then
CType(c, LinkButton).Visible = blPrec
nbLiensTrouvés += 1
End If
' next] link ?
If c.ID = "lnkSuivant" Then
CType(c, LinkButton).Visible = blSuivant
nbLiensTrouvés += 1
End If
Next
End Sub
O procedimento é recursivo. Primeiro, procura entre os controlos filhos do componente [dlAbonnements] por componentes denominados [lnkPrecedent] e [lnkSuivant], que são os IDs dos dois links de paginação. Inicia a pesquisa a partir da parte inferior da árvore de controlos, pois é aí que se encontram. Assim que um link é encontrado, o contador [nbLiensTrouvés] é incrementado e a propriedade [visible] do link é definida com um valor passado como parâmetro ao procedimento. Assim que ambos os links forem encontrados, a árvore de controlos deixa de ser percorrida e o procedimento recursivo termina.
Mencionámos que o método [changePAC], que define a fonte de dados [dtPA] para o componente [dlAbonnements], funciona com o número [pAC] da página atual a ser exibida. Vários procedimentos modificam este número:
Private Sub abonner(ByVal id As String)
' id key theme subscription
..
' add the theme to the subscriptions
..
' update current page number - it's now the last page
pAC = (dtAbonnements.Rows.Count - 1) \ nbAP
' data links
terminer()
End If
End Sub
Após adicionar uma subscrição, esta aparece no final da lista de subscrições. Por isso, a vista é definida para a última página de subscrições, para que o utilizador possa ver a adição que foi feita.
Private Sub dlAbonnements_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles dlAbonnements.ItemCommand
' withdraw a subscription
Dim commande As String = e.CommandName
Select Case commande
Case "retirer"
' we remove the [datatable] subscription
With dtAbonnements.Rows
.Remove(.Find(dlAbonnements.DataKeys(e.Item.ItemIndex)))
End With
' is it necessary to change the current page?
nbAC -= 1
If nbAC = 0 Then pAC -= 1
Case "precedent"
' change current page
pAC -= 1
Case "suivant"
' change current page
pAC += 1
End Select
' data links
terminer()
End Sub
- [nbAC] é o número de linhas exibidas na página atual antes de uma assinatura ser cancelada. Se o novo número de linhas na página for igual a 0, o número da página atual [pAC] é diminuído em um.
- Se o link [Anterior] for clicado, o número da página atual [pAC] é diminuído em um.
- Quando se clica no link [Seguinte], o número da página atual [pAC] é incrementado em um.
Os restantes procedimentos permanecem os mesmos de antes.
9.4.4. Conclusão
Mostrámos neste exemplo que podemos paginar um componente [DataList]. Esta paginação é complexa, sendo preferível recorrer à paginação automática do componente [DataGrid] sempre que possível. Este exemplo também nos mostrou como aceder aos componentes presentes no rodapé do componente [DataList].
9.5. Classe para aceder a uma base de dados de produtos
Estamos novamente a centrar-nos na base de dados ACCESS [products] que já utilizámos. Recorde-se que esta possui uma única tabela denominada [list] com a seguinte estrutura:
![]() | ![]() |
Iremos criar uma classe de acesso para a tabela [list] que nos permitirá ler e atualizá-la. Iremos também criar um cliente de consola que utilizará a classe anterior para atualizar a tabela. Em seguida, iremos criar um cliente web para realizar a mesma tarefa.
9.5.1. A classe ProductException
A classe [Exception] possui um construtor que recebe uma mensagem de erro como parâmetro. Aqui, queremos uma classe de exceção com um construtor que aceite uma lista de mensagens de erro, em vez de uma única mensagem de erro. Esta será a classe [ExceptionProduits] abaixo:
Public Class ExceptionProduits
Inherits Exception
' exception error msg
Private _erreurs As ArrayList
' manufacturer
Public Sub New(ByVal erreurs As ArrayList)
Me._erreurs = erreurs
End Sub
' property
Public ReadOnly Property erreurs() As ArrayList
Get
Return _erreurs
End Get
End Property
End Class
9.5.2. A estrutura [sProduct]
A estrutura [product] representa um produto [id, nome, preço]:
' structure sProduit
Public Structure sProduit
' the fields
Private _id As Integer
Private _nom As String
Private _prix As Double
' property id
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 nom() As String
Get
Return _nom
End Get
Set(ByVal Value As String)
If IsNothing(Value) OrElse Value.Trim = String.Empty Then Throw New Exception
_nom = Value
End Set
End Property
' property price
Public Property prix() As Double
Get
Return _prix
End Get
Set(ByVal Value As Double)
If IsNothing(Value) OrElse Value < 0 Then Throw New Exception
_prix = Value
End Set
End Property
End Structure
A estrutura aceita apenas dados válidos para os campos [name] e [price].
9.5.3. A Classe Produtos
A classe [Products] é a classe que nos permitirá atualizar a tabela [list] na base de dados de produtos. A sua estrutura é a seguinte:
Public Class produits
' instance data
Public Sub New(ByVal chaineConnexionOLEDB As String)
....
End Sub
Public Function getProduits() As DataTable
....
End Function
Public Sub ajouterProduit(ByVal produit As sProduit)
...
End Sub
Public Sub modifierProduit(ByVal produit As sProduit)
...
End Sub
Public Sub supprimerProduit(ByVal id As Integer)
...
End Sub
End Class
Dados de instância
Os dados partilhados pelos vários métodos da classe são os seguintes:
Private connexion As OleDbConnection
Private Const selectText As String = "select id,nom,prix from liste"
Private Const insertText As String = "insert into liste(nom,prix) values(?,?)"
Private Const updateText As String = "update liste set nom=?,prix=? where id=?"
Private Const deleteText As String = "delete from liste where id=?"
Private selectCommand As New OleDbCommand
Dim insertCommand As New OleDbCommand
Dim updateCommand As New OleDbCommand
Dim deleteCommand As New OleDbCommand
Dim adaptateur As New OleDbDataAdapter
A ligação à base de dados será aberta para executar um comando SQL e, em seguida, fechada imediatamente | |
Consulta SQL [select] que recupera toda a tabela [list] | |
consulta que permite a inserção de uma linha (nome, preço) na tabela [lista]. Note que o campo [id] não é especificado. Isto porque o SGBD autoincrementa este campo, pelo que não precisamos de o especificar. | |
Consulta para atualizar os campos (nome, preço) da linha na tabela [list] com a chave [id] | |
Consulta que elimina a linha da tabela [list] com a chave [id] | |
Objeto [OleDbCommand] que executa a consulta [selectText] na ligação [connection] | |
Objeto [OleDbCommand] que executa a consulta [updateText] na ligação [connection] | |
Objeto [OleDbCommand] que executa a consulta [insertText] na ligação [connection] | |
Objeto [OleDbCommand] a executar a consulta [deleteText] na ligação [connection] | |
objeto utilizado para recuperar o resultado da execução de [selectCommand] num objeto [DataSet] |
O construtor
O construtor recebe um único parâmetro [OLEDBConnectionString], que é a cadeia de ligação que especifica a base de dados a utilizar. A partir daí, são preparados os quatro comandos para consultar e atualizar a tabela, bem como o adaptador. Trata-se apenas de uma etapa de preparação, não sendo estabelecida qualquer ligação.
Public Sub New(ByVal chaineConnexionOLEDB As String)
' preparing the connection
connexion = New OleDbConnection(chaineConnexionOLEDB)
' prepare query orders
Dim commandes() As OleDbCommand = {selectCommand, insertCommand, updateCommand, deleteCommand}
Dim textes() As String = {selectText, insertText, updateText, deleteText}
For i As Integer = 0 To commandes.Length - 1
With commandes(i)
.CommandText = textes(i)
.Connection = connexion
End With
Next
' prepare the data access adapter
adaptateur.SelectCommand = selectCommand
End Sub
O método getProducts
Este método recupera o conteúdo da tabela [List] para um objeto [DataTable]. O seu código é o seguinte:
Public Function getProduits() As DataTable
' we put the [list] table in a [dataset]
Dim contenu As New DataSet
' create a DataAdapter object to read data from source OLEDB
Try
With adaptateur
.FillSchema(contenu, SchemaType.Source)
.Fill(contenu)
End With
Catch e As Exception
' pb
Dim erreursCommande As New ArrayList
erreursCommande.Add(String.Format("Erreur d'accès à la base de données : {0}", e.Message))
Throw New ExceptionProduits(erreursCommande)
End Try
' we return the result
Return contenu.Tables(0)
End Function
O trabalho é realizado pelas duas instruções seguintes:
With adaptateur
.FillSchema(contenu, SchemaType.Source)
.Fill(contenu)
End With
O método [FillSchema] define a estrutura (colunas, restrições, relações) do [DataSet] contido com base na estrutura da base de dados referenciada por [adapter.Connection]. Isto permite-nos recuperar a estrutura da tabela [list], incluindo a sua chave primária. A operação [Fill] que se segue preenche o [DataSet] contido com as linhas da tabela [list]. Com esta única operação, teríamos obtido os dados e a estrutura, mas não a chave primária. No entanto, isto será útil para atualizar a tabela [list] na memória. Aqui, tal como nos outros métodos, tratamos quaisquer erros utilizando a classe [ProductExceptions] para obter uma lista (ArrayList) de erros em vez de um único erro. O método [getProducts] devolve a tabela [list] como um objeto [DataTable].
O método addProducts
Este método permite adicionar uma linha (id, nome, preço) à tabela [list]. Esta informação é-lhe fornecida sob a forma de uma estrutura [sProduct] com os campos [id, nome, preço]. O código do método é o seguinte:
Public Sub ajouterProduit(ByVal produit As sProduit)
' add a product [name,price]
' we prepare the parameters for the addition
With insertCommand.Parameters
.Clear()
.Add(New OleDbParameter("nom", produit.nom))
.Add(New OleDbParameter("prix", produit.prix))
End With
' we add
Try
' opening connection
connexion.Open()
' order execution
insertCommand.ExecuteNonQuery()
Catch ex As Exception
' pb
Dim erreursCommande As New ArrayList
erreursCommande.Add(String.Format("Erreur lors de l'ajout : {0}", ex.Message))
Throw New ExceptionProduits(erreursCommande)
Finally
' locking connection
connexion.Close()
End Try
End Sub
Os campos da estrutura [product] são passados como parâmetros para o comando [insertCommand]. Vamos rever a configuração atual deste comando (ver construtor):
Private Const insertText As String = "insert into liste(nom,prix) values(?,?)"
insertCommand.Connexion=connexion
O texto do comando SQL [insert] contém parâmetros formais ? que devem ser substituídos por parâmetros reais. Isto é feito utilizando a coleção [Parameters] da classe [OleDbCommand]. Esta coleção contém elementos do tipo [OleDbParameter] que definem os parâmetros reais que devem substituir os parâmetros formais ?. Uma vez que estes não têm nome, o índice dos parâmetros reais é utilizado para determinar qual o parâmetro formal que corresponde a um determinado parâmetro real. Aqui, o parâmetro real #i na coleção [Parameters] substituirá o parâmetro formal ? #i. Para criar um parâmetro real do tipo [OleDbParameter], usamos o construtor [OleDbParameter (Byval name as String, Byval value as Object)], que define o nome e o valor do parâmetro real. O nome pode ser qualquer coisa. Além disso, não será usado aqui. Os dois parâmetros da instrução SQL [insert] recebem como valores os dos campos [name, price] da estrutura [product]. Uma vez feito isto, a inserção é realizada pela instrução [insertCommand.ExecuteNonQuery].
O método modifyProducts
Este método permite-lhe modificar uma linha na tabela [list]. A informação necessária é fornecida na estrutura [sProduct], que contém os campos [id, name, price].
Public Sub modifierProduit(ByVal produit As sProduit)
' modify a product [id,name,price]
' prepare update parameters
With updateCommand.Parameters
.Clear()
.Add(New OleDbParameter("nom", produit.nom))
.Add(New OleDbParameter("prix", produit.prix))
.Add(New OleDbParameter("id", produit.id))
End With
' make the change
Try
' opening connection
connexion.Open()
' order execution
Dim nbLignes As Integer = updateCommand.ExecuteNonQuery()
If nbLignes = 0 Then Throw New Exception(String.Format("Le produit de clé [{0}] n'existe pas dans la table des données", produit.id))
Catch ex As Exception
' pb
Dim erreursCommande As New ArrayList
erreursCommande.Add(String.Format("Erreur lors de la modification : {0}", ex.Message))
Throw New ExceptionProduits(erreursCommande)
Finally
' locking connection
connexion.Close()
End Try
End Sub
O código é quase idêntico ao do método [addProducts], exceto que o [OleDbCommand] relevante é [updateCommand] em vez de [insertCommand].
O método [deleteProducts]
Este método elimina a linha da tabela [list] com a chave [id] passada como parâmetro. O código é o seguinte:
Public Sub supprimerProduit(ByVal id As Integer)
' deletes key product [id]
' prepare the parameters for the deletion
With deleteCommand.Parameters
.Clear()
.Add(New OleDbParameter("id", id))
End With
' we delete
Try
' opening connection
connexion.Open()
' order execution
Dim nbLignes As Integer = deleteCommand.ExecuteNonQuery()
If nbLignes = 0 Then Throw New Exception(String.Format("Le produit de clé [{0}] n'existe pas dans la table des données", id))
Catch ex As Exception
' pb
Dim erreursCommande As New ArrayList
erreursCommande.Add(String.Format("Erreur lors de la suppression : {0}", ex.Message))
Throw New ExceptionProduits(erreursCommande)
Finally
' locking connection
connexion.Close()
End Try
End Sub
A abordagem é a mesma dos métodos anteriores.
9.5.4. Testar a classe [products]
Um programa de teste baseado na consola [testproducts.vb] poderia ter o seguinte aspeto:
Option Explicit On
Option Strict On
' namespaces
Imports System
Imports System.Data
Imports Microsoft.VisualBasic
Imports System.Collections
Namespace st.istia.univangers.fr
' test pg
Module testproduits
Dim contenu As DataTable
Sub Main(ByVal arguments() As String)
' displays the contents of a product table
' the table is in a ACCESS database whose pg receives the file name
Const syntaxe1 As String = "pg bdACCESS"
' checking program parameters
If arguments.Length <> 1 Then
' error msg
Console.Error.WriteLine(syntaxe1)
' end
Environment.Exit(1)
End If
' prepare the connection chain
Dim chaineConnexion As String = "Provider=Microsoft.Jet.OLEDB.4.0; Ole DB Services=-4; Data Source=" + arguments(0)
' creation of a product object
Dim objProduits As produits = New produits(chaineConnexion)
' display all products
afficheProduits(objProduits)
' insert a product
Dim produit As New sProduit
With produit
.nom = "xxx"
.prix = 1
End With
Try
objProduits.ajouterProduit(produit)
Catch ex As ExceptionProduits
afficheErreurs(ex.erreurs)
End Try
' display all products
afficheProduits(objProduits)
' retrieve the id of the ahjouté product
produit.id = CType(contenu.Rows(contenu.Rows.Count - 1)("id"), Integer)
' modify the added product
produit.prix = 200
Try
objProduits.modifierProduit(produit)
Catch ex As ExceptionProduits
afficheErreurs(ex.erreurs)
End Try
' display all products
afficheProduits(objProduits)
' remove the added product
Try
objProduits.supprimerProduit(produit.id)
Catch ex As ExceptionProduits
afficheErreurs(ex.erreurs)
End Try
' display all products
afficheProduits(objProduits)
End Sub
Sub afficheProduits(ByRef objProduits As produits)
' retrieve the product table from a datatable
Try
contenu = objProduits.getProduits()
Catch ex As ExceptionProduits
afficheErreurs(ex.erreurs)
Environment.Exit(2)
End Try
Dim lignes As DataRowCollection = contenu.Rows
For i As Integer = 0 To lignes.Count - 1
' table line i
Console.Out.WriteLine(lignes(i).Item("id").ToString + "," + lignes(i).Item("nom").ToString + _
"," + lignes(i).Item("prix").ToString)
Next
' stops console flow
Console.WriteLine("...")
Console.ReadLine()
End Sub
Sub afficheErreurs(ByRef erreurs As ArrayList)
' displays errors on the console
If erreurs.Count <> 0 Then
Console.WriteLine("Les erreurs suivantes se sont produites :")
For i As Integer = 0 To erreurs.Count - 1
Console.WriteLine(String.Format("-- {0}", CType(erreurs(i), String)))
Next
End If
' stops console flow
Console.WriteLine("...")
Console.ReadLine()
End Sub
End Module
End Namespace
Compile os dois ficheiros fonte:
dos>vbc /t:library /r:system.dll /r:system.data.dll /r:system.xml.dll produits.vb
dos >vbc /r:produits.dll /r:system.dll /r:system.data.dll /r:system.xml.dll testproduits.vb
dos>dir
07/04/2004 08:40 7 168 produits.dll
04/04/2004 16:38 118 784 produits.mdb
07/04/2004 08:31 6 209 produits.vb
07/04/2004 08:40 5 120 testproduits.exe
03/04/2004 19:02 3 312 testproduits.vb
Em seguida, testamos:
dos>testproduits produits.mdb
1,produit1,10
2,produit2,20
3,produit3,30
...
1,produit1,10
2,produit2,20
3,produit3,30
8,xxx,1
...
1,produit1,10
2,produit2,20
3,produit3,30
8,xxx,200
...
1,produit1,10
2,produit2,20
3,produit3,30
...
Convidamos o leitor a comparar a saída de ecrã acima com o código do programa de teste.
9.6. Aplicação web para atualizar a tabela de produtos em cache
9.6.1. Introdução
Estamos agora a escrever uma aplicação web para atualizar a tabela de produtos (adicionar, eliminar, modificar). A tabela atualizada permanecerá na memória num objeto [DataTable] e será partilhada por todos os utilizadores. Queremos destacar dois pontos:
- a gestão de um objeto [DataTable]
- os desafios das atualizações simultâneas da tabela por vários utilizadores.
A arquitetura MVC da aplicação será a seguinte:
![]() |
9.6.2. Funcionalidades e vistas
A vista inicial da aplicação é a seguinte:

Esta vista, denominada [form], permite ao utilizador aplicar uma condição de filtro aos produtos e definir o número de produtos por página que deseja ver.
N.º | nome | Tipo | função |
LinkButton | exibe a vista [Form] utilizada para definir a condição do filtro | ||
LinkButton | exibe a vista [Produtos], que é utilizada para visualizar e atualizar a tabela de produtos (editar e eliminar) | ||
LinkButton | exibe a vista [Adicionar], que é utilizada para adicionar um produto | ||
FormView | a vista [Formulário] | ||
Caixa de Texto | a condição de filtro | ||
Caixa de Texto | número de produtos por página | ||
Validador de campo obrigatório | verifica se existe um valor em [txtPages] | ||
RangeValidator | verifica se txtPages está no intervalo [3,10] | ||
Botão [submit] que exibe a vista [products] filtrada pela condição (5) | |||
Label | Texto informativo em caso de erros |
Por exemplo, se a vista [formulário] for preenchida da seguinte forma:

obtém-se o seguinte resultado:
![]() |
N.º | nome | tipo | função |
painel | |||
Botão de opção | permite ao utilizador definir a ordem de classificação pretendida ao clicar no título de uma das colunas [nome], [preço]. Os dois botões fazem parte do grupo [rdSort]. | ||
DataGrid | Uma grelha que apresenta uma vista filtrada da tabela de produtos. O filtro é aquele definido pela vista [form]. Também temos .AllowPaging=true, .AllowSorting=true | ||
DataGrid | exibe a tabela de produtos na íntegra - permite o acompanhamento de atualizações | ||
Label | texto informativo, especialmente em caso de erros | ||
DataGrid | irá apresentar os produtos eliminados na tabela de produtos | ||
DataGrid | exibirá os produtos modificados na tabela de produtos | ||
DataGrid | exibirá os produtos adicionados à tabela de produtos |
Existem cinco contentores de dados nesta página. Todos eles apresentam a mesma tabela [dtProduits] através de um [DataView] diferente. Uma vista representa um subconjunto das linhas da tabela de origem da vista. Este subconjunto é criado utilizando as propriedades [RowFilter] e [RowStateFilter] da classe [DataView]:
- [RowFilter] permite definir um filtro nas linhas, como [price>30] acima. Este tipo de filtragem será utilizado pelo [DataGrid1].
- [RowStateFilter] permite definir um filtro com base no estado da linha da tabela. Isto indica o estado da linha em relação ao seu estado original quando a vista da tabela foi criada. Aqui, a tabela [dtProduits] provém de uma base de dados. Inicialmente, todas as suas linhas terão um estado igual a [Original] para indicar que estas são as linhas originais da tabela. Este estado pode então mudar e assumir valores diferentes, alguns dos quais estão listados abaixo:
- [Added]: a linha foi adicionada — não fazia parte da tabela original
- [Deleted]: a linha foi eliminada — ainda se encontra na tabela, mas está «marcada» como «a eliminar»
- [Modified]: a linha foi modificada
[RowStateFilter] permite-lhe exibir as linhas da tabela que têm um determinado estado:
- (continuação)
- [DataViewRowState.Added]: apenas as linhas adicionadas são exibidas. São apresentadas com os seus valores atuais.
- [DataViewRowState.ModifiedOriginal]: apenas as linhas modificadas são exibidas. São apresentadas com os seus valores originais.
- [DataViewRowState.ModifiedCurrent]: apenas as linhas modificadas são apresentadas. São apresentadas com os seus valores atuais.
- [DataViewRowState.Deleted]: apenas as linhas eliminadas são apresentadas. São apresentadas com os seus valores originais.
- [DataViewRowState.CurrentRows]: são apresentadas as linhas não eliminadas. São apresentadas com os seus valores atuais.
Assim, o [RowStateFilter] terá os seguintes valores:
sem filtro no estado da linha | ||
DataViewRowState.CurrentRows | exibe o estado atual da tabela de produtos | |
DataViewRowState.Deleted | exibe as linhas eliminadas da tabela de produtos | |
DataViewRowState.ModifiedOriginal | exibe as linhas modificadas da tabela de produtos juntamente com os seus valores originais | |
DataViewRowState.Added | exibe as linhas adicionadas à tabela de produtos original |
Os contentores [DataGrid1-5] permitir-nos-ão acompanhar as atualizações na tabela [dtProduits]. O componente [DataGrid1] permite a modificação e eliminação de um produto. Veremos como a configuração do componente permite esta atualização. Também é paginado e ordenado. Estas duas funcionalidades já foram abordadas num exemplo anterior.
O link [Add] dá acesso a um formulário de adição de produto:
![]() |
N.º | nome | tipo | função |
painel | |||
TextBox | nome do produto | ||
Validador de campo obrigatório | verifica se existe um valor em [txtName] | ||
TextBox | preço do produto | ||
Validador de campo obrigatório | verifica se existe um valor em [txtPrice] | ||
CompareValidator | verifica se o preço é >= 0 | ||
Botão | Botão [submit] para adicionar o produto | ||
Rótulo | Texto informativo sobre o resultado da operação Adicionar |
A base de dados [products] pode não estar disponível quando a aplicação é iniciada. Neste caso, é apresentada ao utilizador a vista [errors]:
![]() |
N.º | nome | tipo | função |
painel | |||
Repetidor | lista de erros |
9.6.3. Configuração dos contentores de dados
Os cinco contentores [DataGrid] foram configurados no [WebMatrix]. Foram formatados (cores e bordas) utilizando o link [Configuração Automática] no seu painel de propriedades. O contentor [DataGrid1] foi configurado utilizando o link [Gerador de Propriedades] no mesmo painel. A ordenação foi ativada (separador [Geral]):

As colunas não são geradas automaticamente, ao contrário do que acontece nos outros quatro contentores. Foram definidas manualmente utilizando o assistente:

Primeiro, foram criadas duas colunas [Related Column] denominadas [name] e [price]. Estas foram associadas, respetivamente, aos campos [name] e [price] da fonte de dados que os contentores irão apresentar. Aqui, por exemplo, está a configuração da coluna [name]:

A expressão de ordenação é a expressão que deve ser colocada após a cláusula [order by] na instrução SQL [select] que será executada quando o utilizador clicar na coluna de cabeçalho [name] associada ao campo [name] do [DataGrid]. Aqui, introduzimos [nome] para que a cláusula de ordenação seja [order by name]. Veremos que iremos modificar isto para [order by name asc] ou [order by name desc], dependendo da ordem de ordenação escolhida pelo utilizador.
Criámos também duas colunas de botões:

A coluna [Editar, Atualizar, Cancelar] permite-nos editar um produto, e a coluna [Eliminar] permite eliminá-lo. Cada uma destas colunas pode ser configurada. A coluna [Editar, Atualizar, Cancelar] oferece a seguinte configuração:

Podemos ver que os textos dos botões podem ser modificados. No que diz respeito aos botões, temos a escolha entre links e botões (lista suspensa acima). Aqui, foram escolhidos links. A configuração da coluna [Eliminar] é semelhante. Além deste assistente de configuração, utilizámos diretamente a janela de propriedades [DataGrid] para definir a propriedade [DataKeyField], que especifica qual o campo na fonte de dados utilizado para indexar as linhas do [DataGrid]. Aqui, é utilizada a chave primária da tabela de produtos:

Em última análise, esta configuração gera o seguinte código de apresentação:
<asp:DataGrid id="DataGrid1" runat="server" AllowSorting="True" PageSize="4" AllowPaging="True" AutoGenerateColumns="False" DataKeyField="id">
<SelectedItemStyle ...></SelectedItemStyle>
<HeaderStyle ...></HeaderStyle>
<FooterStyle ...></FooterStyle>
<Columns>
<asp:BoundColumn DataField="nom" SortExpression="nom" HeaderText="nom"></asp:BoundColumn>
<asp:BoundColumn DataField="prix" SortExpression="prix" HeaderText="prix"></asp:BoundColumn>
<asp:EditCommandColumn ButtonType="LinkButton" UpdateText="Mettre à jour" CancelText="Annuler" EditText="Modifier"></asp:EditCommandColumn>
<asp:ButtonColumn Text="Supprimer" CommandName="Delete"></asp:ButtonColumn>
</Columns>
<PagerStyle NextPageText="Suivant" PrevPageText="Précédent" ...></PagerStyle>
</asp:DataGrid>
Como sempre, assim que tiver adquirido alguma experiência, poderá escrever todo ou parte do código acima diretamente.
Os outros contentores [DataGrid] têm a configuração padrão obtida através da geração automática das colunas [DataGrid] a partir das colunas da fonte de dados à qual estão associados.
O contentor [Repeater] é utilizado para apresentar uma lista de erros. A sua configuração é feita diretamente no código de apresentação:
<asp:Repeater id="rptErreurs" runat="server" EnableViewState="False">
<HeaderTemplate>
Les erreurs suivantes se sont produites :
<ul>
</HeaderTemplate>
<ItemTemplate>
<li>
<%# Container.DataItem %>
</li>
</ItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</asp:Repeater>
Cada linha do componente apresenta o valor [Container.DataItem], ou seja, o valor correspondente da lista de dados. Este será do tipo [ArrayList] e representará uma lista de erros.
9.6.4. O código de apresentação da aplicação
Este encontra-se no ficheiro [main.aspx]:
<%@ Page src="main.aspx.vb" inherits="main" autoeventwireup="false" Language="vb" %>
<HTML>
<HEAD>
</HEAD>
<body>
<form id="Form1" runat="server">
<P>
<table>
<tr>
<td><FONT size="6">Options :</FONT></td>
<td>
<asp:linkbutton id="lnkFiltre" runat="server" CausesValidation="False">
Filtrage
</asp:linkbutton>
</td>
<td>
<asp:linkbutton id="lnkMisajour" runat="server" CausesValidation="False">
Mise à jour
</asp:linkbutton>
</td>
<td>
<asp:linkbutton id="lnkAjout" runat="server" CausesValidation="False">
Ajout
</asp:linkbutton>
</td>
</tr>
</table>
</P>
<HR width="100%" SIZE="1">
<table>
<tr>
<td>
<asp:panel id="vueFormulaire" runat="server">
<P>Condition de filtrage sur la table LISTE. Exemple : prix<100 and
prix>50</P>
<P>
<asp:TextBox id="txtFiltre" runat="server" Columns="60"></asp:TextBox></P>
<P>Nombre de lignes par page :
<asp:TextBox id="txtPages" runat="server" Columns="3">5</asp:TextBox>
<asp:RequiredFieldValidator id="rfvLignes" runat="server" Display="Dynamic"
ControlToValidate="txtPages" ErrorMessage="Indiquez le nombre de lignes par page"
EnableClientScript="False">
</asp:RequiredFieldValidator></P>
<P>
<asp:RangeValidator id="rvLignes" runat="server" Display="Dynamic"
ControlToValidate="txtPages" ErrorMessage="Vous devez indiquer un nombre entre 3 et 10"
EnableClientScript="False" MaximumValue="10" MinimumValue="3" Type="Integer"
EnableViewState="False">
</asp:RangeValidator></P>
<P>
<asp:Label id="lblinfo1" runat="server"></asp:Label></P>
<P>
<asp:Button id="btnExécuter" runat="server" CausesValidation="False"
EnableViewState="False" Text="Exécuter">
</asp:Button></P>
</asp:panel>
<asp:panel id="vueProduits" runat="server">
<TABLE>
<TR>
<TD align="center" bgColor="#ff9966">
<P>Tri
<asp:RadioButton id="rdCroissant" runat="server" Text="croissant"
GroupName="rdTri" Checked="True">
</asp:RadioButton>
<asp:RadioButton id="rdDécroissant" runat="server" Text="décroissant"
GroupName="rdTri">
</asp:RadioButton></P>
</TD>
<TD align="center" bgColor="#ff9966">Tous les produits
</TD>
<TD align="center" bgColor="#ff9966">Suppressions
</TD>
<TD align="center" bgColor="#ff9966">Modifications
</TD>
<TD align="center" bgColor="#ff9966">Ajouts
</TD>
<TR>
<TD vAlign="top">
<P>
<asp:DataGrid id="DataGrid1" runat="server" AllowSorting="True" PageSize="4"
AllowPaging="True" .... AutoGenerateColumns="False" DataKeyField="id">
<ItemStyle ...></ItemStyle>
<HeaderStyle ...></HeaderStyle>
<FooterStyle ...></FooterStyle>
<Columns>
<asp:BoundColumn DataField="nom" SortExpression="nom" HeaderText="nom">
</asp:BoundColumn>
<asp:BoundColumn DataField="prix" SortExpression="prix" HeaderText="prix">
</asp:BoundColumn>
<asp:EditCommandColumn ButtonType="LinkButton" UpdateText="Mettre à jour"
CancelText="Annuler" EditText="Modifier">
</asp:EditCommandColumn>
<asp:ButtonColumn Text="Supprimer" CommandName="Delete">
</asp:ButtonColumn>
</Columns>
<PagerStyle NextPageText="Suivant" PrevPageText="Précédent" ....>
</PagerStyle>
</asp:DataGrid>
</P>
</TD>
<TD vAlign="top">
<asp:DataGrid id="DataGrid2" runat="server" ...>
<AlternatingItemStyle ...></AlternatingItemStyle>
<ItemStyle ...></ItemStyle>
<HeaderStyle ....></HeaderStyle>
<FooterStyle ...></FooterStyle>
<PagerStyle ... Mode="NumericPages"></PagerStyle>
</asp:DataGrid>
</TD>
<TD vAlign="top">
<asp:DataGrid id="DataGrid3" runat="server" ...>
<ItemStyle ...></ItemStyle>
<HeaderStyle ...></HeaderStyle>
<FooterStyle ...></FooterStyle>
<PagerStyle ...></PagerStyle>
</asp:DataGrid>
</TD>
<TD vAlign="top">
<asp:DataGrid id="DataGrid4" runat="server" ...>
<ItemStyle ....></ItemStyle>
<HeaderStyle ....></HeaderStyle>
<FooterStyle ...></FooterStyle>
<PagerStyle .... Mode="NumericPages"></PagerStyle>
</asp:DataGrid>
</TD>
<TD vAlign="top">
<asp:DataGrid id="DataGrid5" runat="server....>
<AlternatingItemStyle ...></AlternatingItemStyle>
<HeaderStyle ..></HeaderStyle>
<FooterStyle ...></FooterStyle>
<PagerStyle ....></PagerStyle>
</asp:DataGrid>
</TD>
</TR>
</TABLE>
<P></P>
<P>
<asp:Label id="lblInfo2" runat="server"></asp:Label></P>
</asp:panel>
<asp:panel id="vueErreurs" runat="server">
<asp:Repeater id="rptErreurs" runat="server" EnableViewState="False">
<HeaderTemplate>
Les erreurs suivantes se sont produites :
<ul>
</HeaderTemplate>
<ItemTemplate>
<li>
<%# Container.DataItem %>
</li>
</ItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</asp:Repeater>
</asp:panel>
<asp:panel id="vueAjout" EnableViewState="False" Runat="server">
<P>Ajout d'un produit</P>
<P>
<TABLE ... border="1">
<TR>
<TD>nom</TD>
<TD>
<asp:TextBox id="txtNom" runat="server" Columns="30"></asp:TextBox>
<asp:RequiredFieldValidator id="rfvNom" runat="server" ControlToValidate="txtNom"
ErrorMessage="Vous devez indiquer un nom">
</asp:RequiredFieldValidator>
</TD>
</TR>
<TR>
<TD>prix</TD>
<TD>
<asp:TextBox id="txtPrix" runat="server" Columns="10"></asp:TextBox>
<asp:RequiredFieldValidator id="rfvPrix" runat="server" ControlToValidate="txtPrix"
ErrorMessage="Vous devez indiquer un prix">
</asp:RequiredFieldValidator>
<asp:CompareValidator id="cvPrix" runat="server" ControlToValidate="txtPrix"
ErrorMessage="CompareValidator" Type="Double" Operator="GreaterThanEqual"
ValueToCompare="0">
</asp:CompareValidator>
</TD>
</TR>
</TABLE>
</P>
<P>
<asp:Button id="btnAjouter" runat="server" CausesValidation="False"
Text="Ajouter">
</asp:Button></P>
<P>
<asp:Label id="lblInfo3" runat="server"></asp:Label></P>
</asp:panel>
</td>
</tr>
</table>
</form>
</body>
</HTML>
Tenha em atenção os seguintes pontos:
- A página é composta por quatro contentores (painéis) [vueFormulaire, vueProduits, vueAjout, vueErreurs] que formarão as quatro vistas da aplicação.
- Os botões ou links do tipo [submit] têm a propriedade [CausesValidation=false]. A propriedade [CausesValidation=true] aciona a execução de todas as verificações de validação na página. No entanto, neste caso, nem todas as verificações de validação precisam de ser realizadas ao mesmo tempo. Por exemplo, ao adicionar um item, não queremos que as verificações relativas ao número de linhas por página sejam executadas. Por isso, especificaremos nós próprios quais as verificações de validação que devem ser realizadas.
9.6.5. O código de controlo [global.asax]
O controlador [global.asax] é o seguinte:
O código associado [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 chaînedeConnexion As String = ConfigurationSettings.AppSettings("OLEDBStringConnection")
Dim defaultProduitsPage As String = ConfigurationSettings.AppSettings("defaultProduitsPage")
Dim erreurs As New ArrayList
If IsNothing(chaînedeConnexion) Then erreurs.Add("Le paramètre [OLEDBStringConnection] n'a pas été initialisé")
If IsNothing(defaultProduitsPage) Then
erreurs.Add("Le paramètre [defaultProduitsPage] n'a pas été initialisé")
Else
Try
Dim defProduitsPage As Integer = CType(defaultProduitsPage, Integer)
If defProduitsPage <= 0 Then Throw New Exception
Catch ex As Exception
erreurs.Add("Le paramètre [defaultProduitsPage] a une valeur incorrecte")
End Try
End If
' configuration errors?
If erreurs.Count <> 0 Then
' errors are noted
Application("erreurs") = erreurs
' we leave
Exit Sub
End If
' no configuration errors here
' create a product object
Dim dtProduits As DataTable
Try
dtProduits = New produits(chaînedeConnexion).getProduits
Catch ex As ExceptionProduits
'there has been an error accessing the products, this is noted in the application
Application("erreurs") = ex.erreurs
Exit Sub
Catch ex As Exception
' unhandled error
erreurs.Add(ex.Message)
Application("erreurs") = erreurs
' exit sub
End Try
' no initialization errors here
' store the number of products per page
Application("defaultProduitsPage") = defaultProduitsPage
' memorize the product table
Application("dtProduits") = dtProduits
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' init session variables
If IsNothing(Application("erreurs")) Then
' view of the product table
Session("dvProduits") = CType(Application("dtProduits"), DataTable).DefaultView
' products per page
Session("nbProduitsPage") = Application("defaultProduitsPage")
' current page displayed
Session("pageCourante") = 0
End If
End Sub
End Class
Em [Application_Start], começamos por recuperar duas informações do ficheiro de configuração [web.config] da aplicação:
- OLEDBStringConnection: a cadeia de ligação OLEDB à base de dados de produtos
- defaultProductsPage: o número padrão de produtos por página exibidos
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="OLEDBStringConnection" value="Provider=Microsoft.Jet.OLEDB.4.0; Ole DB Services=-4; Data Source=D:\data\devel\aspnet\poly\webforms3\vs\majproduits1\produits.mdb" />
<add key="defaultProduitsPage" value="5" />
</appSettings>
</configuration>
Se alguma destas duas informações estiver em falta, é gerada uma lista de erros e adicionada à aplicação. O mesmo se aplica se o parâmetro [defaultProduitsPage] existir, mas estiver incorreto. Se ambos os parâmetros esperados estiverem presentes e corretos, é criada uma tabela [dtProduits] e adicionada à aplicação. Esta tabela será utilizada e atualizada pelos vários clientes. A base de dados em si permanecerá inalterada. Abordaremos a atualização da base de dados numa aplicação futura. Esta tabela é construída a partir de uma instância da classe [products] discutida anteriormente e do seu método [getProducts]. A recuperação da tabela [dtProducts] pode falhar. Neste caso, sabemos que a classe [products] lança uma exceção do tipo [ProductException]. Esta é interceptada aqui, e a lista de erros associada é armazenada na aplicação sob a chave [errors]. A presença desta chave nas informações armazenadas na aplicação será verificada em cada pedido. Se for encontrada, a vista [errors] será enviada ao cliente.
Se a tabela [dtProducts] for partilhada por todos os clientes web, cada um deles terá, no entanto, a sua própria vista [dvProducts] da mesma. Isto porque cada cliente web pode definir um filtro na tabela [dtProducts], bem como uma ordem de ordenação. Esta informação específica de cada cliente web é armazenada na vista do cliente. Por conseguinte, esta vista é criada em [Session_Start] para que possa ser colocada na sessão específica de cada cliente. Utilizamos o atributo [DefaultView] da tabela [dtProduits] para ter uma vista predefinida da tabela. Inicialmente, não existe nem um filtro nem uma ordem de ordenação. Além disso, duas informações são também colocadas na sessão:
- o número de produtos por página, identificado pela chave [nbProduitsPage]. No início da sessão, este número é igual ao valor predefinido definido no ficheiro de configuração.
- o número da página de produtos atual. Inicialmente, esta é a primeira página.
9.6.6. O código do controlador [main.aspx.vb]
O esqueleto do controlador é o seguinte:
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
' components page
Protected WithEvents txtPages As System.Web.UI.WebControls.TextBox
Protected WithEvents btnExécuter As System.Web.UI.WebControls.Button
Protected WithEvents vueFormulaire As System.Web.UI.WebControls.Panel
Protected WithEvents DataGrid1 As System.Web.UI.WebControls.DataGrid
Protected WithEvents lnkErreurs As System.Web.UI.WebControls.LinkButton
Protected WithEvents vueErreurs As System.Web.UI.WebControls.Panel
Protected WithEvents rdCroissant As System.Web.UI.WebControls.RadioButton
Protected WithEvents rdDécroissant As System.Web.UI.WebControls.RadioButton
Protected WithEvents txtFiltre As System.Web.UI.WebControls.TextBox
Protected WithEvents lblInfo2 As System.Web.UI.WebControls.Label
Protected WithEvents lblinfo1 As System.Web.UI.WebControls.Label
Protected WithEvents lblErreurs As System.Web.UI.WebControls.Label
Protected WithEvents DataGrid2 As System.Web.UI.WebControls.DataGrid
Protected WithEvents vueProduits As System.Web.UI.WebControls.Panel
Protected WithEvents vueAjout As System.Web.UI.WebControls.Panel
Protected WithEvents lnkMisajour As System.Web.UI.WebControls.LinkButton
Protected WithEvents lnkFiltre As System.Web.UI.WebControls.LinkButton
Protected WithEvents txtNom As System.Web.UI.WebControls.TextBox
Protected WithEvents txtPrix As System.Web.UI.WebControls.TextBox
Protected WithEvents btnAjouter As System.Web.UI.WebControls.Button
Protected WithEvents lblInfo3 As System.Web.UI.WebControls.Label
Protected WithEvents rfvLignes As System.Web.UI.WebControls.RequiredFieldValidator
Protected WithEvents rvLignes As System.Web.UI.WebControls.RangeValidator
Protected WithEvents rfvNom As System.Web.UI.WebControls.RequiredFieldValidator
Protected WithEvents rfvPrix As System.Web.UI.WebControls.RequiredFieldValidator
Protected WithEvents cvPrix As System.Web.UI.WebControls.CompareValidator
Protected WithEvents lnkAjout As System.Web.UI.WebControls.LinkButton
Protected WithEvents DataGrid3 As System.Web.UI.WebControls.DataGrid
Protected WithEvents DataGrid4 As System.Web.UI.WebControls.DataGrid
Protected WithEvents DataGrid5 As System.Web.UI.WebControls.DataGrid
' data page
Protected dtProduits As DataTable
Protected dvProduits As DataView
Protected defaultProduitsPage As Integer
Protected erreur As Boolean = False
' loading page
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
End Sub
Private Sub afficheErreurs()
...
End Sub
Private Sub afficheFormulaire()
...
End Sub
Private Sub btnExécuter_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnExécuter.Click
....
End Sub
Private Sub afficheProduits(ByVal page As Integer, ByVal taillePage As Integer)
...
End Sub
Private Sub DataGrid1_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles DataGrid1.PageIndexChanged
...
End Sub
Private Sub DataGrid1_SortCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridSortCommandEventArgs) Handles DataGrid1.SortCommand
....
End Sub
Private Sub DataGrid1_DeleteCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.DeleteCommand
....
End Sub
Private Sub DataGrid1_EditCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.EditCommand
...
End Sub
Private Sub DataGrid1_CancelCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.CancelCommand
...
End Sub
Private Sub DataGrid1_UpdateCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.UpdateCommand
...
End Sub
Private Sub supprimerProduit(ByVal idProduit As Integer)
...
End Sub
Private Sub modifierProduit(ByVal idProduit As Integer, ByVal item As DataGridItem)
....
End Sub
Private Sub lnkMisajour_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkMisajour.Click
...
End Sub
Private Sub lnkAjout_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkAjout.Click
...
End Sub
Private Sub afficheAjout()
...
End Sub
Private Sub lnkFiltre_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkFiltre.Click
....
End Sub
Private Sub btnAjouter_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnAjouter.Click
....
End Sub
End Class
9.6.7. Dados de instância
A classe [main] utiliza os seguintes dados de instância:
Public Class main
Inherits System.Web.UI.Page
' components page
Protected WithEvents txtPages As System.Web.UI.WebControls.TextBox
...
' data page
Protected dtProduits As DataTable
Protected dvProduits As DataView
Protected defaultProduitsPage As Integer
a tabela [DataTable] para produtos — partilhada por todos os clientes | |
o [DataView] para produtos — específico para cada cliente | |
número padrão de produtos por página |
9.6.8. O procedimento [Page_Load] para carregar a página
O código para o procedimento [Page_Load] é o seguinte:
' loading page
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' check for application errors
If Not IsNothing(Application("erreurs")) Then
' the application has not initialized correctly
afficheErreurs(CType(Application("erreurs"), ArrayList))
Exit Sub
End If
' retrieve a reference from the product table
dtProduits = CType(Application("dtProduits"), DataTable)
' product view
dvProduits = CType(Session("dvProduits"), DataView)
' retrieve the number of products per page
defaultProduitsPage = CType(Application("defaultProduitsPage"), Integer)
' cancels any update in progress
DataGrid1.EditItemIndex = -1
'1st request
If Not IsPostBack Then
' the initial form is displayed
txtPages.Text = defaultProduitsPage.ToString
afficheFormulaire()
End If
End Sub
- Primeiro, verificamos se a tabela de produtos foi carregada quando a aplicação foi iniciada. Caso contrário, exibimos a vista [errors] com as mensagens de erro apropriadas e, em seguida, saímos do procedimento após definir o booleano [error] como verdadeiro. Este indicador será verificado por determinados procedimentos.
- Recuperamos a tabela de produtos [dtProduits] da aplicação e armazenamo-la na variável de instância [dtProduits] para que possa ser partilhada por todos os métodos da página.
- Fazemos o mesmo com a vista [dvProducts] recuperada da sessão e com o número padrão de produtos por página recuperado da aplicação.
- Se esta for a primeira solicitação do cliente, exibimos o formulário para definir condições de filtragem e paginação com uma paginação padrão de [defaultProduitsPage] produtos por página.
- O modo de edição do componente [dataGrid1] é cancelado. Este componente possui um atributo [EditItemIndex], que é o índice do item atualmente a ser editado. Se [EditItemIndex]=-1, então nenhum item está atualmente a ser editado. Se [EditItemIndex]=i, então o item número i do componente [DataGrid] está atualmente a ser editado. É então apresentado de forma diferente dos outros itens no [dataGrid]:

Acima, o item [Product8, 80] está no modo [edit]. Conforme mostrado acima, o utilizador pode confirmar a atualização usando o link [Update] ou cancelá-la usando o link [Cancel]. Também pode usar outros links que não têm nada a ver com o item atualmente a ser editado. Ao definir [DataGrid1.EditItemIndex=-1] sempre que a página é carregada, garantimos que o modo de edição do [DataGrid1] é sistematicamente cancelado. Só lhe atribuiremos um valor diferente se um link [Editar] tiver sido clicado. Faremos isso no procedimento que trata deste evento. Teremos as seguintes situações:
- O link [Editar] do item n.º 8 em [DataGrid1] foi clicado. [Page_Load] define primeiro [DataGrid1.EditItemIndex] como -1. Em seguida, o procedimento que trata do evento [Edit] definirá [DataGrid1.EditItemIndex] como 8. Por fim, quando a página for enviada de volta ao cliente, o item n.º 8 estará efetivamente no modo [edit] e aparecerá como mostrado acima.
- O utilizador edita o produto que está no modo [edit] e confirma a alteração utilizando o link [Update]. [Page_Load] define [DataGrid1.EditItemIndex] como -1. Em seguida, o procedimento que lida com o evento [Update] será executado, e o item n.º 8 em [dtProduits] será atualizado. Quando a página for enviada de volta ao cliente, nenhum item em [DataGrid1] estará no modo de edição (DataGrid.EditItemIndex=-1).
- Se um utilizador tiver iniciado a atualização de um produto, faria sentido que cancelasse essa atualização clicando em [Cancelar]. No entanto, nada o impede de clicar noutro link. Devemos, portanto, ter em conta este cenário. Neste caso, tal como nos anteriores, [Page_Load] começa por definir [DataGrid1.EditItemIndex] para -1, e depois o procedimento que trata do evento ocorrido será executado. Este não modificará a propriedade [DataGrid1.EditItemIndex], que permanecerá, portanto, em -1. Quando a página for enviada de volta ao cliente, nenhum item em [DataGrid1] estará no modo de edição.
Podemos ver que, ao definir [EditItemIndex] como -1 quando a página é carregada, evitamos ter de nos preocupar se o utilizador estava ou não no modo de edição quando clicou num link.
9.6.9. Exibição das vistas [errors], [form] e [add]
A vista [errors] é exibida quando a página carrega [Page_Load] se for detetado que a aplicação não inicializou corretamente. Isto é feito ligando o componente de dados [rptErrors] à lista de erros passada como parâmetros e tornando o contentor apropriado visível. Por fim, os três links [Filter, Update, Add] são ocultados porque a aplicação não pode ser utilizada em caso de erro.
Private Sub afficheErreurs(ByVal erreurs As ArrayList)
' associate the error list with repeater rptErreurs
With rptErreurs
.DataSource = erreurs
.DataBind()
End With
'inhibit options
lnkAjout.Visible = False
lnkMisajour.Visible = False
lnkFiltre.Visible = False
' the [errors] view is displayed
vueErreurs.Visible = True
vueFormulaire.Visible = False
vueProduits.Visible = False
vueAjout.Visible = False
End Sub
As outras vistas são solicitadas com base nas opções apresentadas ao utilizador:

A vista [Add] é apresentada quando o utilizador clica na ligação [Add] na página:
Private Sub lnkAjout_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkAjout.Click
' the [add] view is displayed
afficheAjout()
End Sub
Private Sub afficheAjout()
' displays the add view
vueAjout.Visible = True
vueFormulaire.Visible = False
vueProduits.Visible = False
vueErreurs.Visible = False
End Sub
A vista [Form] é apresentada quando o utilizador clica na ligação [Filter] na página. Devemos então apresentar a vista que permite ao utilizador definir
- o filtro para a tabela de produtos
- o número de produtos exibidos em cada página web
É apresentado um formulário que recupera os valores armazenados na sessão:
Private Sub lnkFiltre_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkFiltre.Click
' define filtering conditions
txtFiltre.Text = dvProduits.RowFilter
' set pagination
txtPages.Text = CType(Session("nbProduitsPage"), String)
' the filter form is displayed
afficheFormulaire()
End Sub
A condição de filtragem de uma vista é definida na sua propriedade [RowFilter]. Recorde-se que a vista filtrada e paginada se chama [dvProducts] e foi recuperada da sessão quando a página foi carregada [Page_Load]. A condição de filtragem é, portanto, recuperada de [dvProduits.RowFilter]. O número de produtos por página também é recuperado da sessão. Se o utilizador nunca tiver definido esta informação, este número é igual ao número padrão de produtos por página. Feito isto, apresentamos o formulário que permite ao utilizador definir estas duas informações:
Private Sub afficheFormulaire()
' the [form] view is displayed
vueFormulaire.Visible = True
vueErreurs.Visible = False
vueProduits.Visible = False
vueAjout.Visible = False
End Sub

9.6.10. Validação da vista [form]
O clique no botão [Executar] acima é tratado pelo seguinte procedimento:
Private Sub btnExécuter_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnExécuter.Click
' valid page?
rfvLignes.Validate()
rvLignes.Validate()
If Not rfvLignes.IsValid Or Not rvLignes.IsValid Then
afficheFormulaire()
Exit Sub
End If
' attaching filtered data to the grid
Try
dvProduits.RowFilter = txtFiltre.Text.Trim
Catch ex As Exception
lblinfo1.Text = "Erreur de filtrage (" + ex.Message + ")"
afficheFormulaire()
Exit Sub
End Try
' store the number of products per page
Session("nbProduitsPage") = txtPages.Text
'and the current page
Session("pageCourante") = 0
' all's well - data displayed
afficheProduits(0, CType(txtPages.Text, Integer))
End Sub
Primeiro, lembremo-nos de que a página tem dois componentes de validação:
N.º | nome | tipo | função |
Validador de campo obrigatório | verifica se existe um valor em [txtPages] | ||
RangeValidator | verifica se txtPages está no intervalo [3,10] |
O procedimento começa por executar o código de validação dos dois componentes acima, utilizando o seu método [Validate], e, em seguida, verifica o valor do seu atributo [IsValid]. Este atributo é definido como [true] apenas se os dados validados forem considerados válidos. Se as verificações de validade de qualquer um dos componentes falharem, a vista [form] é novamente apresentada para que o utilizador possa corrigir o(s) erro(s). A condição de filtro é aplicada ao atributo [dvProduits.RowFilter]. Aqui, pode ocorrer uma exceção se o utilizador tiver introduzido um critério de filtro incorreto, conforme mostrado abaixo:

Neste caso, a vista [form] é exibida novamente. Se ambas as informações introduzidas estiverem corretas, então dois dados são colocados na sessão:
- o número de produtos por página selecionado pelo utilizador
- o número da página a ser exibida — inicialmente a página 0
Estas duas informações são utilizadas sempre que a vista [products] é apresentada.
9.6.11. Exibição da vista [products]
A vista [produtos] é a seguinte:

Temos 5 componentes [DataGrid], cada um exibindo uma vista específica da tabela de produtos [dtProduits]. Começando pela esquerda:
nome | função |
Grelha que apresenta uma vista filtrada da tabela de produtos. O filtro é aquele definido pela vista [form]. Temos também .AllowPaging=true, .AllowSorting=true | |
Exibe a tabela de produtos na íntegra — permite o acompanhamento de atualizações | |
exibirá os produtos eliminados na tabela de produtos | |
exibirá os produtos modificados na tabela de produtos | |
exibirá os produtos adicionados na tabela de produtos |
O procedimento [displayProducts] é responsável por exibir a vista anterior:
Private Sub afficheProduits(ByVal page As Integer, ByVal taillePage As Integer)
' attach the data to the two components [DataGrid]
Application.Lock()
With DataGrid1
.DataSource = dvProduits
.PageSize = taillePage
.CurrentPageIndex = page
.DataBind()
End With
Dim dvCurrent As New DataView(dtProduits)
dvCurrent.RowStateFilter = DataViewRowState.CurrentRows
With DataGrid2
.DataSource = dvCurrent
.DataBind()
End With
Dim dvSupp As DataView = New DataView(dtProduits)
dvSupp.RowStateFilter = DataViewRowState.Deleted
With DataGrid3
.DataSource = dvSupp
.DataBind()
End With
Dim dvModif As DataView = New DataView(dtProduits)
dvModif.RowStateFilter = DataViewRowState.ModifiedOriginal
With DataGrid4
.DataSource = dvModif
.DataBind()
End With
Dim dvAjout As DataView = New DataView(dtProduits)
dvAjout.RowStateFilter = DataViewRowState.Added
With DataGrid5
.DataSource = dvAjout
.DataBind()
End With
Application.UnLock()
' the [products] view is displayed
vueProduits.Visible = True
vueFormulaire.Visible = False
vueErreurs.Visible = False
vueAjout.Visible = False
' the current page is saved
Session("pageCourante") = page
End Sub
O procedimento aceita dois parâmetros:
- o número da página [page] a apresentar em [DataGrid1]
- o número de produtos [pageSize] por página
O [DataGrid] está ligado à tabela de produtos através de 5 vistas diferentes.
- [DataGrid1] está ligado à vista paginada e ordenada [dvProduits].
With DataGrid1
.DataSource = dvProduits
.PageSize = taillePage
.CurrentPageIndex = page
.DataBind()
End With
É durante esta ligação do [DataGrid1] à sua fonte de dados que precisamos das duas informações passadas como parâmetros para o procedimento.
- O [DataGrid2] está ligado a uma vista que apresenta todos os itens atuais da tabela [dtProduits]. Tal como o [DataGrid1], apresenta uma vista atualizada da tabela [dtProduits], mas sem paginação nem ordenação.
Dim dvCurrent As New DataView(dtProduits)
dvCurrent.RowStateFilter = DataViewRowState.CurrentRows
With DataGrid2
.DataSource = dvCurrent
.DataBind()
End With
Aqui estamos a utilizar a propriedade [RowStateFilter] da classe [DataView]. Uma linha numa tabela — ou, neste caso, numa vista — tem uma propriedade [RowState] que indica o estado da linha. Aqui estão alguns exemplos:
- (continuação)
- [Added]: a linha foi adicionada
- [Modified]: a linha foi modificada
- [Deleted]: a linha foi eliminada
- [Unchanged]: a linha não foi alterada
Quando a tabela [dtProduits] foi inicialmente criada em [Application_Start], todas as suas linhas estavam no estado [Inalterado]. Este estado irá mudar à medida que os clientes web atualizam os dados:
- (continuação)
- Quando um cliente cria um novo produto, é adicionada uma linha à tabela [dtProduits]. Esta ficará no estado [Adicionado].
- Quando um produto é modificado, o seu estado muda de [Inalterado] para [Modificado].
- Quando um produto é eliminado, a linha não é fisicamente eliminada. Em vez disso, é marcada para eliminação e o seu estado muda para [Deleted]. É possível anular esta eliminação.
O atributo [RowStateFilter] da classe [DataView] permite-lhe filtrar uma vista com base no [RowState] das suas linhas. Os seus valores possíveis são os da enumeração [DataRowViewState]:
- (continuação)
- DataRowViewState.CurrentRows: As linhas que não foram eliminadas são apresentadas com os seus valores atuais
- DataRowViewState.Added: As linhas adicionadas são apresentadas com os seus valores atuais
- DataRowViewState.Deleted: As linhas eliminadas são apresentadas com os seus valores originais
- DataRowViewState.ModifiedOriginal: As linhas modificadas são apresentadas com os seus valores originais
- DataRowViewState.ModifiedCurrent: As linhas modificadas são apresentadas com os seus valores atuais
Uma linha modificada tem dois valores: o valor original, que é o valor que a linha tinha antes da primeira modificação, e o valor atual, que é o valor obtido após uma ou mais modificações. Ambos os valores são mantidos simultaneamente.
As DataGrids 2 a 5 apresentam uma vista da tabela [dtProduits] filtrada pelo atributo [RowStateFilter]
DataGrid | Filtro |
RowStateFilter= DataRowViewState.CurrentRows | |
RowStateFilter = DataRowViewState.Deleted | |
RowStateFilter = DataRowViewState.ModifiedOriginal | |
RowStateFilter = DataRowViewState.Added |
A tabela [dtProducts] é atualizada simultaneamente por diferentes clientes web, o que pode causar conflitos ao aceder à tabela. Quando um cliente apresenta as suas vistas da tabela [dtProducts], queremos impedir que o faça enquanto a tabela se encontra num estado instável, uma vez que está a ser modificada por outro cliente web. Portanto, sempre que um cliente precisar da tabela [dtProducts] para leitura, como acima, ou para gravação ao adicionar, modificar ou eliminar produtos, ele sincronizará com os outros clientes utilizando a seguinte sequência:
Poderá haver várias secções críticas no código da aplicação:
O mecanismo funciona da seguinte forma:
- Um ou mais clientes chegam à instrução [Application.Lock] na secção crítica 1. Trata-se de um distribuidor de tokens de entrada única. Um único cliente obtém este token. Chamemos-lhe C1.
- Até que o cliente C1 devolva o token através de [Application.Unlock], nenhum outro cliente pode entrar numa secção crítica controlada por [Application.Lock]. No exemplo acima, portanto, nenhum cliente pode entrar nas secções críticas 1 e 2.
- O cliente C1 executa [Application.Unlock] e, assim, devolve o token de entrada. Este token pode então ser atribuído a outro cliente. Os passos 1 a 3 são repetidos.
Com este mecanismo, garantimos que apenas um cliente tem acesso à tabela [dtProduits], seja para leitura ou escrita.
O link [Update] também exibe a vista [Products]:

O procedimento associado é o seguinte:
Private Sub lnkMisajour_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkMisajour.Click
' product view display
afficheProduits(CType(Session("pageCourante"), Integer), CType(Session("nbProduitsPage"), Integer))
End Sub
Precisamos de exibir a vista [products]. Isto é feito utilizando o procedimento [displayProducts], que recebe dois parâmetros: o número da página atual a exibir e o número de produtos por página a exibir. Ambas estas informações encontram-se na sessão, possivelmente com os seus valores predefinidos, caso o utilizador nunca as tenha definido.
9.6.12. Paginação e ordenação do [DataGrid1]
Já nos deparámos com os procedimentos que tratam da paginação e ordenação de um [DataGrid] noutro exemplo. Quando a página muda, exibimos a vista [products] passando o novo número da página como parâmetro para o procedimento [displayProducts].
Private Sub DataGrid1_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles DataGrid1.PageIndexChanged
' change page
afficheProduits(e.NewPageIndex, DataGrid1.PageSize)
End Sub
O procedimento [DataGrid1_SortCommand] é executado quando o utilizador clica no cabeçalho de uma das colunas do [DataGrid1]. A expressão de ordenação deve então ser atribuída ao atributo [Sort] da vista [dvProduits] apresentada pelo [DataGrid1]. Esta expressão é sintaticamente equivalente à expressão de ordenação colocada após a cláusula [order by] na instrução SQL SELECT. O argumento [e] do procedimento possui um atributo [SortExpression] que fornece a expressão de ordenação associada à coluna cujo cabeçalho foi clicado. Quando o [ ] do componente [DataGrid1] foi criado, esta expressão de ordenação foi definida. Abaixo está a expressão definida para a coluna [name] de [DataGrid1]:

Se a expressão de ordenação não tiver sido definida durante o desenho do [DataGrid], é utilizado o nome do campo de dados associado à coluna do [DataGrid]. A ordem de ordenação (ascendente, descendente) é definida aqui pelo utilizador através dos botões de opção [rdAscending, rdDescending]:

O procedimento de ordenação é o seguinte:
Private Sub DataGrid1_SortCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridSortCommandEventArgs) Handles DataGrid1.SortCommand
' sort the dataview
With dvProduits
.Sort = e.SortExpression + " " + CType(IIf(rdCroissant.Checked, "asc", "desc"), String)
End With
' we display it
afficheProduits(0, DataGrid1.PageSize)
End Sub
9.6.13. Eliminar um produto
Para eliminar um produto, o utilizador clica na ligação [Eliminar] na linha do produto:

O procedimento [DataGrid1_DeleteCommand] é então executado:
Private Sub DataGrid1_DeleteCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.DeleteCommand
' product deletion
' product key to be deleted
Dim idProduit As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)
' product deletion
Dim erreur As Boolean = False
Try
supprimerProduit(idProduit)
Catch ex As Exception
' pb
lblInfo2.Text = ex.Message
erreur = True
End Try
' change page?
Dim page As Integer = DataGrid1.CurrentPageIndex
If Not erreur AndAlso DataGrid1.Items.Count = 1 Then
page = DataGrid1.CurrentPageIndex - 1
If page < 0 Then page = 0
End If
' product display
afficheProduits(page, DataGrid1.PageSize)
End Sub
Os produtos serão atualizados utilizando a chave [id] do produto. A coluna [id] na tabela [dtProduits] é a chave primária e, utilizando-a, podemos localizar a linha do produto na tabela [dtProduits] que está a ser atualizada no componente [DataGrid1]. O procedimento começa, portanto, por recuperar a chave do produto cujo link [Delete] foi clicado:
' product key to delete
Dim idProduit As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)
Sabemos que [e.Item] é o elemento em [DataGrid1] que desencadeou o evento. Em termos gerais, este elemento é uma linha na [DataGrid]. [e.Item.ItemIndex] é o número da linha que desencadeou o evento. Este índice é relativo à página atualmente apresentada. Assim, a primeira linha da página apresentada tem a propriedade [ItemIndex=0], mesmo que tenha o número 17 na tabela de produtos. [DataGrid1.DataKeys] é a lista de chaves para o [DataGrid]. Como definimos [DataGrid1.DataKey=id] durante o tempo de design, as chaves para [DataGrid1] consistem nos valores da coluna [id] da tabela [dtProduits], que é também a chave primária. Assim, [DataGrid1.DataKeys(e.Item.ItemIndex)] é a chave [id] do produto a ser eliminado. Com isto obtido, solicitamos a sua eliminação utilizando o procedimento [deleteProduct]:
' product deletion
Dim erreur As Boolean = False
Try
supprimerProduit(idProduit)
Catch ex As Exception
' pb
lblInfo2.Text = ex.Message
erreur = True
End Try
Esta eliminação pode falhar em alguns casos. Vamos ver porquê. É então lançada uma exceção. Esta é tratada aqui. Se houver um erro, o [DataGrid1] não precisa de ser alterado. Apenas é adicionada uma mensagem de erro à vista [products]. Se não houver erro e o utilizador tiver acabado de eliminar o único produto na página atual, é então apresentada a página anterior.
' change page?
Dim page As Integer = DataGrid1.CurrentPageIndex
If Not erreur AndAlso DataGrid1.Items.Count = 1 Then
page = DataGrid1.CurrentPageIndex - 1
If page < 0 Then page = 0
End If
Em qualquer caso, a vista [products] é redesenhada utilizando o procedimento [displayProducts]:
O procedimento [deleteProduct] é o seguinte:
Private Sub supprimerProduit(ByVal idProduit As Integer)
Dim erreur As String
Try
' synchronization
Application.Lock()
' search for the line to be deleted
Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
If ligne Is Nothing Then
erreur = String.Format("Produit [{0}] inexistant", idProduit)
Else
' delete line
ligne.Delete()
End If
Catch ex As Exception
erreur = String.Format("Erreur de suppression : {0}", ex.Message)
Finally
' end of synchronization
Application.UnLock()
End Try
' throw an exception if error
If erreur <> String.Empty Then Throw New Exception(erreur)
End Sub
O procedimento recebe a chave do produto a ser eliminado como parâmetro. Se houvesse apenas um cliente a realizar as atualizações, esta eliminação poderia ser feita da seguinte forma:
Com vários clientes a realizar atualizações ao mesmo tempo, a situação torna-se mais complicada. De facto, considere a seguinte sequência de eventos:
![]() |
Hora | Ação |
T1 | O cliente A lê a tabela [dtProducts] — existe um produto com a chave 20 |
T2 | O cliente B lê a tabela [dtProducts] — existe um produto com a chave 20 |
T3 | O cliente A elimina o produto com a chave 20 |
T4 | O cliente B elimina o produto com a chave 20 |
Quando os clientes A e B leem a tabela de produtos e exibem o seu conteúdo numa página web, a tabela contém o produto com a chave 20. Por isso, podem querer realizar uma ação sobre este produto, como, por exemplo, eliminá-lo. Um deles irá certamente fazê-lo primeiro. O que vier a seguir tentará então eliminar um produto que já não existe. Este cenário é tratado pelo procedimento [deleteProducts] de várias formas:
- Primeiro, há uma sincronização utilizando [Application.Lock]. Isto significa que, quando um cliente atravessa esta barreira, nenhum outro cliente pode modificar ou ler a tabela de produtos. Na verdade, todas essas operações são sincronizadas desta forma. Já vimos isto para a leitura.
- Em seguida, verificamos se o produto que queremos eliminar existe. Se sim, é eliminado; caso contrário, é gerada uma mensagem de erro.
<div class="odt-code-rich" data-linenums="false" style="counter-reset: odtline 0;"><pre><code class="language-csharp">
<span class="odt-code-line"><span class="odt-code-line-content"> ' recherche de la ligne à supprimer</span></span>
<span class="odt-code-line"><span class="odt-code-line-content"> <span style="color:#0000ff">Dim</span> ligne <span style="color:#0000ff">As</span> DataRow = dtProduits.Rows.Find(idProduit)</span></span>
<span class="odt-code-line"><span class="odt-code-line-content"> <span style="color:#0000ff">If</span> ligne <span style="color:#0000ff">Is</span> <span style="color:#0000ff">Nothing</span> <span style="color:#0000ff">Then</span></span></span>
<span class="odt-code-line"><span class="odt-code-line-content"> erreur = <span style="color:#0000ff">String</span>.Format("Produit [{0}] inexistant", idProduit)</span></span>
<span class="odt-code-line"><span class="odt-code-line-content"> <span style="color:#0000ff">Else</span><span style="color:#0000ff"></span></span></span>
<span class="odt-code-line"><span class="odt-code-line-content"> ' suppression de la ligne</span></span>
<span class="odt-code-line"><span class="odt-code-line-content"> ligne.Delete()</span></span>
<span class="odt-code-line"><span class="odt-code-line-content"> <span style="color:#0000ff">End</span> <span style="color:#0000ff">If</span></span></span>
</code></pre></div>
- Saímos da secção crítica utilizando [Application.Unlock] para permitir que outros clientes realizem as suas atualizações.
- Se a linha não puder ser eliminada, o procedimento lança uma exceção associada a uma mensagem de erro.
Vejamos um exemplo. Iniciamos um cliente [Mozilla] e selecionamos imediatamente a opção [Update] (visualização parcial):

Fazemos o mesmo com um cliente [Internet Explorer] (visualização parcial):

Com o cliente [Mozilla], eliminamos o produto [product2]. Obtemos a seguinte nova página:

A operação de eliminação foi bem-sucedida. O produto eliminado aparece no [DataGrid] de produtos eliminados e já não aparece nas listas de produtos dos componentes [DataGrid1] e [DataGrid2], que apresentam os produtos atualmente presentes na tabela. Vamos fazer o mesmo com o [Internet Explorer]. Eliminamos o item [product2]:

A resposta recebida é a seguinte:

Uma mensagem de erro informa o utilizador de que o produto com a chave [2] não existe. A resposta devolve uma nova vista ao cliente, refletindo o estado atual da tabela. Podemos ver que o produto com a chave [2] está, de facto, na lista de produtos eliminados. A vista [products] reflete, portanto, as atualizações de todos os clientes, e não apenas de um.
9.6.14. Adicionar um produto
Para adicionar um produto, o utilizador clica na ligação [Adicionar] nas opções. Isto simplesmente apresenta a vista [Adicionar]:


Os dois procedimentos envolvidos nesta ação são os seguintes:
Private Sub lnkAjout_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkAjout.Click
' the [add] view is displayed
afficheAjout()
End Sub
Private Sub afficheAjout()
' displays the add view
vueAjout.Visible = True
vueFormulaire.Visible = False
vueProduits.Visible = False
vueErreurs.Visible = False
End Sub
Note que a vista [Add] tem componentes de validação:
nome | tipo | função |
RequiredFieldValidator | verifica se existe um valor em [txtName] | |
Validador de campo obrigatório | verifica se existe um valor em [txtPrice] | |
CompareValidator | verifica se o preço é >= 0 |
O procedimento para tratar o clique no botão [Adicionar] é o seguinte:
Private Sub btnAjouter_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnAjouter.Click
' add a new item to the product table
' first of all, the data must be valid
rfvNom.Validate()
rfvPrix.Validate()
cvPrix.Validate()
If Not rfvNom.IsValid Or Not rfvPrix.IsValid Or Not cvPrix.IsValid Then
' the input form again
afficheAjout()
Exit Sub
End If
' create a line
Dim produit As DataRow = dtProduits.NewRow
produit("nom") = txtNom.Text.Trim
produit("prix") = txtPrix.Text.Trim
' add the row to the
Application.Lock()
Try
dtProduits.Rows.Add(produit)
lblInfo3.Text = "Ajout réussi"
' cleaning
txtNom.Text = ""
txtPrix.Text = ""
Catch ex As Exception
lblInfo3.Text = String.Format("Erreur : {0}", ex.Message)
End Try
Application.UnLock()
End Sub
Primeiro, realizamos verificações de validade nos três componentes [rfvNom, rfvPrix, cvPrix]. Se alguma das verificações falhar, a vista [Add] é exibida novamente com mensagens de erro para os componentes de validação. Aqui está um exemplo:

Se os dados forem válidos, preparamos a linha a ser inserida na tabela [dtProduits].
' create a line
Dim produit As DataRow = dtProduits.NewRow
produit("nom") = txtNom.Text.Trim
produit("prix") = txtPrix.Text.Trim
Primeiro, criamos uma nova linha na tabela [dtProducts] utilizando o método [DataTable.NewRow]. Esta linha terá as três colunas [id, name, price] da tabela [dtProducts]. As colunas [name, price] são preenchidas com os valores introduzidos no formulário de adição. A coluna [id] não é preenchida. Esta coluna é do tipo [AutoIncrement], o que significa que o SGBD atribuirá a chave máxima existente mais 1 como chave para um novo produto. A linha [product] criada aqui está separada da tabela [dtProducts]. Agora precisamos de a inserir nesta tabela. Uma vez que vamos atualizar a tabela [dtProducts], ativamos a sincronização entre clientes:
Depois de feito isto, tentamos adicionar a linha à tabela [dtProduits]. Se for bem-sucedido, exibimos uma mensagem de sucesso; caso contrário, uma mensagem de erro.
Try
dtProduits.Rows.Add(produit)
lblInfo3.Text = "Ajout réussi"
' nettoyage
txtNom.Text = ""
txtPrix.Text = ""
Catch ex As Exception
lblInfo3.Text = String.Format("Erreur : {0}", ex.Message)
End Try
Normalmente, não devem ocorrer exceções. No entanto, o tratamento de exceções foi implementado como precaução. Assim que a adição estiver concluída, saímos da secção crítica utilizando [Application.Unlock].
9.6.15. Editar um produto
Para modificar um produto, o utilizador clica na ligação [Editar] na linha do produto:

Isto muda para o modo de edição:

Graças ao componente [DataGrid], este resultado é alcançado com muito pouco código:
Private Sub DataGrid1_EditCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.EditCommand
' puts current element in edit mode
DataGrid1.EditItemIndex = e.Item.ItemIndex
' products are re-displayed
afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
End Sub
Clicar no link [Editar] aciona a execução, no lado do servidor, do procedimento [DataGrid1_EditCommand]. O componente [DataGrid1] possui um campo [EditItemIndex]. A linha com o valor de índice de [EditItemIndex] é colocada no modo de edição. Cada valor na linha assim atualizada pode ser modificado numa caixa de entrada, conforme mostrado na captura de ecrã acima. Recuperamos, portanto, o índice do produto em que o link [Editar] foi clicado e atribuímo-lo à propriedade [EditItemIndex] do componente [DataGrid1]. Resta apenas recarregar a vista [produtos]. Esta aparecerá idêntica à anterior, mas com uma linha no modo de edição.
O link [Cancel] do produto que está a ser editado permite ao utilizador cancelar a atualização. O procedimento associado a este link é o seguinte:
Private Sub DataGrid1_CancelCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.CancelCommand
' products are re-displayed
afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
End Sub
Simplesmente volta a apresentar a vista [produtos]. Poder-se-ia sentir a tentação de definir [DataGrid1.EditItemIndex] como -1 para cancelar o modo de atualização. Na verdade, sabemos que o procedimento [Page_Load] faz isso automaticamente. Não há, portanto, necessidade de o fazer novamente.
A alteração é validada pelo link [Update] na linha que está a ser editada. O seguinte procedimento é então executado:
Private Sub DataGrid1_UpdateCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.UpdateCommand
' product key to be modified
Dim idProduit As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)
' modified items
Dim nom As String = CType(e.Item.Cells(0).Controls(0), TextBox).Text.Trim
Dim prix As String = CType(e.Item.Cells(1).Controls(0), TextBox).Text.Trim
' valid modifications?
lblInfo2.Text = ""
If nom = String.Empty Then lblInfo2.Text += "[Indiquez un nom]"
If prix = String.Empty Then
lblInfo2.Text += "[Indiquez un prix]"
Else
Dim nouveauPrix As Double
Try
nouveauPrix = CType(prix, Double)
If nouveauPrix < 0 Then Throw New Exception
Catch ex As Exception
lblInfo2.Text += "[prix invalide]"
End Try
End If
' if error, redisplay update page
If lblInfo2.Text <> String.Empty Then
' return the line to update mode
DataGrid1.EditItemIndex = e.Item.ItemIndex
' product display
afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
' end
Exit Sub
End If
' if no error - we modify the table
Try
modifierProduit(idProduit, e.Item)
Catch ex As Exception
' pb
lblInfo2.Text = ex.Message
End Try
' product display
afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
End Sub
Tal como na eliminação, precisamos de recuperar a chave do produto a modificar para encontrar a sua linha na tabela de produtos [dtProduits]:
' product key to be modified
Dim idProduit As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)
Em seguida, precisamos de recuperar os novos valores a atribuir à linha. Estes encontram-se no componente [DataGrid1]. O argumento [e] do procedimento existe para nos ajudar. [e.Item] representa a linha em [DataGrid1] que desencadeou o evento. Esta é, portanto, a linha que está atualmente a ser atualizada, uma vez que o link [Update] existe apenas nesta linha. Esta linha contém colunas designadas pela coleção [Cells] da linha. Assim, [e.Item.Cells(0)] representa a coluna 0 da linha que está a ser atualizada. Sabemos que os novos valores estão em caixas de texto. A coleção [e.Item.Cells(i).Controls] representa a coleção de controlos na coluna i da linha [e.Item]. As duas instruções seguintes recuperam os valores das caixas de texto da linha que está a ser atualizada em [DataGrid1]:
' modified items
Dim nom As String = CType(e.Item.Cells(0).Controls(0), TextBox).Text.Trim
Dim prix As String = CType(e.Item.Cells(1).Controls(0), TextBox).Text.Trim
Agora temos os novos valores para a linha modificada como cadeias de caracteres. Em seguida, verificamos se estes dados são válidos. O nome não pode estar vazio e o preço deve ser um número positivo ou zero:
' valid modifications?
lblInfo2.Text = ""
If nom = String.Empty Then lblInfo2.Text += "[Indiquez un nom]"
If prix = String.Empty Then
lblInfo2.Text += "[Indiquez un prix]"
Else
Dim nouveauPrix As Double
Try
nouveauPrix = CType(prix, Double)
If nouveauPrix < 0 Then Throw New Exception
Catch ex As Exception
lblInfo2.Text += "[prix invalide]"
End Try
End If
Se ocorrer um erro, o rótulo [lblInfo2] exibirá uma mensagem de erro e a mesma página será simplesmente recarregada:
' if error, redisplay update page
If lblInfo2.Text <> String.Empty Then
' return the line to update mode
DataGrid1.EditItemIndex = e.Item.ItemIndex
' product display
afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
' end
Exit Sub
End If
O que o código acima não mostra é que os valores introduzidos são perdidos. Isto acontece porque [DataGrid1] está ligado aos dados da tabela [dtProduits], que contém os valores originais da linha modificada. Aqui está um exemplo.

O resultado é o seguinte:

Podemos ver que os valores inseridos inicialmente foram perdidos. Numa aplicação profissional, isto seria provavelmente inaceitável. Aqui, deparamo-nos com certas limitações do modo de atualização padrão do componente [DataGrid]. Seria preferível ter uma vista [Edit] análoga à vista [Add].
Se os dados forem válidos, são utilizados para atualizar a tabela [dtProducts]:
' cas où pas d'erreur - on modifie la table
Try
modifierProduit(idProduit, e.Item)
Catch ex As Exception
' pb
lblInfo2.Text = ex.Message
End Try
' affichage produits
afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
Pela mesma razão mencionada ao eliminar um produto, a modificação pode falhar. De facto, entre o momento em que o cliente lê a tabela [dtProduits] e o momento em que tenta modificar um dos seus produtos, esse produto pode ter sido eliminado por outro cliente. O procedimento [modifyProduct] lida com este caso lançando uma exceção. Esta exceção é tratada aqui. Após a atualização ser bem-sucedida ou falhar, a aplicação devolve a vista [products] ao cliente. Ainda precisamos de ver como o procedimento [modifyProduct] realiza a atualização:
Private Sub modifierProduit(ByVal idProduit As Integer, ByVal item As DataGridItem)
Dim erreur As String
Try
' synchronization
Application.Lock()
' search for the line to be modified
Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
If ligne Is Nothing Then
erreur = String.Format("Produit [{0}] inexistant", idProduit)
Else
' modify the line
With ligne
.Item("nom") = CType(item.Cells(0).Controls(0), TextBox).Text.Trim
.Item("prix") = CType(item.Cells(1).Controls(0), TextBox).Text.Trim
End With
End If
Catch ex As Exception
erreur = String.Format("Erreur lors de la modification : {0}", ex.Message)
Finally
' end of synchronization
Application.UnLock()
End Try
' throw an exception if error
If erreur <> String.Empty Then Throw New Exception(erreur)
End Sub
Não entraremos em detalhes sobre este procedimento, cujo código é semelhante ao do procedimento [deleteProduct], que já foi explicado em pormenor. Vejamos apenas dois exemplos. Primeiro, vamos alterar o preço do produto [product1]:

Clicar na ligação [Update] acima produz a seguinte resposta:

A alteração é claramente visível nos componentes [DataGrid] 1 e 2, que refletem o estado atual da tabela [dtProducts]. Também é visível no componente [DataGrid4], que apresenta uma vista das linhas modificadas, mostrando-as com os seus valores originais. Agora, vamos analisar um caso de conflito de concorrência. Tal como no exemplo de eliminação, utilizaremos dois clientes web diferentes. Um cliente [Mozilla] lê a tabela [dtProduits] e começa a editar o produto [product1]:

Um cliente [Internet Explorer] está prestes a eliminar o produto [product1]:

O cliente [Internet Explorer] elimina o [product1]:

Note que [product1] já não se encontra na tabela de linhas modificadas, mas sim na tabela de linhas eliminadas. O cliente [Mozilla] valida a sua atualização:

O cliente [Mozilla] recebe a seguinte resposta à sua atualização:

É possível verificar que [product1] foi eliminado, uma vez que se encontra na lista de produtos eliminados.
9.7. Aplicação web para atualizar a tabela de produtos físicos
9.7.1. Soluções propostas
A aplicação anterior era mais um exemplo de manual destinado a demonstrar a gestão de um objeto [DataTable] em cache do que um cenário do mundo real. De facto, a certa altura, a fonte de dados real tem de ser atualizada. Podem ser escolhidas duas estratégias diferentes:
- Utilizamos o cache [dtProduits] na memória para atualizar a fonte de dados. Pode ser criada uma página na árvore web da aplicação anterior para fornecer acesso ao seu cache [dtProduits]. Esta página permitiria a um administrador sincronizar as alterações feitas no cache [dtProduits] com a fonte de dados física. Para tal, poderíamos adicionar um novo método à classe de acesso [products] que receba o cache [dtProduits] como parâmetro e utilize este cache para atualizar a fonte de dados física.
- A fonte de dados física é atualizada ao mesmo tempo que o cache.
A Estratégia n.º 1 permite abrir apenas uma ligação à fonte de dados física. A Estratégia n.º 2 requer uma ligação para cada atualização. Dependendo da disponibilidade de ligação, uma estratégia pode ser preferível à outra. Uma vez que dispomos das ferramentas para a implementar (a classe [products]), optamos pela Estratégia n.º 2.
9.7.2. Solução 1
Por uma questão de continuidade com a aplicação anterior, escolhemos a seguinte estratégia:
- a fonte física é atualizada ao mesmo tempo que o cache
- o cache é construído apenas uma vez durante a inicialização da aplicação em [global.asax.vb]. Isto significa que, se a fonte de dados física for atualizada por clientes que não sejam clientes web, os clientes web não verão essas alterações. Eles verão apenas as alterações que eles próprios fazem na tabela em cache.
Para atualizar a fonte de dados física, precisamos de uma instância da classe de acesso ao produto. Cada cliente poderia ter a sua própria. Também podemos partilhar uma única instância que seria criada pela aplicação no arranque. Esta é a solução que estamos a escolher aqui. O código de controlo [global.asax.vb] é modificado da seguinte forma:
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 chaînedeConnexion As String = ConfigurationSettings.AppSettings("OLEDBStringConnection")
Dim defaultProduitsPage As String = ConfigurationSettings.AppSettings("defaultProduitsPage")
Dim erreurs As New ArrayList
...
' no configuration errors here
' create a product object
Dim objProduits As New produits(chaînedeConnexion)
Dim dtProduits As DataTable
Try
dtProduits = objProduits.getProduits
Catch ex As ExceptionProduits
'there has been an error accessing the products, this is noted in the application
Application("erreurs") = ex.erreurs
Exit Sub
Catch ex As Exception
' unhandled error
erreurs.Add(ex.Message)
Application("erreurs") = erreurs
' exit sub
End Try
' no initialization errors here
...
' the data access instance is stored
Application("objProduits") = objProduits
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
...
End Sub
End Class
Uma instância da classe de acesso aos dados foi armazenada na aplicação, associada à chave [objProducts]. Cada cliente utilizará esta instância para aceder à fonte de dados física. Será recuperada no procedimento [Page_Load] de [main.aspx.vb]:
Protected objProduits As produits
....
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
' retrieve the product access instance
objProduits = CType(Application("objProduits"), produits)
...
End Sub
A instância de acesso aos dados [objProducts] está acessível a todos os métodos da página. Será utilizada para as três operações de atualização: adicionar, eliminar e modificar.
O procedimento de adição é modificado da seguinte forma:
Private Sub btnAjouter_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnAjouter.Click
...
' we add the line
Application.Lock()
Try
' add the line to the cached table
dtProduits.Rows.Add(produit)
' add the line to the physical source
Dim nouveauProduit As sProduit
With nouveauProduit
.nom = CType(produit("nom"), String)
.prix = CType(produit("prix"), Double)
End With
objProduits.ajouterProduit(nouveauProduit)
' follow-up
lblInfo3.Text = "Ajout réussi"
' cleaning
txtNom.Text = ""
txtPrix.Text = ""
Catch ex As Exception
' error
lblInfo3.Text = String.Format("Erreur : {0}", ex.Message)
End Try
Application.UnLock()
End Sub
O procedimento de modificação é alterado da seguinte forma:
Private Sub modifierProduit(ByVal idProduit As Integer, ByVal item As DataGridItem)
Dim erreur As String
Try
' synchronization
Application.Lock()
' search for the line to be modified
Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
If ligne Is Nothing Then
erreur = String.Format("Produit [{0}] inexistant", idProduit)
Else
' modify the line in the cache
With ligne
.Item("nom") = CType(item.Cells(0).Controls(0), TextBox).Text.Trim
.Item("prix") = CType(item.Cells(1).Controls(0), TextBox).Text.Trim
End With
' modify the line in the physical source
Dim nouveauProduit As sProduit
With nouveauProduit
.id = idProduit
.nom = CType(ligne.Item("nom"), String)
.prix = CType(ligne.Item("prix"), Double)
End With
objProduits.modifierProduit(nouveauProduit)
End If
Catch ex As Exception
erreur = String.Format("Erreur lors de la modification : {0}", ex.Message)
Finally
' end of synchronization
Application.UnLock()
End Try
' throw an exception if error
If erreur <> String.Empty Then Throw New Exception(erreur)
End Sub
O procedimento de eliminação é modificado da seguinte forma:
Private Sub supprimerProduit(ByVal idProduit As Integer)
Dim erreur As String
Try
' synchronization
Application.Lock()
' search for the line to be deleted
Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
If ligne Is Nothing Then
erreur = String.Format("Produit [{0}] inexistant", idProduit)
Else
' delete line from cache
ligne.Delete()
' delete the line in the physical source
objProduits.supprimerProduit(idProduit)
End If
Catch ex As Exception
erreur = String.Format("Erreur de suppression : {0}", ex.Message)
Finally
' end of synchronization
Application.UnLock()
End Try
' throw an exception if error
If erreur <> String.Empty Then Throw New Exception(erreur)
End Sub
9.7.3. Testes
Começamos com a seguinte tabela de dados num ficheiro ACCESS:

É iniciado um cliente web:

Adicionamos um produto:

Estamos a descontinuar o [produto1]:

Estamos a alterar o preço do [produto2]:

Após estas alterações, analisamos o conteúdo da tabela [list] na base de dados do ACCESS:

As três alterações foram corretamente refletidas na tabela física. Agora, vamos examinar um cenário de conflito. Eliminamos a linha de [product2] diretamente no Access:

Voltamos ao nosso cliente web. O cliente não vê a eliminação que foi feita e também quer eliminar [product2]:

Recebe a seguinte resposta:

A linha [product2] foi, de facto, removida da cache, como se pode ver na lista de produtos eliminados. No entanto, a eliminação de [product2] na fonte física falhou, conforme indicado pela mensagem de erro.
9.7.4. Solução 2
Na solução anterior, os clientes web atualizam simultaneamente a fonte de dados física, mas não veem as alterações feitas por outros clientes. Eles só veem as suas próprias. Agora queremos que um cliente consiga ver a fonte de dados física tal como está atualmente, e não como estava quando a aplicação foi iniciada. Para tal, vamos oferecer ao cliente uma nova opção:

Com a opção [Atualizar], o cliente força uma nova leitura da fonte de dados física. Para garantir que isto não afeta outros clientes, a tabela resultante desta leitura deve pertencer ao cliente que realiza a atualização e não deve ser partilhada com outros clientes. Esta é a primeira diferença em relação à aplicação anterior. O cache [dtProduits] da fonte de dados será construído por cada cliente e não pela própria aplicação. A modificação é feita em [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 product object
Dim objProduits As New produits(chaînedeConnexion)
' store the number of products per page
Application("defaultProduitsPage") = defaultProduitsPage
' the data access instance is stored
Application("objProduits") = objProduits
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' init session variables
If IsNothing(Application("erreurs")) Then
' cache the data source
Dim dtProduits As DataTable
Try
Application.Lock()
dtProduits = CType(Application("objProduits"), produits).getProduits
Catch ex As ExceptionProduits
'there has been an error accessing the products, this is noted in the session
Session("erreurs") = ex.erreurs
Exit Sub
Finally
Application.UnLock()
End Try
' cache [dtProduits] in the session
Session("dtProduits") = dtProduits
' view of the product table
Session("dvProduits") = dtProduits.DefaultView
' products per page
Session("nbProduitsPage") = Application("defaultProduitsPage")
' current page displayed
Session("pageCourante") = 0
End If
End Sub
End Class
A informação armazenada na sessão é recuperada com cada pedido no procedimento [Page_Load]:
' data page
Protected dtProduits As DataTable
Protected dvProduits As DataView
Protected objProduits As produits
Protected nbProduitsPage As Integer
Protected pageCourante As Integer
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' check for application errors
If Not IsNothing(Application("erreurs")) Then
' the application has not initialized correctly
afficheErreurs(CType(Application("erreurs"), ArrayList))
Exit Sub
End If
' check for session errors
If Not IsNothing(Session("erreurs")) Then
' the session has not initialized correctly
afficheErreurs(CType(Session("erreurs"), ArrayList))
Exit Sub
End If
' retrieve a reference from the product table
dtProduits = CType(Session("dtProduits"), DataTable)
' product view
dvProduits = CType(Session("dvProduits"), DataView)
' retrieve the number of products per page
nbProduitsPage = CType(Session("nbProduitsPage"), Integer)
' retrieve the current page
pageCourante = CType(Session("pageCourante"), Integer)
' retrieve the product access instance
objProduits = CType(Application("objProduits"), produits)
' cancels any update in progress
DataGrid1.EditItemIndex = -1
'1st request
If Not IsPostBack Then
' the initial form is displayed
txtPages.Text = nbProduitsPage.ToString
afficheFormulaire()
End If
End Sub
As informações recuperadas durante a sessão serão guardadas no final da sessão após cada pedido:
Private Sub Page_PreRender(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.PreRender
' certain information is stored in the session
Session("dtProduits") = dtProduits
Session("dvProduits") = dvProduits
Session("nbProduitsPage") = nbProduitsPage
Session("pageCourante") = pageCourante
End Sub
O evento [PreRender] sinaliza que a resposta está prestes a ser enviada ao cliente. Aproveitamos esta oportunidade para guardar todos os dados que precisam de ser mantidos na sessão. Isto é excessivo, uma vez que, muitas vezes, apenas alguns dos dados foram alterados. Esta gravação sistemática tem a vantagem de nos isentar da gestão da sessão nos outros métodos da página.
A operação de atualização do cache é tratada pelo seguinte procedimento:
Private Sub lnkRefresh_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkRefresh.Click
' refresh the [dtProduits] cache with the physical data source
' synchro start
Application.Lock()
Try
' product table
dtProduits = CType(Application("objProduits"), produits).getProduits
' the current filter is saved
Dim filtre As String = dvProduits.RowFilter
' create the new filtered view
dvProduits = New DataView(dtProduits)
' put the filter back on
dvProduits.RowFilter = filtre
Catch ex As ExceptionProduits
'there has been an error accessing the products, this is noted in the session
Session("erreurs") = ex.erreurs
' display the [errors] view
afficheErreurs(ex.erreurs)
' finish
Exit Sub
Finally
' end synchro
Application.UnLock()
End Try
' it went well - the products are displayed from the 1st page
afficheProduits(0, nbProduitsPage)
End Sub
O procedimento regenera novos valores para o cache [dtProduits] e a vista [dvProduits]. Estes serão colocados na sessão pelo procedimento [Page_PreRender] descrito acima. Assim que o cache [dtProduits] for reconstruído, exibimos os produtos a partir da primeira página.
Aqui está um exemplo de execução. Um cliente [mozilla] é iniciado e exibe os produtos:

Um cliente [Internet Explorer] faz o mesmo:

O cliente [Mozilla] elimina [product1], modifica [product2] e adiciona um novo produto. Obtém a seguinte nova página:

O cliente [Internet Explorer] pretende eliminar [product1].

Recebe a seguinte resposta:

Foi notificado de que [product1] já não existe. O utilizador decide então atualizar a sua cache utilizando o link [Refresh] acima. Recebe a seguinte resposta:

Agora tem a mesma fonte de dados que o cliente [Mozilla].
9.8. Conclusão
Neste capítulo, dedicámos bastante tempo aos contentores de dados e às suas ligações às fontes de dados. Concluímos mostrando como atualizar uma fonte de dados utilizando um componente [DataGrid]. Utilizámos uma tabela de base de dados como fonte de dados. Embora o componente [DataGrid] facilite um pouco a apresentação dos dados, o verdadeiro desafio não reside na camada de apresentação, mas sim na gestão das atualizações à fonte de dados efetuadas por diferentes clientes. Podem surgir conflitos de acesso que têm de ser geridos. Aqui, tratámos deles no controlador utilizando [Application.Lock]. Provavelmente, seria mais sensato sincronizar o acesso à fonte de dados dentro da classe de acesso aos dados, para que o controlador não tenha de se preocupar com esses detalhes que estão fora do seu âmbito.
Na prática, as tabelas numa base de dados estão ligadas umas às outras através de relações, e as suas atualizações devem ter isso em conta. Isto afeta principalmente a classe de acesso aos dados, que se torna mais complexa do que o necessário para uma tabela independente. Também tem, geralmente, impacto na camada de apresentação da aplicação web, uma vez que é frequentemente necessário apresentar não uma única tabela, mas sim tabelas ligadas umas às outras através de relações.
Este capítulo também apresentou várias estruturas de dados, tais como [DataTable, DataView].









