Skip to content

9. Componentes de servidor ASP - 3

9.1. Introduction

Continuamos nuestro trabajo en la interfaz de usuario ampliando las capacidades de los componentes [DataList] y [DataGrid], especialmente en lo que respecta a la actualización de los datos que muestran

9.2. Gestionar los 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 la matriz {"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 ha hecho clic. Aquí queremos mostrar cómo gestionar una lista de botones o enlaces.

9.2.2. Configuración de los componentes

El componente [DataList1] se configura 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 relativo al aspecto 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 denominados, respectivamente, [Infos1] y [Infos2]. La clase [Button] tiene un atributo [CommandName] que se utiliza aquí. Nos permitirá determinar qué botón ha provocado un evento en el [DataList]. Para gestionar los clics en los botones, solo tendremos un único gestor 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 ha producido el clic. El atributo [CommandName] nos permitirá saber en qué botón de la línea se ha producido.

El componente [Repeater1] se configura de forma 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 añadido una sección <SeparatorTemplate> para que los datos sucesivos mostrados por el componente queden separados por una barra horizontal.

Por último, el componente [DataGrid1] se configura 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.). Nos encontramos 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. En este caso, hay una. Hemos añadido 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 guardado 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 incluido 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 ha producido 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 ha producido 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 ha producido 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. En 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 ha originado el evento
    • a: información sobre el evento de tipo [DataListCommandEventArgs], [RepeaterCommandEventArgs] o [DataGridCommandEventArgs], según el caso. El argumento a aporta diversa información. En este caso, nos interesan dos de ellas:
      • a.Item: representa la línea en la que se produjo 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 ha originado el evento. Esta información, junto con la anterior, nos permite saber qué botón del contenedor ha originado el evento [ItemCommand]

9.3. Aplicación: gestión de una lista de suscripciones

Ahora que sabemos cómo interceptar los eventos que se producen 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
id
cadena
clave primaria
thème
cadena
nombre del tema de la lista
description
cadena
una descripción de los temas tratados en la lista

Para no complicar demasiado nuestro ejemplo, el objeto [DataTable] anterior se generará mediante código de forma arbitraria. En una aplicación real, probablemente lo proporcionaría un método de una clase de acceso a datos. La creación de la tabla de listas se realizará en el procedimiento [Application_Start] y la tabla resultante se incluirá 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 almacenará, de nuevo, en un objeto [DataTable] denominado [dtAbonnements], cuya estructura será la siguiente:

nombre
tipo
función
id
cadena
clave primaria
thème
cadena
nombre del tema de la lista

La aplicación de una sola página es la siguiente:

n.º
nombre
tipo
propiedades
función
1
dgThèmes
DataGrid
 
Listas de distribución disponibles para suscribirse
2
dlAbonnements
DataList
 
Lista de suscripciones del usuario a las listas anteriores
3
panelInfos
panel
 
panel de información sobre el tema seleccionado por el usuario con un enlace [Plus d'informations]
4
lblThème
Etiqueta
forma parte de [panelInfos]
nombre del tema
5
lblDescription
Etiqueta
forma parte de [panelInfos]
Descripción del tema
6
lblInfo
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 se producen en las líneas 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 opciones seleccionadas por el usuario. No obstante, resulta realista. Nos acercamos a una aplicación de comercio electrónico en la que un usuario añadiría productos (suscripciones) a su cesta.

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 información se muestra en [panelInfos]:

Image

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

Image

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

Image

Por último, puede darse de baja de cualquiera de los temas mediante 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 tras darse de baja de [thème1]:

Image

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 ha realizado 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&#232;me" HeaderText="Th&#232;me"></asp:BoundColumn>
                                <asp:ButtonColumn Text="Plus d'informations" CommandName="infos"></asp:ButtonColumn>
                                <asp:ButtonColumn Text="S'abonner" CommandName="abonner"></asp:ButtonColumn>
                            </Columns>
                            <PagerStyle HorizontalAlign="Center" ... Mode="NumericPages"></PagerStyle>
                        </asp:datagrid>

Cabe destacar los siguientes puntos:

AutoGenerateColumns=false
Nosotros mismos definimos las columnas que se van a mostrar en la sección <columns>...</columns>
AllowPaging=true
PageSize=5
para la paginación de los datos
<asp:BoundColumn>
define la columna [thème] (HeaderText) de [DataGrid], que se vinculará a la columna [thème] de la fuente de datos (DataField)
<asp:ButtonColumn>
define dos columnas de botones (o enlaces). Para diferenciar los dos enlaces de una misma línea, se utilizará su propiedad [CommandName].

El componente [DataGrid] no está totalmente 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 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>
<HeaderTemplate>
define el texto del encabezado de [DataList]
<ItemTemplate>
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 elecció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:

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

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 introduce la fuente de datos en 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 incluye en 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, crea el [DataTable] de los temas a los que es posible suscribirse. Se crea de forma arbitraria con código. Recordemos la técnica que ya hemos visto. Se crea en el siguiente orden:

  • un objeto [DataTable] vacío, sin estructura y sin datos
  • la estructura de la tabla, definiendo sus columnas (nombre y tipo de datos que contienen)
  • las filas de la tabla que representan los datos útiles

Aquí hemos añadido una clave primaria. La columna «id» es la que hace las veces de 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 añadirse a una tabla. Existen todo tipo de restricciones posibles. La restricción «Primary Key» obliga a la columna a la que se aplica a tener 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 el conjunto de restricciones de una tabla determinada. Para añadir una restricción, se utiliza el método [DataTable.Constraints.Add]. Este tiene varias firmas. En este caso, se ha utilizado el método [Add(Byval nom as String, Byval colonne as DataColumn, Byval cléPrimaire as Boolean)]:

nom
nombre de la restricción: puede ser cualquiera
colonne
columna que será la clave primaria —de tipo [DataColumn]
cléPrimaire
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 denominada «id» en la clave primaria de la tabla [dtAbonnements], se escribe lo siguiente:

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

El procedimiento [Session_Start] se ejecuta cuando la aplicación recibe la primera solicitud de un cliente. Sirve para crear objetos específicos de cada cliente que deben persistir a lo largo de sus diferentes solicitudes. El procedimiento crea 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. También en este caso, la columna «id» sirve como clave primaria. Se ha utilizado una técnica diferente para declarar esta restricción:

[DataTable].PrimaryKey
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, con el fin de 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 solo se realiza durante la primera consulta. En las consultas siguientes, la vinculación no debe realizarse de forma sistemática y, cuando sea necesario hacerlo, a veces hay que esperar a que se produzca 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 forma única las líneas de los contenedores. Normalmente, 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 de 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 ha producido un evento. De hecho, es frecuente que, a partir de la fila de un contenedor en la que se ha producido un evento, sea necesario 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 gestiona de forma clásica:


    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 relacionadas con los enlaces [Plus d'informations] y [S'abonner] se gestionan mediante 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
        ' enlace
        liaisons()
    End Sub

Para diferenciarlos, se utiliza el hecho de que ambos enlaces tienen un atributo [CommandName]. En función del valor de este atributo, se llama al procedimiento [infos] o [abonner], pasando en ambos casos la clave «id» asociada al elemento de [DataGrid] donde se produjo 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 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 introducen los datos [thème] y [description] de dicha fila en el panel de información, que a continuación se hace visible.

El procedimiento [abonner(id)] debe añadir 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 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 comprueba si ya 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 añade 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], se busca en primer lugar la línea de clave [id]. Si se encuentra, se comprueba 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 añade una nueva suscripción a la tabla [dtAbonnements] y se establecen los enlaces de los componentes de la lista de datos con sus respectivas fuentes.

