9. Componentes de servidor ASP - 3
9.1. Introduction
Continuamos nuestro trabajo en la interfaz de usuario profundizando en las capacidades de los componentes [DataList] y [DataGrid], especialmente en el ámbito de la actualización de los datos que muestran
9.2. Manejo de eventos asociados a los datos de los componentes con enlace de datos
9.2.1. El ejemplo
Consideremos la siguiente página:
![]() |
La página incluye tres componentes asociados a una lista de datos:
- un componente [DataList] denominado [DataList1]
- un componente [DataGrid] denominado [DataGrid1]
- un componente [Repeater] denominado [Repeater1]
La lista de datos asociada es el array {"cero", "uno", "dos", "tres"}. A cada uno de estos datos se le asocia un grupo de dos botones denominados [Infos1] y [Infos2]. El usuario hace clic en uno de los botones y aparece un texto con el nombre del botón en el que hizo clic. Aquí queremos mostrar cómo manejar una lista de botones o enlaces.
9.2.2. La configuración de los componentes
El componente [DataList1] está configurado de la siguiente manera:
<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>
Hemos omitido todo lo relacionado con la apariencia del [DataList] para centrarnos únicamente en su contenido:
- la sección <HeaderTemplate> define el encabezado del [DataList] y la sección <FooterTemplate> su pie de página.
- La sección <ItemTemplate> es la plantilla de visualización utilizada para cada uno de los datos de la lista de datos asociada. En ella se encuentran los siguientes elementos:
- el valor del dato actual de la lista de datos asociada al componente: <%# Container.DataItem %>
- dos botones etiquetados respectivamente como [Infos1] y [Infos2]. La clase [Button] tiene un atributo [CommandName] que se utiliza aquí. Esto nos permitirá determinar qué botón originó un evento en el [DataList]. Para manejar los clics en los botones, tendremos un solo manejador de eventos que estará vinculado al propio [DataList] y no a los botones. Este controlador recibirá información que le indicará en qué línea del [DataList] se produjo el clic. El atributo [CommandName] nos permitirá saber en qué botón de la línea ocurrió.
El componente [Repeater1] se configura de manera muy similar:
<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>
Simplemente se ha agregado una sección <SeparatorTemplate> para que los datos sucesivos que muestra el componente estén separados por una barra horizontal.
Por último, el componente [DataGrid1] está configurado de la siguiente manera:
<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>
Aquí también hemos omitido la información de estilo (colores, anchos, etc.). Estamos en el modo de generación automática de columnas, que es el modo predeterminado de [DataGrid]. Esto significa que habrá tantas columnas como las que haya en la fuente de datos. Aquí hay una. Hemos agregado otras dos columnas etiquetadas con <asp:ButtonColumn>. En ellas definimos información similar a la definida para los otros dos componentes, así como el tipo de botón, en este caso [PushButton]. El tipo por defecto es [LinkButton], c.a.d. un enlace. Además, los datos se paginarán [AllowPaging=true] con un tamaño de página de dos elementos [PageSize=2].
9.2.3. El código de presentación de la página
El código de presentación de nuestra página de ejemplo se ha colocado en un archivo [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>
En el código anterior, hemos omitido el código de formato (colores, líneas, tamaños, etc.)
9.2.4. El código de control de la página
El código de control de la aplicación se ha colocado en el archivo [main.aspx.vb]:
Public Class main
Inherits System.Web.UI.Page
' componentes de la página
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
' la fuente de datos
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
'conexiones con la fuente de datos
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
' se produjo un evento en una de las líneas del [datalist]
lblInfo.Text = "Vous avez cliqué sur le bouton [" + e.CommandName + "] de l'élément [" + e.Item.ItemIndex.ToString + "] du composant [DataList]"
End Sub
Private Sub Repeater1_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.RepeaterCommandEventArgs) Handles Repeater1.ItemCommand
' se produjo un evento en una de las líneas del [repeater]
lblInfo.Text = "Vous avez cliqué sur le bouton [" + e.CommandName + "] de l'élément [" + e.Item.ItemIndex.ToString + "] du composant [Repeater]"
End Sub
Private Sub DataGrid1_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.ItemCommand
' Se produjo un evento en una de las líneas del [datagrid]
lblInfo.Text = "Vous avez cliqué sur le bouton [" + e.CommandName + "] de l'élément [" + e.Item.ItemIndex.ToString + "] de la page [" + DataGrid1.CurrentPageIndex.ToString() + "] du composant [DataGrid]"
End Sub
Private Sub DataGrid1_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles DataGrid1.PageIndexChanged
' cambio de página
With DataGrid1
.CurrentPageIndex = e.NewPageIndex
.DataSource = textes
.DataBind()
End With
End Sub
End Class
Comentarios:
- La fuente de datos [textes] es una simple tabla de cadenas de caracteres. Se vinculará a los tres componentes presentes en la página
- esta vinculación se realiza en el procedimiento [Page_Load] durante la primera consulta. Para las consultas siguientes, los tres componentes recuperarán sus valores mediante el mecanismo de [VIEW_STATE].
- Los tres componentes cuentan con un controlador para el evento [ItemCommand]. Este evento se produce cuando se hace clic en un botón o en un enlace en una de las líneas del componente. El controlador recibe dos datos:
- fuente: la referencia del objeto (botón o enlace) que originó el evento
- a: información sobre el evento de tipo [DataListCommandEventArgs], [RepeaterCommandEventArgs] o [DataGridCommandEventArgs], según el caso. El argumento a trae consigo diversa información. Aquí, nos interesan dos de ellas:
- a.Item: representa la línea en la que ocurrió el evento, de tipo [DataListItem], [DataGridItem] o [RepeaterItem]. Sea cual sea el tipo exacto, el elemento [Item] tiene un atributo [ItemIndex] que indica el número de la línea [Item] del contenedor al que pertenece. Aquí mostramos ese número de línea
- a.CommandName: es el atributo [CommandName] del botón (Button, LinkButton, ImageButton) que originó el evento. Esta información, junto con la anterior, nos permite saber qué botón del contenedor originó el evento [ItemCommand]
9.3. Aplicación: gestión de una lista de suscripciones
Ahora que sabemos cómo interceptar los eventos que ocurren dentro de un contenedor de datos, presentamos un ejemplo que muestra cómo se pueden gestionar.
9.3.1. Introducción
La aplicación que se presenta simula una aplicación de suscripciones a listas de distribución. Estas se definen mediante un objeto [DataTable] de tres columnas:
nombre | tipo | función |
cadena | clave primaria | |
cadena | nombre del tema de la lista | |
cadena | una descripción de los temas tratados por la lista |
Para no complicar nuestro ejemplo, el objeto [DataTable] anterior se generará mediante código de manera arbitraria. En una aplicación real, probablemente se proporcionaría mediante un método de una clase de acceso a datos. La construcción de la tabla de listas se realizará en el procedimiento [Application_Start] y la tabla resultante se colocará en la aplicación. La llamaremos tabla [dtThèmes]. El usuario se suscribirá a algunos de los temas de esta tabla. La lista de sus suscripciones se guardará también allí, en un objeto [DataTable] llamado [dtAbonnements], cuya estructura será la siguiente:
nombre | tipo | rol |
cadena | clave primaria | |
cadena | nombre del tema de la lista |
La aplicación de una sola página es la siguiente:
![]() |
n.º | nombre | tipo | propiedades | función |
DataGrid | listas de distribución disponibles para suscribirse | |||
DataList | Lista de suscripciones del usuario a las listas anteriores | |||
panel | panel de información sobre el tema seleccionado por el usuario con un enlace [Plus d'informations] | |||
Etiqueta | forma parte de [panelInfos] | nombre del tema | ||
Etiqueta | forma parte de [panelInfos] | Descripción del tema | ||
Etiqueta | Mensaje informativo de la aplicación |
Nuestro ejemplo tiene como objetivo ilustrar el uso de los componentes [DataGrid] y [DataList], en particular la gestión de los eventos que ocurren en las filas de estos contenedores de datos. Por ello, la aplicación no cuenta con un botón de validación que almacene en una base de datos, por ejemplo, las selecciones realizadas por el usuario. No obstante, es realista. Nos acercamos a una aplicación de comercio electrónico en la que un usuario agregaría productos (suscripciones) a su carrito.
9.3.2. Funcionamiento
La primera vista que ve el usuario es la siguiente:
![]() |
El usuario hace clic en los enlaces [Plus d'informations] para obtener información sobre un tema. Esta se muestra en [panelInfos]:

Hace clic en los enlaces [S'abonner] para suscribirse a un tema. Los temas seleccionados se registran en el componente [dlAbonnements]:

Es posible que el usuario quiera suscribirse a una lista a la que ya está suscrito. Un mensaje se lo indica:

Por último, puede cancelar su suscripción a cualquiera de los temas con los botones [Retirer] que aparecen arriba. No se solicita ninguna confirmación. Esto no es necesario en este caso, ya que el usuario puede volver a suscribirse fácilmente. Esta es la vista después de cancelar la suscripción a [thème1]:

9.3.3. Configuración de los contenedores de datos
El componente [dgThèmes], de tipo [DataGrid], está vinculado a una fuente de tipo [DataTable]. Su formato se ha definido mediante el enlace [Mise en forme automatique] del panel de propiedades del [DataGrid]. La definición de sus propiedades se realizó mediante el enlace [Générateur de proprités] del mismo panel. El código generado es el siguiente (se omite el código de formato):
<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>
Cabe destacar los siguientes puntos:
Nosotros mismos definimos las columnas que se mostrarán en la sección <columns>...</columns> | |
para la paginación de los datos | |
define la columna [thème] (HeaderText) de [DataGrid], que se vinculará a la columna [thème] de la fuente de datos (DataField) | |
define dos columnas de botones (o enlaces). Para diferenciar los dos enlaces de una misma fila, se utilizará su propiedad [CommandName]. |
El componente [DataGrid] no está completamente configurado. Se configurará en el código del controlador.
El componente [dlAbonnements], de tipo [DataList], está vinculado a una fuente de tipo [DataTable]. Su formato se ha definido mediante el enlace [Mise en forme automatique] del panel de propiedades del [DataList]. La definición de sus propiedades, por su parte, se ha realizado directamente en el código de presentación. Este código es el siguiente (se omite el código de formato):
<asp:DataList id="dlAbonnements" ... runat="server" >
<HeaderTemplate>
<div align="center">
Vos abonnements</div>
</HeaderTemplate>
<ItemStyle ...></ItemStyle>
<ItemTemplate>
<TABLE>
<TR>
<TD><%#Container.DataItem("tema")%></TD>
<TD>
<asp:Button id="lnkRetirer" CommandName="retirer" runat="server" Text="Retirer" /></TD>
</TR>
</TABLE>
</ItemTemplate>
<HeaderStyle ...></HeaderStyle>
</asp:DataList>
define el texto del encabezado de [DataList] | |
define el elemento actual del [DataList]; aquí se ha colocado una tabla de dos columnas y una fila. La primera celda servirá para contener el nombre del tema al que el usuario desea suscribirse, y la otra, el botón [Retirer] que le permite cancelar su selección. |
9.3.4. La página de presentación
El código de presentación [main.aspx] es el siguiente:
<%@ 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. Los controladores
El control se distribuye entre los archivos [global.asax] y [main.aspx]. El archivo [global.asax] es el siguiente:
El archivo asociado [global.asax.vb] contiene el siguiente 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)
' se inicializa la fuente de datos
Dim thèmes As New DataTable
' columnas
With thèmes.Columns
.Add("id", GetType(System.Int32))
.Add("thème", GetType(System.String))
.Add("description", GetType(System.String))
End With
' la columna «id» será la clave primaria
thèmes.Constraints.Add("cléprimaire", thèmes.Columns("id"), True)
' filas
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
' se incorpora la fuente de datos a la aplicación
Application("thèmes") = thèmes
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' Inicio de sesión: se crea una tabla de suscripciones vacía
Dim dtAbonnements As New DataTable
With dtAbonnements
' las columnas
.Columns.Add("id", GetType(String))
.Columns.Add("thème", GetType(String))
' la clave primaria
.PrimaryKey = New DataColumn() {.Columns("id")}
End With
' la tabla se agrega a la sesión
Session.Item("abonnements") = dtAbonnements
End Sub
End Class
El procedimiento [Application_Start], que se ejecuta cuando la aplicación recibe su primera solicitud, genera el [DataTable] de los temas a los que uno puede suscribirse. Se genera de manera arbitraria con código. Recordemos la técnica que ya hemos visto. Se genera en el siguiente orden:
- un objeto [DataTable] vacío, sin estructura y sin datos
- la estructura de la tabla al definir sus columnas (nombre y tipo de datos que contienen)
- las filas de la tabla que representan los datos útiles
Aquí hemos agregado una clave primaria. La columna «id» es la que sirve como clave primaria. Hay varias formas de expresarlo. En este caso, hemos utilizado una restricción. En SQL, una restricción es una regla que deben cumplir los datos de una fila para que esta pueda agregarse a una tabla. Existen todo tipo de restricciones posibles. La restricción «Primary Key» obliga a que la columna a la que se aplica tenga valores únicos y no vacíos. De hecho, una clave primaria puede estar formada por una expresión que incluya valores de varias columnas. [DataTable].Constraints es la colección de restricciones de una tabla determinada. Para agregar una restricción, se utiliza el método [DataTable.Constraints.Add]. Este tiene varias firmas. Aquí se utilizó el método [Add(Byval nom as String, Byval colonne as DataColumn, Byval cléPrimaire as Boolean)]:
nombre de la restricción: puede ser cualquiera | |
columna que será la clave primaria — del tipo [DataColumn] | |
debe ser [vrai] para que [colonne] sea la clave primaria. Si es [cléPrimaire=false], solo se aplica la restricción de valores únicos a [colonne] |
Para convertir la columna llamada «id» en la clave primaria de la tabla [dtAbonnements], se escribe lo siguiente:
El procedimiento [Session_Start] se ejecuta cuando la aplicación recibe la primera solicitud de un cliente. Sirve para crear objetos específicos para cada cliente que deben persistir a lo largo de sus diferentes solicitudes. El procedimiento construye la tabla [DataTable] con las suscripciones del cliente. Solo se crea su estructura, ya que inicialmente esta tabla está vacía. Se irá llenando a medida que se realicen las solicitudes. Aquí también, la columna «id» sirve como clave primaria. Se utilizó una técnica diferente para declarar esta restricción:
es la matriz de columnas que forman la clave primaria; en este caso, se ha declarado una matriz de un solo elemento: la columna denominada «id» |
Cuando la solicitud del cliente llega al controlador [main.aspx], los dos objetos [DataTable] están disponibles en la aplicación para la tabla de temas y en la sesión para la tabla de suscripciones. El controlador [main.aspx.vb] es el siguiente:
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
La función principal del procedimiento [Page_Load] es:
- recuperar las dos tablas [dtThèmes] y [dtAbonnements], que se encuentran respectivamente en la aplicación y en la sesión, para que estén disponibles para todos los métodos de la página
- vincular los datos de estas dos fuentes con sus contenedores respectivos. Esto se realiza únicamente durante la primera consulta. En las consultas siguientes, la vinculación no debe realizarse sistemáticamente y, cuando sea necesario hacerlo, a veces hay que esperar a que ocurra un evento posterior a [Page_Load] para llevarla a cabo.
El código de [Page_Load] es el siguiente:
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' se recuperan las fuentes de datos
dtThèmes = CType(Application("thèmes"), DataTable)
dtAbonnements = CType(Session("abonnements"), DataTable)
' vinculación de datos
If Not IsPostBack Then
liaisons()
End If
' se oculta cierta información
panelInfo.Visible = False
End Sub
Private Sub liaisons()
'se vincula la fuente de datos al componente [datagrid]
With dgThèmes
.DataSource = dtThèmes
.DataKeyField = "id"
End With
' se vincula la fuente de datos al componente [datalist]
With dlAbonnements
.DataSource = dtAbonnements
.DataKeyField = "id"
End With
' se asignan los datos a los componentes
Page.DataBind()
End Sub
En el procedimiento [liaisons], utilizamos la propiedad [DataKeyField] de los componentes [DataList] y [DataGrid] para definir la columna de la fuente de datos que servirá para identificar de manera única las filas de los contenedores. Por lo general, esta columna es la clave primaria de la fuente de datos, pero no es obligatorio. Basta con que la columna no contenga duplicados ni valores vacíos. Para el contenedor [dgThèmes], la columna «id» de la fuente [dtThèmes] servirá como clave primaria, y para el contenedor [dlAbonnements] será la columna «id» de la fuente [dtAbonnements]. No es necesario que el contenedor muestre la columna que le sirve como clave primaria. En este caso, ninguno de los dos contenedores muestra la columna de clave primaria. La ventaja de que un contenedor tenga una clave primaria es que esta permite localizar fácilmente en la fuente de datos la información relacionada con la fila del contenedor en la que se produjo un evento. De hecho, es frecuente que, a partir de la fila de un contenedor en la que se ha producido un evento, se deba actuar sobre la fila correspondiente de la fuente de datos vinculada a ella. La clave primaria facilita esta tarea.
La paginación del [DataGrid] se maneja de manera convencional:
Private Sub dgThèmes_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles dgThèmes.PageIndexChanged
' cambio de página
dgThèmes.CurrentPageIndex = e.NewPageIndex
' vinculación
liaisons()
End Sub
Las acciones en los enlaces [Plus d'informations] y [S'abonner] son gestionadas por el procedimiento [dgThèmes_ItemCommand]:
Private Sub dgThèmes_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles dgThèmes.ItemCommand
' evento en una línea del [datagrid]
Dim commande As String = e.CommandName
Select Case commande
Case "infos"
infos(dgThèmes.DataKeys(e.Item.ItemIndex))
Case "abonner"
abonner(dgThèmes.DataKeys(e.Item.ItemIndex))
End Select
' vinculación
liaisons()
End Sub
Se utiliza el hecho de que ambos enlaces tienen un atributo [CommandName] para diferenciarlos. Según el valor de este atributo, se llama al procedimiento [infos] o [abonner], pasando en ambos casos la clave «id» asociada al elemento de [DataGrid] donde ocurrió el evento. Con esta información, el procedimiento [info] mostrará la información del tema elegido por el usuario:
Private Sub infos(ByVal id As String)
' información sobre el tema de la clave id
' se recupera la línea de [datatable] correspondiente a la clave
Dim ligne As DataRow
ligne = dtThèmes.Rows.Find(id)
If Not ligne Is Nothing Then
' se muestra la información
lblThème.Text = CType(ligne("thème"), String)
lblDescription.Text = CType(ligne("description"), String)
panelInfo.Visible = True
End If
End Sub
Dado que la tabla [dtThèmes] tiene una clave primaria, el método [dtThèmes.Rows.Find("P")] permite encontrar la fila con la clave primaria P. Si se encuentra, se obtiene un objeto [DataRow]. En este caso, debemos buscar la fila con la clave primaria [id], donde se pasa [id] como parámetro. Si se encuentra la fila, se colocan los datos [thème] y [description] de esa fila en el panel de información, el cual luego se hace visible.
El procedimiento [abonner(id)] debe agregar el tema de clave [id] a la lista de suscripciones. Su código es el siguiente:
Private Sub abonner(ByVal id As String)
' suscripción al tema de la clave id
' se recupera la fila de [datatable] correspondiente a la clave
Dim ligne As DataRow
ligne = dtThèmes.Rows.Find(id)
If Not ligne Is Nothing Then
' se verifica si ya no se está suscrito
Dim abonnement As DataRow
abonnement = dtAbonnements.Rows.Find(id)
If Not abonnement Is Nothing Then
' se notifica el error
lblInfo.Text = "Vous êtes déjà abonné au thème [" + ligne("thème") + "]"
Else
' se agrega el tema a las suscripciones
abonnement = dtAbonnements.NewRow
abonnement("id") = id
abonnement("thème") = ligne("thème")
dtAbonnements.Rows.Add(abonnement)
' se establecen las conexiones
liaisons()
End If
End If
End Sub
En la lista de temas [dtThèmes], primero se busca la línea de clave [id]. Si se encuentra, se verifica que ese tema no esté ya presente en la lista de suscripciones para evitar registrarlo dos veces. Si es así, se muestra un mensaje de error. De lo contrario, se agrega una nueva suscripción a la tabla [dtAbonnements] y se establecen los enlaces de los componentes de la lista de datos con sus fuentes respectivas.
Cuando el usuario hace clic en un botón [Retirer], se debe eliminar un elemento de la tabla [dtAbonnements]. Esto se realiza mediante el siguiente procedimiento:
Private Sub dlAbonnements_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles dlAbonnements.ItemCommand
' se elimina una suscripción
Dim commande As String = e.CommandName
If commande = "retirer" Then
' se elimina la suscripción de [datatable]
With dtAbonnements.Rows
.Remove(.Find(dlAbonnements.DataKeys(e.Item.ItemIndex)))
End With
' vínculos
liaisons()
End If
End Sub
En primer lugar, se verifica el atributo [CommandName] del elemento que originó el evento. En realidad, esto es bastante innecesario, ya que el botón [Retirer] es el único control capaz de generar un evento en el componente [DataList]. Por lo tanto, no hay ambigüedad. Para eliminar una fila de un objeto [DataTable], se utiliza el método [DataList.Remove(DataRow)], que elimina de la tabla la fila de tipo [DataRow] pasada como parámetro. Esta línea se localiza mediante el método [DataList.Find], al que se le ha pasado la clave primaria de la línea buscada. Una vez eliminada la línea, se realiza la vinculación de los datos a los componentes
9.4. Gestionar un [DataList] paginado
Retomamos el ejemplo anterior para paginar el componente [DataList], que representa la lista de suscripciones del usuario. A diferencia del componente [DataGrid], el componente [DataList] no ofrece ninguna facilidad para la paginación. Veremos que su implementación es compleja, lo que nos hará apreciar en su justo valor la paginación automática del [DataGrid].
9.4.1. Funcionamiento
La única diferencia es la paginación del [DataList]. Las páginas tendrán dos suscripciones. Si el usuario tiene cinco suscripciones, habrá tres páginas. La primera será la siguiente:

La segunda página se obtiene a través del enlace [Suivant]:

La tercera página:

Cabe señalar que los enlaces [Précédent] y [Suivant] solo son visibles si hay, respectivamente, una página anterior y una página posterior a la página actual.
9.4.2. Código de presentación
Los enlaces [Précédent] y [Suivant] se obtienen al agregar una etiqueta <FooterTemplate> a [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 control
El archivo asociado [global.asax.vb] cambia de la siguiente manera:
...
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)
' inicio de sesión: se crea una tabla de suscripciones vacía
Dim dtAbonnements As New DataTable
With dtAbonnements
' las columnas
.Columns.Add("id", GetType(String))
.Columns.Add("thème", GetType(String))
' la clave primaria
.PrimaryKey = New DataColumn() {.Columns("id")}
End With
' la tabla se agrega a la sesión
Session.Item("abonnements") = dtAbonnements
' la página actual es la página 0
Session.Item("pAC") = 0
' el número de suscripciones en esta página es 0
Session.Item("nbAC") = 0
End Sub
Además de la tabla de suscripciones [dtAbonnements], se incluyen en la sesión otros dos datos:
del tipo [Integer]: es el número de la página actual que se mostró en la última consulta | |
de tipo [Integer]: número de líneas mostradas en la página anterior |
Al inicio de una sesión, tanto el número de la página actual como el número de líneas de esa página son cero.
El controlador [main.aspx.vb] evoluciona de la siguiente manera:
....
Public Class main
Inherits System.Web.UI.Page
....
' datos de la aplicación
Protected dtThèmes As DataTable
Protected dtAbonnements As DataTable
Protected dtPA As DataTable ' la page d'abonnements affichée
Protected Const nbAP As Integer = 2 ' nbre abonnements par page
Protected pAC As Integer ' page abonnement courant
Protected nbAC As Integer ' nombre d'abonnements dans page courante
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
End Sub
Private Sub terminer()
...
End Sub
Private Sub dgThèmes_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles dgThèmes.PageIndexChanged
...
End Sub
Private Sub dgThèmes_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles dgThèmes.ItemCommand
...
End Sub
Private Sub infos(ByVal id As String)
...
End Sub
Private Sub abonner(ByVal id As String)
...
End Sub
Private Sub dlAbonnements_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles dlAbonnements.ItemCommand
...
End Sub
Private Sub changePAC()
...
End Sub
Private Sub setLiens(ByVal ctl As Control, ByVal blPrec As Boolean, ByVal blSuivant As Boolean)
...
End Sub
End Class
Aquí se definen nuevos datos relacionados con la paginación de las suscripciones:
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
Parte de esta información se almacena en la sesión y se recupera en cada solicitud en el procedimiento [Page_Load]:
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' se recuperan las fuentes de datos
dtThèmes = CType(Application("thèmes"), DataTable)
dtAbonnements = CType(Session("abonnements"), DataTable)
' y la información de visualización de las suscripciones
pAC = CType(Session("pAC"), Integer)
nbAC = CType(Session("nbAC"), Integer)
' vinculación de datos
If Not IsPostBack Then
' visualización de una lista de suscripciones vacía
terminer()
End If
' se oculta cierta información
panelInfo.Visible = False
End Sub
La información recuperada de [pAC] y [nbAC] corresponde a la página de suscripciones que se mostró en la solicitud anterior:
es el número de la página actual que se mostró en la consulta anterior | |
número de filas mostradas en esta página actual |
El método [terminer] establece las conexiones entre los componentes y sus fuentes de datos, tal como lo hacía el método [liaisons] en la aplicación anterior. La novedad aquí es la vinculación del [DataList] con la tabla [dtPA], que es la página de suscripciones que se debe mostrar:
Private Sub terminer()
' se vincula la fuente de datos al componente [datagrid]
With dgThèmes
.DataSource = dtThèmes
.DataKeyField = "id"
End With
' se vincula la página de suscripciones al componente [datalist] teniendo en cuenta la página actual pAC
changePAC()
' se visualiza la página p
With dlAbonnements
.DataSource = dtPA
.DataKeyField = "id"
End With
' se asignan los datos a los componentes
Page.DataBind()
' gestión de los enlaces [précédent] y [suivant] del [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)
' se guarda la información de la página actual en la sesión
Session("pAC") = pAC
Session("nbAC") = dtPA.Rows.Count
End Sub
Cabe destacar lo siguiente:
- la fuente [dtPA] depende del número de página actual [pAC] que se va a mostrar. La variable [pAC] es una variable global de la clase, manipulada por los métodos que se encargan de modificar este número de página actual. El método [changePAC] se encarga de construir la tabla [dtPA], la cual se vinculará al componente [dlAbonnements].
- El método [setLiens] tiene la función de mostrar u ocultar los enlaces [Précédent] y [Suivant], dependiendo de si la página actual [pAC] que se muestra está precedida y seguida por otra página. Cuenta con cuatro parámetros:
- [dlAbonnements]: el control [DataList], cuyo árbol de controles exploraremos para encontrar los dos enlaces. De hecho, aunque estos dos enlaces se encuentran en un lugar específico, que es el pie de página del [DataList], no parece haber una forma sencilla de hacer referencia a ellos directamente. Al menos, aquí no se ha encontrado.
- [blPrecedent]: valor booleano que se debe asignar a la propiedad [visible] del enlace [Precedent]; es verdadero si la página actual es distinta de 0
- [blSuivant]: valor booleano que se debe asignar a la propiedad [visible] del enlace [Suivant]; es verdadero si la página actual no es la última página de la lista de suscripciones
- [nbLiensTrouvés]: un parámetro de salida que cuenta el número de enlaces encontrados. Tan pronto como este número sea igual a dos, el método finaliza.
- La información de [pAC] y [nbAC] se guarda en la sesión para la próxima consulta.
El método [changePAC] crea la tabla [dtPA], que se vinculará al componente [dlAbonnements]. Lo hace en función del número [pAC] de la página actual que se va a mostrar. La tabla [dtPA] debe mostrar ciertas filas de la tabla de suscripciones [dtAbonnements]. Cabe recordar que esta tabla se almacena en la sesión y se actualiza (aumentando o disminuyendo) a medida que se realizan las consultas. Comenzamos por establecer el intervalo [premier,dernier] de los números de línea de la tabla [dtAbonnements] que la tabla [dtPA] debe mostrar:
Private Sub changePAC()
' establece la página pAC como la página actual de las suscripciones
' gestión de las páginas de [datalist]
Dim nbAbonnements = dtAbonnements.Rows.Count
Dim dernièrePage = (nbAbonnements - 1) \ nbAP
' primera y última suscripción
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
Una vez hecho esto, podemos crear la tabla [dtPA]. Primero se define su estructura [id,thème] y luego se llena copiando en ella las filas de [dtAbonnements] cuyo número se encuentra dentro del intervalo [premier,dernier] calculado anteriormente.
' creación de la tabla de datos dtpa
dtPA = New DataTable
With dtPA
' las columnas
.Columns.Add("id", GetType(String))
.Columns.Add("thème", GetType(String))
' la clave primaria
.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
Al final del método [changePAC], se ha creado la tabla [dtPA], la cual puede vincularse al componente [DataList]. Esto se realiza en el método [terminer]. En este mismo método, se utiliza el procedimiento [setLiens] para establecer el estado de los enlaces [Précédent] y [Suivant] del [DataList]. El código de este procedimiento es el siguiente:
Private Sub setLiens(ByVal ctl As Control, ByVal blPrec As Boolean, ByVal blSuivant As Boolean, ByRef nbLiensTrouvés As Integer)
' se buscan los enlaces [précédent] y [suivant]
' en el árbol de controles de [datalist]
' ¿Se han encontrado todos los enlaces?
If nbLiensTrouvés = 2 Then Exit Sub
' revisión de los controles secundarios
Dim c As Control
For Each c In ctl.Controls
' primero se trabaja en profundidad: los enlaces se encuentran en la parte inferior del árbol
setLiens(c, blPrec, blSuivant, nbLiensTrouvés)
' ¿Enlace [Précédent]?
If c.ID = "lnkPrecedent" Then
CType(c, LinkButton).Visible = blPrec
nbLiensTrouvés += 1
End If
' ¿Enlace [Suivant]?
If c.ID = "lnkSuivant" Then
CType(c, LinkButton).Visible = blSuivant
nbLiensTrouvés += 1
End If
Next
End Sub
El procedimiento es recursivo. Primero busca, entre los controles hijos del componente [dlAbonnements] los componentes denominados [lnkPrecedent] y [lnkSuivant], que son las identidades (atributo ID) de los dos enlaces de paginación. Los busca primero partiendo de la base del árbol de controles, ya que es ahí donde se encuentran. Tan pronto como se encuentra un enlace, se incrementa el contador [nbLiensTrouvés] y se asigna a la propiedad [visible] del enlace un valor pasado como parámetro del procedimiento. Una vez que se han encontrado los dos enlaces, ya no se sigue explorando el árbol de controles y el procedimiento recursivo finaliza.
Ya mencionamos que el método [changePAC], que establece la fuente de datos [dtPA] para el componente [dlAbonnements], trabajaba con el número [pAC] de la página actual que se va a mostrar. Varios procedimientos modifican este número:
Private Sub abonner(ByVal id As String)
' suscripción al tema con la clave id
..
' se agrega el tema a las suscripciones
..
' actualización del número de la página actual: ahora es la última página
pAC = (dtAbonnements.Rows.Count - 1) \ nbAP
' vinculaciones de datos
terminer()
End If
End Sub
Después de agregar una suscripción, esta aparece al final de la lista de suscripciones. Por lo tanto, se pasa a la última página de las suscripciones para que el usuario vea la suscripción que se ha agregado.
Private Sub dlAbonnements_ItemCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataListCommandEventArgs) Handles dlAbonnements.ItemCommand
' se elimina una suscripción
Dim commande As String = e.CommandName
Select Case commande
Case "retirer"
' se elimina la suscripción de [datatable]
With dtAbonnements.Rows
.Remove(.Find(dlAbonnements.DataKeys(e.Item.ItemIndex)))
End With
' ¿Hay que cambiar la página actual?
nbAC -= 1
If nbAC = 0 Then pAC -= 1
Case "precedent"
' cambio de página actual
pAC -= 1
Case "suivant"
' cambio de página actual
pAC += 1
End Select
' vínculos de datos
terminer()
End Sub
- [nbAC] es el número de líneas que se muestran en la página actual antes de cancelar una suscripción. Si el nuevo número de líneas de la página es igual a 0, el número de página actual [pAC] se reduce en una unidad.
- Al hacer clic en el enlace [Precedent], el número de página actual [pAC] se reduce en una unidad.
- Al hacer clic en el enlace [Suivant], el número de página actual [pAC] se incrementa en una unidad.
Los demás procedimientos siguen siendo los mismos que antes.
9.4.4. Conclusión
En este ejemplo hemos demostrado que podemos paginar un componente [DataList]. Esta paginación es delicada y es preferible recurrir a la paginación automática del componente [DataGrid] siempre que sea posible. Este ejemplo también nos ha mostrado cómo acceder a los componentes presentes en el pie de página del componente [DataList].
9.5. Clase de acceso a una base de productos
Volvemos a centrarnos en la base de datos ACCESS [produits] que ya hemos utilizado. Recordemos que cuenta con una única tabla llamada [liste], cuya estructura es la siguiente:
![]() | ![]() |
Vamos a crear una clase de acceso a la tabla [liste] que permita leerla y actualizarla. También crearemos un cliente de consola que utilizará la clase anterior para actualizar la tabla. En una segunda etapa, crearemos un cliente web para realizar la misma tarea.
9.5.1. La clase ExceptionProduits
La clase [Exception] tiene un constructor que admite un mensaje de error como parámetro. En este caso, queremos contar con una clase de excepción que tenga un constructor que admita una lista de mensajes de error en lugar de un solo mensaje de error. Esa será la clase [ExceptionProduits] que se muestra a continuación:
Public Class ExceptionProduits
Inherits Exception
' Mensajes de error relacionados con la excepción
Private _erreurs As ArrayList
' constructor
Public Sub New(ByVal erreurs As ArrayList)
Me._erreurs = erreurs
End Sub
' propiedad
Public ReadOnly Property erreurs() As ArrayList
Get
Return _erreurs
End Get
End Property
End Class
9.5.2. La estructura [sProduit]
La estructura [produit] representará un producto [id, nom, prix]:
' estructura sProduit
Public Structure sProduit
' los campos
Private _id As Integer
Private _nom As String
Private _prix As Double
' propiedad id
Public Property id() As Integer
Get
Return _id
End Get
Set(ByVal Value As Integer)
_id = Value
End Set
End Property
' propiedad nombre
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
' propiedad precio
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
La estructura solo admite datos válidos para los campos [nom] y [prix].
9.5.3. La clase Productos
La clase [Produits] es la clase que nos permitirá actualizar la tabla [liste] de la base de datos de productos. Su estructura es la siguiente:
Public Class produits
' datos de instancia
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
Los datos de instancia
Los datos compartidos por los distintos métodos de la clase son los siguientes:
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
la conexión a la base de datos se abrirá para ejecutar un comando SQL y se cerrará inmediatamente después | |
Consulta SQL [select] que recupera toda la tabla [liste] | |
consulta que permite insertar una fila (nombre, precio) en la tabla [liste]. Cabe señalar que no se especifica el campo [id]. De hecho, este campo es autoincrementado por el SGBD y, por lo tanto, no es necesario especificarlo. | |
Consulta que permite actualizar los campos (nombre, precio) de la fila de la tabla [liste] que tiene la clave [id] | |
consulta que permite eliminar la fila de la tabla [liste] con la clave [id] | |
objeto [OleDbCommand] que ejecuta la consulta [selectText] en la conexión [connexion] | |
objeto [OleDbCommand] ejecutando la consulta [updateText] en la conexión [connexion] | |
objeto [OleDbCommand] ejecutando la consulta [insertText] en la conexión [connexion] | |
objeto [OleDbCommand] ejecutando la consulta [deleteText] en la conexión [connexion] | |
objeto que permite recuperar el resultado de la ejecución de [selectCommand] en un objeto [DataSet] |
El constructor
El constructor recibe un único parámetro, [chaineConnexionOLEDB], que es la cadena de conexión que designa la base de datos a consultar. A partir de esta, se preparan los cuatro comandos de consulta y actualización de la tabla, así como el adaptador. Se trata únicamente de una preparación y no se establece ninguna conexión.
Public Sub New(ByVal chaineConnexionOLEDB As String)
' se prepara la conexión
connexion = New OleDbConnection(chaineConnexionOLEDB)
' se preparan los comandos de consulta
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
' se está preparando el adaptador de acceso a datos
adaptateur.SelectCommand = selectCommand
End Sub
El método getProduits
Este método permite obtener el contenido de la tabla [Liste] en un objeto [DataTable]. Su código es el siguiente:
Public Function getProduits() As DataTable
' se coloca la tabla [liste] en una [dataset]
Dim contenu As New DataSet
' se crea un objeto DataAdapter para leer los datos de la fuente 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
' se devuelve el resultado
Return contenu.Tables(0)
End Function
El trabajo se realiza mediante las dos instrucciones siguientes:
With adaptateur
.FillSchema(contenu, SchemaType.Source)
.Fill(contenu)
End With
El método [FillSchema] define la estructura (columnas, restricciones, relaciones) de [DataSet] y contenu a partir de la estructura de la base de datos a la que hace referencia [adaptateur.Connexion]. Esto nos permite recuperar la estructura de la tabla [liste] y, en particular, su clave primaria. La siguiente operación [Fill] llena las tablas [Dataset] y contenu con las filas de la tabla [liste]. Con esta única operación, habríamos obtenido los datos y la estructura, pero no la clave primaria. Sin embargo, esta nos será útil para actualizar la tabla [liste] en memoria. Aquí, al igual que en los otros métodos, manejamos los posibles errores utilizando la clase [ExceptionProduits] para obtener una lista (ArrayList) de errores en lugar de un solo error. El método [getProduits] devuelve la tabla [liste] en forma de un objeto [DataTable].
El método ajouterProduits
Este método permite agregar una fila (id, nombre, precio) a la tabla [liste]. Esta información se le proporciona en forma de una estructura [sProduit] compuesta por campos [id, nom, prix]. El código del método es el siguiente:
Public Sub ajouterProduit(ByVal produit As sProduit)
' se agrega un producto [nom,prix]
' se preparan los parámetros de la adición
With insertCommand.Parameters
.Clear()
.Add(New OleDbParameter("nom", produit.nom))
.Add(New OleDbParameter("prix", produit.prix))
End With
' se realiza la adición
Try
' inicio de sesión
connexion.Open()
' ejecución de la orden
insertCommand.ExecuteNonQuery()
Catch ex As Exception
' problema
Dim erreursCommande As New ArrayList
erreursCommande.Add(String.Format("Erreur lors de l'ajout : {0}", ex.Message))
Throw New ExceptionProduits(erreursCommande)
Finally
' cierre de la conexión
connexion.Close()
End Try
End Sub
Los campos de la estructura [produit] se insertan en los parámetros del comando [insertCommand]. Recordemos la configuración actual de este comando (véase el fabricante):
Private Const insertText As String = "insert into liste(nom,prix) values(?,?)"
insertCommand.Connexion=connexion
¿El texto del comando SQL [insert] contiene parámetros formales que deben sustituirse por parámetros efectivos? Esto se realiza mediante la colección [Parameters] de la clase [OleDbCommand]. Esta contiene elementos de tipo [OleDbParameter] que definen los parámetros efectivos que deben sustituir a los parámetros formales ?. Dado que estos últimos no tienen nombre, se utiliza el índice de los parámetros efectivos para determinar a qué parámetro formal corresponde cada parámetro efectivo. En este caso, el parámetro efectivo n.º i de la colección [Parameters] reemplazará al parámetro formal ? n.º i. Para crear un parámetro efectivo de tipo [OleDbParameter], aquí se utiliza el constructor [OleDbParameter (Byval nom as String, Byval valeur as Object)], que define el nombre y el valor del parámetro efectivo. El nombre puede ser cualquiera. Además, en este caso no se utilizará. Los dos parámetros de la instrucción SQL [insert] reciben como valores los de los campos [nom, prix] de la estructura [produit]. Una vez hecho esto, la inserción se realiza mediante la instrucción [insertCommand.ExecuteNonQuery].
El método modifierProduits
Este método permite modificar la fila de la tabla [liste]. La información necesaria para ello se proporciona en la estructura [sProduit] de los campos [id, nom, prix].
Public Sub modifierProduit(ByVal produit As sProduit)
' se modifica un producto [id,nom,prix]
' se preparan los parámetros de la actualización
With updateCommand.Parameters
.Clear()
.Add(New OleDbParameter("nom", produit.nom))
.Add(New OleDbParameter("prix", produit.prix))
.Add(New OleDbParameter("id", produit.id))
End With
' se realiza la modificación
Try
' inicio de sesión
connexion.Open()
' ejecución del comando
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
' problema
Dim erreursCommande As New ArrayList
erreursCommande.Add(String.Format("Erreur lors de la modification : {0}", ex.Message))
Throw New ExceptionProduits(erreursCommande)
Finally
' cierre de la conexión
connexion.Close()
End Try
End Sub
El código es casi idéntico al del método [ajouterProduits], salvo que el comando [OleDbCommand] en cuestión es [updateCommand] en lugar de [insertCommand].
El método supprimerProduits
Este método permite eliminar la fila de la tabla [liste] con la clave [id] pasada como parámetro. El código es el siguiente:
Public Sub supprimerProduit(ByVal id As Integer)
' elimina el producto de la clave [id]
' se preparan los parámetros para la eliminación
With deleteCommand.Parameters
.Clear()
.Add(New OleDbParameter("id", id))
End With
' se lleva a cabo la eliminación
Try
' inicio de sesión
connexion.Open()
' ejecución del comando
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
' problema
Dim erreursCommande As New ArrayList
erreursCommande.Add(String.Format("Erreur lors de la suppression : {0}", ex.Message))
Throw New ExceptionProduits(erreursCommande)
Finally
' cierre de la conexión
connexion.Close()
End Try
End Sub
Se sigue el mismo procedimiento que en los métodos anteriores.
9.5.4. Pruebas de la clase [produits]
Un programa de pruebas [testproduits.vb] de tipo consola podría ser el siguiente:
Option Explicit On
Option Strict On
' espacios de nombres
Imports System
Imports System.Data
Imports Microsoft.VisualBasic
Imports System.Collections
Namespace st.istia.univangers.fr
' página de prueba
Module testproduits
Dim contenu As DataTable
Sub Main(ByVal arguments() As String)
' muestra el contenido de una tabla de productos
' la tabla se encuentra en una base de datos ACCESS, cuya página recibe el nombre del archivo
Const syntaxe1 As String = "pg bdACCESS"
' verificación de los parámetros del programa
If arguments.Length <> 1 Then
' mensaje de error
Console.Error.WriteLine(syntaxe1)
' fin
Environment.Exit(1)
End If
' se prepara la cadena de conexión
Dim chaineConnexion As String = "Provider=Microsoft.Jet.OLEDB.4.0; Ole DB Services=-4; Data Source=" + arguments(0)
' creación de un objeto de productos
Dim objProduits As produits = New produits(chaineConnexion)
' se muestran todos los productos
afficheProduits(objProduits)
' se inserta un producto
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
' se muestran todos los productos
afficheProduits(objProduits)
' se recupera el ID del producto agregado
produit.id = CType(contenu.Rows(contenu.Rows.Count - 1)("id"), Integer)
' se modifica el producto agregado
produit.prix = 200
Try
objProduits.modifierProduit(produit)
Catch ex As ExceptionProduits
afficheErreurs(ex.erreurs)
End Try
' se muestran todos los productos
afficheProduits(objProduits)
' se elimina el producto agregado
Try
objProduits.supprimerProduit(produit.id)
Catch ex As ExceptionProduits
afficheErreurs(ex.erreurs)
End Try
' se muestran todos los productos
afficheProduits(objProduits)
End Sub
Sub afficheProduits(ByRef objProduits As produits)
' se recupera la tabla de productos en una tabla de datos
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
' fila i de la tabla
Console.Out.WriteLine(lignes(i).Item("id").ToString + "," + lignes(i).Item("nom").ToString + _
"," + lignes(i).Item("prix").ToString)
Next
' detiene el flujo de la consola
Console.WriteLine("...")
Console.ReadLine()
End Sub
Sub afficheErreurs(ByRef erreurs As ArrayList)
' muestra los errores en la consola
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
' detiene el flujo de la consola
Console.WriteLine("...")
Console.ReadLine()
End Sub
End Module
End Namespace
Compilamos los dos archivos fuente:
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
luego probamos:
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
...
Se invita al lector a comparar la salida en pantalla anterior con el código del programa de prueba.
9.6. Aplicación web para actualizar la tabla de productos en caché
9.6.1. Introducción
Ahora vamos a desarrollar una aplicación web para actualizar la tabla de productos (agregar, eliminar, modificar). La tabla actualizada se mantendrá en memoria en un objeto [DataTable] y será compartida por todos los usuarios. Queremos destacar dos puntos:
- la gestión de un objeto [DataTable]
- los problemas de actualización simultánea de una tabla por parte de varios usuarios.
La arquitectura MVC de la aplicación será la siguiente:
![]() |
9.6.2. Funcionamiento y vistas
La vista de inicio de la aplicación es la siguiente:

Esta vista, denominada [formulaire], permite al usuario aplicar un filtro a los productos y establecer el número de productos por página que desea ver.
N.º | nombre | tipo | función |
LinkButton | muestra la vista [Formulaire], que sirve para establecer la condición de filtrado | ||
LinkButton | muestra la vista [Produits], que sirve para visualizar y actualizar la tabla de productos (modificación y eliminación) | ||
LinkButton | muestra la vista [Ajout], que sirve para agregar un producto | ||
vueFormulaire | la vista [Formulaire] | ||
TextBox | la condición de filtrado | ||
TextBox | número de productos por página | ||
RequiredFieldValidator | verifica si hay un valor en [txtPages] | ||
RangeValidator | verifica que txtPages esté dentro del intervalo [3,10] | ||
botón [submit] que muestra la vista [produits] filtrada por la condición (5) | |||
Etiqueta | Texto informativo en caso de errores |
Por ejemplo, si la vista [formulaire] se completa de la siguiente manera:

se obtiene el siguiente resultado:
![]() |
n.º | nombre | tipo | función |
panel | |||
RadioButton | permite al usuario establecer el orden de clasificación deseado al hacer clic en el título de una de las columnas [nom], [prix]. Ambos botones forman parte del grupo [rdTri]. | ||
DataGrid | cuadrícula de visualización de una vista filtrada de la tabla de productos. El filtro es el establecido por la vista [formulaire]. También tenemos .AllowPaging=true, .AllowSorting=true | ||
DataGrid | muestra la tabla completa de productos; permite dar seguimiento a las actualizaciones | ||
Etiqueta | texto informativo, especialmente en caso de errores | ||
DataGrid | mostrará los productos eliminados en la tabla de productos | ||
DataGrid | mostrará los productos modificados en la tabla de productos | ||
DataGrid | mostrará los productos agregados en la tabla de productos |
Hay cinco contenedores de datos en esta página. Todos muestran la misma tabla [dtProduits] a través de una vista [DataView] diferente. Una vista representa un subconjunto de las filas de la tabla de origen de la vista. Este subconjunto se crea a partir de las propiedades [RowFilter] y [RowStateFilter] de la clase [DataView]:
- [RowFilter] permite aplicar un filtro a las filas, como por ejemplo el anterior [prix>30]. Este tipo de filtrado será utilizado por [DataGrid1].
- [RowStateFilter] permite establecer un filtro según el estado de la fila de la tabla. Este indica el estado de la fila en relación con el estado original que tenía cuando se creó la vista de la tabla. En este caso, la tabla [dtProduits] proviene de una base de datos. Inicialmente, todas sus filas tendrán un estado igual a [Original] para indicar que se trata de las filas originales de la tabla. Este estado puede cambiar posteriormente y tomar diferentes valores, entre los que se incluyen los siguientes:
- [Added]: la fila se ha agregado; no formaba parte de la tabla original
- [Deleted]: la fila se ha eliminado; aún se encuentra en la tabla, pero está «marcada» como «por eliminar»
- [Modified]: la fila se ha modificado
[RowStateFilter] permite mostrar las filas de la tabla que tienen un estado determinado:
- (continuación)
- [DataViewRowState.Added]: solo se muestran las filas agregadas. Se muestran con sus valores actuales.
- [DataViewRowState.ModifiedOriginal]: solo se muestran las filas modificadas. Se muestran con sus valores originales.
- [DataViewRowState.ModifiedCurrent]: solo se muestran las filas modificadas. Se muestran con sus valores actuales.
- [DataViewRowState.Deleted]: solo se muestran las filas eliminadas. Se muestran con sus valores originales.
- [DataViewRowState.CurrentRows]: se muestran las líneas no eliminadas. Se muestran con sus valores actuales.
De este modo, el filtro [RowStateFilter] tendrá los siguientes valores:
sin filtro sobre el estado de las líneas | ||
DataViewRowState.CurrentRows | muestra el estado actual de la tabla de productos | |
DataViewRowState.Deleted | muestra las filas eliminadas de la tabla de productos | |
DataViewRowState.ModifiedOriginal | muestra las filas modificadas de la tabla de productos con sus valores iniciales | |
DataViewRowState.Added | muestra las filas agregadas a la tabla de productos inicial |
Los contenedores [DataGrid1-5] nos permitirán dar seguimiento a la actualización de la tabla [dtProduits]. El componente [DataGrid1] permite modificar y eliminar un producto. Veremos cómo la configuración del componente permite esta actualización. Además, está paginado y ordenado. Estos dos puntos ya se han analizado en un ejemplo anterior.
El enlace [Ajout] da acceso a un formulario para agregar un producto:
![]() |
n.º | nombre | tipo | función |
panel | |||
TextBox | nombre del producto | ||
RequiredFieldValidator | verifica si hay un valor en [txtNom] | ||
TextBox | precio del producto | ||
RequiredFieldValidator | verifica si hay un valor en [txtPrix] | ||
CompareValidator | verifica que precio >= 0 | ||
Botón | botón [submit] para agregar el producto | ||
Etiqueta | Texto informativo sobre el resultado de la operación de agregar |
Es posible que la base de datos [produits] no esté disponible al iniciar la aplicación. En ese caso, se muestra al usuario la vista [erreurs]:
![]() |
N.º | nombre | tipo | función |
panel | |||
Repetidor | lista de errores |
9.6.3. Configuración de los contenedores de datos
Los cinco contenedores [DataGrid] se han configurado en [WebMatrix]. Se les ha aplicado formato (colores y bordes) mediante el enlace [Configuration automatique] de su panel de propiedades. El contenedor [DataGrid1] se ha configurado mediante el enlace [Générateur de propriétés] de ese mismo panel. Se ha habilitado la ordenación (pestaña [Général]):

Las columnas no se generan automáticamente, a diferencia de los otros cuatro contenedores. Se definieron manualmente mediante el asistente:

En primer lugar, se crearon dos columnas de tipo [Colonne connexe] denominadas [nom] y [prix]. Se asociaron, respectivamente, a los campos [nom] y [prix] de la fuente de datos que mostrarán los contenedores. A continuación se muestra, por ejemplo, la configuración de la columna [nom]:

La expresión de ordenación es la que deberá colocarse detrás de la cláusula [order by] de la instrucción SQL [select], la cual se ejecutará cuando elusuario haga clic en el nombre de la columna de encabezado [nom] asociada al campo [nom] del [DataGrid]. Aquí hemos puesto [nom], de modo que la cláusula de ordenación será [order by nom]. Veremos que modificaremos esta para que sea [order by nom asc] o [order by nom desc], según el orden de clasificación elegido por el usuario.
Además, hemos creado dos columnas de botones:

La columna [Modifier, Mettre à jour, Annuler] nos permitirá modificar un producto y la columna [Supprimer], eliminarlo. Cada una de estas columnas se puede configurar. La columna [Modifier, Mettre à jour, Annuler] ofrece la siguiente configuración:

Se observa que se pueden modificar los textos de los botones. En cuanto a los botones, se puede elegir entre enlaces y botones (lista desplegable arriba). En este caso, se han elegido los enlaces. La configuración de la columna [Supprimer] es similar. Además de este asistente de configuración, utilizamos directamente la ventana de propiedades de [DataGrid] para completar la propiedad [DataKeyField], que indica con qué campo de la fuente de datos se indexan las filas de [DataGrid]. En este caso, se utiliza la clave primaria de la tabla de productos:

Al final, esta configuración genera el siguiente código de presentación:
<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 siempre, una vez que se ha adquirido cierta experiencia, se puede escribir todo o parte del código anterior directamente.
Los demás contenedores [DataGrid] tienen la configuración predeterminada obtenida mediante la generación automática de las columnas del [DataGrid] a partir de las de la fuente de datos a la que está asociado.
El contenedor [Repeater] sirve para mostrar una lista de errores. Su configuración se realiza directamente en el código de presentación:
<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 línea del componente muestra el valor [Container.DataItem], c.a.d. El valor correspondiente de la lista de datos. Esta será del tipo [ArrayList] y representará una lista de errores.
9.6.4. El código de presentación de la aplicación
Este se encuentra en el archivo [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>
Cabe destacar los siguientes puntos:
- la página está compuesta por cuatro contenedores (panel) [vueFormulaire, vueProduits, vueAjout, vueErreurs] que formarán las cuatro vistas de la aplicación.
- Los botones o enlaces de tipo [submit] tienen la propiedad [CausesValidation=false]. La propiedad [causesValidation=true] provoca la ejecución de todos los controles de validación de la página. Sin embargo, en este caso, no es necesario realizar todos los controles de validez al mismo tiempo. Por ejemplo, al agregar un elemento, no queremos que se ejecuten los controles relacionados con el número de líneas por página. Por lo tanto, especificaremos nosotros mismos qué controles de validez deben realizarse.
9.6.5. El código de control [global.asax]
El controlador [global.asax] es el siguiente:
El código asociado [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)
' se recupera la información de configuración
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
' ¿Hay errores de configuración?
If erreurs.Count <> 0 Then
' se anotan los errores
Application("erreurs") = erreurs
' salimos
Exit Sub
End If
' aquí no hay errores de configuración
' se crea un objeto de productos
Dim dtProduits As DataTable
Try
dtProduits = New produits(chaînedeConnexion).getProduits
Catch ex As ExceptionProduits
'se produjo un error al acceder a los productos; se registra en la aplicación
Application("erreurs") = ex.erreurs
Exit Sub
Catch ex As Exception
' error no manejado
erreurs.Add(ex.Message)
Application("erreurs") = erreurs
' salida de la subrutina
End Try
': aquí no hay errores de inicialización
' se guarda el número de productos por página
Application("defaultProduitsPage") = defaultProduitsPage
' se guarda la tabla de productos
Application("dtProduits") = dtProduits
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' inicialización de las variables de sesión
If IsNothing(Application("erreurs")) Then
' vista de la tabla de productos
Session("dvProduits") = CType(Application("dtProduits"), DataTable).DefaultView
' número de productos por página
Session("nbProduitsPage") = Application("defaultProduitsPage")
' página actual mostrada
Session("pageCourante") = 0
End If
End Sub
End Class
En [Application_Start], primero se recuperan dos datos del archivo de configuración [web.config] de la aplicación:
- OLEDBStringConnection: la cadena OLEDB de conexión a la base de datos de productos
- defaultProduitsPage: el número predeterminado de productos por página mostrada
<?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>
Si falta alguno de estos dos datos, se genera una lista de errores y se incluye en la aplicación. Lo mismo ocurre si el parámetro [defaultProduitsPage] existe pero es incorrecto. Si los dos parámetros esperados están presentes y son correctos, se crea una tabla [dtProduits] y se incorpora a la aplicación. Es esta tabla la que será utilizada y actualizada por los distintos clientes. La base de datos, por su parte, permanecerá sin cambios. Nos ocuparemos de su actualización en una próxima aplicación. Esta tabla se crea a partir de una instancia de la clase [produits], que ya analizamos anteriormente, y del método [getProduits] de dicha clase. La obtención de la tabla [dtProduits] puede fallar. En ese caso, se sabe que la clase [produits] lanza una excepción de tipo [ExceptionProduits]. Aquí se intercepta dicha excepción y la lista de errores asociada se coloca en la aplicación vinculada a la clave [erreurs]. La presencia de esta clave en la información registrada en la aplicación se verificará en cada procesamiento de solicitud. Si se encuentra, se enviará la vista [erreurs] al cliente.
Si bien la tabla [dtProduits] es compartida por todos los clientes web, cada uno de ellos tendrá, no obstante, su propia vista [dvProduits] sobre ella. De hecho, cada cliente web tiene la posibilidad de establecer un filtro en la tabla [dtProduits], así como un orden de clasificación. Esta información específica de cada cliente web se almacena en la vista del cliente. Por ello, esta se crea en [Session_Start] para ser colocada en la sesión específica de cada cliente. Se utiliza el atributo [DefaultView] de la tabla [dtProduits] para obtener una vista predeterminada de la tabla. Inicialmente, no hay ni filtro ni orden de clasificación. Además, también se incluyen dos datos en la sesión:
- el número de productos por página de la clave [nbProduitsPage]. Al inicio de la sesión, este número es igual al número por defecto definido en el archivo de configuración.
- el número de la página actual de productos. Al inicio, es la primera página.
9.6.6. El código de control [main.aspx.vb]
El esqueleto del controlador es el siguiente:
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
' componentes de la página
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
' Datos de la página
Protected dtProduits As DataTable
Protected dvProduits As DataView
Protected defaultProduitsPage As Integer
Protected erreur As Boolean = False
' carga de la página
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. Datos de instancia
La clase [main] utiliza los siguientes datos de instancia:
Public Class main
Inherits System.Web.UI.Page
' componentes de la página
Protected WithEvents txtPages As System.Web.UI.WebControls.TextBox
...
' datos de la página
Protected dtProduits As DataTable
Protected dvProduits As DataView
Protected defaultProduitsPage As Integer
la tabla [DataTable] de productos —común a todos los clientes— | |
la vista [DataView] de productos —específica para cada cliente | |
Número de productos por página propuesto por defecto |
9.6.8. El procedimiento [Page_Load] para cargar la página
El código del procedimiento [page_Load] es el siguiente:
' carga de la página
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' se verifica si la aplicación presenta un error
If Not IsNothing(Application("erreurs")) Then
' la aplicación no se ha inicializado correctamente
afficheErreurs(CType(Application("erreurs"), ArrayList))
Exit Sub
End If
' se recupera una referencia de la tabla de productos
dtProduits = CType(Application("dtProduits"), DataTable)
' se recupera la vista de los productos
dvProduits = CType(Session("dvProduits"), DataView)
' se recupera el número de productos por página
defaultProduitsPage = CType(Application("defaultProduitsPage"), Integer)
' se cancela cualquier actualización que esté en curso
DataGrid1.EditItemIndex = -1
'Primera consulta
If Not IsPostBack Then
' se muestra el formulario inicial
txtPages.Text = defaultProduitsPage.ToString
afficheFormulaire()
End If
End Sub
- En primer lugar, se verifica si la tabla de productos se pudo cargar al iniciar la aplicación. Si no es así, se muestra la vista [erreurs] con los mensajes de error correspondientes y luego se sale del procedimiento después de establecer el valor booleano [erreur] en verdadero. Este indicador será verificado por ciertos procedimientos.
- Se recupera la tabla de productos [dtProduits] de la aplicación y se asigna a la variable de instancia [dtProduits] para que sea compartida por todos los métodos de la página.
- Se hace lo mismo con la vista [dvProduits], que se recupera de la sesión, y con el número predeterminado de productos por página, que se recupera de la aplicación.
- Si es la primera solicitud del cliente, se le presenta el formulario para definir las condiciones de filtrado y paginación con una paginación por defecto de [defaultProduitsPage] productos por página.
- Se cancela el modo de edición del componente [dataGrid1]. Este componente tiene un atributo [EditItemIndex] que es el índice del elemento que se está modificando. Si [EditItemIndex]=-1, entonces no hay ningún elemento en proceso de modificación. Si [EditItemIndex] = i, entonces el elemento n.º i del componente [DataGrid] se está editando. En ese caso, se presenta de manera diferente a los demás elementos del [dataGrid]:

En la imagen anterior, el elemento [Produit8, 80] se encuentra en el modo [édition]. Como se ve arriba, el usuario puede confirmar su actualización mediante el enlace [Mettre à jour], o cancelarla mediante el enlace [Annuler]. También puede utilizar otros enlaces que no tengan nada que ver con el elemento que se está editando. Al incluir [DataGrid1.EditItemIndex=-1] cada vez que se carga la página, nos aseguramos de cancelar sistemáticamente el modo de edición de [DataGrid1]. Solo se le asignará un valor diferente si se ha hecho clic en un enlace [Modifier]. Esto se hará en el procedimiento que maneja este evento. Se darán las siguientes situaciones:
- se ha hecho clic en el enlace [Modifier] del elemento n.º 8 de [DataGrid1]. [Page_Load] primero asigna -1 a [DataGrid1.EditItemIndex]. Luego, el procedimiento que procesa el evento [Modifier] asignará un 8 a [DataGrid1.EditItemIndex]. Finalmente, cuando la página se envíe de vuelta al cliente, el elemento n.º 8 estará efectivamente en el modo [édition] y este aparecerá tal como se muestra arriba.
- El usuario modifica el producto que se encuentra en el modo [édition] y confirma su modificación mediante el enlace [Mettre à jour]. [Page_Load] asigna el valor -1 a [DataGrid1.EditItemIndex]. Luego, se ejecutará el procedimiento que procesa el evento [Mettre à jour] y se actualizará el elemento n.º 8 de [dtProduits]. Cuando la página se envíe de vuelta al cliente, ningún elemento de [DataGrid1] estará en modo de edición (DataGrid.EditItemIndex=-1).
- Si un usuario ha iniciado la actualización de un producto, lo lógico sería que cancelara dicha actualización mediante [Annuler]. Sin embargo, nada le impide hacer clic en otro enlace. Por lo tanto, debemos prever este caso. En este caso, al igual que en los anteriores, [Page_Load] comienza por asignar el valor -1 a [DataGrid1.EditItemIndex]; luego, se ejecutará el procedimiento que maneja el evento que ocurrió. No modificará la propiedad [DataGrid1.EditItemIndex], que por lo tanto seguirá teniendo el valor -1. Cuando la página se envíe de vuelta al cliente, ningún elemento de [DataGrid1] estará en modo de edición.
Se observa que al establecer [EditItemIndex] en -1 al cargar la página, se evita tener que preocuparse por si el usuario estaba o no en modo de actualización cuando hizo clic en un enlace.
9.6.9. Visualización de las vistas [erreurs], [formulaire] y [ajout]
Se solicita la visualización de la vista [erreurs] al cargar la página [Page_Load] si se detecta que la aplicación no se ha inicializado correctamente. Esto se logra asociando el componente de datos [rptErreurs] a la lista de errores pasada como parámetros y haciendo visible el contenedor correspondiente. Por último, los tres enlaces [Filtre, Mise à jour, Ajout] se ocultan, ya que, en caso de error, la aplicación no puede utilizarse.
Private Sub afficheErreurs(ByVal erreurs As ArrayList)
' se asocia la lista de errores al repetidor rptErreurs
With rptErreurs
.DataSource = erreurs
.DataBind()
End With
'se desactivan las opciones
lnkAjout.Visible = False
lnkMisajour.Visible = False
lnkFiltre.Visible = False
' se muestra la vista [erreurs]
vueErreurs.Visible = True
vueFormulaire.Visible = False
vueProduits.Visible = False
vueAjout.Visible = False
End Sub
Las demás vistas se solicitan a partir de las opciones que se le ofrecen al usuario:

La visualización de la vista [Ajout] se solicita cuando el usuario hace clic en el enlace [Ajout] de la página:
Private Sub lnkAjout_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkAjout.Click
' se muestra la vista [ajout]
afficheAjout()
End Sub
Private Sub afficheAjout()
' muestra la vista de agregación
vueAjout.Visible = True
vueFormulaire.Visible = False
vueProduits.Visible = False
vueErreurs.Visible = False
End Sub
Se solicita la visualización de la vista [Formulaire] cuando el usuario hace clic en el enlace [Filtrage] de la página. En ese momento, se debe mostrar la vista que permite definir
- el filtro de la tabla de productos
- el número de productos que se muestran en cada página web
Se muestra un formulario que incluye los valores almacenados en la sesión:
Private Sub lnkFiltre_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkFiltre.Click
' definir las condiciones de filtrado
txtFiltre.Text = dvProduits.RowFilter
' definir la paginación
txtPages.Text = CType(Session("nbProduitsPage"), String)
' se muestra el formulario de filtrado
afficheFormulaire()
End Sub
La condición de filtrado de una vista se define en su atributo [RowFilter]. Recordemos que la vista filtrada y paginada se llama [dvProduits] y se recuperó en la sesión al cargar la página [Page_Load]. Por lo tanto, la condición de filtrado se recupera de [dvProduits.RowFilter]. El número de productos por página también se recupera de la sesión. Si el usuario nunca ha definido esta información, este número es igual al número predeterminado de productos por página. Una vez hecho esto, se muestra el formulario que permite definir estos dos datos:
Private Sub afficheFormulaire()
' se muestra la vista [formulaire]
vueFormulaire.Visible = True
vueErreurs.Visible = False
vueProduits.Visible = False
vueAjout.Visible = False
End Sub

9.6.10. Validación de la vista [formulaire]
Al hacer clic en el botón [Exécuter] anterior, se ejecuta el siguiente procedimiento:
Private Sub btnExécuter_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnExécuter.Click
' ¿Página válida?
rfvLignes.Validate()
rvLignes.Validate()
If Not rfvLignes.IsValid Or Not rvLignes.IsValid Then
afficheFormulaire()
Exit Sub
End If
' Incorporación de los datos filtrados a la tabla
Try
dvProduits.RowFilter = txtFiltre.Text.Trim
Catch ex As Exception
lblinfo1.Text = "Erreur de filtrage (" + ex.Message + ")"
afficheFormulaire()
Exit Sub
End Try
' se guarda el número de productos por página
Session("nbProduitsPage") = txtPages.Text
'y la página actual
Session("pageCourante") = 0
' todo está bien: se muestran los datos
afficheProduits(0, CType(txtPages.Text, Integer))
End Sub
Recordemos, en primer lugar, que la página tiene dos componentes de validación:
n.º | nombre | tipo | función |
RequiredFieldValidator | verifica si hay un valor en [txtPages] | ||
RangeValidator | verifica que txtPages esté dentro del intervalo de [3,10] |
El procedimiento comienza ejecutando el código de control de los dos componentes anteriores mediante su método [Validate] y luego verifica el valor de su atributo [IsValid]. Este valor será [true] únicamente si los datos verificados se consideraron válidos. Si las pruebas de validez de alguno de los dos componentes fallaron, se vuelve a mostrar la vista [formulaire] para que el usuario corrija su(s) error(es). La condición de filtrado se aplica al atributo [dvProduits.RowFilter]. Aquí puede producirse una excepción si el usuario ha introducido un criterio de filtrado incorrecto, como se muestra a continuación:

En este caso, se vuelve a mostrar la vista [formulaire]. Si los dos datos ingresados son correctos, se almacenan dos datos en la sesión:
- el número de productos por página elegido por el usuario
- el número de la página que se mostrará —inicialmente, la página 0—
Esta información se utiliza cada vez que se muestra la vista [produits].
9.6.11. Visualización de la vista [produits]
La vista [produits] es la siguiente:

Tenemos 5 componentes [DataGrid], cada uno de los cuales muestra una vista específica de la tabla de productos [dtProduits]. Comenzando por la izquierda:
nombre | función |
cuadrícula de visualización de una vista filtrada de la tabla de productos. El filtro es el establecido por la vista [formulaire]. También tenemos .AllowPaging=true, .AllowSorting=true | |
muestra la tabla completa de productos; permite dar seguimiento a las actualizaciones | |
mostrará los productos eliminados en la tabla de productos | |
mostrará los productos modificados en la tabla de productos | |
mostrará los productos agregados en la tabla de productos |
El procedimiento [afficheProduits] se encarga de mostrar la vista anterior:
Private Sub afficheProduits(ByVal page As Integer, ByVal taillePage As Integer)
' se vinculan los datos a los dos componentes [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()
' se muestra la vista [produits]
vueProduits.Visible = True
vueFormulaire.Visible = False
vueErreurs.Visible = False
vueAjout.Visible = False
' se guarda la página actual
Session("pageCourante") = page
End Sub
El procedimiento admite dos parámetros:
- el número de página [page] que se mostrará en [DataGrid1]
- el número de productos [taillePage] por página
La conexión de [DataGrid] con la tabla de productos se realiza a través de 5 vistas diferentes.
- [DataGrid1] está vinculada a la vista paginada y ordenada [dvProduits].
With DataGrid1
.DataSource = dvProduits
.PageSize = taillePage
.CurrentPageIndex = page
.DataBind()
End With
Es durante esta vinculación de [DataGrid1] a su fuente de datos cuando se necesitan los dos datos que se pasan como parámetros al procedimiento.
- [DataGrid2] está vinculado a una vista que muestra todos los elementos actuales de la tabla [dtProduits]. Al igual que [DataGrid1], presenta una vista actualizada de la tabla [dtProduits], pero sin paginación ni ordenación.
Dim dvCurrent As New DataView(dtProduits)
dvCurrent.RowStateFilter = DataViewRowState.CurrentRows
With DataGrid2
.DataSource = dvCurrent
.DataBind()
End With
Aquí utilizamos el atributo [RowStateFilter] de la clase [DataView]. Una fila de una tabla o, en este caso, de una vista, cuenta con un atributo [RowState] que indica el estado de la fila. A continuación se muestran algunos ejemplos:
- (continuación)
- [Added]: la fila se ha agregado
- [Modified]: la fila se ha modificado
- [Deleted]: la fila se ha eliminado
- [Unchanged]: la fila no ha cambiado
Cuando la tabla [dtProduits] se creó inicialmente en [Application_Start], todas sus líneas se encontraban en el estado [Unchanged]. Este estado irá cambiando a medida que los clientes web realicen actualizaciones:
- (continuación)
- cuando un cliente crea un nuevo producto, se agrega una fila a la tabla [dtProduits]. Esta estará en el estado [Added].
- Cuando se modifica un producto, su estado cambia de [Unchanged] a [Modified].
- Cuando se elimina un producto, la fila no se elimina físicamente. En cambio, se marca como pendiente de eliminar y su estado cambia a [Deleted]. Es posible cancelar esta eliminación.
El atributo [RowStateFilter] de la clase [DataView] permite filtrar una vista según el estado [RowState] de sus líneas. Sus valores posibles son los de la enumeración [DataRowViewState]:
- (continuación)
- DataRowViewState.CurrentRows: las líneas no eliminadas se muestran con sus valores actuales
- DataRowViewState.Added: las filas agregadas se muestran con sus valores actuales
- DataRowViewState.Deleted: las líneas eliminadas se muestran con sus valores originales
- DataRowViewState.ModifiedOriginal: las líneas modificadas se muestran con sus valores originales
- DataRowViewState.ModifiedCurrent: las filas modificadas se muestran con sus valores actuales
Una fila modificada tiene dos valores: el valor original, que es el que tenía la fila antes de la primera modificación, y el valor actual, que se obtuvo después de una o varias modificaciones. Estos dos valores se conservan simultáneamente.
Las líneas DataGrid 2 a 5 muestran una vista de la tabla [dtProduits] filtrada por el atributo [RowStateFilter]
DataGrid | Filtro |
RowStateFilter= DataRowViewState.CurrentRows | |
RowStateFilter= DataRowViewState.Deleted | |
RowStateFilter = DataRowViewState.ModifiedOriginal | |
RowStateFilter= DataRowViewState.Added |
La tabla [dtPoduits] es actualizada simultáneamente por diferentes clientes web, lo que puede provocar conflictos de acceso a la tabla. Cuando un cliente muestra sus vistas de la tabla [dtProduits], queremos evitar que lo haga mientras la tabla se encuentre en un estado inestable debido a que otro cliente web la está modificando. Asimismo, cada vez que un cliente necesite la tabla [dtProduits] en modo de lectura, como en el caso anterior, o en modo de escritura para agregar, modificar o eliminar productos, se sincronizará con los demás clientes mediante la siguiente secuencia:
Pueden existir varias secciones críticas en el código de la aplicación:
El funcionamiento es el siguiente:
- uno o más clientes llegan a la instrucción [Application.Lock] de la sección crítica 1. Se trata de un distribuidor de token de entrada único. Un solo cliente obtiene este token. Llamémoslo C1.
- Mientras el cliente C1 no haya devuelto el token mediante [Application.Unlock], ningún otro cliente tiene permiso para ingresar a una sección crítica controlada por [Application.Lock]. En el ejemplo anterior, ningún cliente puede, por lo tanto, ingresar a las secciones críticas 1 y 2.
- El cliente C1 ejecuta [Application.Unlock] y, por lo tanto, devuelve el token de entrada. Este token puede entonces asignarse a otro cliente. Se repiten los pasos 1 a 3.
Con este mecanismo, nos aseguraremos de que solo un cliente tenga acceso a la tabla [dtProduits], ya sea para lectura o escritura.
El enlace [Mise à jour] también muestra la vista [Produits]:

El procedimiento asociado es el siguiente:
Private Sub lnkMisajour_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkMisajour.Click
' visualización de la vista de productos
afficheProduits(CType(Session("pageCourante"), Integer), CType(Session("nbProduitsPage"), Integer))
End Sub
Se debe mostrar la vista [produits]. Esto se realiza mediante el procedimiento [afficheProduits], que admite dos parámetros: el número de la página actual que se debe mostrar y el número de productos por página que se deben mostrar. Esta información se encuentra en la sesión, posiblemente con sus valores iniciales si el usuario nunca los ha definido por sí mismo.
9.6.12. Paginación y ordenación de [DataGrid1]
Ya hemos visto en otro ejemplo los procedimientos que gestionan la paginación y la ordenación de un [DataGrid]. En caso de cambiar de página, se muestra la vista [produits] pasando el número de la nueva página como parámetro al procedimiento [afficheProduits].
Private Sub DataGrid1_PageIndexChanged(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridPageChangedEventArgs) Handles DataGrid1.PageIndexChanged
' cambio de página
afficheProduits(e.NewPageIndex, DataGrid1.PageSize)
End Sub
El procedimiento [DataGrid1_SortCommand] se ejecuta cuando el usuario hace clic en el encabezado de una de las columnas de [DataGrid1]. En ese momento, se debe asignar la expresión de ordenación al atributo [Sort] de la vista [dvProduits] que muestra [DataGrid1]. Esta es sintácticamente equivalente a la expresión de ordenación que se encuentra detrás de la cláusula [order by] de la instrucción SQL SELECT. El argumento [e] del procedimiento tiene un atributo [SortExpression] que nos proporciona la expresión de ordenación asociada a la columna cuyo encabezado se ha seleccionado. Cuando se creó el componente [DataGrid1], ya se había definido esta expresión de ordenación. A continuación, la definida para la columna [nom] de [DataGrid1]:

Si la expresión de ordenación no se definió al diseñar el [DataGrid], se utiliza el nombre del campo de datos asociado a la columna del [DataGrid]. El orden de clasificación (ascendente, descendente) lo establece aquí el usuario mediante los botones de opción [rdCroissant, rd Décroissant]:

El procedimiento de ordenación es el siguiente:
Private Sub DataGrid1_SortCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridSortCommandEventArgs) Handles DataGrid1.SortCommand
' se ordena la vista de datos
With dvProduits
.Sort = e.SortExpression + " " + CType(IIf(rdCroissant.Checked, "asc", "desc"), String)
End With
' se muestra
afficheProduits(0, DataGrid1.PageSize)
End Sub
9.6.13. Eliminación de un producto
Para eliminar un producto, el usuario utiliza el enlace [Supprimer] de la línea del producto:

A continuación, se ejecuta el procedimiento [DataGrid1_DeleteCommand]:
Private Sub DataGrid1_DeleteCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.DeleteCommand
' eliminación de un producto
' clave del producto a eliminar
Dim idProduit As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)
' Eliminación del producto
Dim erreur As Boolean = False
Try
supprimerProduit(idProduit)
Catch ex As Exception
' problema
lblInfo2.Text = ex.Message
erreur = True
End Try
' ¿Cambio de página?
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
' visualización de productos
afficheProduits(page, DataGrid1.PageSize)
End Sub
La actualización de los productos se realizará mediante la clave [id] del producto. De hecho, la columna [id] de la tabla [dtProduits] es la clave primaria y, gracias a ella, podemos localizar en la tabla [dtProduits] la fila del producto que se actualiza en el componente [DataGrid1]. Por lo tanto, el procedimiento comienza recuperando la clave del producto cuyo enlace [Supprimer] se ha seleccionado:
' clave del producto a eliminar
Dim idProduit As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)
Sabemos que [e.Item] es el elemento de [DataGrid1] que originó el evento. En términos generales, este elemento es una línea de [DataGrid]. [e.Item.ItemIndex] es el número de la línea que originó el evento. Este índice se refiere a la página actual que se está mostrando. Así, la primera línea de la página que se muestra tiene la propiedad [ItemIndex=0], aunque tenga el número 17 en la tabla de productos. [DataGrid1.DataKeys] es la lista de claves de [DataGrid]. Dado que, en el diseño, escribimos [DataGrid1.DataKey=id], las claves de [DataGrid1] están formadas por los valores de la columna [id] de la tabla [dtProduits], que a la vez es la clave primaria. Así, [DataGrid1.DataKeys(e.Item.ItemIndex)] es la clave [id] del producto que se va a eliminar. Una vez obtenido esto, solicitamos su eliminación mediante el procedimiento [supprimerProduit]:
' eliminación del producto
Dim erreur As Boolean = False
Try
supprimerProduit(idProduit)
Catch ex As Exception
' problema
lblInfo2.Text = ex.Message
erreur = True
End Try
En algunos casos, esta eliminación puede fallar. Veremos por qué. En ese caso, se genera una excepción, la cual se maneja aquí. Si ocurre un error, el [DataGrid1] no tiene que modificarse. Solo se agrega un mensaje de error a la vista [produits]. Si no hay error y el usuario acaba de eliminar el único producto de la página actual, se muestra la página anterior.
' ¿Cambio de página?
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
En todos los casos, la vista [produits] se vuelve a mostrar con el procedimiento [afficheProduits]:
El procedimiento [supprimerProduit] es el siguiente:
Private Sub supprimerProduit(ByVal idProduit As Integer)
Dim erreur As String
Try
' sincronización
Application.Lock()
' búsqueda de la línea a eliminar
Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
If ligne Is Nothing Then
erreur = String.Format("Produit [{0}] inexistant", idProduit)
Else
' eliminación de la línea
ligne.Delete()
End If
Catch ex As Exception
erreur = String.Format("Erreur de suppression : {0}", ex.Message)
Finally
' fin de la sincronización
Application.UnLock()
End Try
' se lanza una excepción si hay un error
If erreur <> String.Empty Then Throw New Exception(erreur)
End Sub
El procedimiento recibe como parámetro la clave del producto que se va a eliminar. Si solo hubiera un cliente que realizara las actualizaciones, esta eliminación se podría llevar a cabo de la siguiente manera:
Cuando varios clientes realizan las actualizaciones al mismo tiempo, el proceso es más complicado. De hecho, consideremos la siguiente secuencia temporal:
![]() |
Tiempo | Acción |
T1 | El cliente A lee la tabla [dtProduits]: hay un producto con clave 20 |
T2 | El cliente B lee la tabla [dtProduits]: hay un producto con clave 20 |
T3 | El cliente A elimina el producto con clave 20 |
T4 | El cliente B elimina el producto con clave 20 |
Cuando los clientes A y B leen la tabla de productos y muestran su contenido en una página web, la tabla contiene el producto con clave 20. Por lo tanto, es posible que quieran realizar alguna acción sobre ese producto, por ejemplo, eliminarlo. Inevitablemente, uno de los dos lo hará primero. El que llegue después querrá eliminar un producto que ya no existe. El procedimiento [supprimerProduits] aborda este caso de varias maneras:
- en primer lugar, se realiza una sincronización con [Application.Lock]. Esto significa que, cuando un cliente cruza esta barrera, no hay nadie más que pueda modificar o leer la tabla de productos. De hecho, todas estas operaciones se sincronizan de esta manera. Ya lo hemos visto en el caso de la lectura.
- Luego se verifica si el producto que se desea eliminar existe. Si es así, se elimina; de lo contrario, se genera un mensaje de error.
' búsqueda de la línea a eliminar
Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
If ligne Is Nothing Then
erreur = String.Format("Produit [{0}] inexistant", idProduit)
Else
' eliminación de la línea
ligne.Delete()
End If
- Se sale de la secuencia crítica mediante [Application.Unlock] para permitir que los demás clientes realicen sus actualizaciones.
- Si no se pudo eliminar la línea, el procedimiento genera una excepción junto con un mensaje de error.
Veamos un ejemplo. Iniciamos un cliente [Mozilla] y seleccionamos inmediatamente la opción [Mise à jour] (vista parcial):