Cuando el usuario hace clic en un botón [Retirer], hay que 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 comprueba el atributo [CommandName] del elemento que origina 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 línea de un objeto [DataTable], se utiliza el método [DataList.Remove(DataRow)], que elimina de la tabla la línea 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á valorar en su justa medida 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:

Image

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

Image

La tercera página:

Image

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 añadiendo 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] evoluciona 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 incluye en 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:

pAC
de tipo [Integer]: es el número de la página actual mostrada en la última consulta
nbAC
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 dicha 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

En él 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:

pAC
es el número de la página actual mostrada durante la solicitud anterior
nbAC
número de líneas mostradas en esta página actual

El método [terminer] establece las relaciones entre los componentes y sus fuentes de datos, tal y como lo hacía el método [liaisons] en la aplicación anterior. La novedad, en este caso, es la vinculación del [DataList] con la tabla [dtPA], que es la página de suscripciones que se va a 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

Hay que tener en cuenta 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 dicho número de página actual. El método [changePAC] se encarga de construir la tabla [dtPA], que se vinculará al componente [dlAbonnements].
  • El método [setLiens] se encarga de mostrar u ocultar los enlaces [Précédent] y [Suivant], dependiendo de si la página actual [pAC] que se muestra va precedida o seguida de otra página. Tiene cuatro parámetros:
    • [dlAbonnements]: el control [DataList], cuya estructura de controles vamos a explorar para encontrar los dos enlaces. De hecho, aunque estos dos enlaces se encuentran en un lugar concreto, que es el pie de página del [DataList], no parece que haya una forma sencilla de hacer referencia a ellos directamente. En cualquier caso, 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]; toma el valor «verdadero» si la página actual no es la última de la lista de suscripciones
    • [nbLiensTrouvés]: un parámetro de salida que cuenta el número de enlaces encontrados. En cuanto 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 determinadas líneas de la tabla de suscripciones [dtAbonnements]. Recordemos que esta tabla se almacena en la sesión y se actualiza (aumentando o disminuyendo) a medida que se realizan las consultas. Empezamos por establecer el intervalo [premier,dernier] de los números de línea de la tabla [dtAbonnements] que debe mostrar la tabla [dtPA]:


    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]. En primer lugar, se define su estructura [id,thème] y, a continuación, se rellena copiando en ella las líneas de [dtAbonnements] cuyo número se encuentra en el 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], que puede vincularse al componente [DataList]. Esto se lleva a cabo 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. En primer lugar, 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. En cuanto se encuentra un enlace, se incrementa el contador [nbLiensTrouvés] y se rellena la propiedad [visible] del enlace con un valor pasado como parámetro del procedimiento. En cuanto se han encontrado los dos enlaces, deja de explorarse el árbol de controles y el procedimiento recursivo finaliza.

Hemos dicho 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. Hay varios procedimientos que modifican este número:


    Private Sub abonner(ByVal id As String)
        ' suscripción al tema con la clave id
..
            ' se añade el tema a las suscripciones
..
             ' actualización del número de página actual: ahora es la última página
            pAC = (dtAbonnements.Rows.Count - 1) \ nbAP
             ' vinculaciones de datos
            terminer()
        End If
    End Sub

Tras añadir una suscripción, esta aparece al final de la lista de suscripciones. Por ello, se pasa a la última página de suscripciones para que el usuario pueda ver la suscripción que se acaba de añadir.


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

El resto de 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 denominada [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 fase, crearemos un cliente web para realizar esta 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 disponer de una clase de excepción con un constructor que admita una lista de mensajes de error en lugar de un único mensaje de error. 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
connexion
La conexión a la base de datos se abrirá para ejecutar un comando SQL y se cerrará inmediatamente después
selectText
Consulta SQL [select] que recupera toda la tabla [liste]
insertText
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 se autoincrementa mediante el SGBD, por lo que no es necesario especificarlo.
updateText
Consulta que permite actualizar los campos (nombre, precio) de la fila de la tabla [liste] que tiene la clave [id]
deleteText
consulta que permite eliminar la línea de la tabla [liste] con la clave [id]
selectCommand
objeto [OleDbCommand] que ejecuta la consulta [selectText] en la conexión [connexion]
updateCommand
objeto [OleDbCommand] ejecutando la consulta [updateText] en la conexión [connexion]
insertCommand
objeto [OleDbCommand] que ejecuta la consulta [insertText] en la conexión [connexion]
deleteCommand
objeto [OleDbCommand] que ejecuta la consulta [deleteText] en la conexión [connexion]
adaptateur
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 que se va a utilizar. A partir de ella, 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] establece 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 operación [Fill] que sigue a continuación rellena 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 demás métodos, gestionamos los posibles errores mediante la clase [ExceptionProduits] para obtener una lista (ArrayList) de errores en lugar de un único error. El método [getProduits] devuelve la tabla [liste] en forma de un objeto [DataTable].

El método ajouterProduits

Este método permite añadir una línea (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 añade 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 incorporació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 sesión
                connexion.Close()
            End Try
        End Sub

Los campos de la estructura [produit] se introducen 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 saber a qué parámetro formal corresponde cada parámetro efectivo. En este caso, el parámetro efectivo n.º i de la colección [Parameters] sustituirá al parámetro formal «?» n.º i. Para crear un parámetro efectivo de tipo [OleDbParameter], se utiliza aquí 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 línea 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
                ' se inicia la 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 prácticamente 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 línea 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 de 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

El procedimiento es el mismo 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"

             ' comprobació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 añadido
            produit.id = CType(contenu.Rows(contenu.Rows.Count - 1)("id"), Integer)
             ' se modifica el producto añadido
            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 añadido
            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

y luego lo 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 crear una aplicación web para actualizar la tabla de productos (añadir, eliminar, modificar). La tabla actualizada permanecerá 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 que plantea la 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:

Image

 

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
1
lnkFiltre
LinkButton
muestra la vista [Formulaire], que sirve para establecer la condición de filtrado
2
lnkMisaJour
LinkButton
muestra la vista [Produits], que sirve para visualizar y actualizar la tabla de productos (modificación y eliminación)
3
lnkAjout
LinkButton
muestra la vista [Ajout], que sirve para añadir un producto
4
panel
vueFormulaire
la vista [Formulaire]
5
txtFiltre
TextBox
la condición de filtrado
6
txtPages
TextBox
Número de productos por página
7
rfvLignes
RequiredFieldValidator
Comprueba si hay un valor en [txtPages]
8
rvLignes
RangeValidator
comprueba que txtPages esté dentro del intervalo [3,10]
9
btnExécuter
 
botón [submit] que muestra la vista [produits] filtrada según la condición (5)
10
lblInfo1
Etiqueta
Texto informativo en caso de errores

Por ejemplo, si la vista [formulaire] se rellena de la siguiente manera:

Image

se obtiene el siguiente resultado:

n.º
nombre
tipo
función
1
vueProduits
panel
 
2
rdCroissant
rdDécroissant
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].
3
DataGrid1
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 se tiene .AllowPaging=true, .AllowSorting=true
4
DataGrid2
DataGrid
muestra la tabla completa de productos; permite realizar un seguimiento de las actualizaciones
5
LblInfo2
Etiqueta
texto informativo, especialmente en caso de errores
6
DataGrid3
DataGrid
mostrará los productos eliminados en la tabla de productos
7
DataGrid4
DataGrid
mostrará los productos modificados en la tabla de productos
8
DataGrid5
DataGrid
mostrará los productos añadidos en la tabla de productos

Hay cinco contenedores de datos en esta página. Todos ellos 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 líneas, como por ejemplo el anterior [prix>30]. Este tipo de filtrado será utilizado por [DataGrid1].
  • [RowStateFilter] permite establecer un filtro en función del estado de la fila de la tabla. Este indica el estado de la fila con respecto al estado original que tenía cuando se creó la vista de la tabla. En este caso, la tabla [dtProduits] procede 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 evolucionar posteriormente y adoptar diferentes valores, entre los que se incluyen los siguientes:
    • [Added]: la fila se ha añadido; no formaba parte de la tabla original
    • [Deleted]: la línea se ha eliminado; sigue estando en la tabla, pero está «marcada» como «por eliminar»
    • [Modified]: la línea se ha modificado

[RowStateFilter] permite mostrar las líneas de la tabla que tienen un estado determinado:

  • (continuación)
    • [DataViewRowState.Added]: solo se muestran las líneas añadidas. Se muestran con sus valores actuales.
    • [DataViewRowState.ModifiedOriginal]: solo se muestran las líneas modificadas. Se muestran con sus valores originales.
    • [DataViewRowState.ModifiedCurrent]: solo se muestran las líneas modificadas. Se muestran con sus valores actuales.
    • [DataViewRowState.Deleted]: solo se muestran las líneas 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:

DataGrid1
sin filtro sobre el estado de las líneas
 
DataGrid2
DataViewRowState.CurrentRows
muestra el estado actual de la tabla de productos
DataGrid3
DataViewRowState.Deleted
muestra las líneas eliminadas de la tabla de productos
DataGrid4
DataViewRowState.ModifiedOriginal
muestra las líneas modificadas de la tabla de productos con sus valores iniciales
DataGrid5
DataViewRowState.Added
muestra las líneas añadidas a la tabla de productos inicial

Los contenedores [DataGrid1-5] nos permitirán realizar un seguimiento de 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 aspectos ya se han estudiado en un ejemplo anterior.

El enlace [Ajout] da acceso a un formulario para añadir un producto:

n.º
nombre
tipo
función
1
vueAjout
panel
 