Hacemos lo mismo con un cliente [Internet Explorer] (vista parcial):

Con el cliente [Mozilla] eliminamos el producto [produit2]. Obtenemos la siguiente página nueva:

La operación de eliminación se realizó correctamente. El producto eliminado aparece correctamente en [DataGrid], en la lista de productos eliminados, y ya no aparece en la lista de productos de los componentes [DataGrid1] y [DataGrid2], que muestran los productos actualmente presentes en la tabla. Hagamos lo mismo con [Interbet Explorer]. Eliminamos el elemento [produit2]:

La respuesta obtenida es la siguiente:

Un mensaje de error le indica al usuario que el producto con clave [2] no existe. La respuesta le devuelve al cliente una nueva vista que refleja el estado actual de la tabla. Se observa que, efectivamente, el producto con clave [2] se encuentra en la lista de productos eliminados. Por lo tanto, la vista [produits] refleja las actualizaciones de todos los clientes y no solo de uno.
9.6.14. Agregar un producto
Para agregar un producto, el usuario utiliza el enlace [Ajout] de las opciones. Este enlace simplemente muestra la vista [Ajout]:


Los dos procedimientos involucrados en esta acción son los siguientes:
Private Sub lnkAjout_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkAjout.Click
' se muestra la vista [ajout]
afficheAjout()
End Sub
Private Sub afficheAjout()
' muestra la vista de agregación
vueAjout.Visible = True
vueFormulaire.Visible = False
vueProduits.Visible = False
vueErreurs.Visible = False
End Sub
Recordemos que la vista [Ajout] tiene componentes de validación:
nombre | tipo | función |
RequiredFieldValidator | verifica si hay un valor en [txtNom] | |
RequiredFieldValidator | verifica si hay un valor en [txtPrix] | |
CompareValidator | verifica que precio >= 0 |
El procedimiento para gestionar el clic en el botón [Ajouter] es el siguiente:
Private Sub btnAjouter_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnAjouter.Click
' Agregación de un nuevo elemento a la tabla de productos
': primero deben ser válidos los datos
rfvNom.Validate()
rfvPrix.Validate()
cvPrix.Validate()
If Not rfvNom.IsValid Or Not rfvPrix.IsValid Or Not cvPrix.IsValid Then
' de nuevo el formulario de ingreso
afficheAjout()
Exit Sub
End If
' se crea una fila
Dim produit As DataRow = dtProduits.NewRow
produit("nom") = txtNom.Text.Trim
produit("prix") = txtPrix.Text.Trim
' se agrega la fila a la tabla
Application.Lock()
Try
dtProduits.Rows.Add(produit)
lblInfo3.Text = "Ajout réussi"
' limpieza
txtNom.Text = ""
txtPrix.Text = ""
Catch ex As Exception
lblInfo3.Text = String.Format("Erreur : {0}", ex.Message)
End Try
Application.UnLock()
End Sub
En primer lugar, se ejecutan las verificaciones de validez de los tres componentes [rfvNom, rfvPrix, cvPrix]. Si alguna de las verificaciones falla, se vuelve a mostrar la vista [Ajout] con los mensajes de error de los componentes de validación. A continuación se muestra un ejemplo:

Si los datos son válidos, se prepara la fila para insertarla en la tabla [dtProduits].
' se crea una fila
Dim produit As DataRow = dtProduits.NewRow
produit("nom") = txtNom.Text.Trim
produit("prix") = txtPrix.Text.Trim
En primer lugar, se crea una nueva fila en la tabla [dtProduits] mediante el método [DataTable.NewRow]. Esta fila tendrá las tres columnas [id, nom, prix] de la tabla [dtProduits]. Las columnas [nom, prix] se completan con los valores ingresados en el formulario de agregación. La columna [id] no se completa. Esta columna es del tipo [AutoIncrement], c.a.d, y SGBD, que servirá como clave para un nuevo producto, es decir, la clave máxima existente incrementada en 1. La fila [produit] creada aquí está separada de la tabla [dtProduits]. Ahora debemos insertarla en esta tabla. Como vamos a actualizar la tabla [dtProduits], activamos la sincronización entre clientes:
Una vez hecho esto, intentamos agregar la fila a la tabla [dtProduits]. Si tenemos éxito, mostramos un mensaje de éxito; de lo contrario, un mensaje de error.
Try
dtProduits.Rows.Add(produit)
lblInfo3.Text = "Ajout réussi"
' limpieza
txtNom.Text = ""
txtPrix.Text = ""
Catch ex As Exception
lblInfo3.Text = String.Format("Erreur : {0}", ex.Message)
End Try
Normalmente no hay excepciones posibles. Sin embargo, como medida de precaución, se ha implementado un manejo de excepciones. Una vez realizada la adición, se sale de la sección crítica mediante [Application.Unlock].
9.6.15. Modificación de un producto
Para modificar un producto, el usuario utiliza el enlace [Modifier] de la línea del producto:

A continuación, se pasa al modo de edición:

Gracias al componente [DataGrid], este resultado se logra con muy poco código:
Private Sub DataGrid1_EditCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.EditCommand
' se pone el elemento actual en modo de edición
DataGrid1.EditItemIndex = e.Item.ItemIndex
' se vuelven a mostrar los productos
afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
End Sub
Al hacer clic en el enlace [Modifier], se ejecuta, en el lado del servidor, el procedimiento [DataGrid1_EditCommand]. El componente [DataGrid1] tiene un campo [EditItemIndex]. La fila cuyo índice es el valor de [EditItemIndex] pasa al modo de edición. Cada valor de la fila así actualizada se puede modificar en un campo de entrada, como se muestra en la captura de pantalla anterior. Por lo tanto, recuperamos el índice del producto en el que se hizo clic en el enlace [Modifier] y lo asignamos al atributo [EditItemIndex] del componente [DataGrid1]. Ahora solo nos queda volver a mostrar la vista [produits]. Se verá igual que antes, pero con una fila en modo de edición.
El enlace [Annuler] del producto que se está editando permite al usuario cancelar su actualización. El procedimiento asociado a este enlace es el siguiente:
Private Sub DataGrid1_CancelCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.CancelCommand
' se vuelven a mostrar los productos
afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
End Sub
Simplemente vuelve a mostrar la vista [produits]. Podríamos sentir la tentación de ingresar -1 en [DataGrid1.EditItemIndex] para anular el modo de actualización. De hecho, sabemos que el procedimiento [Page_Load] lo hace sistemáticamente. Por lo tanto, no hay necesidad de volver a hacerlo.
La modificación se valida mediante el enlace [Mettre à jour] de la línea que se está editando. A continuación, se ejecuta el siguiente procedimiento:
Private Sub DataGrid1_UpdateCommand(ByVal source As Object, ByVal e As System.Web.UI.WebControls.DataGridCommandEventArgs) Handles DataGrid1.UpdateCommand
' clave del producto a modificar
Dim idProduit As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)
' elementos modificados
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
' ¿Son válidas las modificaciones?
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
' si hay un error, se vuelve a mostrar la página de actualización
If lblInfo2.Text <> String.Empty Then
' se vuelve a poner la línea en modo de actualización
DataGrid1.EditItemIndex = e.Item.ItemIndex
' visualización de productos
afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
' fin
Exit Sub
End If
' si no hay error, se modifica la tabla
Try
modifierProduit(idProduit, e.Item)
Catch ex As Exception
' problema
lblInfo2.Text = ex.Message
End Try
' visualización de productos
afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
End Sub
Al igual que para la eliminación, debemos obtener la clave del producto que se va a modificar para localizar su fila en la tabla de productos [dtProduits]:
' clave del producto a modificar
Dim idProduit As Integer = CType(DataGrid1.DataKeys(e.Item.ItemIndex), Integer)
A continuación, debemos recuperar los nuevos valores que se asignarán a la línea. Estos se encuentran en el componente [DataGrid1]. El argumento [e] del procedimiento nos sirve de ayuda. [e.Item] representa la línea de [DataGrid1] que es la fuente del evento. Por lo tanto, es la línea que se está actualizando, ya que el enlace [Mettre à jour] solo existe en esta línea. Esta línea contiene columnas designadas por la colección [Cells] de la línea. Así, [e.Item.Cells(0)] representa la columna 0 de la línea que se está actualizando. Sabemos que los nuevos valores se encuentran en los campos de entrada. La colección [e.Item.Cells(i).Controls] representa la colección de controles de la columna i de la línea [e.Item]. Las dos instrucciones siguientes recuperan los valores de los campos de entrada de la línea actualizada en [DataGrid1]:
' elementos modificados
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
Así pues, contamos con los nuevos valores de la línea modificada en forma de cadenas de caracteres. A continuación, verificamos si estos datos son válidos. El nombre no debe estar vacío y el precio debe ser un número positivo o cero:
' ¿Modificaciones válidas?
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
En caso de error, la etiqueta [lblInfo2] mostrará un mensaje de error y simplemente se volverá a mostrar la misma página:
' si hay un error, se vuelve a mostrar la página de actualización
If lblInfo2.Text <> String.Empty Then
' se vuelve a poner la fila en modo de actualización
DataGrid1.EditItemIndex = e.Item.ItemIndex
' visualización de productos
afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
' fin
Exit Sub
End If
Lo que no muestra el código anterior es que los valores ingresados se pierden. De hecho, [DataGrid1] está vinculado a los datos de la tabla [dtProduits], que contiene los valores originales de la fila modificada. A continuación, un ejemplo.