2
txtNom
TextBox
nombre del producto
3
rfvNom
RequiredFieldValidator
Comprueba si hay un valor en [txtNom]
4
txtPrix
TextBox
precio del producto
5
rfvPrix
RequiredFieldValidator
Comprueba si hay un valor en [txtPrix]
6
cvPrix
CompareValidator
comprueba que precio >= 0
7
btnAjouter
Botón
Botón [submit] para añadir el producto
8
lblInfo3
Etiqueta
Texto informativo sobre el resultado de la operación de «Añadir»

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
1
vueErreurs
panel
 
2
rptErreurs
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 un 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]):

Image

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

Image

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

Image

La expresión de ordenación es la que deberá colocarse tras la cláusula [order by] de la instrucción SQL [select], que se ejecutará cuando elel usuario haga clic en el nombre de la columna de cabecera [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:

Image

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:

Image

Se puede observar que es posible modificar los textos de los botones. En cuanto a los botones, podemos elegir entre enlaces y botones (lista desplegable de 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, hemos utilizado directamente la ventana de propiedades de [DataGrid] para rellenar la propiedad [DataKeyField], que indica con qué campo de la fuente de datos se indexan las líneas de [DataGrid]. En este caso se utiliza la clave primaria de la tabla de productos:

Image

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 &#224; jour" CancelText="Annuler" EditText="Modifier"></asp:EditCommandColumn>
          <asp:ButtonColumn Text="Supprimer" CommandName="Delete"></asp:ButtonColumn>
  </Columns>
<PagerStyle NextPageText="Suivant" PrevPageText="Pr&#233;c&#233;dent" ...></PagerStyle>
</asp:DataGrid>

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 por defecto 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á de 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&nbsp;la table LISTE.&nbsp;Exemple : prix&lt;100 and 
                                prix&gt;50</P>
                            <P>
                                <asp:TextBox id="txtFiltre" runat="server" Columns="60"></asp:TextBox></P>
                            <P>Nombre de lignes par page :
                                <asp:TextBox id="txtPages" runat="server" Columns="3">5</asp:TextBox>
                                <asp:RequiredFieldValidator id="rfvLignes" runat="server" Display="Dynamic"
                            ControlToValidate="txtPages" ErrorMessage="Indiquez le nombre de lignes par page"
                                    EnableClientScript="False">
                     </asp:RequiredFieldValidator></P>
                            <P>
                                <asp:RangeValidator id="rvLignes" runat="server" Display="Dynamic"
                        ControlToValidate="txtPages" ErrorMessage="Vous devez indiquer un nombre entre 3 et 10"
                                    EnableClientScript="False" MaximumValue="10" MinimumValue="3" Type="Integer"
                             EnableViewState="False">
                             </asp:RangeValidator></P>
                            <P>
                                <asp:Label id="lblinfo1" runat="server"></asp:Label></P>
                            <P>
                                <asp:Button id="btnExécuter" runat="server" CausesValidation="False"
                              EnableViewState="False" Text="Exécuter">
                             </asp:Button></P>
                        </asp:panel>

                      <asp:panel id="vueProduits" runat="server">
                            <TABLE>
                                <TR>
                                    <TD align="center" bgColor="#ff9966">
                                        <P>Tri
                                            <asp:RadioButton id="rdCroissant" runat="server" Text="croissant"
                                     GroupName="rdTri" Checked="True">
                                 </asp:RadioButton>
                                            <asp:RadioButton id="rdDécroissant" runat="server" Text="décroissant"
                                                 GroupName="rdTri">
                                    </asp:RadioButton></P>
                                    </TD>
                                    <TD align="center" bgColor="#ff9966">Tous les produits
                                    </TD>
                                    <TD align="center" bgColor="#ff9966">Suppressions
                                    </TD>
                                    <TD align="center" bgColor="#ff9966">Modifications
                                    </TD>
                                    <TD align="center" bgColor="#ff9966">Ajouts
                                    </TD>
                                <TR>
                                    <TD vAlign="top">
                                        <P>
                                            <asp:DataGrid id="DataGrid1" runat="server" AllowSorting="True" PageSize="4"
                                                 AllowPaging="True" ....     AutoGenerateColumns="False" DataKeyField="id">
                                                <ItemStyle ...></ItemStyle>
                                                <HeaderStyle ...></HeaderStyle>
                                                <FooterStyle ...></FooterStyle>
                                                <Columns>
                                                    <asp:BoundColumn DataField="nom" SortExpression="nom" HeaderText="nom">
                                               </asp:BoundColumn>
                                                    <asp:BoundColumn DataField="prix" SortExpression="prix" HeaderText="prix">
                                          </asp:BoundColumn>
                                                    <asp:EditCommandColumn ButtonType="LinkButton" UpdateText="Mettre &#224; jour"
                                                         CancelText="Annuler" EditText="Modifier">
                                       </asp:EditCommandColumn>
                                                    <asp:ButtonColumn Text="Supprimer" CommandName="Delete">
                                     </asp:ButtonColumn>
                                                </Columns>
                                                <PagerStyle NextPageText="Suivant" PrevPageText="Pr&#233;c&#233;dent" ....>
                                           </PagerStyle>
                                            </asp:DataGrid>
                                       </P>
                                    </TD>
                                    <TD vAlign="top">
                                        <asp:DataGrid id="DataGrid2" runat="server" ...>
                                            <AlternatingItemStyle ...></AlternatingItemStyle>
                                            <ItemStyle ...></ItemStyle>
                                            <HeaderStyle ....></HeaderStyle>
                                            <FooterStyle ...></FooterStyle>
                                            <PagerStyle ... Mode="NumericPages"></PagerStyle>
                                        </asp:DataGrid>
                            </TD>
                                    <TD vAlign="top">
                                        <asp:DataGrid id="DataGrid3" runat="server" ...>
                                            <ItemStyle ...></ItemStyle>
                                            <HeaderStyle ...></HeaderStyle>
                                            <FooterStyle ...></FooterStyle>
                                            <PagerStyle ...></PagerStyle>
                                        </asp:DataGrid>
                                   </TD>
                                    <TD vAlign="top">
                                        <asp:DataGrid id="DataGrid4" runat="server" ...>
                                            <ItemStyle ....></ItemStyle>
                                            <HeaderStyle ....></HeaderStyle>
                                            <FooterStyle ...></FooterStyle>
                                            <PagerStyle .... Mode="NumericPages"></PagerStyle>
                                        </asp:DataGrid>
                       </TD>
                                    <TD vAlign="top">
                                        <asp:DataGrid id="DataGrid5" runat="server....>
                                            <AlternatingItemStyle ...></AlternatingItemStyle>
                                            <HeaderStyle ..></HeaderStyle>
                                            <FooterStyle ...></FooterStyle>
                                            <PagerStyle ....></PagerStyle>
                                        </asp:DataGrid>
                         </TD>
                                </TR>
                            </TABLE>
                            <P></P>
                            <P>
                                <asp:Label id="lblInfo2" runat="server"></asp:Label></P>
                    </asp:panel>

                   <asp:panel id="vueErreurs" runat="server">
                            <asp:Repeater id="rptErreurs" runat="server" EnableViewState="False">
                                <HeaderTemplate>
                                    Les erreurs suivantes se sont produites :
                                    <ul>
                                </HeaderTemplate>
                                <ItemTemplate>
                                    <li>
                                        <%# Container.DataItem %>
                                    </li>
                                </ItemTemplate>
                                <FooterTemplate>
                                    </ul>
                                </FooterTemplate>
                            </asp:Repeater>
                        </asp:panel>

                  <asp:panel id="vueAjout" EnableViewState="False" Runat="server">
                            <P>Ajout d'un produit</P>
                            <P>
                                <TABLE ... border="1">
                                    <TR>
                                        <TD>nom</TD>
                                        <TD>
                                            <asp:TextBox id="txtNom" runat="server" Columns="30"></asp:TextBox>
                                            <asp:RequiredFieldValidator id="rfvNom" runat="server" ControlToValidate="txtNom"
                                                     ErrorMessage="Vous devez indiquer un nom">
                               </asp:RequiredFieldValidator>
                                      </TD>
                                    </TR>
                                    <TR>
                                        <TD>prix</TD>
                                        <TD>
                                            <asp:TextBox id="txtPrix" runat="server" Columns="10"></asp:TextBox>
                                            <asp:RequiredFieldValidator id="rfvPrix" runat="server" ControlToValidate="txtPrix"
                                                 ErrorMessage="Vous devez indiquer un prix">
                                           </asp:RequiredFieldValidator>
                                            <asp:CompareValidator id="cvPrix" runat="server" ControlToValidate="txtPrix"
                                                     ErrorMessage="CompareValidator" Type="Double" Operator="GreaterThanEqual"
                                                     ValueToCompare="0">
                             </asp:CompareValidator>
                                   </TD>
                                    </TR>
                                </TABLE>
                            </P>
                            <P>
                                <asp:Button id="btnAjouter" runat="server" CausesValidation="False"
                                     Text="Ajouter">
                               </asp:Button></P>
                            <P>
                                <asp:Label id="lblInfo3" runat="server"></asp:Label></P>
                        </asp:panel>

                </td>
                </tr>
            </table>
        </form>
    </body>
</HTML>

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 añadir un elemento, no queremos que se ejecuten los controles relativos al 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:

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

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 recogen los datos 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 ha producido un error al acceder a los productos; se anota en la aplicación
            Application("erreurs") = ex.erreurs
            Exit Sub
        Catch ex As Exception
            ' error no gestionado
            erreurs.Add(ex.Message)
            Application("erreurs") = erreurs
            ' salida de la subrutina
        End Try
        ' aquí no hay errores de inicialización
        ' se almacena el número de productos por página
        Application("defaultProduitsPage") = defaultProduitsPage
        ' se almacena 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], se empieza por recuperar 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], analizada anteriormente, y del método [getProduits] de la misma. 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 introduce en la aplicación vinculada a la clave [erreurs]. La presencia de esta clave en la información registrada en la aplicación se comprobará cada vez que se procese una solicitud. Si se encuentra, se enviará al cliente la vista [erreurs].

Aunque la tabla [dtProduits] sea 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 incluida en la sesión específica de cada cliente. Se utiliza el atributo [DefaultView] de la tabla [dtProduits] para obtener una vista por defecto 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. Inicialmente, es la primera página.

9.6.6. El código de control [main.aspx.vb]

La estructura del controlador es la 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
dtProduits
la tabla [DataTable] de productos, común a todos los clientes
dvProduits
la vista [DataView] de productos —específica para cada cliente
defaultProduitsPage
Número de productos por página propuesto por defecto

9.6.8. El procedimiento [Page_Load] de carga de 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 comprueba si la aplicación presenta algún 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 se esté realizando
        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 comprueba si la tabla de productos se ha cargado al iniciar la aplicación. Si no es así, se muestra la vista [erreurs] con los mensajes de error correspondientes y, a continuación, se sale del procedimiento tras establecer el valor booleano [erreur] en verdadero. Este indicador será comprobado por determinados procedimientos.
  • Se recupera la tabla de productos [dtProduits] de la aplicación y se almacena en 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], recuperada de la sesión, y con el número predeterminado de productos por página, recuperado de la aplicación.
  • Si se trata de la primera solicitud del cliente, se le muestra el formulario de definición de 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 forma diferente al resto de elementos del [dataGrid]:

Image

En el ejemplo 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] y 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 gestiona dicho evento. Se darán las siguientes situaciones:

  1. se ha hecho clic en el enlace [Modifier] del elemento n.º 8 de [DataGrid1]. [Page_Load] establece primero -1 en [DataGrid1.EditItemIndex]. A continuación, el procedimiento que gestiona el evento [Modifier] introducirá un 8 en [DataGrid1.EditItemIndex]. Finalmente, cuando la página se envíe de nuevo al cliente, el elemento n.º 8 estará efectivamente en el modo [édition] y este aparecerá tal y como se muestra arriba.
  2. El usuario modifica el producto que se encuentra en el modo [édition] y valida su modificación mediante el enlace [Mettre à jour]. [Page_Load] introduce el valor -1 en [DataGrid1.EditItemIndex]. A continuación, se ejecutará el procedimiento que gestiona el evento [Mettre à jour] y se actualizará el elemento n.º 8 de [dtProduits]. Cuando la página se envíe de nuevo al cliente, ningún elemento de [DataGrid1] estará en modo de edición (DataGrid.EditItemIndex=-1).
  3. Si un usuario ha iniciado la actualización de un producto, lo lógico sería que abandonara 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 establecer el valor -1 en [DataGrid1.EditItemIndex]; a continuación, se ejecutará el procedimiento que gestiona el evento que se ha producido. No modificará la propiedad [DataGrid1.EditItemIndex], que, por lo tanto, mantendrá 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 se encontraba 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. Esta visualización se realiza 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 se puede utilizar.


    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 ofrecen al usuario:

Image

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 «Añadir»
        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. A continuación, debe mostrarse 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 con 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 denomina [dvProduits] y se ha recuperado 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, dicho 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

Image

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
        ' Inclusión de los datos filtrados en 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 almacena el número de productos por página
        Session("nbProduitsPage") = txtPages.Text
        'y la página actual
        Session("pageCourante") = 0
        ' todo va 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
7
rfvLignes
RequiredFieldValidator
comprueba si hay un valor en [txtPages]
8
rvLignes
RangeValidator
comprueba que txtPages se encuentre dentro del intervalo [3,10]

El procedimiento comienza ejecutando el código de control de los dos componentes anteriores mediante su método [Validate] y, a continuación, comprueba el valor de su atributo [IsValid]. Este valor será [true] únicamente si los datos comprobados se han considerado válidos. Si las comprobaciones de validez de alguno de los dos componentes han fallado, se vuelve a mostrar la vista [formulaire] para que el usuario corrija su error o errores. La condición de filtrado se aplica al atributo [dvProduits.RowFilter]. En este caso, puede producirse una excepción si el usuario ha introducido un criterio de filtrado incorrecto, tal y como se muestra a continuación:

Image

En este caso, se vuelve a mostrar la vista [formulaire]. Si los dos datos introducidos 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 va a mostrar —inicialmente, la página 0—

Estos dos datos se utilizan cada vez que se muestra la vista [produits].

9.6.11. Visualización de la vista [produits]

La vista [produits] es la siguiente:

Image

Tenemos 5 componentes [DataGrid], cada uno de los cuales muestra una vista concreta de la tabla de productos [dtProduits]. Empezando por la izquierda:

nombre
función
DataGrid1
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
DataGrid2
muestra la tabla completa de productos; permite realizar un seguimiento de las actualizaciones
DataGrid3
mostrará los productos eliminados en la tabla de productos
DataGrid4
mostrará los productos modificados en la tabla de productos
DataGrid5
mostrará los productos añadidos 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 va a mostrar en [DataGrid1]
  • el número de productos [taillePage] por página

La vinculación de [DataGrid] a 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, tiene un atributo [RowState] que indica el estado de la fila. Estos son algunos de ellos:

  • (continuación)
    • [Added]: la fila se ha añadido
    • [Modified]: la fila se ha modificado
    • [Deleted]: la fila se ha eliminado
    • [Unchanged]: la línea 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 añade una línea a la tabla [dtProduits]. Esta línea estará en el estado [Added].
    • Cuando se modifica un producto, su estado pasa de [Unchanged] a [Modified].
    • Cuando se elimina un producto, la línea no se elimina físicamente. En su lugar, se marca como pendiente de eliminación y su estado pasa a ser [Deleted]. Es posible anular 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 líneas añadidas 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 líneas modificadas se muestran con sus valores actuales

Una línea modificada tiene dos valores: el valor original, que es el que tenía la línea antes de la primera modificación, y el valor actual, que es el obtenido tras 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
DataGrid2
RowStateFilter= DataRowViewState.CurrentRows
DataGrid3
RowStateFilter= DataRowViewState.Deleted
DataGrid4
RowStateFilter = DataRowViewState.ModifiedOriginal
DataGrid5
RowStateFilter= DataRowViewState.Added

La tabla [dtPoduits] se actualiza 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 porque 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 añadir, modificar o eliminar productos, se sincronizará con los demás clientes mediante la siguiente secuencia:

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

Puede haber varias secciones críticas en el código de la aplicación:

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

El funcionamiento es el siguiente:

  1. uno o varios clientes llegan a la instrucción [Application.Lock] de la sección crítica 1. Se trata de un distribuidor de tokens de entrada único. Un único cliente obtiene este token. Llamémoslo C1.
  2. Mientras el cliente C1 no haya devuelto el token mediante [Application.Unlock], ningún otro cliente está autorizado a entrar en una sección crítica controlada por [Application.Lock]. Por lo tanto, en el ejemplo anterior, ningún cliente puede acceder a las secciones críticas 1 y 2.
  3. 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 del 1 al 3.

Con este mecanismo, nos aseguraremos de que solo un cliente tenga acceso a la tabla [dtProduits], ya sea en lectura o en escritura.

El enlace [Mise à jour] también muestra la vista [Produits]:

Image

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

Hay que 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 va a mostrar y el número de productos por página que se van a mostrar. Estos dos datos se encuentran 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 cambio 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