La respuesta obtenida es la siguiente:

Se observa que se han perdido los valores ingresados inicialmente. En una aplicación profesional, esto probablemente no sería aceptable. Aquí nos topamos con ciertas limitaciones del modo estándar de actualización del componente [DataGrid]. Sería preferible contar con una vista [Modification] similar a la vista [Ajout].
Si los datos son válidos, se utilizan para actualizar la tabla [dtProduits]:
' si no hay error, se modifica la tabla
Try
modifierProduit(idProduit, e.Item)
Catch ex As Exception
' problema
lblInfo2.Text = ex.Message
End Try
' visualización de productos
afficheProduits(DataGrid1.CurrentPageIndex, DataGrid1.PageSize)
Por la misma razón mencionada al eliminar un producto, la modificación podría fallar. De hecho, entre el momento en que el cliente consultó la tabla [dtProduits] y el momento en que va a modificar uno de sus productos, este podría haber sido eliminado por otro cliente. El procedimiento [modifierProduit] maneja este caso lanzando una excepción. Esta excepción se maneja aquí. Tras el éxito o el fracaso de la actualización, la aplicación devuelve la vista [produits] al cliente. Nos queda por ver cómo el procedimiento [modifierProduit] lleva a cabo la actualización:
Private Sub modifierProduit(ByVal idProduit As Integer, ByVal item As DataGridItem)
Dim erreur As String
Try
' sincronización
Application.Lock()
' búsqueda de la línea a modificar
Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
If ligne Is Nothing Then
erreur = String.Format("Produit [{0}] inexistant", idProduit)
Else
' se modifica la línea
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
' fin de la sincronización
Application.UnLock()
End Try
' se lanza una excepción si hay un error
If erreur <> String.Empty Then Throw New Exception(erreur)
End Sub
No entraremos en detalles sobre este procedimiento, cuyo código es similar al del procedimiento [supprimerProduit], que ya se ha explicado ampliamente. Veamos simplemente dos ejemplos. En primer lugar, modificamos el precio del producto [produit1]:

Al utilizar el enlace [Mettre à jour] anterior, se obtiene la siguiente respuesta:

La modificación sí aparece en los componentes [DataGrid] 1 y 2, que reflejan el estado actual de la tabla [dtProduits]. También está presente en el componente [DataGrid4], que es una vista de las filas modificadas, las cuales se muestran con sus valores originales. Ahora veamos un caso de conflicto de acceso. Al igual que en el ejemplo de eliminación, utilizaremos dos clientes web diferentes. Un cliente [Mozilla] lee la tabla [dtProduits] y entra en modo de modificación del producto [produit1]:

Un cliente [Internet Explorer] está a punto de eliminar el producto [produit1]:

El cliente [Internet Explorer] elimina [produit1]:

Cabe destacar que [produit1] ya no aparece en la tabla de líneas modificadas, sino en la de líneas eliminadas. El cliente [Mozilla], por su parte, valida su actualización:

El cliente [Mozilla] recibe la siguiente respuesta a su actualización:

Puede observar que [produit1] ha sido eliminado, ya que aparece en la lista de productos eliminados.
9.7. Aplicación web para actualizar la tabla física de productos
9.7.1. Soluciones propuestas
La aplicación anterior era más bien un caso de estudio destinado a mostrar la gestión de un objeto [DataTable] en caché que un caso común. De hecho, en algún momento es necesario actualizar la fuente real de los datos. Se puede optar por dos estrategias diferentes:
- se utiliza la caché [dtProduits] en memoria para actualizar la fuente de datos. Se puede crear una página ubicada en la estructura web de la aplicación anterior para tener acceso a la caché [dtProduits] de esta. Esta página le ofrecería a un administrador la posibilidad de reflejar en la fuente física de los datos las modificaciones realizadas en la caché [dtProduits]. Para ello, se podría agregar un nuevo método a la clase de acceso [produits] que aceptara como parámetro la caché [dtProduits] y que, con esta caché, actualizara la fuente física de datos.
- Se actualiza la fuente física de datos al mismo tiempo que la caché.
La estrategia n.º 1 permite abrir solo una conexión con la fuente de datos física. La estrategia n.º 2 requiere una conexión en cada actualización. Dependiendo de la disponibilidad de las conexiones, se podrá optar por una u otra estrategia. Dado que contamos con las herramientas para implementarla (la clase [produits]), elegimos la estrategia n.º 2.
9.7.2. Solución 1
En aras de la continuidad con la aplicación anterior, elegimos la siguiente estrategia:
- la fuente física se actualiza al mismo tiempo que la caché
- la caché se construye solo una vez, al iniciar la aplicación en [global.asax.vb]. Esto significa que, si la fuente física de datos es actualizada por otros clientes que no sean los clientes web, estos no ven dichos cambios. Solo ven los cambios que ellos mismos realizan en la tabla almacenada en caché.
Para actualizar la fuente física de datos, necesitamos una instancia de la clase de acceso a los productos. Cada cliente podría tener la suya propia. También se puede compartir una única instancia que sería creada por la aplicación al iniciarse. Esa es la solución que elegimos aquí. El código de control [global.asax.vb] se modifica de la siguiente manera:
Imports System
Imports System.Web
...
Public Class Global
Inherits System.Web.HttpApplication
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
' se recupera la información de configuración
Dim chaînedeConnexion As String = ConfigurationSettings.AppSettings("OLEDBStringConnection")
Dim defaultProduitsPage As String = ConfigurationSettings.AppSettings("defaultProduitsPage")
Dim erreurs As New ArrayList
...
' aquí no hay errores de configuración
' se crea un objeto de productos
Dim objProduits As New produits(chaînedeConnexion)
Dim dtProduits As DataTable
Try
dtProduits = objProduits.getProduits
Catch ex As ExceptionProduits
'Se produjo un error al acceder a los productos; se registra en la aplicación
Application("erreurs") = ex.erreurs
Exit Sub
Catch ex As Exception
' error no manejado
erreurs.Add(ex.Message)
Application("erreurs") = erreurs
' salida de la subrutina
End Try
' aquí no hay errores de inicialización
...
' se guarda la instancia de acceso a los datos
Application("objProduits") = objProduits
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
...
End Sub
End Class
Se ha almacenado una instancia de la clase de acceso a datos en la aplicación, asociada a la clave [objProduits]. Cada cliente utilizará esta instancia para acceder a la fuente física de datos. Se recuperará en el procedimiento [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
...
' se recupera la instancia de acceso a los productos
objProduits = CType(Application("objProduits"), produits)
...
End Sub
La instancia de acceso a datos [objProduits] está disponible para todos los métodos de la página. Se utilizará para las tres operaciones de actualización: agregar, eliminar y modificar.
El procedimiento de inserción se modifica de la siguiente manera:
Private Sub btnAjouter_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnAjouter.Click
...
' se agrega la fila
Application.Lock()
Try
' se agrega la fila a la tabla en caché
dtProduits.Rows.Add(produit)
' se agrega la fila a la fuente física
Dim nouveauProduit As sProduit
With nouveauProduit
.nom = CType(produit("nom"), String)
.prix = CType(produit("prix"), Double)
End With
objProduits.ajouterProduit(nouveauProduit)
' seguimiento
lblInfo3.Text = "Ajout réussi"
' limpieza
txtNom.Text = ""
txtPrix.Text = ""
Catch ex As Exception
' error
lblInfo3.Text = String.Format("Erreur : {0}", ex.Message)
End Try
Application.UnLock()
End Sub
El procedimiento de modificación se modifica de la siguiente manera:
Private Sub modifierProduit(ByVal idProduit As Integer, ByVal item As DataGridItem)
Dim erreur As String
Try
' sincronización
Application.Lock()
' búsqueda de la línea que se debe modificar
Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
If ligne Is Nothing Then
erreur = String.Format("Produit [{0}] inexistant", idProduit)
Else
' se modifica la línea en la caché
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
' se modifica la línea en la fuente física
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
' fin de la sincronización
Application.UnLock()
End Try
' se lanza una excepción si hay un error
If erreur <> String.Empty Then Throw New Exception(erreur)
End Sub
El procedimiento de eliminación se modifica de la siguiente manera:
Private Sub supprimerProduit(ByVal idProduit As Integer)
Dim erreur As String
Try
' sincronización
Application.Lock()
' búsqueda de la línea a eliminar
Dim ligne As DataRow = dtProduits.Rows.Find(idProduit)
If ligne Is Nothing Then
erreur = String.Format("Produit [{0}] inexistant", idProduit)
Else
' eliminación de la línea en la caché
ligne.Delete()
' eliminación de la línea en la fuente física
objProduits.supprimerProduit(idProduit)
End If
Catch ex As Exception
erreur = String.Format("Erreur de suppression : {0}", ex.Message)
Finally
' fin de la sincronización
Application.UnLock()
End Try
' se lanza una excepción si hay un error
If erreur <> String.Empty Then Throw New Exception(erreur)
End Sub
9.7.3. Pruebas
Partimos de la siguiente tabla de datos en un archivo ACCESS:

Se inicia un cliente web:

Agregamos un producto:

Eliminamos [produit1]:

Modificamos el precio de [produit2]:

Después de estos cambios, revisamos el contenido de la tabla [liste] en la base de datos ACCESS:

Las tres modificaciones se han reflejado correctamente en la tabla física. Ahora veamos un caso de conflicto. Eliminamos, directamente debajo de ACCESS, la fila de [produit2]:

Volvemos a nuestro cliente web. Este no ve la eliminación que se ha realizado y quiere eliminar a su vez [produit2]:

Recibe la siguiente respuesta:

La línea [produit2] sí se ha eliminado de la caché, tal como lo indica la lista de productos eliminados. Sin embargo, la eliminación de [produit2] en la fuente física falló, tal como lo indica el mensaje de error.
9.7.4. Solución 2
En la solución anterior, los clientes web actualizan simultáneamente la fuente de datos física, pero no ven los cambios realizados por los demás clientes. Solo ven los suyos. Ahora quisiéramos que un cliente pudiera ver la fuente de datos física tal como está actualmente y no tal como estaba al iniciar la aplicación. Para ello, le ofreceremos al cliente una nueva opción:

Con la opción [Rafraîchir], el cliente fuerza una nueva lectura de la fuente de datos física. Para que esto no afecte a los demás clientes, es necesario que la tabla resultante de esta lectura pertenezca al cliente que realiza la actualización y no se comparta con los demás clientes. Esta es la primera diferencia con respecto a la aplicación anterior. La caché [dtProduits] de la fuente de datos será creada por cada cliente y no por la propia aplicación. La modificación se realiza en [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)
' se recupera la información de configuración
...
' aquí no hay errores de configuración
' se crea un objeto de productos
Dim objProduits As New produits(chaînedeConnexion)
' se guarda el número de productos por página
Application("defaultProduitsPage") = defaultProduitsPage
' se almacena la instancia de acceso a los datos
Application("objProduits") = objProduits
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' inicializa las variables de sesión
If IsNothing(Application("erreurs")) Then
' se almacena la fuente de datos en caché
Dim dtProduits As DataTable
Try
Application.Lock()
dtProduits = CType(Application("objProduits"), produits).getProduits
Catch ex As ExceptionProduits
'Se produjo un error al acceder a los productos; se registra en la sesión
Session("erreurs") = ex.erreurs
Exit Sub
Finally
Application.UnLock()
End Try
' se guarda la caché [dtProduits] en la sesión
Session("dtProduits") = dtProduits
' vista de la tabla de productos
Session("dvProduits") = dtProduits.DefaultView
' número de productos por página
Session("nbProduitsPage") = Application("defaultProduitsPage")
' página actual mostrada
Session("pageCourante") = 0
End If
End Sub
End Class
La información de la sesión se recupera en cada solicitud en el procedimiento [Page_Load]:
' datos de la página
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
' se verifica si la aplicación presenta un error
If Not IsNothing(Application("erreurs")) Then
' la aplicación no se ha inicializado correctamente
afficheErreurs(CType(Application("erreurs"), ArrayList))
Exit Sub
End If
' se verifica si la sesión presenta un error
If Not IsNothing(Session("erreurs")) Then
' la sesión no se ha inicializado correctamente
afficheErreurs(CType(Session("erreurs"), ArrayList))
Exit Sub
End If
' se recupera una referencia de la tabla de productos
dtProduits = CType(Session("dtProduits"), DataTable)
' se recupera la vista de los productos
dvProduits = CType(Session("dvProduits"), DataView)
' se recupera el número de productos por página
nbProduitsPage = CType(Session("nbProduitsPage"), Integer)
' se recupera la página actual
pageCourante = CType(Session("pageCourante"), Integer)
' se recupera la instancia de acceso a los productos
objProduits = CType(Application("objProduits"), produits)
' se cancela cualquier actualización en curso
DataGrid1.EditItemIndex = -1
'Primera consulta
If Not IsPostBack Then
' se muestra el formulario inicial
txtPages.Text = nbProduitsPage.ToString
afficheFormulaire()
End If
End Sub
La información recuperada durante la sesión se guardará al final de la misma cada vez que finalice una consulta:
Private Sub Page_PreRender(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.PreRender
' se almacena cierta información en la sesión
Session("dtProduits") = dtProduits
Session("dvProduits") = dvProduits
Session("nbProduitsPage") = nbProduitsPage
Session("pageCourante") = pageCourante
End Sub
El evento [PreRender] es el que indica que la respuesta se enviará al cliente. Aprovechamos para guardar en la sesión todos los datos que hay que conservar. Esto resulta excesivo, ya que con bastante frecuencia solo algunos de ellos han cambiado de valor. Este guardado sistemático tiene la ventaja de liberarnos de la gestión de la sesión en los demás métodos de la página.
La operación de actualización de la caché se lleva a cabo mediante el siguiente procedimiento:
Private Sub lnkRefresh_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lnkRefresh.Click
' se debe actualizar la caché [dtProduits] con la fuente de datos física
' Inicio de la sincronización
Application.Lock()
Try
' la tabla de productos
dtProduits = CType(Application("objProduits"), produits).getProduits
' se guarda el filtro actual
Dim filtre As String = dvProduits.RowFilter
' se crea la nueva vista filtrada
dvProduits = New DataView(dtProduits)
' se restablece el filtro
dvProduits.RowFilter = filtre
Catch ex As ExceptionProduits
'se produjo un error al acceder a los productos; se registra en la sesión
Session("erreurs") = ex.erreurs
' se muestra la vista [erreurs]
afficheErreurs(ex.erreurs)
' listo
Exit Sub
Finally
' fin de la sincronización
Application.UnLock()
End Try
' todo salió bien: se muestran los productos a partir de la primera página
afficheProduits(0, nbProduitsPage)
End Sub
El procedimiento regenera nuevos valores para la caché [dtProduits] y la vista [dvProduits]. Estos serán colocados en la sesión mediante el procedimiento [Page_PreRender] descrito anteriormente. Una vez reconstruida la caché [dtProduits], se muestran los productos a partir de la primera página.
A continuación se muestra un ejemplo de ejecución. Se inicia un cliente [mozilla] y muestra los productos:

Un cliente [Internet Explorer] hace lo mismo:

El cliente [Mozilla] elimina [produit1], modifica [produit2] y agrega un nuevo producto. Obtiene la siguiente página nueva:

El cliente [Internet Explorer] quiere eliminar [produit1].

Obtiene la siguiente respuesta:

Se le informó que [produit1] ya no existía. El usuario decide entonces actualizar su caché con el enlace [Rafraîchir] mencionado anteriormente. Recibe la siguiente respuesta:

Ahora cuenta con la misma fuente de datos que el cliente [Mozilla].
9.8. Conclusion
En este capítulo, hemos dedicado bastante tiempo a los contenedores de datos y sus vínculos con las fuentes de datos. Hemos concluido mostrando cómo se puede actualizar una fuente de datos utilizando un componente [DataGrid]. Tomamos como fuente de datos una tabla de base de datos. Aunque el componente [DataGrid] facilita un poco la representación de los datos, la verdadera dificultad no radica en la presentación, sino en la gestión de las actualizaciones de la fuente de datos realizadas por los distintos clientes. Pueden surgir conflictos de acceso que deben gestionarse. En este caso, los hemos gestionado en el controlador mediante el uso de [Application.Lock]. Probablemente sería más aconsejable sincronizar los accesos a la fuente de datos en la clase de acceso a los mismos, para que el controlador no tenga que ocuparse de tales detalles que no son de su competencia.
En la práctica, las tablas de una base de datos están vinculadas entre sí mediante relaciones y su actualización debe tenerlas en cuenta. Esto tiene consecuencias, en primer lugar, en la clase de acceso a los datos, que se vuelve más compleja que la necesaria para una tabla independiente. Por lo general, también tiene consecuencias en la parte de presentación de la aplicación web, ya que a menudo es necesario mostrar no solo una tabla, sino las tablas vinculadas entre sí mediante relaciones.
Este capítulo ha permitido, además, presentar diferentes estructuras de datos, como [DataTable, DataView].