La rutina [DataGrid1_SortCommand] se ejecuta cuando el usuario hace clic en el encabezado de una de las columnas de [DataGrid1]. A continuación, hay que asignar la expresión de ordenación al atributo [Sort] de la vista [dvProduits] mostrada por [DataGrid1]. Esta es sintácticamente equivalente a la expresión de ordenación situada tras 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]:

Image

Si la expresión de ordenación no se ha definido 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]:

Image

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:

Image

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 que se va 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 pulsado:


         ' 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 ha originado el evento. A grandes rasgos, 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 es relativo a la página actual que se está mostrando. Así, la primera línea de la página mostrada 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, hemos escrito [DataGrid1.DataKey=id], las claves de [DataGrid1] están formadas por los valores de la columna [id] de la tabla [dtProduits], que es al mismo tiempo la clave primaria. Así, [DataGrid1.DataKeys(e.Item.ItemIndex)] es la clave [id] del producto que se va a eliminar. Una vez obtenido esto, se solicita su eliminación mediante el procedimiento [supprimerProduit]:


         ' eliminación del producto
        Dim erreur As Boolean = False
        Try
            supprimerProduit(idProduit)
        Catch ex As Exception
            ' pb
            lblInfo2.Text = ex.Message
            erreur = True
        End Try

Esta eliminación puede fallar en algunos casos. Veremos por qué. En ese caso, se lanza una excepción. Aquí se gestiona dicha excepción. Si se produce un error, el [DataGrid1] no tiene que modificarse. Solo se añade un mensaje de error a la vista [produits]. Si no hay ningún 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]:

         ' visualización de productos
        afficheProduits(page, DataGrid1.PageSize)

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 que se va 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:

             ' eliminación de la línea [idProduit]
            dtProduits.Rows.Find(idProduit).Delete()

Cuando hay varios clientes realizando las actualizaciones al mismo tiempo, la situación se complica. De hecho, consideremos la siguiente secuencia temporal:

Hora
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 este producto, por ejemplo, eliminarlo. Inevitablemente, uno de los dos lo hará primero. El que llegue después querrá entonces eliminar un producto que ya no existe. El procedimiento [supprimerProduits] tiene en cuenta este caso de varias maneras:

  • en primer lugar, se realiza una sincronización con [Application.Lock]. Esto significa que, cuando un cliente supera esta barrera, no hay ningún otro 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.
  • A continuación, se comprueba 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 que se va 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 ha podido eliminar la línea, el procedimiento lanza una excepción asociada a un mensaje de error.

Veamos un ejemplo. Iniciamos un cliente [Mozilla] y seleccionamos inmediatamente la opción [Mise à jour] (vista parcial):

Image

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

Image

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

Image

La operación de eliminación se ha realizado correctamente. El producto eliminado aparece correctamente en [DataGrid], dentro de los 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]:

Image

La respuesta obtenida es la siguiente:

Image

Un mensaje de error indica al usuario que el producto con clave [2] no existe. La respuesta devuelve al cliente una nueva vista que refleja el estado actual de la tabla. Se observa que, efectivamente, el producto con clave [2] figura 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. Añadir un producto

Para añadir un producto, el usuario utiliza el enlace [Ajout] de las opciones. Este enlace se limita a mostrar la vista [Ajout]:

Image

Image

Los dos procedimientos implicados 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 «Añadir»
        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
rfvNom
RequiredFieldValidator
comprueba si hay un valor en [txtNom]
rfvPrix
RequiredFieldValidator
comprueba si hay un valor en [txtPrix]
cvPrix
CompareValidator
comprueba que precio >= 0

El procedimiento de gestión al hacer 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
        ' Añadido 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 introducción de datos
            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 añade 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 comprobaciones de validez de los tres componentes [rfvNom, rfvPrix, cvPrix]. Si falla alguna de las comprobaciones, 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:

Image

Si los datos son válidos, se prepara la línea que se va a insertar 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 rellenan con los valores introducidos en el formulario de alta. La columna [id] no se rellena. 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 línea [produit] creada aquí está separada de la tabla [dtProduits]. Ahora tenemos que insertarla en esta tabla. Como vamos a actualizar la tabla [dtProduits], activamos la sincronización entre clientes:

        Application.Lock()

Una vez hecho esto, intentamos añadir la línea a la tabla [dtProduits]. Si lo conseguimos, mostramos un mensaje de éxito; en caso 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 ninguna excepción posible. No obstante, por precaución, se ha implementado una gestión 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:

Image

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

Image

Gracias al componente [DataGrid], este resultado se consigue 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 línea cuyo índice es el valor de [EditItemIndex] pasa a modo de edición. Cada valor de la línea así actualizada puede modificarse en un campo de entrada, tal y como se muestra en la captura de pantalla anterior. Así pues, recuperamos el índice del producto en el que se ha hecho clic en el enlace [Modifier] y lo asignamos al atributo [EditItemIndex] del componente [DataGrid1]. Ahora solo nos queda volver a mostrar la vista [produits]. Aparecerá igual que antes, pero con una línea 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 introducir -1 en [DataGrid1.EditItemIndex] para anular el modo de actualización. De hecho, sabemos que el procedimiento [Page_Load] lo hace de forma sistemática. Por lo tanto, no es necesario 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 que se va 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 en el caso de la eliminación, debemos recuperar la clave del producto que queremos modificar para localizar su entrada 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 hay que asignar 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 el origen 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, disponemos de los nuevos valores de la línea modificada en forma de cadenas de caracteres. A continuación, comprobamos si estos datos son válidos. El nombre no debe estar vacío y el precio debe ser un número positivo o cero:


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

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

Lo que no se aprecia en el código anterior es que los valores introducidos se pierden. De hecho, [DataGrid1] está vinculado a los datos de la tabla [dtProduits], que contiene los valores originales de la línea modificada. A continuación se muestra un ejemplo.

Image

La respuesta obtenida es la siguiente:

Image

Se observa que se han perdido los valores introducidos inicialmente. En una aplicación profesional, esto probablemente no sería aceptable. Aquí nos encontramos con ciertas limitaciones del modo estándar de actualización del componente [DataGrid]. Sería preferible disponer de una vista [Modification] análoga 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 puede fallar. De hecho, entre el momento en que el cliente ha leído 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] gestiona este caso lanzando una excepción. Esta excepción se gestiona 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 que se va 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]:

Image

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

Image

La modificación aparece correctamente 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 líneas modificadas, las cuales se muestran con sus valores originales. Veamos ahora 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 pasa a modificar el producto [produit1]:

Image

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

Image

El cliente [Internet Explorer] elimina [produit1]:

Image

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

Image

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

Image

Puede comprobar que [produit1] se ha 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 habitual. De hecho, en algún momento hay que actualizar la fuente real de los datos. Se puede optar por dos estrategias diferentes:

  1. utilizar la caché [dtProduits] en memoria para actualizar la fuente de datos. Se puede crear una página situada en el árbol web de la aplicación anterior para tener acceso a la caché [dtProduits] de esta. Esta página ofrecería al administrador la posibilidad de reflejar en la fuente física de los datos los cambios realizados en la caché [dtProduits]. Para ello, se podría añadir un nuevo método a la clase de acceso [produits] que admitiera como parámetro la caché [dtProduits] y que, con dicha caché, actualizara la fuente física de datos.
  2. Se actualiza la fuente física de datos al mismo tiempo que la caché.

La estrategia n.º 1 permite abrir una sola conexión con la fuente de datos física. La estrategia n.º 2 requiere una conexión en cada actualización. En función de la disponibilidad de las conexiones, se podrá optar por una u otra estrategia. Dado que disponemos de 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é solo se crea una vez, al iniciar la aplicación en [global.asax.vb]. Esto significa que, si la fuente de datos física la actualizan otros clientes distintos de los clientes web, estos no ven dichos cambios. Solo ven los que ellos mismos introducen en la tabla de la 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 recuperan los datos 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 ha producido un error al acceder a los productos; se anota en la aplicación
            Application("erreurs") = ex.erreurs
            Exit Sub
        Catch ex As Exception
            ' error no gestionado
            erreurs.Add(ex.Message)
            Application("erreurs") = erreurs
            ' salida de la subrutina
        End Try
        ' aquí no hay errores de inicialización
...
        ' 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)
...
    End Sub
End Class

Se ha almacenado en la aplicación una instancia de la clase de acceso a datos, 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: añadir, eliminar y modificar.

El procedimiento de alta se modifica de la siguiente manera:


    Private Sub btnAjouter_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnAjouter.Click
...
        ' se añade la línea
        Application.Lock()
        Try
            ' se añade la línea a la tabla en caché
            dtProduits.Rows.Add(produit)
             ' se añade la línea 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 va 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 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 en caso de 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 que se va 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 en caso de 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:

Image

Se inicia un cliente web:

Image

Añadimos un producto:

Image

Eliminamos [produit1]:

Image

Modificamos el precio de [produit2]:

Image

Tras estos cambios, consultamos el contenido de la tabla [liste] en la base de datos ACCESS:

Image

Las tres modificaciones se han aplicado correctamente en la tabla física. Veamos ahora un caso de conflicto. Eliminamos, justo debajo de ACCESS, la línea de [produit2]:

Image

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

Image

Recibe la siguiente respuesta:

Image

La línea [produit2] sí se ha eliminado de la caché, tal y como indica la lista de productos eliminados. Sin embargo, la eliminación de [produit2] en la fuente física ha fallado, tal y como 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 propios. Ahora queremos que un cliente pueda ver la fuente de datos física tal y como está en ese momento y no tal y como estaba al iniciar la aplicación. Para ello, vamos a ofrecer al cliente una nueva opción:

Image

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 la creará cada cliente y no 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 almacena 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)
        ' inicialización de 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 ha producido un error al acceder a los productos; se anota en la sesión
                Session("erreurs") = ex.erreurs
                Exit Sub
            Finally
                Application.UnLock()
            End Try
            ' se almacena 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 comprueba si la aplicación presenta algún 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 comprueba si la sesión presenta algún 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 que se esté realizando
        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 solicitud:


    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 va a enviarse 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 almacenamiento 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
        ' Hay que 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 vuelve a aplicar el filtro
            dvProduits.RowFilter = filtre
        Catch ex As ExceptionProduits
            'se ha producido un error al acceder a los productos; se anota en la sesión
            Session("erreurs") = ex.erreurs
            ' se muestra la vista [erreurs]
            afficheErreurs(ex.erreurs)
            ' fin
            Exit Sub
        Finally
            ' fin de la sincronización
            Application.UnLock()
        End Try
        ' todo ha ido 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 se colocarán 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:

Image

Un cliente [Internet Explorer] hace lo mismo:

Image

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

Image

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

Image

Obtiene la siguiente respuesta:

Image

Se le ha indicado que [produit1] ya no existe. El usuario decide entonces actualizar su caché con el enlace [Rafraîchir] anterior. Obtiene la siguiente respuesta:

Image

Ahora dispone de 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 a sus vínculos con las fuentes de datos. Hemos concluido mostrando cómo se puede actualizar una fuente de datos mediante un componente [DataGrid]. Hemos utilizado una tabla de base de datos como fuente de datos. Aunque el componente [DataGrid] facilita un poco la representación de los datos, la verdadera dificultad no radica en la parte de 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, de modo 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 de lo que sería necesario 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 una única tabla, sino las tablas vinculadas entre sí mediante relaciones.

Este capítulo ha permitido, además, presentar diferentes estructuras de datos, como [DataTable, DataView].